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.
/// @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
- 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);
}
}