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.
-
This mimics the typical mint/redeem functionality available in other protocols.
-
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
- The hook permission which includes
beforeSwapReturnsDelta
as the hook modify the delta in beforeSwap. - How the hook take/settle the currency and the BeforeSwapDelta returned.
View complete source code here
// 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
// 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);
}
}