Skip to main content

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

...
}

Refer to Building your first hook

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

NOTE: While developing, make sure that you verify the contracts are valid before adding them to the list of trusted routers.

Here are some examples of trusted routers:* https://github.com/Uniswap/universal-router/tree/main * https://github.com/z0r0z/v4-router

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

}