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 = `
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
+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)));
+
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