Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/dynamic fee first version #15

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/pancake-v4-universal-router
Submodule pancake-v4-universal-router updated 47 files
+1 −1 .forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeIn.snap
+1 −1 .forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeOut_RouterRecipient.snap
+1 −1 .forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactInSingle.snap
+1 −1 .forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_MultiHop.snap
+1 −1 .forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_SingleHop.snap
+1 −1 .forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_MultiHop.snap
+1 −1 .forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_SingleHop.snap
+1 −1 .forge-snapshots/BinPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap
+1 −1 .forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeIn.snap
+1 −1 .forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeOut.snap
+1 −1 .forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactInSingle.snap
+1 −1 .forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_MultiHop.snap
+1 −1 .forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_SingleHop.snap
+1 −1 .forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap
+1 −1 .forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_MultiHop.snap
+1 −1 .forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_SingleHop.snap
+1 −1 .forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactInput0For1.snap
+1 −1 .forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactOutput0For1.snap
+1 −1 .forge-snapshots/PancakeSwapV3Test#test_v3Swap_ExactInput0For1.snap
+1 −1 .forge-snapshots/PancakeSwapV3Test#test_v3Swap_exactOutput0For1.snap
+1 −1 .forge-snapshots/StableSwapTest#test_stableSwap_ExactInput0For1.snap
+1 −1 .forge-snapshots/StableSwapTest#test_stableSwap_ExactInput1For0.snap
+1 −1 .forge-snapshots/UniversalRouterBytecodeSize.snap
+1 −1 .forge-snapshots/UniversalRouterTest#test_sweep_token.snap
+1 −1 .forge-snapshots/V3ToV4MigrationNativeTest#test_v4CLPositionmanager_Mint_Native.snap
+1 −1 .forge-snapshots/V3ToV4MigrationTest#test_v3PositionManager_burn.snap
+1 −1 .forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity.snap
+1 −1 .forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity_Native.snap
+1 −1 .forge-snapshots/V3ToV4MigrationTest#test_v4CLPositionmanager_Mint.snap
+1 −1 lib/pancake-v4-periphery
+11 −15 src/UniversalRouter.sol
+0 −13 src/base/Callbacks.sol
+66 −33 src/base/Dispatcher.sol
+4 −0 src/base/Lock.sol
+21 −34 src/libraries/BytesLib.sol
+8 −21 src/libraries/Commands.sol
+0 −1 src/modules/Payments.sol
+1 −0 src/modules/Permit2Payments.sol
+4 −0 src/modules/V3ToV4Migrator.sol
+2 −0 src/modules/pancakeswap/v3/V3SwapRouter.sol
+205 −10 test/UniversalRouter.t.sol
+4 −6 test/V3ToV4Migration.t.sol
+4 −6 test/V3ToV4MigrationNative.t.sol
+1 −1 test/v4/BinNativePancakeSwapV4.t.sol
+1 −1 test/v4/BinPancakeSwapV4.t.sol
+4 −6 test/v4/CLNativePancakeSwapV4.t.sol
+4 −6 test/v4/CLPancakeSwapV4.t.sol
1 change: 1 addition & 0 deletions lib/prb-math
Submodule prb-math added at 39eec8
2 changes: 2 additions & 0 deletions src/pool-cl/CLBaseHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down
204 changes: 204 additions & 0 deletions src/pool-cl/dynamic-fee/CLDynamicFeeHook.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
36 changes: 36 additions & 0 deletions src/pool-cl/dynamic-fee/PriceFeed.sol
Original file line number Diff line number Diff line change
@@ -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?
}
}
21 changes: 21 additions & 0 deletions src/pool-cl/dynamic-fee/interfaces/AggregatorV3Interface.sol
Original file line number Diff line number Diff line change
@@ -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);
}
12 changes: 12 additions & 0 deletions src/pool-cl/dynamic-fee/interfaces/IPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion test/pool-bin/helpers/Deployers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
1 change: 0 additions & 1 deletion test/pool-cl/CLFullRangeHook.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading