Swap Hooks
Swaps are the most common interaction with the Uniswap protocol. When it comes to swap there are two hook functions available to customize and extend its behavior:
beforeSwap
afterSwap
As the names suggest beforeSwap
/afterSwap
are functions called before or after a swap is executed on a pool.
This guide will explain the mechanism of encoded flags for hooks, and go through the parameters and examples for beforeSwap
and afterSwap
.
Note: The swap examples are not production ready code, and are implemented in a simplistic manner for the purpose of learning.
Hook Flags
As mentioned in Concept of Hooks, hook contracts indicate their implemented functions by encoding flags in the address of the contract. The PoolManager
uses these permissions to determine which hook functions to call for a given pool.
Each hook function e.g. beforeSwap
- corresponds to a certain flag. For example, the beforeSwap
function is correlated to the BEFORE_SWAP_FLAG
which has a value of 1 << 7
.
These flags represent specific bits in the address of the hook smart contract - and the value of the bit (a one or a zero) represents whether that flag is true or false. An example:
Addresses on Ethereum are 20 bytes long (160 bits). So for example the address:
0x00000000000000000000000000000000000000C0
represented in binary is:
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 1100 0000
In binary it goes from right-to-left - so the trailing 8 bits of this address are 1100 0000
where:
1st Bit to 6th Bit = 0
7th Bit to 8th Bit = 1
The AFTER_SWAP
flag is represented by the 7th bit - which is set to 1
for the example contract address. In the PoolManager's
swap execution flow, it will observe the flag and make a call to the hook's afterSwap
function.
Similarly, the 8th bit which is also a 1
, actually corresponds to the BEFORE_SWAP
i.e. the beforeSwap
hook function - which will also be called by the PoolManager
during a swap
workflow.
A full list of all flags can be found here.
Set Up the Contract
Declare the solidity version used to compile the contract, since transient storage is used the solidity version will be >=0.8.24
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
Import the relevant dependencies from v4-core
and v4-periphery
:
import {BaseHook} from "v4-periphery/src/base/hooks/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";
Create a contract called SwapHook, use PoolIdLibrary
to attach functions of computing ID of a pool to PoolKey
. Declare two mappings to act as counters when calling beforeSwap
and afterSwap
.
contract SwapHook is BaseHook {
using PoolIdLibrary for PoolKey;
// NOTE: ---------------------------------------------------------
// state variables should typically be unique to a pool
// a single hook contract should be able to service multiple pools
// ---------------------------------------------------------------
mapping(PoolId => uint256 count) public beforeSwapCount;
mapping(PoolId => uint256 count) public afterSwapCount;
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
Override getHookPermissions
from BaseHook.sol
to return a struct of permissions to signal which hook functions are to be implemented.
It will also be used at deployment to validate the address correctly represents the expected permissions.
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: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
beforeSwap
Here the example shows that every time before a swap is executed in a pool, beforeSwapCount
for that pool will be incremented by one.
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata)
external
override
returns (bytes4, BeforeSwapDelta, uint24)
{
beforeSwapCount[key.toId()]++;
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
beforeSwap
Parameters
When triggering the beforeSwap
hook function, there are some parameters we can make use of to customize or extend the behavior of swap
. These parameters are described in beforeSwap
from IHooks.sol
.
A brief overview of the parameters:
sender
The initialmsg.sender
for thePoolManager.swap
call. Typically a swap routerkey
The key for the poolparams
The parameters for the swap i.e.SwapParams
fromIPoolManager.sol
hookData
Arbitrary data handed into thePoolManager
by the swapper to be be passed on to the hook
afterSwap
Similiar as above, every time after a swap is executed in a pool, afterSwapCount
for that pool will be incremented by one.
function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata)
external
override
returns (bytes4, int128)
{
afterSwapCount[key.toId()]++;
return (BaseHook.afterSwap.selector, 0);
}
afterSwap
Parameters
When triggering the afterSwap
hook function, there are some parameters we can make use of to customize or extend the behavior of swap
. These parameters are described in afterSwap
from IHooks.sol
.
A brief overview of the parameters:
sender
The initialmsg.sender
for thePoolManager.swap
call. Typically a swap routerkey
The key for the poolparams
The parameters for the swap i.e.SwapParams
fromIPoolManager.sol
delta
The amount owed to the caller (positive) or owed to the pool (negative)hookData
Arbitrary data handed into thePoolManager
by the swapper to be be passed on to the hook
A Complete Swap Hook Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {BaseHook} from "v4-periphery/src/base/hooks/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";
contract SwapHook is BaseHook {
using PoolIdLibrary for PoolKey;
// NOTE: ---------------------------------------------------------
// state variables should typically be unique to a pool
// a single hook contract should be able to service multiple pools
// ---------------------------------------------------------------
mapping(PoolId => uint256 count) public beforeSwapCount;
mapping(PoolId => uint256 count) public afterSwapCount;
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: true,
afterAddLiquidity: false,
beforeRemoveLiquidity: true,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// -----------------------------------------------
// NOTE: see IHooks.sol for function documentation
// -----------------------------------------------
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata)
external
override
returns (bytes4, BeforeSwapDelta, uint24)
{
beforeSwapCount[key.toId()]++;
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata)
external
override
returns (bytes4, int128)
{
afterSwapCount[key.toId()]++;
return (BaseHook.afterSwap.selector, 0);
}
}