Liquidity Hooks
This guide will walk through on an example of adding and removing liquidity. There are four hook functions available to customize and extend these behavior:
beforeAddLiquidityafterAddLiquiditybeforeRemoveLiquidityafterRemoveLiquidity
As the names suggest beforeAddLiquidity/afterAddLiquidity are functions called before or after liquidity is added to a pool.
Similarly beforeRemoveLiquidity/afterRemoveLiquidity are functions called before or after liquidity is removed from a pool.
This guide will go through the parameters and examples specifically for beforeAddLiquidity and beforeRemoveLiquidity.
Note: The liquidity examples are not production ready code, and are implemented in a simplistic manner for the purpose of learning.
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 LiquidityHook, use PoolIdLibrary to attach functions of computing ID of a pool to PoolKey. Declare two mappings to act as counters when calling beforeAddLiquidity and beforeRemoveLiquidity.
contract LiquidityHook 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 beforeAddLiquidityCount;
mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount;
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: true,
afterAddLiquidity: false,
beforeRemoveLiquidity: true,
afterRemoveLiquidity: false,
beforeSwap: false,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeAddLiquidityReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
beforeAddLiquidity
Here the example shows that every time before liquidity is added to a pool, beforeAddLiquidityCount for that pool will be incremented by one.
function beforeAddLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override returns (bytes4) {
beforeAddLiquidityCount[key.toId()]++;
return BaseHook.beforeAddLiquidity.selector;
}
beforeAddLiquidity Parameters
When triggering the beforeAddLiquidity hook function, there are some parameters we can make use of to customize or extend the behavior of modifyLiquidity. These parameters are described in beforeAddLiquidity from IHooks.sol.
A brief overview of the parameters:
senderThe initialmsg.senderfor the add liquidity callkeyThe key for the poolparamsThe parameters for adding liquidity i.e.ModifyLiquidityParamsfromIPoolManager.solhookDataArbitrary data handed into thePoolManagerby the liquidity provider to be be passed on to the hook
beforeRemoveLiquidity
Similiar as above, every time before liquidity is removed from a pool, beforeRemoveLiquidityCount for that pool will be incremented by one.
function beforeRemoveLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override returns (bytes4) {
beforeRemoveLiquidityCount[key.toId()]++;
return BaseHook.beforeRemoveLiquidity.selector;
}
beforeRemoveLiquidity Parameters
When triggering the beforeRemoveLiquidity hook function, there are some parameters we can make use of to customize or extend the behavior of modifyLiquidity. These parameters are described in beforeRemoveLiquidity from IHooks.sol.
A brief overview of the parameters:
senderThe initial msg.sender for the remove liquidity callkeyThe key for the poolparamsThe parameters for removing liquidity i.e.ModifyLiquidityParamsfromIPoolManager.solhookDataArbitrary data handed into thePoolManagerby the liquidity provider to be be passed on to the hook
A Complete Liquidity 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 LiquidityHook 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 beforeAddLiquidityCount;
mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount;
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: false,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeAddLiquidityReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// -----------------------------------------------
// NOTE: see IHooks.sol for function documentation
// -----------------------------------------------
function beforeAddLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override returns (bytes4) {
beforeAddLiquidityCount[key.toId()]++;
return BaseHook.beforeAddLiquidity.selector;
}
function beforeRemoveLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override returns (bytes4) {
beforeRemoveLiquidityCount[key.toId()]++;
return BaseHook.beforeRemoveLiquidity.selector;
}
}