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
- 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
- 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 "pancake-v4-core/src/types/PoolKey.sol";
import {BalanceDelta, toBalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol";
import {Currency} from "pancake-v4-core/src/types/Currency.sol";
import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol";
import {CurrencySettlement} from "pancake-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/src/test/utils/mocks/MockERC20.sol";
import {Test} from "forge-std/Test.sol";
import {Constants} from "pancake-v4-core/test/pool-cl/helpers/Constants.sol";
import {Currency} from "pancake-v4-core/src/types/Currency.sol";
import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {CLPoolParametersHelper} from "pancake-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 "pancake-v4-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 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 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);
}
}