-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from uniswapfoundation/hook-fees-access-lock
Static Hook Fees
- Loading branch information
Showing
18 changed files
with
619 additions
and
24 deletions.
There are no files selected for viewing
Submodule v4-core
updated
45 files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.19; | ||
|
||
// TODO: update to v4-periphery/BaseHook.sol when its compatible | ||
import {BaseHook} from "../forks/BaseHook.sol"; | ||
|
||
import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; | ||
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; | ||
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; | ||
|
||
contract FixedHookFee is BaseHook { | ||
using PoolIdLibrary for PoolKey; | ||
using CurrencyLibrary for Currency; | ||
|
||
uint256 public constant FIXED_HOOK_FEE = 0.0001e18; | ||
|
||
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} | ||
|
||
function getHookPermissions() public pure override returns (Hooks.Permissions memory) { | ||
return Hooks.Permissions({ | ||
beforeInitialize: false, | ||
afterInitialize: false, | ||
beforeModifyPosition: false, | ||
afterModifyPosition: false, | ||
beforeSwap: true, | ||
afterSwap: false, | ||
beforeDonate: false, | ||
afterDonate: false, | ||
noOp: false, | ||
accessLock: true // -- Required to take a fee -- // | ||
}); | ||
} | ||
|
||
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) | ||
external | ||
override | ||
returns (bytes4) | ||
{ | ||
// take a fixed fee of 0.0001 of the input token | ||
params.zeroForOne | ||
? poolManager.mint(key.currency0, address(this), FIXED_HOOK_FEE) | ||
: poolManager.mint(key.currency1, address(this), FIXED_HOOK_FEE); | ||
|
||
return BaseHook.beforeSwap.selector; | ||
} | ||
|
||
/// @dev Hook fees are kept as PoolManager claims, so collecting ERC20s will require locking | ||
function collectFee(address recipient, Currency currency) external returns (uint256 amount) { | ||
amount = abi.decode( | ||
poolManager.lock( | ||
abi.encodeCall( | ||
this.handleCollectFee, | ||
(recipient, currency) | ||
) | ||
), | ||
(uint256) | ||
); | ||
} | ||
|
||
/// @dev requires the lock pattern in order to call poolManager.burn | ||
function handleCollectFee(address recipient, Currency currency) external returns (uint256 amount) { | ||
// convert the fee (Claims) into ERC20 tokens | ||
amount = poolManager.balanceOf(address(this), currency); | ||
poolManager.burn(currency, amount); | ||
|
||
// direct claims (the tokens) to the recipient | ||
poolManager.take(currency, recipient, amount); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// NOTE: ----------------------------------------------------------------------------------------------- // | ||
// December 1, 2023: // | ||
// Recent v4-core changes are uncompatible with an outdated v4-periphery/BaseHook.sol // | ||
// This is a *temporary* fix until v4-periphery is updated. // | ||
// ----------------------------------------------------------------------------------------------------- // | ||
|
||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; | ||
import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; | ||
|
||
abstract contract BaseHook is IHooks { | ||
error NotPoolManager(); | ||
error NotSelf(); | ||
error InvalidPool(); | ||
error LockFailure(); | ||
error HookNotImplemented(); | ||
|
||
/// @notice The address of the pool manager | ||
IPoolManager public immutable poolManager; | ||
|
||
constructor(IPoolManager _poolManager) { | ||
poolManager = _poolManager; | ||
validateHookPermissions(this); | ||
} | ||
|
||
/// @dev Only the pool manager may call this function | ||
modifier poolManagerOnly() { | ||
if (msg.sender != address(poolManager)) revert NotPoolManager(); | ||
_; | ||
} | ||
|
||
/// @dev Only this address may call this function | ||
modifier selfOnly() { | ||
if (msg.sender != address(this)) revert NotSelf(); | ||
_; | ||
} | ||
|
||
/// @dev Only pools with hooks set to this contract may call this function | ||
modifier onlyValidPools(IHooks hooks) { | ||
if (hooks != this) revert InvalidPool(); | ||
_; | ||
} | ||
|
||
function getHookPermissions() public pure virtual returns (Hooks.Permissions memory); | ||
|
||
// this function is virtual so that we can override it during testing, | ||
// which allows us to deploy an implementation to any address | ||
// and then etch the bytecode into the correct address | ||
function validateHookPermissions(BaseHook _this) internal pure virtual { | ||
Hooks.validateHookPermissions(_this, getHookPermissions()); | ||
} | ||
|
||
function lockAcquired(bytes calldata data) external virtual poolManagerOnly returns (bytes memory) { | ||
(bool success, bytes memory returnData) = address(this).call(data); | ||
if (success) return returnData; | ||
if (returnData.length == 0) revert LockFailure(); | ||
// if the call failed, bubble up the reason | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
revert(add(returnData, 32), mload(returnData)) | ||
} | ||
} | ||
|
||
function beforeInitialize(address, PoolKey calldata, uint160, bytes calldata) external virtual returns (bytes4) { | ||
revert HookNotImplemented(); | ||
} | ||
|
||
function afterInitialize(address, PoolKey calldata, uint160, int24, bytes calldata) | ||
external | ||
virtual | ||
returns (bytes4) | ||
{ | ||
revert HookNotImplemented(); | ||
} | ||
|
||
function beforeModifyPosition(address, PoolKey calldata, IPoolManager.ModifyPositionParams calldata, bytes calldata) | ||
external | ||
virtual | ||
returns (bytes4) | ||
{ | ||
revert HookNotImplemented(); | ||
} | ||
|
||
function afterModifyPosition( | ||
address, | ||
PoolKey calldata, | ||
IPoolManager.ModifyPositionParams calldata, | ||
BalanceDelta, | ||
bytes calldata | ||
) external virtual returns (bytes4) { | ||
revert HookNotImplemented(); | ||
} | ||
|
||
function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) | ||
external | ||
virtual | ||
returns (bytes4) | ||
{ | ||
revert HookNotImplemented(); | ||
} | ||
|
||
function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) | ||
external | ||
virtual | ||
returns (bytes4) | ||
{ | ||
revert HookNotImplemented(); | ||
} | ||
|
||
function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) | ||
external | ||
virtual | ||
returns (bytes4) | ||
{ | ||
revert HookNotImplemented(); | ||
} | ||
|
||
function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) | ||
external | ||
virtual | ||
returns (bytes4) | ||
{ | ||
revert HookNotImplemented(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.19; | ||
|
||
import "forge-std/Test.sol"; | ||
import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; | ||
import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; | ||
import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; | ||
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; | ||
import {Constants} from "@uniswap/v4-core/contracts/../test/utils/Constants.sol"; | ||
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; | ||
import {HookTest} from "../utils/HookTest.sol"; | ||
import {FixedHookFee} from "../../src/examples/FixedHookFee.sol"; | ||
import {HookMiner} from "../utils/HookMiner.sol"; | ||
|
||
contract FixedHookFeeTest is HookTest { | ||
using PoolIdLibrary for PoolKey; | ||
using CurrencyLibrary for Currency; | ||
|
||
FixedHookFee hook; | ||
PoolKey poolKey; | ||
PoolId poolId; | ||
|
||
address alice = makeAddr("alice"); | ||
|
||
function setUp() public { | ||
// creates the pool manager, test tokens, and other utility routers | ||
HookTest.initHookTestEnv(); | ||
|
||
// Deploy the hook to an address with the correct flags | ||
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.ACCESS_LOCK_FLAG); | ||
(address hookAddress, bytes32 salt) = | ||
HookMiner.find(address(this), flags, type(FixedHookFee).creationCode, abi.encode(address(manager))); | ||
hook = new FixedHookFee{salt: salt}(IPoolManager(address(manager))); | ||
require(address(hook) == hookAddress, "FixedHookFeeTest: hook address mismatch"); | ||
|
||
// Create the pool | ||
poolKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, IHooks(hook)); | ||
poolId = poolKey.toId(); | ||
initializeRouter.initialize(poolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES); | ||
|
||
// Provide liquidity to the pool | ||
modifyPositionRouter.modifyPosition(poolKey, IPoolManager.ModifyPositionParams(-60, 60, 10 ether), ZERO_BYTES); | ||
modifyPositionRouter.modifyPosition(poolKey, IPoolManager.ModifyPositionParams(-120, 120, 10 ether), ZERO_BYTES); | ||
modifyPositionRouter.modifyPosition( | ||
poolKey, | ||
IPoolManager.ModifyPositionParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10_000 ether), | ||
ZERO_BYTES | ||
); | ||
} | ||
|
||
function test_hookFee() public { | ||
uint256 balanceBefore = token0.balanceOf(address(this)); | ||
// Perform a test swap // | ||
int256 amount = 1e18; | ||
bool zeroForOne = true; | ||
swap(poolKey, amount, zeroForOne, ZERO_BYTES); | ||
// ------------------- // | ||
uint256 balanceAfter = token0.balanceOf(address(this)); | ||
|
||
// swapper paid for the fixed hook fee | ||
assertEq(balanceBefore - balanceAfter, uint256(amount) + hook.FIXED_HOOK_FEE()); | ||
|
||
// collect the hook fees | ||
assertEq(token0.balanceOf(alice), 0); | ||
hook.collectFee(alice, Currency.wrap(address(token0))); | ||
assertEq(token0.balanceOf(alice), hook.FIXED_HOOK_FEE()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.