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.

/// @param delta The caller's balance delta after removing liquidity; the sum of principal delta, fees accrued, and hook delta
function afterRemoveLiquidity
    (address sender,
    PoolKey calldata key,
    ICLPoolManager.ModifyLiquidityParams calldata params,
    BalanceDelta delta,
    BalanceDelta feesAccrued,
    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 "infinity-core/src/types/PoolKey.sol";
import {BalanceDelta, toBalanceDelta} from "infinity-core/src/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "infinity-core/src/types/PoolId.sol";
import {Currency} from "infinity-core/src/types/Currency.sol";
import {ICLPoolManager} from "infinity-core/src/pool-cl/interfaces/ICLPoolManager.sol";
import {CurrencySettlement} from "infinity-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,
                beforeSwapReturnDelta: false,
                afterSwapReturnDelta: false,
                afterAddLiquidityReturnDelta: false,
                afterRemoveLiquidityReturnDelta: true
            })
        );
    }
 
    function _afterRemoveLiquidity(
        address sender,
        PoolKey calldata key,
        ICLPoolManager.ModifyLiquidityParams calldata params,
        BalanceDelta delta,
        BalanceDelta feesAccrued,
        bytes calldata hookData
    ) internal override 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/src/test/utils/mocks/MockERC20.sol";
import {Test} from "forge-std/Test.sol";
import {Constants} from "infinity-core/test/pool-cl/helpers/Constants.sol";
import {Currency} from "infinity-core/src/types/Currency.sol";
import {PoolKey} from "infinity-core/src/types/PoolKey.sol";
import {CLPoolParametersHelper} from "infinity-core/src/pool-cl/libraries/CLPoolParametersHelper.sol";
import {LiquidityRemovalFeeHook} from "../../src/pool-cl/LiquidityRemovalFeeHook.sol";
import {CLTestUtils} from "./utils/CLTestUtils.sol";
import {PoolIdLibrary} from "infinity-core/src/types/PoolId.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
        poolManager.initialize(key, Constants.SQRT_RATIO_1_1);
 
        // approve from alice for liquidity operation in the test cases below
        permit2Approve(alice, currency0, address(positionManager));
        permit2Approve(alice, currency1, address(positionManager));
    }
 
    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 = addLiquidity(key, 10 ether, 10 ether, -60, 60, alice);
 
        assertEq(MockERC20(Currency.unwrap(currency0)).balanceOf(alice), 0 ether);
        assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(alice), 0 ether);
 
        // remove all liqudiity
        decreaseLiquidity(tokenId, key, 10 ether, 10 ether, -60, 60);
 
        // 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);
    }
}