From 341cd003730a00b94d58dfed6c26777ce8ce613d Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 1 Dec 2023 16:36:40 -0500 Subject: [PATCH 1/4] update v4-core; fix compatibility --- contracts/lib/v4-core | 2 +- contracts/src/Counter.sol | 10 ++- contracts/src/examples/NoOpSwap.sol | 9 +- contracts/src/forks/BaseHook.sol | 130 ++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 contracts/src/forks/BaseHook.sol diff --git a/contracts/lib/v4-core b/contracts/lib/v4-core index 06564d33..6b8ec7c6 160000 --- a/contracts/lib/v4-core +++ b/contracts/lib/v4-core @@ -1 +1 @@ -Subproject commit 06564d33b2fa6095830c914461ee64d34d39c305 +Subproject commit 6b8ec7c6dff9a2bef4ff408f886b4cc1737179ae diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol index 0bc6d19c..532c5183 100644 --- a/contracts/src/Counter.sol +++ b/contracts/src/Counter.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {BaseHook} from "v4-periphery/BaseHook.sol"; +// 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"; @@ -25,8 +26,8 @@ contract Counter is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: true, @@ -35,7 +36,8 @@ contract Counter is BaseHook { afterSwap: true, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }); } diff --git a/contracts/src/examples/NoOpSwap.sol b/contracts/src/examples/NoOpSwap.sol index ed4f4fed..447cb2d1 100644 --- a/contracts/src/examples/NoOpSwap.sol +++ b/contracts/src/examples/NoOpSwap.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {BaseHook} from "v4-periphery/BaseHook.sol"; +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"; @@ -22,8 +22,8 @@ contract NoOpSwap is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -32,7 +32,8 @@ contract NoOpSwap is BaseHook { afterSwap: true, beforeDonate: false, afterDonate: false, - noOp: true + noOp: true, + accessLock: false }); } diff --git a/contracts/src/forks/BaseHook.sol b/contracts/src/forks/BaseHook.sol new file mode 100644 index 00000000..5ef2fce9 --- /dev/null +++ b/contracts/src/forks/BaseHook.sol @@ -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(); + } +} From 6cb7ccfba0e1411c7937408959694744af2d2100 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 3 Dec 2023 11:55:19 -0500 Subject: [PATCH 2/4] example contracts of hook fees and collecting them --- contracts/src/examples/FixedHookFee.sol | 72 ++++++++++++++++++++++ contracts/test/examples/FixedHookFee.t.sol | 71 +++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 contracts/src/examples/FixedHookFee.sol create mode 100644 contracts/test/examples/FixedHookFee.t.sol diff --git a/contracts/src/examples/FixedHookFee.sol b/contracts/src/examples/FixedHookFee.sol new file mode 100644 index 00000000..2a2408a8 --- /dev/null +++ b/contracts/src/examples/FixedHookFee.sol @@ -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 + }); + } + + 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); + } +} diff --git a/contracts/test/examples/FixedHookFee.t.sol b/contracts/test/examples/FixedHookFee.t.sol new file mode 100644 index 00000000..1d3a3bf1 --- /dev/null +++ b/contracts/test/examples/FixedHookFee.t.sol @@ -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()); + } +} From 98f9b5a9ce98b859e7e2051cefe61a800c3775ac Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 3 Dec 2023 12:29:41 -0500 Subject: [PATCH 3/4] updated snippet for Permissions verbiage --- src/pages/hooks/no-op/NoOpSwap.sol | 5 +++-- src/pages/hooks/no-op/index.html.ts | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pages/hooks/no-op/NoOpSwap.sol b/src/pages/hooks/no-op/NoOpSwap.sol index 019ce8da..62057fd6 100644 --- a/src/pages/hooks/no-op/NoOpSwap.sol +++ b/src/pages/hooks/no-op/NoOpSwap.sol @@ -10,13 +10,14 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol contract NoOpSwap is BaseHook { using PoolIdLibrary for PoolKey; + mapping(PoolId => uint256 count) public beforeSwapCount; mapping(PoolId => uint256 count) public afterSwapCount; constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, diff --git a/src/pages/hooks/no-op/index.html.ts b/src/pages/hooks/no-op/index.html.ts index 859ff8b8..8a9361be 100644 --- a/src/pages/hooks/no-op/index.html.ts +++ b/src/pages/hooks/no-op/index.html.ts @@ -20,7 +20,7 @@ export const codes = [ }, { fileName: "NoOpSwap.sol", - code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yMDsKCmltcG9ydCB7QmFzZUhvb2t9IGZyb20gInY0LXBlcmlwaGVyeS9CYXNlSG9vay5zb2wiOwoKaW1wb3J0IHtIb29rc30gZnJvbSAiQHVuaXN3YXAvdjQtY29yZS9jb250cmFjdHMvbGlicmFyaWVzL0hvb2tzLnNvbCI7CmltcG9ydCB7SVBvb2xNYW5hZ2VyfSBmcm9tICJAdW5pc3dhcC92NC1jb3JlL2NvbnRyYWN0cy9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gIkB1bmlzd2FwL3Y0LWNvcmUvY29udHJhY3RzL3R5cGVzL1Bvb2xLZXkuc29sIjsKaW1wb3J0IHtQb29sSWQsIFBvb2xJZExpYnJhcnl9IGZyb20gIkB1bmlzd2FwL3Y0LWNvcmUvY29udHJhY3RzL3R5cGVzL1Bvb2xJZC5zb2wiOwoKY29udHJhY3QgTm9PcFN3YXAgaXMgQmFzZUhvb2sgewogICAgdXNpbmcgUG9vbElkTGlicmFyeSBmb3IgUG9vbEtleTsKICAgIG1hcHBpbmcoUG9vbElkID0+IHVpbnQyNTYgY291bnQpIHB1YmxpYyBiZWZvcmVTd2FwQ291bnQ7CiAgICBtYXBwaW5nKFBvb2xJZCA9PiB1aW50MjU2IGNvdW50KSBwdWJsaWMgYWZ0ZXJTd2FwQ291bnQ7CgogICAgY29uc3RydWN0b3IoSVBvb2xNYW5hZ2VyIF9wb29sTWFuYWdlcikgQmFzZUhvb2soX3Bvb2xNYW5hZ2VyKSB7fQoKICAgIGZ1bmN0aW9uIGdldEhvb2tzQ2FsbHMoKSBwdWJsaWMgcHVyZSBvdmVycmlkZSByZXR1cm5zIChIb29rcy5DYWxscyBtZW1vcnkpIHsKICAgICAgICByZXR1cm4gSG9va3MuQ2FsbHMoewogICAgICAgICAgICBiZWZvcmVJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlTW9kaWZ5UG9zaXRpb246IGZhbHNlLAogICAgICAgICAgICBhZnRlck1vZGlmeVBvc2l0aW9uOiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlU3dhcDogdHJ1ZSwgLy8gLS0gTm8tb3AnaW5nIHRoZSBzd2FwIC0tICAvLwogICAgICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckRvbmF0ZTogZmFsc2UsCiAgICAgICAgICAgIG5vT3A6IHRydWUgLy8gLS0gRU5BQkxFIE5PLU9QIC0tICAvLwogICAgICAgIH0pOwogICAgfQoKICAgIGZ1bmN0aW9uIGJlZm9yZVN3YXAoYWRkcmVzcywgUG9vbEtleSBjYWxsZGF0YSBrZXksIElQb29sTWFuYWdlci5Td2FwUGFyYW1zIGNhbGxkYXRhIHBhcmFtcywgYnl0ZXMgY2FsbGRhdGEpCiAgICAgICAgZXh0ZXJuYWwKICAgICAgICBvdmVycmlkZQogICAgICAgIHJldHVybnMgKGJ5dGVzNCkKICAgIHsKICAgICAgICAvLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIC8vCiAgICAgICAgLy8gRXhhbXBsZSBOb09wOiBpZiBzd2FwIGFtb3VudCBpcyA2OWUxOCwgdGhlbiB0aGUgc3dhcCB3aWxsIGJlIHNraXBwZWQgICAgICAgICAgICAvLwogICAgICAgIC8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gLy8KICAgICAgICBpZiAocGFyYW1zLmFtb3VudFNwZWNpZmllZCA9PSA2OWUxOCkgcmV0dXJuIEhvb2tzLk5PX09QX1NFTEVDVE9SOwoKICAgICAgICBiZWZvcmVTd2FwQ291bnRba2V5LnRvSWQoKV0rKzsKICAgICAgICByZXR1cm4gQmFzZUhvb2suYmVmb3JlU3dhcC5zZWxlY3RvcjsKICAgIH0KfQo=", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yMDsKCmltcG9ydCB7QmFzZUhvb2t9IGZyb20gInY0LXBlcmlwaGVyeS9CYXNlSG9vay5zb2wiOwoKaW1wb3J0IHtIb29rc30gZnJvbSAiQHVuaXN3YXAvdjQtY29yZS9jb250cmFjdHMvbGlicmFyaWVzL0hvb2tzLnNvbCI7CmltcG9ydCB7SVBvb2xNYW5hZ2VyfSBmcm9tICJAdW5pc3dhcC92NC1jb3JlL2NvbnRyYWN0cy9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gIkB1bmlzd2FwL3Y0LWNvcmUvY29udHJhY3RzL3R5cGVzL1Bvb2xLZXkuc29sIjsKaW1wb3J0IHtQb29sSWQsIFBvb2xJZExpYnJhcnl9IGZyb20gIkB1bmlzd2FwL3Y0LWNvcmUvY29udHJhY3RzL3R5cGVzL1Bvb2xJZC5zb2wiOwoKY29udHJhY3QgTm9PcFN3YXAgaXMgQmFzZUhvb2sgewogICAgdXNpbmcgUG9vbElkTGlicmFyeSBmb3IgUG9vbEtleTsKCiAgICBtYXBwaW5nKFBvb2xJZCA9PiB1aW50MjU2IGNvdW50KSBwdWJsaWMgYmVmb3JlU3dhcENvdW50OwogICAgbWFwcGluZyhQb29sSWQgPT4gdWludDI1NiBjb3VudCkgcHVibGljIGFmdGVyU3dhcENvdW50OwoKICAgIGNvbnN0cnVjdG9yKElQb29sTWFuYWdlciBfcG9vbE1hbmFnZXIpIEJhc2VIb29rKF9wb29sTWFuYWdlcikge30KCiAgICBmdW5jdGlvbiBnZXRIb29rUGVybWlzc2lvbnMoKSBwdWJsaWMgcHVyZSBvdmVycmlkZSByZXR1cm5zIChIb29rcy5QZXJtaXNzaW9ucyBtZW1vcnkpIHsKICAgICAgICByZXR1cm4gSG9va3MuUGVybWlzc2lvbnMoewogICAgICAgICAgICBiZWZvcmVJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlTW9kaWZ5UG9zaXRpb246IGZhbHNlLAogICAgICAgICAgICBhZnRlck1vZGlmeVBvc2l0aW9uOiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlU3dhcDogdHJ1ZSwgLy8gLS0gTm8tb3AnaW5nIHRoZSBzd2FwIC0tICAvLwogICAgICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckRvbmF0ZTogZmFsc2UsCiAgICAgICAgICAgIG5vT3A6IHRydWUgLy8gLS0gRU5BQkxFIE5PLU9QIC0tICAvLwogICAgICAgIH0pOwogICAgfQoKICAgIGZ1bmN0aW9uIGJlZm9yZVN3YXAoYWRkcmVzcywgUG9vbEtleSBjYWxsZGF0YSBrZXksIElQb29sTWFuYWdlci5Td2FwUGFyYW1zIGNhbGxkYXRhIHBhcmFtcywgYnl0ZXMgY2FsbGRhdGEpCiAgICAgICAgZXh0ZXJuYWwKICAgICAgICBvdmVycmlkZQogICAgICAgIHJldHVybnMgKGJ5dGVzNCkKICAgIHsKICAgICAgICAvLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIC8vCiAgICAgICAgLy8gRXhhbXBsZSBOb09wOiBpZiBzd2FwIGFtb3VudCBpcyA2OWUxOCwgdGhlbiB0aGUgc3dhcCB3aWxsIGJlIHNraXBwZWQgICAgICAgICAgICAvLwogICAgICAgIC8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gLy8KICAgICAgICBpZiAocGFyYW1zLmFtb3VudFNwZWNpZmllZCA9PSA2OWUxOCkgcmV0dXJuIEhvb2tzLk5PX09QX1NFTEVDVE9SOwoKICAgICAgICBiZWZvcmVTd2FwQ291bnRba2V5LnRvSWQoKV0rKzsKICAgICAgICByZXR1cm4gQmFzZUhvb2suYmVmb3JlU3dhcC5zZWxlY3RvcjsKICAgIH0KfQo=", }, { fileName: "SetNoOpPermission.sol", @@ -57,13 +57,14 @@ const html = `
    contract NoOpSwap is BaseHook { using PoolIdLibrary for PoolKey; + mapping(PoolId => uint256 count) public beforeSwapCount; mapping(PoolId => uint256 count) public afterSwapCount; constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, From abca37f003889ed6b033091157f91fc478718491 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 3 Dec 2023 12:29:52 -0500 Subject: [PATCH 4/4] static hook fee example --- contracts/src/examples/FixedHookFee.sol | 4 +- src/keywords.json | 7 + src/nav.ts | 21 +-- .../EnableAccessLock.solsnippet | 16 +++ .../fees/fixed-hook-fee/FixedHookFee.sol | 64 +++++++++ .../SetAccessLockPermission.solsnippet | 7 + src/pages/fees/fixed-hook-fee/index.html.ts | 132 ++++++++++++++++++ src/pages/fees/fixed-hook-fee/index.md | 41 ++++++ src/pages/fees/fixed-hook-fee/index.tsx | 29 ++++ src/routes.tsx | 5 + src/search.json | 15 +- 11 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 src/pages/fees/fixed-hook-fee/EnableAccessLock.solsnippet create mode 100644 src/pages/fees/fixed-hook-fee/FixedHookFee.sol create mode 100644 src/pages/fees/fixed-hook-fee/SetAccessLockPermission.solsnippet create mode 100644 src/pages/fees/fixed-hook-fee/index.html.ts create mode 100644 src/pages/fees/fixed-hook-fee/index.md create mode 100644 src/pages/fees/fixed-hook-fee/index.tsx diff --git a/contracts/src/examples/FixedHookFee.sol b/contracts/src/examples/FixedHookFee.sol index 2a2408a8..5a51e328 100644 --- a/contracts/src/examples/FixedHookFee.sol +++ b/contracts/src/examples/FixedHookFee.sol @@ -30,7 +30,7 @@ contract FixedHookFee is BaseHook { beforeDonate: false, afterDonate: false, noOp: false, - accessLock: true + accessLock: true // -- Required to take a fee -- // }); } @@ -60,7 +60,7 @@ contract FixedHookFee is BaseHook { ); } - /// @dev requires the lock pattern in order to call `poolManager.burn` + /// @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); diff --git a/src/keywords.json b/src/keywords.json index fa67b2bb..ff115759 100644 --- a/src/keywords.json +++ b/src/keywords.json @@ -21,6 +21,13 @@ "swap", "skip swap" ], + "/fees/fixed-hook-fee": [ + "hook", + "hooks", + "fee", + "static fee", + "hook fee" + ], "/create-liquidity": [ "liquidity", "LP", diff --git a/src/nav.ts b/src/nav.ts index f88f2963..9b07e68d 100644 --- a/src/nav.ts +++ b/src/nav.ts @@ -32,7 +32,12 @@ export const HOOK_ROUTES: Route[] = [ } ] -const HACK_ROUTES: Route[] = [] +const FEE_ROUTES: Route[] = [ + { + path: "fixed-hook-fee", + title: "Static Hook Fee" + } +] export const TEST_ROUTES: Route[] = [] @@ -53,13 +58,13 @@ export const ROUTES_BY_CATEGORY = [ path: `/hooks/${route.path}`, })), }, - // { - // title: "Hacks", - // routes: HACK_ROUTES.map((route) => ({ - // ...route, - // path: `/hacks/${route.path}`, - // })), - // }, + { + title: "Fees", + routes: FEE_ROUTES.map((route) => ({ + ...route, + path: `/fees/${route.path}`, + })), + }, // { // title: "Tests", // routes: TEST_ROUTES.map((route) => ({ diff --git a/src/pages/fees/fixed-hook-fee/EnableAccessLock.solsnippet b/src/pages/fees/fixed-hook-fee/EnableAccessLock.solsnippet new file mode 100644 index 00000000..60489d7a --- /dev/null +++ b/src/pages/fees/fixed-hook-fee/EnableAccessLock.solsnippet @@ -0,0 +1,16 @@ +import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; + +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 // -- ENABLE ACCESS LOCK -- // + }); +} \ No newline at end of file diff --git a/src/pages/fees/fixed-hook-fee/FixedHookFee.sol b/src/pages/fees/fixed-hook-fee/FixedHookFee.sol new file mode 100644 index 00000000..64a86779 --- /dev/null +++ b/src/pages/fees/fixed-hook-fee/FixedHookFee.sol @@ -0,0 +1,64 @@ +// 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); + } +} diff --git a/src/pages/fees/fixed-hook-fee/SetAccessLockPermission.solsnippet b/src/pages/fees/fixed-hook-fee/SetAccessLockPermission.solsnippet new file mode 100644 index 00000000..087d5d18 --- /dev/null +++ b/src/pages/fees/fixed-hook-fee/SetAccessLockPermission.solsnippet @@ -0,0 +1,7 @@ +// Hook can take a fee via ACCESS_LOCK +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))); diff --git a/src/pages/fees/fixed-hook-fee/index.html.ts b/src/pages/fees/fixed-hook-fee/index.html.ts new file mode 100644 index 00000000..4a1dd0dc --- /dev/null +++ b/src/pages/fees/fixed-hook-fee/index.html.ts @@ -0,0 +1,132 @@ +// metadata +export const version = "0.8.20" +export const title = "Static Hook Fee" +export const description = "Charge a static hook fee" + +export const keywords = [ + "hook", + "hooks", + "fee", + "static fee", + "hook fee", +] + +export const codes = [ + { + fileName: "EnableAccessLock.sol", + code: "aW1wb3J0IHtIb29rc30gZnJvbSAiQHVuaXN3YXAvdjQtY29yZS9jb250cmFjdHMvbGlicmFyaWVzL0hvb2tzLnNvbCI7CgpmdW5jdGlvbiBnZXRIb29rUGVybWlzc2lvbnMoKSBwdWJsaWMgcHVyZSBvdmVycmlkZSByZXR1cm5zIChIb29rcy5QZXJtaXNzaW9ucyBtZW1vcnkpIHsKICAgIHJldHVybiBIb29rcy5QZXJtaXNzaW9ucyh7CiAgICAgICAgYmVmb3JlSW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgYWZ0ZXJJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICBiZWZvcmVNb2RpZnlQb3NpdGlvbjogZmFsc2UsCiAgICAgICAgYWZ0ZXJNb2RpZnlQb3NpdGlvbjogZmFsc2UsCiAgICAgICAgYmVmb3JlU3dhcDogdHJ1ZSwKICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgIGJlZm9yZURvbmF0ZTogZmFsc2UsCiAgICAgICAgYWZ0ZXJEb25hdGU6IGZhbHNlLAogICAgICAgIG5vT3A6IGZhbHNlLAogICAgICAgIGFjY2Vzc0xvY2s6IHRydWUgLy8gLS0gRU5BQkxFIEFDQ0VTUyBMT0NLIC0tICAvLwogICAgfSk7Cn0=", + }, + { + fileName: "FixedHookFee.sol", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4xOTsKCi8vIFRPRE86IHVwZGF0ZSB0byB2NC1wZXJpcGhlcnkvQmFzZUhvb2suc29sIHdoZW4gaXRzIGNvbXBhdGlibGUKaW1wb3J0IHtCYXNlSG9va30gZnJvbSAiLi4vZm9ya3MvQmFzZUhvb2suc29sIjsKCmltcG9ydCB7SG9va3N9IGZyb20gIkB1bmlzd2FwL3Y0LWNvcmUvY29udHJhY3RzL2xpYnJhcmllcy9Ib29rcy5zb2wiOwppbXBvcnQge0lQb29sTWFuYWdlcn0gZnJvbSAiQHVuaXN3YXAvdjQtY29yZS9jb250cmFjdHMvaW50ZXJmYWNlcy9JUG9vbE1hbmFnZXIuc29sIjsKaW1wb3J0IHtQb29sS2V5fSBmcm9tICJAdW5pc3dhcC92NC1jb3JlL2NvbnRyYWN0cy90eXBlcy9Qb29sS2V5LnNvbCI7CmltcG9ydCB7UG9vbElkLCBQb29sSWRMaWJyYXJ5fSBmcm9tICJAdW5pc3dhcC92NC1jb3JlL2NvbnRyYWN0cy90eXBlcy9Qb29sSWQuc29sIjsKaW1wb3J0IHtCYWxhbmNlRGVsdGF9IGZyb20gIkB1bmlzd2FwL3Y0LWNvcmUvY29udHJhY3RzL3R5cGVzL0JhbGFuY2VEZWx0YS5zb2wiOwppbXBvcnQge0N1cnJlbmN5LCBDdXJyZW5jeUxpYnJhcnl9IGZyb20gIkB1bmlzd2FwL3Y0LWNvcmUvY29udHJhY3RzL3R5cGVzL0N1cnJlbmN5LnNvbCI7Cgpjb250cmFjdCBGaXhlZEhvb2tGZWUgaXMgQmFzZUhvb2sgewogICAgdXNpbmcgUG9vbElkTGlicmFyeSBmb3IgUG9vbEtleTsKICAgIHVzaW5nIEN1cnJlbmN5TGlicmFyeSBmb3IgQ3VycmVuY3k7CgogICAgdWludDI1NiBwdWJsaWMgY29uc3RhbnQgRklYRURfSE9PS19GRUUgPSAwLjAwMDFlMTg7CgogICAgY29uc3RydWN0b3IoSVBvb2xNYW5hZ2VyIF9wb29sTWFuYWdlcikgQmFzZUhvb2soX3Bvb2xNYW5hZ2VyKSB7fQoKICAgIGZ1bmN0aW9uIGdldEhvb2tQZXJtaXNzaW9ucygpIHB1YmxpYyBwdXJlIG92ZXJyaWRlIHJldHVybnMgKEhvb2tzLlBlcm1pc3Npb25zIG1lbW9yeSkgewogICAgICAgIHJldHVybiBIb29rcy5QZXJtaXNzaW9ucyh7CiAgICAgICAgICAgIGJlZm9yZUluaXRpYWxpemU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckluaXRpYWxpemU6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVNb2RpZnlQb3NpdGlvbjogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyTW9kaWZ5UG9zaXRpb246IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVTd2FwOiB0cnVlLAogICAgICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckRvbmF0ZTogZmFsc2UsCiAgICAgICAgICAgIG5vT3A6IGZhbHNlLAogICAgICAgICAgICBhY2Nlc3NMb2NrOiB0cnVlIC8vIC0tIFJlcXVpcmVkIHRvIHRha2UgYSBmZWUgLS0gLy8KICAgICAgICB9KTsKICAgIH0KCiAgICBmdW5jdGlvbiBiZWZvcmVTd2FwKGFkZHJlc3MsIFBvb2xLZXkgY2FsbGRhdGEga2V5LCBJUG9vbE1hbmFnZXIuU3dhcFBhcmFtcyBjYWxsZGF0YSBwYXJhbXMsIGJ5dGVzIGNhbGxkYXRhKQogICAgICAgIGV4dGVybmFsCiAgICAgICAgb3ZlcnJpZGUKICAgICAgICByZXR1cm5zIChieXRlczQpCiAgICB7CiAgICAgICAgLy8gdGFrZSBhIGZpeGVkIGZlZSBvZiAwLjAwMDEgb2YgdGhlIGlucHV0IHRva2VuCiAgICAgICAgcGFyYW1zLnplcm9Gb3JPbmUKICAgICAgICAgICAgPyBwb29sTWFuYWdlci5taW50KGtleS5jdXJyZW5jeTAsIGFkZHJlc3ModGhpcyksIEZJWEVEX0hPT0tfRkVFKQogICAgICAgICAgICA6IHBvb2xNYW5hZ2VyLm1pbnQoa2V5LmN1cnJlbmN5MSwgYWRkcmVzcyh0aGlzKSwgRklYRURfSE9PS19GRUUpOwoKICAgICAgICByZXR1cm4gQmFzZUhvb2suYmVmb3JlU3dhcC5zZWxlY3RvcjsKICAgIH0KCiAgICAvLy8gQGRldiBIb29rIGZlZXMgYXJlIGtlcHQgYXMgUG9vbE1hbmFnZXIgY2xhaW1zLCBzbyBjb2xsZWN0aW5nIEVSQzIwcyB3aWxsIHJlcXVpcmUgbG9ja2luZwogICAgZnVuY3Rpb24gY29sbGVjdEZlZShhZGRyZXNzIHJlY2lwaWVudCwgQ3VycmVuY3kgY3VycmVuY3kpIGV4dGVybmFsIHJldHVybnMgKHVpbnQyNTYgYW1vdW50KSB7CiAgICAgICAgYW1vdW50ID0gYWJpLmRlY29kZShwb29sTWFuYWdlci5sb2NrKGFiaS5lbmNvZGVDYWxsKHRoaXMuaGFuZGxlQ29sbGVjdEZlZSwgKHJlY2lwaWVudCwgY3VycmVuY3kpKSksICh1aW50MjU2KSk7CiAgICB9CgogICAgLy8vIEBkZXYgcmVxdWlyZXMgdGhlIGxvY2sgcGF0dGVybiBpbiBvcmRlciB0byBjYWxsIHBvb2xNYW5hZ2VyLmJ1cm4KICAgIGZ1bmN0aW9uIGhhbmRsZUNvbGxlY3RGZWUoYWRkcmVzcyByZWNpcGllbnQsIEN1cnJlbmN5IGN1cnJlbmN5KSBleHRlcm5hbCByZXR1cm5zICh1aW50MjU2IGFtb3VudCkgewogICAgICAgIC8vIGNvbnZlcnQgdGhlIGZlZSAoQ2xhaW1zKSBpbnRvIEVSQzIwIHRva2VucwogICAgICAgIGFtb3VudCA9IHBvb2xNYW5hZ2VyLmJhbGFuY2VPZihhZGRyZXNzKHRoaXMpLCBjdXJyZW5jeSk7CiAgICAgICAgcG9vbE1hbmFnZXIuYnVybihjdXJyZW5jeSwgYW1vdW50KTsKCiAgICAgICAgLy8gZGlyZWN0IGNsYWltcyAodGhlIHRva2VucykgdG8gdGhlIHJlY2lwaWVudAogICAgICAgIHBvb2xNYW5hZ2VyLnRha2UoY3VycmVuY3ksIHJlY2lwaWVudCwgYW1vdW50KTsKICAgIH0KfQo=", + }, + { + fileName: "SetAccessLockPermission.sol", + code: "Ly8gSG9vayBjYW4gdGFrZSBhIGZlZSB2aWEgQUNDRVNTX0xPQ0sKdWludDE2MCBmbGFncyA9IHVpbnQxNjAoSG9va3MuQkVGT1JFX1NXQVBfRkxBRyB8IEhvb2tzLkFDQ0VTU19MT0NLX0ZMQUcpOwoKKGFkZHJlc3MgaG9va0FkZHJlc3MsIGJ5dGVzMzIgc2FsdCkgPQogICAgSG9va01pbmVyLmZpbmQoYWRkcmVzcyh0aGlzKSwgZmxhZ3MsIHR5cGUoRml4ZWRIb29rRmVlKS5jcmVhdGlvbkNvZGUsIGFiaS5lbmNvZGUoYWRkcmVzcyhtYW5hZ2VyKSkpOwoKaG9vayA9IG5ldyBGaXhlZEhvb2tGZWV7c2FsdDogc2FsdH0oSVBvb2xNYW5hZ2VyKGFkZHJlc3MobWFuYWdlcikpKTsK", + }, +] + +const html = `
      +
    • Charge a static hook fee
    • +
    +

    Optional hook fees are taken (from swappers) via the Access Lock. Hook fees can be dynamically calculated, or simply set to a fixed amount.

    +

    This example showcases a static fee amount

    +
    +

    Example Static Hook Fee

    +

    FIXED_HOOK_FEE = 0.0001e18

    +

    The hook fee is on the input token. If swapAmount = 1e18, the swapper pays 1.0001e18

    +
    // 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);
    +    }
    +}
    +

    Collecting the fee, to recipient alice

    +
    hook.collectFee(address(alice), Currency.wrap(address(TOKEN)));
    +

    Enabling Access Lock

    +

    To allow a hook to call poolManager.mint (fee taking), without a lock, you need to enable the ACCESS_LOCK permission

    +
    import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol";
    +
    +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 // -- ENABLE ACCESS LOCK --  //
    +    });
    +}
    +

    Example permissions during hook deployment:

    +
    // Hook can take a fee via ACCESS_LOCK
    +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)));
    +
    ` + +export default html diff --git a/src/pages/fees/fixed-hook-fee/index.md b/src/pages/fees/fixed-hook-fee/index.md new file mode 100644 index 00000000..4672e20a --- /dev/null +++ b/src/pages/fees/fixed-hook-fee/index.md @@ -0,0 +1,41 @@ +--- +title: Static Hook Fee +version: 0.8.20 +description: Charge a static hook fee +keywords: [hook, hooks, fee, static fee, hook fee] +--- + +- Charge a static hook fee + +Optional hook fees are taken (from swappers) via the Access Lock. Hook fees can be dynamically calculated, or simply set to a fixed amount. + +This example showcases a static fee amount + +--- +## Example Static Hook Fee + +`FIXED_HOOK_FEE = 0.0001e18` + +The hook fee is on the *input* token. If `swapAmount = 1e18`, the swapper pays `1.0001e18` + +```solidity +{{{FixedHookFee}}} +``` + +Collecting the fee, to recipient `alice` +```solidity +hook.collectFee(address(alice), Currency.wrap(address(TOKEN))); +``` + +#### Enabling Access Lock + +To allow a hook to call `poolManager.mint` (fee taking), without a lock, you need to enable the `ACCESS_LOCK` permission + +```solidity +{{{EnableAccessLock}}} +``` + +Example permissions during hook deployment: +```solidity +{{{SetAccessLockPermission}}} +``` diff --git a/src/pages/fees/fixed-hook-fee/index.tsx b/src/pages/fees/fixed-hook-fee/index.tsx new file mode 100644 index 00000000..1c63a4f4 --- /dev/null +++ b/src/pages/fees/fixed-hook-fee/index.tsx @@ -0,0 +1,29 @@ +import React from "react" +import Example from "../../../components/Example" +import html, { version, title, description, codes } from "./index.html" + +interface Path { + path: string + title: string +} + +interface Props { + prev: Path | null + next: Path | null +} + +const ExamplePage: React.FC = ({ prev, next }) => { + return ( + + ) +} + +export default ExamplePage diff --git a/src/routes.tsx b/src/routes.tsx index c4fcb217..f5bca906 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,4 +1,5 @@ import component_create_liquidity from "./pages/create-liquidity" +import component_fees_fixed_hook_fee from "./pages/fees/fixed-hook-fee" import component_hooks_no_op from "./pages/hooks/no-op" import component_initialize from "./pages/initialize" import component_swap from "./pages/swap" @@ -25,6 +26,10 @@ const routes: Route[] = [ path: "/create-liquidity", component: component_create_liquidity }, + { + path: "/fees/fixed-hook-fee", + component: component_fees_fixed_hook_fee + }, { path: "/hooks/no-op", component: component_hooks_no_op diff --git a/src/search.json b/src/search.json index 6278a75c..eac4af14 100644 --- a/src/search.json +++ b/src/search.json @@ -28,10 +28,12 @@ "/initialize" ], "hook": [ - "/hooks/no-op" + "/hooks/no-op", + "/fees/fixed-hook-fee" ], "hooks": [ - "/hooks/no-op" + "/hooks/no-op", + "/fees/fixed-hook-fee" ], "noop": [ "/hooks/no-op" @@ -45,6 +47,15 @@ "skip swap": [ "/hooks/no-op" ], + "fee": [ + "/fees/fixed-hook-fee" + ], + "static fee": [ + "/fees/fixed-hook-fee" + ], + "hook fee": [ + "/fees/fixed-hook-fee" + ], "liquidity": [ "/create-liquidity" ],