Skip to content

Overwrite amm-curve via hook

In this guide, we'll build a hook which overwrites the default pricing curve by the AMM. This guide assume you have read develop-a-hook guide.

Requirements

1.A new protocol has a new stablecoin and wants to leverage PCS infrastructure to allow users to swap between their stablecoin and USDC with 0 slippage.

  1. This mimics the typical mint/redeem functionality available in other protocols.

  2. To keep this guide simple, there will not be any fees from the hook.

Step by Step guide

Step 1: Implementation idea

Within beforeSwap() callback: we'll check how much amount user are swapping and return the appropriate BalanceDelta.

For example if the user swap exactIn 100 token0 for token1 with -100 amountSpecified, beforeSwap() will return BeforeSwapDelta of (100, -100). User will then get 100 tokenOut in this case implying a 1:1 swap.

Step 2: Implement the hook

Take note of

  1. The hook permission which includes beforeSwapReturnsDelta as the hook modify the delta in beforeSwap.
  2. How the hook take/settle the currency and the BeforeSwapDelta returned.
View complete source code here
src/pool-cl/CustomAMMCurveHook.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {BeforeSwapDelta, toBeforeSwapDelta} from "pancake-v4-core/src/types/BeforeSwapDelta.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 {LPFeeLibrary} from "pancake-v4-core/src/libraries/LPFeeLibrary.sol";
import {CurrencySettlement} from "pancake-v4-core/test/helpers/CurrencySettlement.sol";
import {CLBaseHook} from "./CLBaseHook.sol";
 
/// @notice CustomAMMCurveHook override AMM curve with 1:1 curve and 0 trading slippage
contract CustomAMMCurveHook 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: false,
                beforeSwap: true,
                afterSwap: false,
                beforeDonate: false,
                afterDonate: false,
                beforeSwapReturnsDelta: true,
                afterSwapReturnsDelta: false,
                afterAddLiquidityReturnsDelta: false,
                afterRemoveLiquidityReturnsDelta: false
            })
        );
    }
 
    function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata params, bytes calldata)
        external
        override
        poolManagerOnly
        returns (bytes4, BeforeSwapDelta, uint24)
    {
        (Currency inputCurrency, Currency outputCurrency, uint256 amount) = _getInputOutputAndAmount(key, params);
 
        // 1. Take input currency and amount
        inputCurrency.take(vault, address(this), amount, false);
 
        // 2. Give output currency and amount achieving a 1:1 swap
        outputCurrency.settle(vault, address(this), amount, false);
 
        BeforeSwapDelta hookDelta = toBeforeSwapDelta(int128(-params.amountSpecified), int128(params.amountSpecified));
        return (this.beforeSwap.selector, hookDelta, 0);
    }
 
    /// @notice Get input, output currencies and amount from swap params
    function _getInputOutputAndAmount(PoolKey calldata key, ICLPoolManager.SwapParams calldata params)
        internal
        pure
        returns (Currency input, Currency output, uint256 amount)
    {
        (input, output) = params.zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0);
 
        amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);
    }
}

Step 3: Write the test

The test is straight-forward, with 4 test cases of swapping exactIn / exactOut and zeroForOne / oneForZero.

View complete source code here
src/pool-cl/CustomAMMCurveHook.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 {CustomAMMCurveHook} from "../../src/pool-cl/CustomAMMCurveHook.sol";
import {CLTestUtils} from "./utils/CLTestUtils.sol";
import {PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol";
import {LPFeeLibrary} from "pancake-v4-core/src/libraries/LPFeeLibrary.sol";
import {ICLRouterBase} from "pancake-v4-periphery/src/pool-cl/interfaces/ICLRouterBase.sol";
 
import {console2} from "forge-std/console2.sol";
 
contract CustomAMMCurveHookTest is Test, CLTestUtils {
    using PoolIdLibrary for PoolKey;
    using CLPoolParametersHelper for bytes32;
 
    CustomAMMCurveHook hook;
    Currency currency0;
    Currency currency1;
    PoolKey key;
    address alice = makeAddr("alice");
 
    function setUp() public {
        (currency0, currency1) = deployContractsWithTokens();
        hook = new CustomAMMCurveHook(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)));
 
        // Add some liquidity so currency does not go negative and negate in Vault.sol
        MockERC20(Currency.unwrap(currency0)).mint(address(this), 100 ether);
        MockERC20(Currency.unwrap(currency1)).mint(address(this), 100 ether);
        addLiquidity(key, 100 ether, 100 ether, -60, 60, address(this));
 
        // approve from alice for swap in the test cases below
        permit2Approve(alice, currency0, address(universalRouter));
        permit2Approve(alice, currency1, address(universalRouter));
 
        // mint alice token for trade later
        MockERC20(Currency.unwrap(currency0)).mint(address(alice), 100 ether);
        MockERC20(Currency.unwrap(currency1)).mint(address(alice), 100 ether);
 
        // mint hook some token for take/settle
        MockERC20(Currency.unwrap(currency0)).mint(address(hook), 100 ether);
        MockERC20(Currency.unwrap(currency1)).mint(address(hook), 100 ether);
    }
 
    function testSwapZeroForOne_exactIn() public {
        uint256 amt1Before = MockERC20(Currency.unwrap(currency1)).balanceOf(address(alice));
 
        vm.prank(alice);
        exactInputSingle(
            ICLRouterBase.CLSwapExactInputSingleParams({
                poolKey: key,
                zeroForOne: true,
                amountIn: 1 ether,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0,
                hookData: new bytes(0)
            })
        );
 
        uint256 amt1After = MockERC20(Currency.unwrap(currency1)).balanceOf(address(alice));
        uint256 amtOut = amt1After - amt1Before;
 
        assertEq(amtOut, 1 ether);
    }
 
    function testSwapOneForZero_exactIn() public {
        uint256 amt0Before = MockERC20(Currency.unwrap(currency0)).balanceOf(address(alice));
 
        vm.prank(alice);
        exactInputSingle(
            ICLRouterBase.CLSwapExactInputSingleParams({
                poolKey: key,
                zeroForOne: false,
                amountIn: 1 ether,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0,
                hookData: new bytes(0)
            })
        );
 
        uint256 amt0After = MockERC20(Currency.unwrap(currency0)).balanceOf(address(alice));
        uint256 amtOut = amt0After - amt0Before;
 
        assertEq(amtOut, 1 ether);
    }
 
    function testSwapZeroForOne_exactOut() public {
        uint256 amt0Before = MockERC20(Currency.unwrap(currency0)).balanceOf(address(alice));
 
        vm.prank(alice);
        exactOutputSingle(
            ICLRouterBase.CLSwapExactOutputSingleParams({
                poolKey: key,
                zeroForOne: true,
                amountOut: 1 ether,
                amountInMaximum: type(uint128).max,
                sqrtPriceLimitX96: 0,
                hookData: new bytes(0)
            })
        );
 
        uint256 amt0After = MockERC20(Currency.unwrap(currency0)).balanceOf(address(alice));
        uint256 amtIn = amt0Before - amt0After;
 
        assertEq(amtIn, 1 ether);
    }
 
    function testSwapOneForZero_exactOut() public {
        uint256 amt1Before = MockERC20(Currency.unwrap(currency1)).balanceOf(address(alice));
 
        vm.prank(alice);
        exactOutputSingle(
            ICLRouterBase.CLSwapExactOutputSingleParams({
                poolKey: key,
                zeroForOne: false,
                amountOut: 1 ether,
                amountInMaximum: type(uint128).max,
                sqrtPriceLimitX96: 0,
                hookData: new bytes(0)
            })
        );
 
        uint256 amt1After = MockERC20(Currency.unwrap(currency1)).balanceOf(address(alice));
        uint256 amtIn = amt1Before - amt1After;
 
        assertEq(amtIn, 1 ether);
    }
}