diff --git a/forge-test/GetCurrentLockCaller.t.sol b/forge-test/GetCurrentLockCaller.t.sol new file mode 100644 index 00000000..4026279f --- /dev/null +++ b/forge-test/GetCurrentLockCaller.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {Hooks} from "v4-core/libraries/Hooks.sol"; +import {TickMath} from "v4-core/libraries/TickMath.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol"; +import {Constants} from "v4-core/../test/utils/Constants.sol"; +import {CurrencyLibrary, Currency} from "v4-core/types/Currency.sol"; +import {HookTest} from "@v4-by-example/utils/HookTest.sol"; +import {GetCurrentLockCaller} from "src/pages/hooks/msg-sender/GetCurrentLockCaller.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; + +contract GetCurrentLockCallerTest is HookTest { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + GetCurrentLockCaller counter; + 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); + (address hookAddress, bytes32 salt) = + HookMiner.find(address(this), flags, type(GetCurrentLockCaller).creationCode, abi.encode(address(manager))); + counter = new GetCurrentLockCaller{salt: salt}(IPoolManager(address(manager))); + require(address(counter) == hookAddress, "GetCurrentLockCallerTest: hook address mismatch"); + + // Create the pool + poolKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, IHooks(counter)); + poolId = poolKey.toId(); + initializeRouter.initialize(poolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES); + + // Provide liquidity to the pool + modifyPositionRouter.modifyLiquidity(poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES); + modifyPositionRouter.modifyLiquidity( + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + ); + modifyPositionRouter.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + ZERO_BYTES + ); + } + + function test_getCurrentLockCaller() public { + counter.setAllowedUser(alice, true); + token0.mint(alice, 100 ether); + + vm.startPrank(alice); + token0.approve(address(swapRouter), type(uint256).max); + + // Perform a test swap // + int256 amount = 1e18; + bool zeroForOne = true; + lockSwap(poolKey, amount, zeroForOne, ZERO_BYTES, false); + // ------------------- // + vm.stopPrank(); + } + + function test_getCurrentLockCallerRevert() public { + counter.setAllowedUser(alice, false); + token0.mint(alice, 100 ether); + + vm.startPrank(alice); + token0.approve(address(swapRouter), type(uint256).max); + + // Perform a test swap // + int256 amount = 1e18; + bool zeroForOne = true; + vm.expectRevert(); + lockSwap(poolKey, amount, zeroForOne, ZERO_BYTES, true); + // ------------------- // + vm.stopPrank(); + } + + function lockSwap( + PoolKey memory key, + int256 amountSpecified, + bool zeroForOne, + bytes memory hookData, + bool expectRevert + ) internal returns (BalanceDelta swapDelta) { + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact + }); + + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + + bytes memory result; + if (expectRevert) { + vm.expectRevert(); + result = manager.lock( + address(swapRouter), + abi.encode(PoolSwapTest.CallbackData(address(this), testSettings, key, params, hookData)) + ); + } else { + result = manager.lock( + address(swapRouter), + abi.encode(PoolSwapTest.CallbackData(address(this), testSettings, key, params, hookData)) + ); + } + + swapDelta = abi.decode(result, (BalanceDelta)); + } +} diff --git a/forge-test/MsgSenderHookData.t.sol b/forge-test/MsgSenderHookData.t.sol new file mode 100644 index 00000000..15883c27 --- /dev/null +++ b/forge-test/MsgSenderHookData.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {Hooks} from "v4-core/libraries/Hooks.sol"; +import {TickMath} from "v4-core/libraries/TickMath.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol"; +import {Constants} from "v4-core/../test/utils/Constants.sol"; +import {CurrencyLibrary, Currency} from "v4-core/types/Currency.sol"; +import {HookTest} from "@v4-by-example/utils/HookTest.sol"; +import {MsgSenderHookData} from "src/pages/hooks/msg-sender/MsgSenderHookData.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; + +contract MsgSenderHookDataTest is HookTest { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + MsgSenderHookData counter; + 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); + (address hookAddress, bytes32 salt) = + HookMiner.find(address(this), flags, type(MsgSenderHookData).creationCode, abi.encode(address(manager))); + counter = new MsgSenderHookData{salt: salt}(IPoolManager(address(manager))); + require(address(counter) == hookAddress, "MsgSenderHookDataTest: hook address mismatch"); + + // Create the pool + poolKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, IHooks(counter)); + poolId = poolKey.toId(); + initializeRouter.initialize(poolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES); + + // Provide liquidity to the pool + modifyPositionRouter.modifyLiquidity(poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES); + modifyPositionRouter.modifyLiquidity( + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + ); + modifyPositionRouter.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + ZERO_BYTES + ); + } + + function test_msgSenderHookData() public { + counter.setAllowedUser(alice, true); + token0.mint(alice, 100 ether); + + vm.startPrank(alice); + token0.approve(address(swapRouter), type(uint256).max); + + // Perform a test swap // + int256 amount = 1e18; + bool zeroForOne = true; + swap(poolKey, amount, zeroForOne, abi.encode(alice)); + // ------------------- // + vm.stopPrank(); + } + + function test_msgSenderRevert() public { + counter.setAllowedUser(alice, false); + token0.mint(alice, 100 ether); + + vm.startPrank(alice); + token0.approve(address(swapRouter), type(uint256).max); + + // Perform a test swap // + int256 amount = 1e18; + bool zeroForOne = true; + vm.expectRevert(); + swap(poolKey, amount, zeroForOne, abi.encode(alice)); + // ------------------- // + vm.stopPrank(); + } +} diff --git a/src/keywords.json b/src/keywords.json index 298e092a..b775be3a 100644 --- a/src/keywords.json +++ b/src/keywords.json @@ -30,6 +30,13 @@ "swap", "skip swap" ], + "/hooks/msg-sender": [ + "hook", + "hooks", + "msg.sender", + "msgsender", + "sender" + ], "/hooks/custom-curve": [ "hook", "hooks", diff --git a/src/nav.ts b/src/nav.ts index abb8c262..57ee4f2b 100644 --- a/src/nav.ts +++ b/src/nav.ts @@ -37,6 +37,10 @@ export const HOOK_ROUTES: Route[] = [ { path: "custom-curve", title: "Custom Curve" + }, + { + path: "msg-sender", + title: "Access msg.sender" } ] diff --git a/src/pages/hooks/msg-sender/GetCurrentLockCaller.sol b/src/pages/hooks/msg-sender/GetCurrentLockCaller.sol new file mode 100644 index 00000000..004f65ff --- /dev/null +++ b/src/pages/hooks/msg-sender/GetCurrentLockCaller.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// TODO: update to v4-periphery/BaseHook.sol when its compatible +import {BaseHook} from "@v4-by-example/utils/BaseHook.sol"; + +import {console2} from "forge-std/console2.sol"; +import {Hooks} from "v4-core/libraries/Hooks.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Lockers} from "v4-core/libraries/Lockers.sol"; + +contract GetCurrentLockCaller is BaseHook { + mapping(address user => bool allowed) public allowedUsers; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata hookData) + external + override + returns (bytes4) + { + // --- Read the user's address --- // + (, address user) = poolManager.getLock(1); + require(allowedUsers[user], "GetCurrentLockCaller: User not allowed"); + return BaseHook.beforeSwap.selector; + } + + // Helper function for demonstration + function setAllowedUser(address user, bool allowed) external { + allowedUsers[user] = allowed; + } + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + noOp: false, + accessLock: false + }); + } +} diff --git a/src/pages/hooks/msg-sender/MsgSenderHookData.sol b/src/pages/hooks/msg-sender/MsgSenderHookData.sol new file mode 100644 index 00000000..a2837142 --- /dev/null +++ b/src/pages/hooks/msg-sender/MsgSenderHookData.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// TODO: update to v4-periphery/BaseHook.sol when its compatible +import {BaseHook} from "@v4-by-example/utils/BaseHook.sol"; + +import {Hooks} from "v4-core/libraries/Hooks.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; + +contract MsgSenderHookData is BaseHook { + using PoolIdLibrary for PoolKey; + + mapping(address user => bool allowed) public allowedUsers; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata hookData) + external + override + returns (bytes4) + { + // --- Read the user's address --- // + address user = abi.decode(hookData, (address)); + require(allowedUsers[user], "MsgSenderHookData: User not allowed"); + return BaseHook.beforeSwap.selector; + } + + // Helper function for demonstration + function setAllowedUser(address user, bool allowed) external { + allowedUsers[user] = allowed; + } + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + noOp: false, + accessLock: false + }); + } +} diff --git a/src/pages/hooks/msg-sender/PoolManagerLock.solsnippet b/src/pages/hooks/msg-sender/PoolManagerLock.solsnippet new file mode 100644 index 00000000..aa567c87 --- /dev/null +++ b/src/pages/hooks/msg-sender/PoolManagerLock.solsnippet @@ -0,0 +1,7 @@ +// User directly locks on the PoolManager +// - poolManager.getLock(1) returns alice's address +vm.prank(alice); +manager.lock( + address(swapRouter), + abi.encode(PoolSwapTest.CallbackData(address(this), testSettings, key, params, hookData)) +); \ No newline at end of file diff --git a/src/pages/hooks/msg-sender/PoolSwapTestHookData.solsnippet b/src/pages/hooks/msg-sender/PoolSwapTestHookData.solsnippet new file mode 100644 index 00000000..df129a88 --- /dev/null +++ b/src/pages/hooks/msg-sender/PoolSwapTestHookData.solsnippet @@ -0,0 +1,7 @@ +IPoolManager.SwapParams memory params = ...; + +PoolSwapTest.TestSettings memory testSettings = ...; + +// provide the user's address as hookData to be available inside the hook function +bytes memory hookData = abi.encode(address(USER_ADDRESS)); +swapRouter.swap(key, params, testSettings, hookData); \ No newline at end of file diff --git a/src/pages/hooks/msg-sender/index.html.ts b/src/pages/hooks/msg-sender/index.html.ts new file mode 100644 index 00000000..f303506a --- /dev/null +++ b/src/pages/hooks/msg-sender/index.html.ts @@ -0,0 +1,176 @@ +// metadata +export const version = "0.8.20" +export const title = "Access msg.sender within a Hook" +export const description = "Access msg.sender within a hook" + +export const keywords = [ + "hook", + "hooks", + "msg.sender", + "msgsender", + "sender", +] + +export const codes = [ + { + fileName: "GetCurrentLockCaller.sol", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4xOTsKCi8vIFRPRE86IHVwZGF0ZSB0byB2NC1wZXJpcGhlcnkvQmFzZUhvb2suc29sIHdoZW4gaXRzIGNvbXBhdGlibGUKaW1wb3J0IHtCYXNlSG9va30gZnJvbSAiQHY0LWJ5LWV4YW1wbGUvdXRpbHMvQmFzZUhvb2suc29sIjsKCmltcG9ydCB7Y29uc29sZTJ9IGZyb20gImZvcmdlLXN0ZC9jb25zb2xlMi5zb2wiOwppbXBvcnQge0hvb2tzfSBmcm9tICJ2NC1jb3JlL2xpYnJhcmllcy9Ib29rcy5zb2wiOwppbXBvcnQge0lQb29sTWFuYWdlcn0gZnJvbSAidjQtY29yZS9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gInY0LWNvcmUvdHlwZXMvUG9vbEtleS5zb2wiOwppbXBvcnQge0JhbGFuY2VEZWx0YX0gZnJvbSAidjQtY29yZS90eXBlcy9CYWxhbmNlRGVsdGEuc29sIjsKaW1wb3J0IHtMb2NrZXJzfSBmcm9tICJ2NC1jb3JlL2xpYnJhcmllcy9Mb2NrZXJzLnNvbCI7Cgpjb250cmFjdCBHZXRDdXJyZW50TG9ja0NhbGxlciBpcyBCYXNlSG9vayB7CiAgICBtYXBwaW5nKGFkZHJlc3MgdXNlciA9PiBib29sIGFsbG93ZWQpIHB1YmxpYyBhbGxvd2VkVXNlcnM7CgogICAgY29uc3RydWN0b3IoSVBvb2xNYW5hZ2VyIF9wb29sTWFuYWdlcikgQmFzZUhvb2soX3Bvb2xNYW5hZ2VyKSB7fQoKICAgIGZ1bmN0aW9uIGJlZm9yZVN3YXAoYWRkcmVzcywgUG9vbEtleSBjYWxsZGF0YSBrZXksIElQb29sTWFuYWdlci5Td2FwUGFyYW1zIGNhbGxkYXRhLCBieXRlcyBjYWxsZGF0YSBob29rRGF0YSkKICAgICAgICBleHRlcm5hbAogICAgICAgIG92ZXJyaWRlCiAgICAgICAgcmV0dXJucyAoYnl0ZXM0KQogICAgewogICAgICAgIC8vIC0tLSBSZWFkIHRoZSB1c2VyJ3MgYWRkcmVzcyAtLS0gLy8KICAgICAgICAoLCBhZGRyZXNzIHVzZXIpID0gcG9vbE1hbmFnZXIuZ2V0TG9jaygxKTsKICAgICAgICByZXF1aXJlKGFsbG93ZWRVc2Vyc1t1c2VyXSwgIkdldEN1cnJlbnRMb2NrQ2FsbGVyOiBVc2VyIG5vdCBhbGxvd2VkIik7CiAgICAgICAgcmV0dXJuIEJhc2VIb29rLmJlZm9yZVN3YXAuc2VsZWN0b3I7CiAgICB9CgogICAgLy8gSGVscGVyIGZ1bmN0aW9uIGZvciBkZW1vbnN0cmF0aW9uCiAgICBmdW5jdGlvbiBzZXRBbGxvd2VkVXNlcihhZGRyZXNzIHVzZXIsIGJvb2wgYWxsb3dlZCkgZXh0ZXJuYWwgewogICAgICAgIGFsbG93ZWRVc2Vyc1t1c2VyXSA9IGFsbG93ZWQ7CiAgICB9CgogICAgZnVuY3Rpb24gZ2V0SG9va1Blcm1pc3Npb25zKCkgcHVibGljIHB1cmUgb3ZlcnJpZGUgcmV0dXJucyAoSG9va3MuUGVybWlzc2lvbnMgbWVtb3J5KSB7CiAgICAgICAgcmV0dXJuIEhvb2tzLlBlcm1pc3Npb25zKHsKICAgICAgICAgICAgYmVmb3JlSW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVySW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZUFkZExpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZVJlbW92ZUxpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyQWRkTGlxdWlkaXR5OiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJSZW1vdmVMaXF1aWRpdHk6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVTd2FwOiB0cnVlLAogICAgICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckRvbmF0ZTogZmFsc2UsCiAgICAgICAgICAgIG5vT3A6IGZhbHNlLAogICAgICAgICAgICBhY2Nlc3NMb2NrOiBmYWxzZQogICAgICAgIH0pOwogICAgfQp9Cg==", + }, + { + fileName: "MsgSenderHookData.sol", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4xOTsKCi8vIFRPRE86IHVwZGF0ZSB0byB2NC1wZXJpcGhlcnkvQmFzZUhvb2suc29sIHdoZW4gaXRzIGNvbXBhdGlibGUKaW1wb3J0IHtCYXNlSG9va30gZnJvbSAiQHY0LWJ5LWV4YW1wbGUvdXRpbHMvQmFzZUhvb2suc29sIjsKCmltcG9ydCB7SG9va3N9IGZyb20gInY0LWNvcmUvbGlicmFyaWVzL0hvb2tzLnNvbCI7CmltcG9ydCB7SVBvb2xNYW5hZ2VyfSBmcm9tICJ2NC1jb3JlL2ludGVyZmFjZXMvSVBvb2xNYW5hZ2VyLnNvbCI7CmltcG9ydCB7UG9vbEtleX0gZnJvbSAidjQtY29yZS90eXBlcy9Qb29sS2V5LnNvbCI7CmltcG9ydCB7UG9vbElkLCBQb29sSWRMaWJyYXJ5fSBmcm9tICJ2NC1jb3JlL3R5cGVzL1Bvb2xJZC5zb2wiOwppbXBvcnQge0JhbGFuY2VEZWx0YX0gZnJvbSAidjQtY29yZS90eXBlcy9CYWxhbmNlRGVsdGEuc29sIjsKCmNvbnRyYWN0IE1zZ1NlbmRlckhvb2tEYXRhIGlzIEJhc2VIb29rIHsKICAgIHVzaW5nIFBvb2xJZExpYnJhcnkgZm9yIFBvb2xLZXk7CgogICAgbWFwcGluZyhhZGRyZXNzIHVzZXIgPT4gYm9vbCBhbGxvd2VkKSBwdWJsaWMgYWxsb3dlZFVzZXJzOwoKICAgIGNvbnN0cnVjdG9yKElQb29sTWFuYWdlciBfcG9vbE1hbmFnZXIpIEJhc2VIb29rKF9wb29sTWFuYWdlcikge30KCiAgICBmdW5jdGlvbiBiZWZvcmVTd2FwKGFkZHJlc3MsIFBvb2xLZXkgY2FsbGRhdGEga2V5LCBJUG9vbE1hbmFnZXIuU3dhcFBhcmFtcyBjYWxsZGF0YSwgYnl0ZXMgY2FsbGRhdGEgaG9va0RhdGEpCiAgICAgICAgZXh0ZXJuYWwKICAgICAgICBvdmVycmlkZQogICAgICAgIHJldHVybnMgKGJ5dGVzNCkKICAgIHsKICAgICAgICAvLyAtLS0gUmVhZCB0aGUgdXNlcidzIGFkZHJlc3MgLS0tIC8vCiAgICAgICAgYWRkcmVzcyB1c2VyID0gYWJpLmRlY29kZShob29rRGF0YSwgKGFkZHJlc3MpKTsKICAgICAgICByZXF1aXJlKGFsbG93ZWRVc2Vyc1t1c2VyXSwgIk1zZ1NlbmRlckhvb2tEYXRhOiBVc2VyIG5vdCBhbGxvd2VkIik7CiAgICAgICAgcmV0dXJuIEJhc2VIb29rLmJlZm9yZVN3YXAuc2VsZWN0b3I7CiAgICB9CgogICAgLy8gSGVscGVyIGZ1bmN0aW9uIGZvciBkZW1vbnN0cmF0aW9uCiAgICBmdW5jdGlvbiBzZXRBbGxvd2VkVXNlcihhZGRyZXNzIHVzZXIsIGJvb2wgYWxsb3dlZCkgZXh0ZXJuYWwgewogICAgICAgIGFsbG93ZWRVc2Vyc1t1c2VyXSA9IGFsbG93ZWQ7CiAgICB9CgogICAgZnVuY3Rpb24gZ2V0SG9va1Blcm1pc3Npb25zKCkgcHVibGljIHB1cmUgb3ZlcnJpZGUgcmV0dXJucyAoSG9va3MuUGVybWlzc2lvbnMgbWVtb3J5KSB7CiAgICAgICAgcmV0dXJuIEhvb2tzLlBlcm1pc3Npb25zKHsKICAgICAgICAgICAgYmVmb3JlSW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVySW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZUFkZExpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZVJlbW92ZUxpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyQWRkTGlxdWlkaXR5OiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJSZW1vdmVMaXF1aWRpdHk6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVTd2FwOiB0cnVlLAogICAgICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckRvbmF0ZTogZmFsc2UsCiAgICAgICAgICAgIG5vT3A6IGZhbHNlLAogICAgICAgICAgICBhY2Nlc3NMb2NrOiBmYWxzZQogICAgICAgIH0pOwogICAgfQp9Cg==", + }, + { + fileName: "PoolManagerLock.sol", + code: "Ly8gVXNlciBkaXJlY3RseSBsb2NrcyBvbiB0aGUgUG9vbE1hbmFnZXIKLy8gLSBwb29sTWFuYWdlci5nZXRMb2NrKDEpIHJldHVybnMgYWxpY2UncyBhZGRyZXNzCnZtLnByYW5rKGFsaWNlKTsKbWFuYWdlci5sb2NrKAogICAgYWRkcmVzcyhzd2FwUm91dGVyKSwKICAgIGFiaS5lbmNvZGUoUG9vbFN3YXBUZXN0LkNhbGxiYWNrRGF0YShhZGRyZXNzKHRoaXMpLCB0ZXN0U2V0dGluZ3MsIGtleSwgcGFyYW1zLCBob29rRGF0YSkpCik7", + }, + { + fileName: "PoolSwapTestHookData.sol", + code: "SVBvb2xNYW5hZ2VyLlN3YXBQYXJhbXMgbWVtb3J5IHBhcmFtcyA9IC4uLjsKClBvb2xTd2FwVGVzdC5UZXN0U2V0dGluZ3MgbWVtb3J5IHRlc3RTZXR0aW5ncyA9IC4uLjsKCi8vIHByb3ZpZGUgdGhlIHVzZXIncyBhZGRyZXNzIGFzIGhvb2tEYXRhIHRvIGJlIGF2YWlsYWJsZSBpbnNpZGUgdGhlIGhvb2sgZnVuY3Rpb24KYnl0ZXMgbWVtb3J5IGhvb2tEYXRhID0gYWJpLmVuY29kZShhZGRyZXNzKFVTRVJfQUREUkVTUykpOwpzd2FwUm91dGVyLnN3YXAoa2V5LCBwYXJhbXMsIHRlc3RTZXR0aW5ncywgaG9va0RhdGEpOw==", + }, +] + +const html = `

Please note there are multiple solutions -- each includes different tradeoffs

+

Use-cases

+

Accessing the user's address inside a hook provides a lot of expressivity such as:

+ +

Using hookData

+

Callers (EOAs / contracts / multisigs) of periphery contracts (PoolSwapTest) can provide the user's address as the hookData argument

+ +

Get Lock Caller

+

Callers directly use PoolManager.lock to invoke a periphery router. The lock caller (i.e. EOA) is saved in transient storage and can be accessed within a hook

+ +
+

bytes memory hookData

+

Provide the user's address to the PoolSwapTest

+
IPoolManager.SwapParams memory params = ...;
+
+PoolSwapTest.TestSettings memory testSettings = ...;
+
+// provide the user's address as hookData to be available inside the hook function
+bytes memory hookData = abi.encode(address(USER_ADDRESS));
+swapRouter.swap(key, params, testSettings, hookData);
+

Decode the hookData into an address type

+
// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.19;
+
+// TODO: update to v4-periphery/BaseHook.sol when its compatible
+import {BaseHook} from "@v4-by-example/utils/BaseHook.sol";
+
+import {Hooks} from "v4-core/libraries/Hooks.sol";
+import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
+import {PoolKey} from "v4-core/types/PoolKey.sol";
+import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
+import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
+
+contract MsgSenderHookData is BaseHook {
+    using PoolIdLibrary for PoolKey;
+
+    mapping(address user => bool allowed) public allowedUsers;
+
+    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
+
+    function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata hookData)
+        external
+        override
+        returns (bytes4)
+    {
+        // --- Read the user's address --- //
+        address user = abi.decode(hookData, (address));
+        require(allowedUsers[user], "MsgSenderHookData: User not allowed");
+        return BaseHook.beforeSwap.selector;
+    }
+
+    // Helper function for demonstration
+    function setAllowedUser(address user, bool allowed) external {
+        allowedUsers[user] = allowed;
+    }
+
+    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
+        return Hooks.Permissions({
+            beforeInitialize: false,
+            afterInitialize: false,
+            beforeAddLiquidity: false,
+            beforeRemoveLiquidity: false,
+            afterAddLiquidity: false,
+            afterRemoveLiquidity: false,
+            beforeSwap: true,
+            afterSwap: false,
+            beforeDonate: false,
+            afterDonate: false,
+            noOp: false,
+            accessLock: false
+        });
+    }
+}
+

Get Lock Caller

+

Invoke poolManager.lock with the user, and specify the PoolSwapTest router

+
// User directly locks on the PoolManager
+// - poolManager.getLock(1) returns alice's address
+vm.prank(alice);
+manager.lock(
+    address(swapRouter),
+    abi.encode(PoolSwapTest.CallbackData(address(this), testSettings, key, params, hookData))
+);
+

Access the Lock Caller via poolManager.getLock()

+
// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.19;
+
+// TODO: update to v4-periphery/BaseHook.sol when its compatible
+import {BaseHook} from "@v4-by-example/utils/BaseHook.sol";
+
+import {console2} from "forge-std/console2.sol";
+import {Hooks} from "v4-core/libraries/Hooks.sol";
+import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
+import {PoolKey} from "v4-core/types/PoolKey.sol";
+import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
+import {Lockers} from "v4-core/libraries/Lockers.sol";
+
+contract GetCurrentLockCaller is BaseHook {
+    mapping(address user => bool allowed) public allowedUsers;
+
+    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
+
+    function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata hookData)
+        external
+        override
+        returns (bytes4)
+    {
+        // --- Read the user's address --- //
+        (, address user) = poolManager.getLock(1);
+        require(allowedUsers[user], "GetCurrentLockCaller: User not allowed");
+        return BaseHook.beforeSwap.selector;
+    }
+
+    // Helper function for demonstration
+    function setAllowedUser(address user, bool allowed) external {
+        allowedUsers[user] = allowed;
+    }
+
+    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
+        return Hooks.Permissions({
+            beforeInitialize: false,
+            afterInitialize: false,
+            beforeAddLiquidity: false,
+            beforeRemoveLiquidity: false,
+            afterAddLiquidity: false,
+            afterRemoveLiquidity: false,
+            beforeSwap: true,
+            afterSwap: false,
+            beforeDonate: false,
+            afterDonate: false,
+            noOp: false,
+            accessLock: false
+        });
+    }
+}
+
` + +export default html diff --git a/src/pages/hooks/msg-sender/index.md b/src/pages/hooks/msg-sender/index.md new file mode 100644 index 00000000..d72a7ebc --- /dev/null +++ b/src/pages/hooks/msg-sender/index.md @@ -0,0 +1,57 @@ +--- +title: Access msg.sender within a Hook +version: 0.8.20 +description: Access msg.sender within a hook +keywords: [hook, hooks, msg.sender, msgsender, sender] +--- + +Please note there are multiple solutions -- each includes different tradeoffs + +### Use-cases + +Accessing the user's address inside a hook provides a lot of expressivity such as: + +- Volume-based discounts +- Permissioned / compliant access + +### Using `hookData` + +Callers (EOAs / contracts / multisigs) of periphery contracts (`PoolSwapTest`) can provide the user's address as the `hookData` argument + +- Tradeoff: Routing, quoters, and user interfaces will need to be aware of this non-standard parameter. `hookData` breaks generic/conventional paths + +### Get Lock Caller + +Callers directly use `PoolManager.lock` to invoke a periphery router. The lock caller (i.e. EOA) is saved in transient storage and can be accessed within a hook + +- Tradeoff: the transaction entrypoint is `poolManager.lock` and not `PoolSwapTest.swap`, leading to unconventional UX + +--- + +## `bytes memory hookData` + +Provide the user's address to the `PoolSwapTest` + +```solidity +{{{PoolSwapTestHookData}}} +``` + +Decode the `hookData` into an `address` type + +```solidity +{{{MsgSenderHookData}}} +``` + +## Get Lock Caller + +Invoke `poolManager.lock` with the user, and specify the `PoolSwapTest` router + +```solidity +{{{PoolManagerLock}}} +``` + +Access the Lock Caller via `poolManager.getLock()` + +```solidity +{{{GetCurrentLockCaller}}} +``` diff --git a/src/pages/hooks/msg-sender/index.tsx b/src/pages/hooks/msg-sender/index.tsx new file mode 100644 index 00000000..1c63a4f4 --- /dev/null +++ b/src/pages/hooks/msg-sender/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/pages/index.tsx b/src/pages/index.tsx index 2bdaac71..d7916d1f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -8,7 +8,7 @@ import styles from "./index.module.css" import youTube from "../components/youtube.png" import { ROUTES, ROUTES_BY_CATEGORY, TRANSLATIONS } from "../nav" -const UPDATES = ["2024/01/23 - Dynamic Fees", "2024/01/08 - Quoter", "2023/12/15 - Update v4", "2023/12/11 - Custom Curves", "2023/12/03 - Static Hook Fee", "2023/11/28 - Updated pool initialization", "2023/11/28 - NoOp", "2023/11/13 - Make snippets concise", "2023/10/18 - Initial V4 Snippets"] +const UPDATES = ["2024/01/31 - msg.sender", "2024/01/23 - Dynamic Fees", "2024/01/08 - Quoter", "2023/12/15 - Update v4", "2023/12/11 - Custom Curves", "2023/12/03 - Static Hook Fee", "2023/11/28 - Updated pool initialization", "2023/11/28 - NoOp", "2023/11/13 - Make snippets concise", "2023/10/18 - Initial V4 Snippets"] export default function HomePage() { const [query, setQuery] = useState("") diff --git a/src/routes.tsx b/src/routes.tsx index 023ff0a0..8a9201d2 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -2,6 +2,7 @@ import component_create_liquidity from "./pages/create-liquidity" import component_fees_dynamic_fee from "./pages/fees/dynamic-fee" import component_fees_fixed_hook_fee from "./pages/fees/fixed-hook-fee" import component_hooks_custom_curve from "./pages/hooks/custom-curve" +import component_hooks_msg_sender from "./pages/hooks/msg-sender" import component_hooks_no_op from "./pages/hooks/no-op" import component_initialize from "./pages/initialize" import component_quoter from "./pages/quoter" @@ -41,6 +42,10 @@ const routes: Route[] = [ path: "/hooks/custom-curve", component: component_hooks_custom_curve }, + { + path: "/hooks/msg-sender", + component: component_hooks_msg_sender + }, { path: "/hooks/no-op", component: component_hooks_no_op diff --git a/src/search.json b/src/search.json index 17493745..2ac6fb57 100644 --- a/src/search.json +++ b/src/search.json @@ -50,11 +50,13 @@ ], "hook": [ "/hooks/no-op", + "/hooks/msg-sender", "/hooks/custom-curve", "/fees/fixed-hook-fee" ], "hooks": [ "/hooks/no-op", + "/hooks/msg-sender", "/hooks/custom-curve", "/fees/fixed-hook-fee" ], @@ -72,6 +74,15 @@ "skip swap": [ "/hooks/no-op" ], + "msg.sender": [ + "/hooks/msg-sender" + ], + "msgsender": [ + "/hooks/msg-sender" + ], + "sender": [ + "/hooks/msg-sender" + ], "custom curve": [ "/hooks/custom-curve" ],