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
+Accessing the user's address inside a hook provides a lot of expressivity such as:
+hookData
Callers (EOAs / contracts / multisigs) of periphery contracts (PoolSwapTest
) can provide the user's address as the hookData
argument
hookData
breaks generic/conventional pathsCallers 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
poolManager.lock
and not PoolSwapTest.swap
, leading to unconventional UXbytes 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
+ });
+ }
+}
+
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