Skip to content

Taking a fee via hook

In this guide, we'll build a hook which takes a fee when user remove liquidity. This guide assume you have read develop-a-hook guide.

Requirements

  1. The hook needs to take 10% amt0 and amt1 whenever user removes liquidity.

Step by Step guide

Step 1: Implementation idea

In the afterRemoveLiquidity() callback, we calculate the fee based on the amount the user receives from liquidity removal. This amount can be found in the BalanceDelta delta parameter.

function afterRemoveLiquidity
    (address sender,
    PoolKey calldata key,
    ICLPoolManager.ModifyLiquidityParams calldata params,
    BalanceDelta delta,
    bytes calldata hookData
) external override poolManagerOnly returns (bytes4, BalanceDelta) {
 
    // calculate how much fee
    uint128 amt0Fee = uint128(delta.amount0()) / 10;
    uint128 amt1Fee = uint128(delta.amount1()) / 10;

Step 2: Implement the hook

Take note of

  1. The hook permission which includes afterRemoveLiquidityReturnsDelta as the hook modify the delta in afterRemoveLiquidity().
View complete source code here
src/pool-cl/LiquidityRemovalFeeHook.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
import {PoolKey} from "@pancakeswap/v4-core/src/types/PoolKey.sol";
import {BalanceDelta, toBalanceDelta} from "@pancakeswap/v4-core/src/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "@pancakeswap/v4-core/src/types/PoolId.sol";
import {Currency} from "@pancakeswap/v4-core/src/types/Currency.sol";
import {ICLPoolManager} from "@pancakeswap/v4-core/src/pool-cl/interfaces/ICLPoolManager.sol";
import {CurrencySettlement} from "@pancakeswap/v4-core/test/helpers/CurrencySettlement.sol";
import {CLBaseHook} from "./CLBaseHook.sol";
 
/// @notice LiquidityRemovalFeeHook takes 10% fee when user remove liquidity
contract LiquidityRemovalFeeHook is CLBaseHook {
    using PoolIdLibrary for PoolKey;
    using CurrencySettlement for Currency;
 
    constructor(ICLPoolManager _poolManager) CLBaseHook(_poolManager) {}
 
    function getHooksRegistrationBitmap() external pure override returns (uint16) {
        return _hooksRegistrationBitmapFrom(
            Permissions({
                beforeInitialize: false,
                afterInitialize: false,
                beforeAddLiquidity: false,
                afterAddLiquidity: false,
                beforeRemoveLiquidity: false,
                afterRemoveLiquidity: true,
                beforeSwap: false,
                afterSwap: false,
                beforeDonate: false,
                afterDonate: false,
                beforeSwapReturnsDelta: false,
                afterSwapReturnsDelta: false,
                afterAddLiquidityReturnsDelta: false,
                afterRemoveLiquidityReturnsDelta: true
            })
        );
    }
 
    function afterRemoveLiquidity(
        address sender,
        PoolKey calldata key,
        ICLPoolManager.ModifyLiquidityParams calldata params,
        BalanceDelta delta,
        bytes calldata hookData
    ) external override poolManagerOnly returns (bytes4, BalanceDelta) {
 
        // delta would be positive here as user is removing liquidity
        uint128 amt0Fee = uint128(delta.amount0()) / 10;
        uint128 amt1Fee = uint128(delta.amount1()) / 10;
 
        key.currency0.take(vault, address(this), amt0Fee, false);
        key.currency1.take(vault, address(this), amt1Fee, false);
 
        // take 10% fee 
        BalanceDelta feeDelta = toBalanceDelta(int128(amt0Fee), int128(amt1Fee));
 
        return (this.afterRemoveLiquidity.selector, feeDelta);
    }
}

Step 3: Write the test

The test is straight-forward, add liquidity first then remove liquidity and verify fee is taken.

View complete source code here
src/pool-cl/LiquidityRemovalFeeHook.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
 
import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol";
import {Test} from "forge-std/Test.sol";
import {Constants} from "@pancakeswap/v4-core/test/pool-cl/helpers/Constants.sol";
import {Currency} from "@pancakeswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@pancakeswap/v4-core/src/types/PoolKey.sol";
import {CLPoolParametersHelper} from "@pancakeswap/v4-core/src/pool-cl/libraries/CLPoolParametersHelper.sol";
import {LiquidityRemovalFeeHook} from "../../src/pool-cl/LiquidityRemovalFeeHook.sol";
import {CLTestUtils} from "./utils/CLTestUtils.sol";
import {PoolIdLibrary} from "@pancakeswap/v4-core/src/types/PoolId.sol";
import {ICLSwapRouterBase} from "@pancakeswap/v4-periphery/src/pool-cl/interfaces/ICLSwapRouterBase.sol";
import {INonfungiblePositionManager} from
    "@pancakeswap/v4-periphery/src/pool-cl/interfaces/INonfungiblePositionManager.sol";
 
contract LiquidityRemovalFeeHookTest is Test, CLTestUtils {
    using PoolIdLibrary for PoolKey;
    using CLPoolParametersHelper for bytes32;
 
    LiquidityRemovalFeeHook hook;
    Currency currency0;
    Currency currency1;
    PoolKey key;
    address alice = makeAddr("alice");
 
    function setUp() public {
        (currency0, currency1) = deployContractsWithTokens();
        hook = new LiquidityRemovalFeeHook(poolManager);
 
        // create the pool key
        key = PoolKey({
            currency0: currency0,
            currency1: currency1,
            hooks: hook,
            poolManager: poolManager,
            fee: uint24(3000), // 0.3% fee
            parameters: bytes32(uint256(hook.getHooksRegistrationBitmap())).setTickSpacing(10)
        });
 
        // initialize pool at 1:1 price point and set 3000 as initial lp fee, lpFee is stored in the hook
        poolManager.initialize(key, Constants.SQRT_RATIO_1_1, abi.encode(uint24(3000)));
 
        // approve from alice for liquidity ops in the test cases below
        vm.startPrank(alice);
        MockERC20(Currency.unwrap(currency0)).approve(address(nfp), type(uint256).max);
        MockERC20(Currency.unwrap(currency1)).approve(address(nfp), type(uint256).max);
        vm.stopPrank();
    }
 
    function testRemoveLiquidity() public {
        MockERC20(Currency.unwrap(currency0)).mint(address(alice), 10 ether);
        MockERC20(Currency.unwrap(currency1)).mint(address(alice), 10 ether);
 
        vm.startPrank(alice);
 
        assertEq(MockERC20(Currency.unwrap(currency0)).balanceOf(alice), 10 ether);
        assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(alice), 10 ether);
 
        // add 10 eth liquidity on each side
        (uint256 tokenId, uint128 liquidity) = _addLiquidity(key, 10 ether, 10 ether, -60, 60);
 
        assertEq(MockERC20(Currency.unwrap(currency0)).balanceOf(alice), 0 ether);
        assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(alice), 0 ether);
 
        // remove all liqudiity
        _removeLiquidity(tokenId, liquidity);
 
        // verify that only 9 ether received as 10% fee taken
        assertEq(MockERC20(Currency.unwrap(currency0)).balanceOf(alice), 9 ether);
        assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(alice), 9 ether);
    }
 
    function _addLiquidity(PoolKey memory key, uint256 amount0, uint256 amount1, int24 tickLower, int24 tickUpper)
        internal
        returns (uint256 tokenId, uint128 liquidity)
    {
        INonfungiblePositionManager.MintParams memory param = INonfungiblePositionManager.MintParams({
            poolKey: key,
            tickLower: tickLower,
            tickUpper: tickUpper,
            salt: bytes32(0),
            amount0Desired: amount0,
            amount1Desired: amount1,
            amount0Min: 0,
            amount1Min: 0,
            recipient: address(alice),
            deadline: block.timestamp
        });
 
        (tokenId, liquidity,,) = nfp.mint(param);
    }
 
    function _removeLiquidity(uint256 tokenId, uint128 liquidity) internal {
        INonfungiblePositionManager.DecreaseLiquidityParams memory param = INonfungiblePositionManager
            .DecreaseLiquidityParams({
            tokenId: tokenId,
            liquidity: liquidity,
            amount0Min: 0,
            amount1Min: 0,
            deadline: block.timestamp
        });
 
        nfp.decreaseLiquidity(param);
    }
}