From 4e9941a0c5741a85ec01fc064af7d6fdcaefadfe Mon Sep 17 00:00:00 2001 From: Chef Snoopy Date: Thu, 19 Sep 2024 16:29:06 +0800 Subject: [PATCH] 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); +}