From 485ff604f042d7b86fc63c726a8765d237f74dde Mon Sep 17 00:00:00 2001 From: Chef Snoopy Date: Wed, 18 Sep 2024 16:09:22 +0800 Subject: [PATCH 1/5] feat: Update lib --- lib/forge-gas-snapshot | 2 +- lib/forge-std | 2 +- lib/pancake-v4-universal-router | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/forge-gas-snapshot b/lib/forge-gas-snapshot index 9fc447c..03b10b1 160000 --- a/lib/forge-gas-snapshot +++ b/lib/forge-gas-snapshot @@ -1 +1 @@ -Subproject commit 9fc447c732c89b6dd6352c096042d8d82b44faed +Subproject commit 03b10b10574e069081f6b84f5e1244e42041511d diff --git a/lib/forge-std b/lib/forge-std index 1714bee..beb836e 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d +Subproject commit beb836e33f9a207f4927abb7cd09ad0afe4b3f9f diff --git a/lib/pancake-v4-universal-router b/lib/pancake-v4-universal-router index 8cefacb..e655693 160000 --- a/lib/pancake-v4-universal-router +++ b/lib/pancake-v4-universal-router @@ -1 +1 @@ -Subproject commit 8cefacb472e8fdecdb7b0391527d42b6310b817d +Subproject commit e6556937a9e7cd1ccbb8a0ce958e99e32108e47c From edf67af3f1e92148fc665b47fd8b0b24d17f73d7 Mon Sep 17 00:00:00 2001 From: Chef Snoopy Date: Thu, 19 Sep 2024 14:21:21 +0800 Subject: [PATCH 2/5] feat: Update codes to adapt to latest v4 codes --- src/pool-cl/CLBaseHook.sol | 2 + test/pool-bin/helpers/Deployers.sol | 2 +- test/pool-cl/CLFullRangeHook.t.sol | 1 - test/pool-cl/CLGeomeanOracle.t.sol | 46 ++++++------------- test/pool-cl/CLLimitOrder.t.sol | 8 ++-- test/pool-cl/CLVeCakeExclusiveHook.t.sol | 8 ++-- .../pool-cl/helpers/MockCLPositionManager.sol | 23 +++++----- 7 files changed, 36 insertions(+), 54 deletions(-) diff --git a/src/pool-cl/CLBaseHook.sol b/src/pool-cl/CLBaseHook.sol index 348f263..4579e80 100644 --- a/src/pool-cl/CLBaseHook.sol +++ b/src/pool-cl/CLBaseHook.sol @@ -140,6 +140,7 @@ abstract contract CLBaseHook is ICLHooks { PoolKey calldata, ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, + BalanceDelta, bytes calldata ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); @@ -161,6 +162,7 @@ abstract contract CLBaseHook is ICLHooks { PoolKey calldata, ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, + BalanceDelta, bytes calldata ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); diff --git a/test/pool-bin/helpers/Deployers.sol b/test/pool-bin/helpers/Deployers.sol index 8de2813..0a84106 100644 --- a/test/pool-bin/helpers/Deployers.sol +++ b/test/pool-bin/helpers/Deployers.sol @@ -65,7 +65,7 @@ contract Deployers { function createFreshManager() internal returns (IVault vault, BinPoolManager manager) { vault = new Vault(); - manager = new BinPoolManager(vault, 500000); + manager = new BinPoolManager(vault); vault.registerApp(address(manager)); } } diff --git a/test/pool-cl/CLFullRangeHook.t.sol b/test/pool-cl/CLFullRangeHook.t.sol index 9856d06..1d19a66 100644 --- a/test/pool-cl/CLFullRangeHook.t.sol +++ b/test/pool-cl/CLFullRangeHook.t.sol @@ -23,7 +23,6 @@ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol" import {MockCLSwapRouter} from "./helpers/MockCLSwapRouter.sol"; import {MockCLPositionManager} from "./helpers/MockCLPositionManager.sol"; -import {PositionConfig} from "pancake-v4-periphery/src/pool-cl/libraries/PositionConfig.sol"; import {CLFullRange} from "../../src/pool-cl/full-range/CLFullRange.sol"; import {PancakeV4ERC20} from "../../src/pool-cl/full-range/libraries/PancakeV4ERC20.sol"; diff --git a/test/pool-cl/CLGeomeanOracle.t.sol b/test/pool-cl/CLGeomeanOracle.t.sol index ac26441..a22056e 100644 --- a/test/pool-cl/CLGeomeanOracle.t.sol +++ b/test/pool-cl/CLGeomeanOracle.t.sol @@ -24,8 +24,6 @@ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol" import {MockCLSwapRouter} from "./helpers/MockCLSwapRouter.sol"; import {MockCLPositionManager} from "./helpers/MockCLPositionManager.sol"; -import {PositionConfig} from "pancake-v4-periphery/src/pool-cl/libraries/PositionConfig.sol"; - import {CLGeomeanOracle} from "../../src/pool-cl/geomean-oracle/CLGeomeanOracle.sol"; import {Oracle} from "../../src/pool-cl/geomean-oracle/libraries/Oracle.sol"; @@ -157,14 +155,10 @@ contract CLGeomeanOracleHookTest is Test, Deployers, DeployPermit2 { function testBeforeModifyPositionNoObservations() public { poolManager.initialize(key, Constants.SQRT_RATIO_2_1, ZERO_BYTES); - PositionConfig memory config = PositionConfig({ - poolKey: key, - tickLower: TickMath.minUsableTick(MAX_TICK_SPACING), - tickUpper: TickMath.maxUsableTick(MAX_TICK_SPACING) - }); - cpm.mint( - config, + key, + TickMath.minUsableTick(MAX_TICK_SPACING), + TickMath.maxUsableTick(MAX_TICK_SPACING), // liquidity: 10e18, // amount0Max: @@ -193,14 +187,10 @@ contract CLGeomeanOracleHookTest is Test, Deployers, DeployPermit2 { poolManager.initialize(key, Constants.SQRT_RATIO_2_1, ZERO_BYTES); vm.warp(3); // advance 2 seconds - PositionConfig memory config = PositionConfig({ - poolKey: key, - tickLower: TickMath.minUsableTick(MAX_TICK_SPACING), - tickUpper: TickMath.maxUsableTick(MAX_TICK_SPACING) - }); - cpm.mint( - config, + key, + TickMath.minUsableTick(MAX_TICK_SPACING), + TickMath.maxUsableTick(MAX_TICK_SPACING), // liquidity: 10e18, // amount0Max: @@ -234,14 +224,10 @@ contract CLGeomeanOracleHookTest is Test, Deployers, DeployPermit2 { assertEq(observationState.cardinality, 1); assertEq(observationState.cardinalityNext, 2); - PositionConfig memory config = PositionConfig({ - poolKey: key, - tickLower: TickMath.minUsableTick(MAX_TICK_SPACING), - tickUpper: TickMath.maxUsableTick(MAX_TICK_SPACING) - }); - cpm.mint( - config, + key, + TickMath.minUsableTick(MAX_TICK_SPACING), + TickMath.maxUsableTick(MAX_TICK_SPACING), // liquidity: 10e18, // amount0Max: @@ -279,14 +265,10 @@ contract CLGeomeanOracleHookTest is Test, Deployers, DeployPermit2 { poolManager.initialize(key, Constants.SQRT_RATIO_2_1, ZERO_BYTES); vm.warp(3); // advance 2 seconds - PositionConfig memory config = PositionConfig({ - poolKey: key, - tickLower: TickMath.minUsableTick(MAX_TICK_SPACING), - tickUpper: TickMath.maxUsableTick(MAX_TICK_SPACING) - }); - (uint256 tokenId, uint128 liquidity) = cpm.mint( - config, + key, + TickMath.minUsableTick(MAX_TICK_SPACING), + TickMath.maxUsableTick(MAX_TICK_SPACING), // liquidity: 10e18, // amount0Max: @@ -309,8 +291,8 @@ contract CLGeomeanOracleHookTest is Test, Deployers, DeployPermit2 { cpm.decreaseLiquidity( // tokenId: tokenId, - // config: - config, + // poolKey: + key, // liquidity: liquidity, // amount0Min: diff --git a/test/pool-cl/CLLimitOrder.t.sol b/test/pool-cl/CLLimitOrder.t.sol index 07e3f3f..1535f7b 100644 --- a/test/pool-cl/CLLimitOrder.t.sol +++ b/test/pool-cl/CLLimitOrder.t.sol @@ -23,8 +23,6 @@ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol" import {MockCLSwapRouter} from "./helpers/MockCLSwapRouter.sol"; import {MockCLPositionManager} from "./helpers/MockCLPositionManager.sol"; -import {PositionConfig} from "pancake-v4-periphery/src/pool-cl/libraries/PositionConfig.sol"; - import {CLLimitOrder, Epoch, EpochLibrary} from "../../src/pool-cl/limit-order/CLLimitOrder.sol"; contract CLLimitOrderHookTest is Test, Deployers, DeployPermit2 { @@ -81,10 +79,10 @@ contract CLLimitOrderHookTest is Test, Deployers, DeployPermit2 { poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - cpm.mint( - config, + key, + -120, + 120, // liquidity: 10e18, // amount0Max: diff --git a/test/pool-cl/CLVeCakeExclusiveHook.t.sol b/test/pool-cl/CLVeCakeExclusiveHook.t.sol index 1eccacd..0210e25 100644 --- a/test/pool-cl/CLVeCakeExclusiveHook.t.sol +++ b/test/pool-cl/CLVeCakeExclusiveHook.t.sol @@ -22,7 +22,6 @@ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol" import {MockCLSwapRouter} from "./helpers/MockCLSwapRouter.sol"; import {MockCLPositionManager} from "./helpers/MockCLPositionManager.sol"; -import {PositionConfig} from "pancake-v4-periphery/src/pool-cl/libraries/PositionConfig.sol"; import {CLVeCakeExclusiveHook} from "../../src/pool-cl/vecake-exclusive/CLVeCakeExclusiveHook.sol"; contract CLVeCakeExclusiveHookTest is Test, Deployers, DeployPermit2 { @@ -45,7 +44,6 @@ contract CLVeCakeExclusiveHookTest is Test, Deployers, DeployPermit2 { Currency currency1; PoolKey key; PoolId id; - PositionConfig config; MockERC20 veCake; address nonHolder = address(0x1); @@ -80,13 +78,15 @@ contract CLVeCakeExclusiveHookTest is Test, Deployers, DeployPermit2 { fee: 3000, parameters: bytes32(uint256(veCakeExclusiveHook.getHooksRegistrationBitmap())).setTickSpacing(60) }); - config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + id = key.toId(); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); cpm.mint( - config, + key, + -120, + 120, // liquidity: 10e18, // amount0Max: diff --git a/test/pool-cl/helpers/MockCLPositionManager.sol b/test/pool-cl/helpers/MockCLPositionManager.sol index 65bc4e9..c724c52 100644 --- a/test/pool-cl/helpers/MockCLPositionManager.sol +++ b/test/pool-cl/helpers/MockCLPositionManager.sol @@ -11,17 +11,19 @@ import {CLPositionManager} from "pancake-v4-periphery/src/pool-cl/CLPositionMana import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {Planner, Plan} from "pancake-v4-periphery/src/libraries/Planner.sol"; import {Actions} from "pancake-v4-periphery/src/libraries/Actions.sol"; -import {PositionConfig} from "pancake-v4-periphery/src/pool-cl/libraries/PositionConfig.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; contract MockCLPositionManager is CLPositionManager, CommonBase { using Planner for Plan; constructor(IVault _vault, ICLPoolManager _clPoolManager, IAllowanceTransfer _permit2) - CLPositionManager(_vault, _clPoolManager, _permit2) + CLPositionManager(_vault, _clPoolManager, _permit2, 500000) {} function mint( - PositionConfig calldata config, + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, uint256 liquidity, uint128 amount0Max, uint128 amount1Max, @@ -29,32 +31,31 @@ contract MockCLPositionManager is CLPositionManager, CommonBase { bytes calldata hookData ) external payable returns (uint256 tokenId, uint128 liquidityMinted) { Plan memory planner = Planner.init().add( - Actions.CL_MINT_POSITION, abi.encode(config, liquidity, amount0Max, amount1Max, owner, hookData) + Actions.CL_MINT_POSITION, + abi.encode(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData) ); - bytes memory data = planner.finalizeModifyLiquidityWithClose(config.poolKey); - - // vm.recordLogs(); + bytes memory data = planner.finalizeModifyLiquidityWithClose(poolKey); tokenId = nextTokenId; vm.prank(msg.sender); this.modifyLiquidities(data, block.timestamp); - liquidityMinted = getPositionLiquidity(tokenId, config); + liquidityMinted = _getLiquidity(tokenId, poolKey, tickLower, tickUpper); } function decreaseLiquidity( uint256 tokenId, - PositionConfig calldata config, + PoolKey calldata poolKey, uint256 liquidity, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData ) external payable { Plan memory planner = Planner.init().add( - Actions.CL_DECREASE_LIQUIDITY, abi.encode(tokenId, config, liquidity, amount0Min, amount1Min, hookData) + Actions.CL_DECREASE_LIQUIDITY, abi.encode(tokenId, liquidity, amount0Min, amount1Min, hookData) ); - bytes memory data = planner.finalizeModifyLiquidityWithClose(config.poolKey); + bytes memory data = planner.finalizeModifyLiquidityWithClose(poolKey); vm.prank(msg.sender); this.modifyLiquidities(data, block.timestamp); From 175955314790353949f7e17a5b71fe6d23bff2f5 Mon Sep 17 00:00:00 2001 From: Chef Snoopy Date: Thu, 19 Sep 2024 15:46:52 +0800 Subject: [PATCH 3/5] forge install: prb-math v4.0.3 --- .gitmodules | 3 +++ lib/prb-math | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/prb-math diff --git a/.gitmodules b/.gitmodules index 80d4583..40917bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/pancake-v4-universal-router"] path = lib/pancake-v4-universal-router url = https://github.com/pancakeswap/pancake-v4-universal-router +[submodule "lib/prb-math"] + path = lib/prb-math + url = https://github.com/PaulRBerg/prb-math diff --git a/lib/prb-math b/lib/prb-math new file mode 160000 index 0000000..39eec81 --- /dev/null +++ b/lib/prb-math @@ -0,0 +1 @@ +Subproject commit 39eec818282a29df7406b8280b29c084c9a3f3b5 From 4e9941a0c5741a85ec01fc064af7d6fdcaefadfe Mon Sep 17 00:00:00 2001 From: Chef Snoopy Date: Thu, 19 Sep 2024 16:29:06 +0800 Subject: [PATCH 4/5] feat: Add dynamic fee first version prb-math: https://github.com/PaulRBerg/prb-math --- src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol | 204 ++++++++++++++++++ src/pool-cl/dynamic-fee/PriceFeed.sol | 36 ++++ .../interfaces/AggregatorV3Interface.sol | 21 ++ .../dynamic-fee/interfaces/IPriceFeed.sol | 12 ++ 4 files changed, 273 insertions(+) create mode 100644 src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol create mode 100644 src/pool-cl/dynamic-fee/PriceFeed.sol create mode 100644 src/pool-cl/dynamic-fee/interfaces/AggregatorV3Interface.sol create mode 100644 src/pool-cl/dynamic-fee/interfaces/IPriceFeed.sol diff --git a/src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol b/src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol new file mode 100644 index 0000000..2c43046 --- /dev/null +++ b/src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +import {CLBaseHook} from "../CLBaseHook.sol"; +import {FullMath} from "pancake-v4-core/src/pool-cl/libraries/FullMath.sol"; +import {FixedPoint96} from "pancake-v4-core/src/pool-cl/libraries/FixedPoint96.sol"; +import {LPFeeLibrary} from "pancake-v4-core/src/libraries/LPFeeLibrary.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "pancake-v4-core/src/types/BeforeSwapDelta.sol"; +import { + HOOKS_BEFORE_INITIALIZE_OFFSET, + HOOKS_BEFORE_SWAP_OFFSET +} from "pancake-v4-core/src/pool-cl/interfaces/ICLHooks.sol"; +import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol"; +import {CLPoolManager} from "pancake-v4-core/src/pool-cl/CLPoolManager.sol"; +import {SD59x18, UNIT, convert, sub, mul, div, inv, exp, lt} from "prb-math/SD59x18.sol"; + +import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; + +contract CLDynamicFeeHook is CLBaseHook { + using PoolIdLibrary for PoolKey; + using LPFeeLibrary for uint24; + + struct PoolInfo { + IPriceFeed priceFeed; + uint24 DFF_max; // in hundredth of bips + } + + struct InitializeHookData { + IPriceFeed priceFeed; + uint24 DFF_max; + } + + struct CallbackData { + address sender; + PoolKey key; + ICLPoolManager.SwapParams params; + bytes hookData; + } + + mapping(PoolId id => PoolInfo poolInfo) public poolInfos; + + uint24 private _fee; + bool private _isSim; + + error NotDynamicFeePool(); + error PriceFeedTokensNotMatch(); + error DFFMaxTooLarge(); + error DFFTooLarge(); + error SwapAndRevert(uint160 sqrtPriceX96); + + constructor(ICLPoolManager poolManager) CLBaseHook(poolManager) {} + + function getHooksRegistrationBitmap() external view override returns (uint16) { + return uint16(1 << HOOKS_BEFORE_INITIALIZE_OFFSET | 1 << HOOKS_BEFORE_SWAP_OFFSET); + } + + function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, bytes calldata hookData) + external + override + poolManagerOnly + returns (bytes4) + { + if (!key.fee.isDynamicLPFee()) { + revert NotDynamicFeePool(); + } + + InitializeHookData memory initializeHookData = abi.decode(hookData, (InitializeHookData)); + + IPriceFeed priceFeed = IPriceFeed(initializeHookData.priceFeed); + if ( + address(priceFeed.token0()) != Currency.unwrap(key.currency0) + || address(priceFeed.token1()) != Currency.unwrap(key.currency1) + ) { + revert PriceFeedTokensNotMatch(); + } + + if (initializeHookData.DFF_max > 1000000) { + revert DFFMaxTooLarge(); + } + + poolInfos[key.toId()] = PoolInfo({priceFeed: priceFeed, DFF_max: initializeHookData.DFF_max}); + + return this.beforeInitialize.selector; + } + + function beforeSwap( + address sender, + PoolKey calldata key, + ICLPoolManager.SwapParams calldata params, + bytes calldata hookData + ) external override returns (bytes4, BeforeSwapDelta, uint24) { + uint24 staticFee = key.fee & (~LPFeeLibrary.DYNAMIC_FEE_FLAG); + + if (_isSim) { + _fee = staticFee; + poolManager.updateDynamicLPFee(key, _fee); + + return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + PoolId id = key.toId(); + + (uint160 sqrtPriceX96Before,,,) = poolManager.getSlot0(id); + uint160 sqrtPriceX96After = _simulateSwap(key, params, hookData); + + uint160 priceX96Before = uint160(FullMath.mulDiv(sqrtPriceX96Before, sqrtPriceX96Before, FixedPoint96.Q96)); + uint160 priceX96After = uint160(FullMath.mulDiv(sqrtPriceX96After, sqrtPriceX96After, FixedPoint96.Q96)); + + PoolInfo memory poolInfo = poolInfos[id]; + uint256 priceX96Oracle = poolInfo.priceFeed.getPriceX96(); + + uint256 sfX96; + { + if (priceX96After > priceX96Before && priceX96Oracle > priceX96Before) { + sfX96 = + FullMath.mulDiv(priceX96After - priceX96Before, FixedPoint96.Q96, priceX96Oracle - priceX96Before); + } + if (priceX96After < priceX96Before && priceX96Oracle < priceX96Before) { + sfX96 = + FullMath.mulDiv(priceX96Before - priceX96After, FixedPoint96.Q96, priceX96Before - priceX96Oracle); + } + } + + uint256 ipX96; + { + uint256 r = FullMath.mulDiv(priceX96Before, FixedPoint96.Q96, priceX96Oracle); + ipX96 = r > FixedPoint96.Q96 ? r - FixedPoint96.Q96 : FixedPoint96.Q96 - r; + } + + uint256 pifX96 = FullMath.mulDiv(sfX96, ipX96, FixedPoint96.Q96); + + SD59x18 DFF; + uint256 fX96 = FullMath.mulDiv(key.fee.getInitialLPFee(), FixedPoint96.Q96, 1_000_000); + if (pifX96 > fX96) { + SD59x18 inter = inv( + exp( + convert(int256(FullMath.mulDiv(pifX96 - fX96, FixedPoint96.Q96, fX96))) + / convert(int256(FixedPoint96.Q96)) + ) + ); + if (inter < UNIT) { + DFF = convert(int256(int24(poolInfo.DFF_max))) * (UNIT - inter); + } + } + + if (DFF.isZero()) { + _fee = staticFee; + poolManager.updateDynamicLPFee(key, _fee); + + return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + if (DFF > convert(1_000_000)) { + revert DFFTooLarge(); + } + + _fee = uint24(int24(convert(DFF))); + poolManager.updateDynamicLPFee(key, _fee); + + return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + /// @dev Simulate `swap` + function _simulateSwap(PoolKey calldata key, ICLPoolManager.SwapParams calldata params, bytes calldata hookData) + internal + returns (uint160 sqrtPriceX96) + { + _isSim = true; + // TODO: Ugly, should add vault() to IFees interface! + try CLPoolManager(address(poolManager)).vault().lock( + abi.encode(CallbackData({sender: msg.sender, key: key, params: params, hookData: hookData})) + ) { + revert(); + } catch (bytes memory reason) { + bytes4 selector; + assembly { + selector := mload(add(reason, 0x20)) + } + if (selector != SwapAndRevert.selector) { + revert(); + } + // Extract data by trimming the custom error selector (first 4 bytes) + bytes memory data = new bytes(reason.length - 4); + for (uint256 i = 4; i < reason.length; ++i) { + data[i - 4] = reason[i]; + } + sqrtPriceX96 = abi.decode(data, (uint160)); + } + _isSim = false; + } + + /// @dev Revert a custom error on purpose to achieve simulation of `swap` + function lockAcquired(bytes calldata rawData) external override returns (bytes memory) { + CallbackData memory data = abi.decode(rawData, (CallbackData)); + + poolManager.swap(data.key, data.params, data.hookData); + + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(data.key.toId()); + revert SwapAndRevert(sqrtPriceX96); + } +} diff --git a/src/pool-cl/dynamic-fee/PriceFeed.sol b/src/pool-cl/dynamic-fee/PriceFeed.sol new file mode 100644 index 0000000..7ee7226 --- /dev/null +++ b/src/pool-cl/dynamic-fee/PriceFeed.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {AggregatorV3Interface} from "./interfaces/AggregatorV3Interface.sol"; +import {FullMath} from "pancake-v4-core/src/pool-cl/libraries/FullMath.sol"; +import {FixedPoint96} from "pancake-v4-core/src/pool-cl/libraries/FixedPoint96.sol"; + +import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; + +contract PriceFeed is IPriceFeed { + IERC20Metadata public immutable token0; + IERC20Metadata public immutable token1; + + AggregatorV3Interface public immutable oracle; + + constructor(address token0_, address token1_, address oracle_) { + if (token0_ > token1_) { + (token0_, token1_) = (token1_, token0_); + } + token0 = IERC20Metadata(token0_); + token1 = IERC20Metadata(token1_); + oracle = AggregatorV3Interface(oracle_); + } + + /// @dev Override if the oracle base quote tokens do not match the order of + /// token0 and token1, i.e., the price from oracle needs to be inversed, or + /// if there is no corresponding oracle for token0 token1 pair so that + /// combination of two oracles is required + function getPriceX96() external view virtual returns (uint160 priceX96) { + (, int256 answer,,,) = oracle.latestRoundData(); + priceX96 = uint160(FullMath.mulDiv(uint256(answer), FixedPoint96.Q96, 10 ** oracle.decimals())); + priceX96 = uint160(FullMath.mulDiv(priceX96, token1.decimals(), token0.decimals())); + // TODO: Is it better to cache the result? + } +} diff --git a/src/pool-cl/dynamic-fee/interfaces/AggregatorV3Interface.sol b/src/pool-cl/dynamic-fee/interfaces/AggregatorV3Interface.sol new file mode 100644 index 0000000..aff76d4 --- /dev/null +++ b/src/pool-cl/dynamic-fee/interfaces/AggregatorV3Interface.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @dev Copy from @chainlink/contracts(1.2.0) +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData(uint80 _roundId) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} diff --git a/src/pool-cl/dynamic-fee/interfaces/IPriceFeed.sol b/src/pool-cl/dynamic-fee/interfaces/IPriceFeed.sol new file mode 100644 index 0000000..709887f --- /dev/null +++ b/src/pool-cl/dynamic-fee/interfaces/IPriceFeed.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +interface IPriceFeed { + function token0() external view returns (IERC20Metadata); + + function token1() external view returns (IERC20Metadata); + + function getPriceX96() external view returns (uint160 priceX96); +} From c8dc695b8bac6c1c77ed61ad81db8c9c8f36674b Mon Sep 17 00:00:00 2001 From: Chef Snoopy Date: Fri, 20 Sep 2024 12:34:18 +0800 Subject: [PATCH 5/5] feat: Update latest version --- src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol | 89 +++++++++++++------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol b/src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol index 2c43046..814b772 100644 --- a/src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol +++ b/src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import {CLBaseHook} from "../CLBaseHook.sol"; import {FullMath} from "pancake-v4-core/src/pool-cl/libraries/FullMath.sol"; @@ -9,10 +9,6 @@ import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "pancake-v4-core/src/types/BeforeSwapDelta.sol"; -import { - HOOKS_BEFORE_INITIALIZE_OFFSET, - HOOKS_BEFORE_SWAP_OFFSET -} from "pancake-v4-core/src/pool-cl/interfaces/ICLHooks.sol"; import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol"; import {CLPoolManager} from "pancake-v4-core/src/pool-cl/CLPoolManager.sol"; import {SD59x18, UNIT, convert, sub, mul, div, inv, exp, lt} from "prb-math/SD59x18.sol"; @@ -23,14 +19,16 @@ contract CLDynamicFeeHook is CLBaseHook { using PoolIdLibrary for PoolKey; using LPFeeLibrary for uint24; - struct PoolInfo { + struct PoolConfig { IPriceFeed priceFeed; uint24 DFF_max; // in hundredth of bips + uint24 baseLpFee; } struct InitializeHookData { IPriceFeed priceFeed; uint24 DFF_max; + uint24 baseLpFee; } struct CallbackData { @@ -40,21 +38,49 @@ contract CLDynamicFeeHook is CLBaseHook { bytes hookData; } - mapping(PoolId id => PoolInfo poolInfo) public poolInfos; + // ============================== Variables ================================ - uint24 private _fee; + mapping(PoolId id => PoolConfig) public poolConfigs; + + // TODO: Make it transient bool private _isSim; + // ============================== Events =================================== + + // ============================== Errors =================================== + error NotDynamicFeePool(); error PriceFeedTokensNotMatch(); error DFFMaxTooLarge(); + error BaseLpFeeTooLarge(); error DFFTooLarge(); error SwapAndRevert(uint160 sqrtPriceX96); + // ============================== Modifiers ================================ + + // ========================= External Functions ============================ + constructor(ICLPoolManager poolManager) CLBaseHook(poolManager) {} - function getHooksRegistrationBitmap() external view override returns (uint16) { - return uint16(1 << HOOKS_BEFORE_INITIALIZE_OFFSET | 1 << HOOKS_BEFORE_SWAP_OFFSET); + function getHooksRegistrationBitmap() external pure override returns (uint16) { + return _hooksRegistrationBitmapFrom( + Permissions({ + beforeInitialize: true, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnsDelta: false, + afterSwapReturnsDelta: false, + afterAddLiquidiyReturnsDelta: false, + afterRemoveLiquidiyReturnsDelta: false + }) + ); } function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, bytes calldata hookData) @@ -77,11 +103,21 @@ contract CLDynamicFeeHook is CLBaseHook { revert PriceFeedTokensNotMatch(); } - if (initializeHookData.DFF_max > 1000000) { + if (initializeHookData.DFF_max > 1_000_000) { revert DFFMaxTooLarge(); } - poolInfos[key.toId()] = PoolInfo({priceFeed: priceFeed, DFF_max: initializeHookData.DFF_max}); + if (initializeHookData.baseLpFee > 1_000_000) { + revert BaseLpFeeTooLarge(); + } + + poolConfigs[key.toId()] = PoolConfig({ + priceFeed: priceFeed, + DFF_max: initializeHookData.DFF_max, + baseLpFee: initializeHookData.baseLpFee + }); + + poolManager.updateDynamicLPFee(key, initializeHookData.baseLpFee); return this.beforeInitialize.selector; } @@ -92,25 +128,21 @@ contract CLDynamicFeeHook is CLBaseHook { ICLPoolManager.SwapParams calldata params, bytes calldata hookData ) external override returns (bytes4, BeforeSwapDelta, uint24) { - uint24 staticFee = key.fee & (~LPFeeLibrary.DYNAMIC_FEE_FLAG); + PoolId id = key.toId(); + PoolConfig memory poolConfig = poolConfigs[id]; + uint24 baseLpFee = poolConfig.baseLpFee; if (_isSim) { - _fee = staticFee; - poolManager.updateDynamicLPFee(key, _fee); - return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } - PoolId id = key.toId(); - (uint160 sqrtPriceX96Before,,,) = poolManager.getSlot0(id); uint160 sqrtPriceX96After = _simulateSwap(key, params, hookData); uint160 priceX96Before = uint160(FullMath.mulDiv(sqrtPriceX96Before, sqrtPriceX96Before, FixedPoint96.Q96)); uint160 priceX96After = uint160(FullMath.mulDiv(sqrtPriceX96After, sqrtPriceX96After, FixedPoint96.Q96)); - PoolInfo memory poolInfo = poolInfos[id]; - uint256 priceX96Oracle = poolInfo.priceFeed.getPriceX96(); + uint256 priceX96Oracle = poolConfig.priceFeed.getPriceX96(); uint256 sfX96; { @@ -133,7 +165,7 @@ contract CLDynamicFeeHook is CLBaseHook { uint256 pifX96 = FullMath.mulDiv(sfX96, ipX96, FixedPoint96.Q96); SD59x18 DFF; - uint256 fX96 = FullMath.mulDiv(key.fee.getInitialLPFee(), FixedPoint96.Q96, 1_000_000); + uint256 fX96 = FullMath.mulDiv(baseLpFee, FixedPoint96.Q96, 1_000_000); if (pifX96 > fX96) { SD59x18 inter = inv( exp( @@ -142,14 +174,11 @@ contract CLDynamicFeeHook is CLBaseHook { ) ); if (inter < UNIT) { - DFF = convert(int256(int24(poolInfo.DFF_max))) * (UNIT - inter); + DFF = convert(int256(int24(poolConfig.DFF_max))) * (UNIT - inter); } } if (DFF.isZero()) { - _fee = staticFee; - poolManager.updateDynamicLPFee(key, _fee); - return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } @@ -157,20 +186,20 @@ contract CLDynamicFeeHook is CLBaseHook { revert DFFTooLarge(); } - _fee = uint24(int24(convert(DFF))); - poolManager.updateDynamicLPFee(key, _fee); + uint24 lpFee = uint24(int24(convert(DFF))); - return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFee | LPFeeLibrary.OVERRIDE_FEE_FLAG); } + // ========================= Internal Functions ============================ + /// @dev Simulate `swap` function _simulateSwap(PoolKey calldata key, ICLPoolManager.SwapParams calldata params, bytes calldata hookData) internal returns (uint160 sqrtPriceX96) { _isSim = true; - // TODO: Ugly, should add vault() to IFees interface! - try CLPoolManager(address(poolManager)).vault().lock( + try poolManager.vault().lock( abi.encode(CallbackData({sender: msg.sender, key: key, params: params, hookData: hookData})) ) { revert();