Access msg.sender Inside a Hook
Introduction
In Uniswap v4, when a hook is triggered, msg.sender
is always the PoolManager contract, not the EOA or smart-account that initiated the swap. This behavior occurs because the PoolManager acts as an intermediary, executing the swap logic on behalf of the user.
Securely Retrieving the Original msg.sender
address in a Hook
Since a smart contract executes the swap, the sender
parameter passed to beforeSwap()
represents the caller of PoolManager.swap()
.
This is typically a router contract, such as a custom swap router or the Universal Router. The challenge is distinguishing between different routers and securely obtaining the original msg.sender.
This guide explains how to securely retrieve the EOA or smart account in a hook.
Hook Overview
To identify the true sender of a swap:
- Maintain a trusted list of swap routers in the hook.
- When a swap is initiated, check if the sender is a trusted router.
- If trusted, call
msgSender()
view function on the router to retrieve the original EOA.
Implementing a Trusted Router Mechanism
Implement the Hook
Lets start with a simple hook that wants to access msg.sender
in beforeSwap()
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Import statements for a hook
contract AccessSenderHook is BaseHook {
// constructor, state, interface, etc
// ...
function _beforeSwap(address sender, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// read msg.sender
// ...
return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
}
...
}
Define an Interface for Trusted Routers
Each trusted router should implement a standard function that exposes the original msg.sender
interface IMsgSender {
function msgSender() external view returns (address);
}
This function allows hooks to query the router for the actual sender.
Store a List of Trusted Routers
The hook should keep track of which router contracts can be trusted to return a valid msgSender()
This can be done with the help of add and remove functions implemented in the hook.
mapping(address swapRouter => bool approved) public verifiedRouters;
NOTE:Make sure you include an address mapping in your hook for the routers before adding the functions.
function addRouter(address _router) external {
verifiedRouters[_router] = true;
console.log("Router added:", _router);
}
This function allows hook to add the router to the list of trusted routers.
function removeRouter(address _router) external {
verifiedRouters[_router] = false;
console.log("Router removed:", _router);
}
This function allows the hook to remove the router from the list of trusted routers if it's no longer needed.
Implementing beforeSwap
Now that we have implemented a basic hook and have added necessary functions, let us implement the beforeSwap function:
function _beforeSwap(address sender, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
if (verifiedRouters[sender]) {
address swapper = IMsgSender(sender).msgSender();
console.log("Swap initiated by account:", swapper);
}
return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
}
Here are some examples of trusted routers:* https://github.com/Uniswap/universal-router/tree/main * https://github.com/z0r0z/v4-routerNOTE: While developing, make sure that you verify the contracts are valid before adding them to the list of trusted routers.
Here is the full working code sample:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
import "forge-std/console.sol";
interface IMsgSender {
function msgSender() external view returns (address);
}
contract AccessSenderHook is BaseHook {
mapping(address swapRouter => bool approved) public verifiedRouters;
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {
}
function _beforeSwap(
address sender,
PoolKey calldata,
IPoolManager.SwapParams calldata,
bytes calldata
) internal override returns (bytes4, BeforeSwapDelta, uint24) {
if (verifiedRouters[sender]) {
address swapper = IMsgSender(sender).msgSender();
console.log("Swap initiated by account:", swapper);
}
return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
}
function addRouter(address _router) external {
verifiedRouters[_router] = true;
console.log("Router added:", _router);
}
function removeRouter(address _router) external {
verifiedRouters[_router] = false;
console.log("Router removed:", _router);
}
function isVerifiedRouter(address _router) external view returns (bool) {
return verifiedRouters[_router];
}
function getHookPermissions()
public
pure
override
returns (Hooks.Permissions memory)
{
return
Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
}