From 4e77c83cadc774f7726cac48f8791ad4c72b0864 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:42:26 -0400 Subject: [PATCH] Cache dynamic fee in slot0 (#360) --- ...swap hook, already cached dynamic fee.snap | 1 + .../cached dynamic fee, no hooks.snap | 1 + .forge-snapshots/donate gas with 1 token.snap | 2 +- .../donate gas with 2 tokens.snap | 2 +- .forge-snapshots/initialize.snap | 2 +- .forge-snapshots/poolExtsloadSlot0.snap | 1 - .../poolExtsloadTickInfoStruct.snap | 1 - .forge-snapshots/simple swap.snap | 2 +- ...p against liquidity with native token.snap | 2 +- .forge-snapshots/swap against liquidity.snap | 2 +- .forge-snapshots/swap with dynamic fee.snap | 2 +- .forge-snapshots/swap with hooks.snap | 2 +- .forge-snapshots/swap with native.snap | 2 +- .../update dynamic fee in before swap.snap | 1 + contracts/Fees.sol | 9 ++ contracts/PoolManager.sol | 43 ++++--- contracts/interfaces/IDynamicFeeManager.sol | 4 +- contracts/interfaces/IFees.sol | 2 + contracts/interfaces/IPoolManager.sol | 5 + contracts/libraries/Pool.sol | 31 ++++- contracts/test/DynamicFeesTest.sol | 47 ++++++++ .../PoolManager.gas.spec.ts.snap | 38 +++--- test/__snapshots__/PoolManager.spec.ts.snap | 2 +- test/foundry-tests/DynamicFees.t.sol | 108 +++++++++++++----- test/foundry-tests/Pool.t.sol | 16 +-- test/foundry-tests/utils/Deployers.sol | 8 +- 26 files changed, 239 insertions(+), 97 deletions(-) create mode 100644 .forge-snapshots/before swap hook, already cached dynamic fee.snap create mode 100644 .forge-snapshots/cached dynamic fee, no hooks.snap delete mode 100644 .forge-snapshots/poolExtsloadSlot0.snap delete mode 100644 .forge-snapshots/poolExtsloadTickInfoStruct.snap create mode 100644 .forge-snapshots/update dynamic fee in before swap.snap create mode 100644 contracts/test/DynamicFeesTest.sol diff --git a/.forge-snapshots/before swap hook, already cached dynamic fee.snap b/.forge-snapshots/before swap hook, already cached dynamic fee.snap new file mode 100644 index 000000000..4286be758 --- /dev/null +++ b/.forge-snapshots/before swap hook, already cached dynamic fee.snap @@ -0,0 +1 @@ +85384 \ No newline at end of file diff --git a/.forge-snapshots/cached dynamic fee, no hooks.snap b/.forge-snapshots/cached dynamic fee, no hooks.snap new file mode 100644 index 000000000..498e90680 --- /dev/null +++ b/.forge-snapshots/cached dynamic fee, no hooks.snap @@ -0,0 +1 @@ +82539 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 1 token.snap b/.forge-snapshots/donate gas with 1 token.snap index c6c9a5eca..506b62cf5 100644 --- a/.forge-snapshots/donate gas with 1 token.snap +++ b/.forge-snapshots/donate gas with 1 token.snap @@ -1 +1 @@ -96005 \ No newline at end of file +95983 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 2 tokens.snap b/.forge-snapshots/donate gas with 2 tokens.snap index 0410160a8..a947c028a 100644 --- a/.forge-snapshots/donate gas with 2 tokens.snap +++ b/.forge-snapshots/donate gas with 2 tokens.snap @@ -1 +1 @@ -153358 \ No newline at end of file +153336 \ No newline at end of file diff --git a/.forge-snapshots/initialize.snap b/.forge-snapshots/initialize.snap index 6e9819a96..dd943dc86 100644 --- a/.forge-snapshots/initialize.snap +++ b/.forge-snapshots/initialize.snap @@ -1 +1 @@ -38009 \ No newline at end of file +38126 \ No newline at end of file diff --git a/.forge-snapshots/poolExtsloadSlot0.snap b/.forge-snapshots/poolExtsloadSlot0.snap deleted file mode 100644 index 691cbe1f9..000000000 --- a/.forge-snapshots/poolExtsloadSlot0.snap +++ /dev/null @@ -1 +0,0 @@ -1151 diff --git a/.forge-snapshots/poolExtsloadTickInfoStruct.snap b/.forge-snapshots/poolExtsloadTickInfoStruct.snap deleted file mode 100644 index ece5108dd..000000000 --- a/.forge-snapshots/poolExtsloadTickInfoStruct.snap +++ /dev/null @@ -1 +0,0 @@ -2785 diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index 3c61501a3..a85af06f0 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -49720 \ No newline at end of file +49771 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity with native token.snap b/.forge-snapshots/swap against liquidity with native token.snap index 1595951f2..1496e4d38 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -124982 \ No newline at end of file +125033 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index a8deb1131..c76e3582a 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -109692 \ No newline at end of file +109743 \ No newline at end of file diff --git a/.forge-snapshots/swap with dynamic fee.snap b/.forge-snapshots/swap with dynamic fee.snap index 6bcef9952..5132f8c5f 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -91502 \ No newline at end of file +84493 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index a1d40464f..9e83cf7c0 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -49691 \ No newline at end of file +49742 \ No newline at end of file diff --git a/.forge-snapshots/swap with native.snap b/.forge-snapshots/swap with native.snap index 3c61501a3..a85af06f0 100644 --- a/.forge-snapshots/swap with native.snap +++ b/.forge-snapshots/swap with native.snap @@ -1 +1 @@ -49720 \ No newline at end of file +49771 \ No newline at end of file diff --git a/.forge-snapshots/update dynamic fee in before swap.snap b/.forge-snapshots/update dynamic fee in before swap.snap new file mode 100644 index 000000000..9fc0fc1a0 --- /dev/null +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -0,0 +1 @@ +91231 \ No newline at end of file diff --git a/contracts/Fees.sol b/contracts/Fees.sol index e8c5255f3..72b79f298 100644 --- a/contracts/Fees.sol +++ b/contracts/Fees.sol @@ -9,6 +9,7 @@ import {FeeLibrary} from "./libraries/FeeLibrary.sol"; import {Pool} from "./libraries/Pool.sol"; import {PoolKey} from "./types/PoolKey.sol"; import {Owned} from "./Owned.sol"; +import {IDynamicFeeManager} from "./interfaces/IDynamicFeeManager.sol"; abstract contract Fees is IFees, Owned { using FeeLibrary for uint24; @@ -16,6 +17,9 @@ abstract contract Fees is IFees, Owned { uint8 public constant MIN_PROTOCOL_FEE_DENOMINATOR = 4; + // the swap fee is represented in hundredths of a bip, so the max is 100% + uint24 public constant MAX_SWAP_FEE = 1000000; + mapping(Currency currency => uint256) public protocolFeesAccrued; mapping(address hookAddress => mapping(Currency currency => uint256)) public hookFeesAccrued; @@ -61,6 +65,11 @@ abstract contract Fees is IFees, Owned { } } + function _fetchDynamicSwapFee(PoolKey memory key) internal view returns (uint24 dynamicSwapFee) { + dynamicSwapFee = IDynamicFeeManager(address(key.hooks)).getFee(msg.sender, key); + if (dynamicSwapFee >= MAX_SWAP_FEE) revert FeeTooLarge(); + } + /// @dev Only the lower 12 bits are used here to encode the fee denominator. function _checkProtocolFee(uint16 fee) internal pure { if (fee != 0) { diff --git a/contracts/PoolManager.sol b/contracts/PoolManager.sol index 448821834..359fbada1 100644 --- a/contracts/PoolManager.sol +++ b/contracts/PoolManager.sol @@ -120,9 +120,10 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC1155, IERC1155Rec } PoolId id = key.toId(); - uint24 protocolFees = _fetchProtocolFees(key); - uint24 hookFees = _fetchHookFees(key); - tick = pools[id].initialize(sqrtPriceX96, protocolFees, hookFees); + + uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee(); + + tick = pools[id].initialize(sqrtPriceX96, _fetchProtocolFees(key), _fetchHookFees(key), swapFee); if (key.hooks.shouldCallAfterInitialize()) { if ( @@ -133,6 +134,7 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC1155, IERC1155Rec } } + // On intitalize we emit the key's fee, which tells us all fee settings a pool can have: either a static swap fee or dynamic swap fee and if the hook has enabled swap or withdraw fees. emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks); } @@ -251,23 +253,14 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC1155, IERC1155Rec } } - // Set the total swap fee, either through the hook or as the static fee set an initialization. - uint24 totalSwapFee; - if (key.fee.isDynamicFee()) { - totalSwapFee = IDynamicFeeManager(address(key.hooks)).getFee(msg.sender, key, params, hookData); - if (totalSwapFee >= 1000000) revert FeeTooLarge(); - } else { - // clear the top 4 bits since they may be flagged for hook fees - totalSwapFee = key.fee.getStaticFee(); - } + PoolId id = key.toId(); uint256 feeForProtocol; uint256 feeForHook; + uint24 swapFee; Pool.SwapState memory state; - PoolId id = key.toId(); - (delta, feeForProtocol, feeForHook, state) = pools[id].swap( + (delta, feeForProtocol, feeForHook, swapFee, state) = pools[id].swap( Pool.SwapParams({ - fee: totalSwapFee, tickSpacing: key.tickSpacing, zeroForOne: params.zeroForOne, amountSpecified: params.amountSpecified, @@ -294,14 +287,7 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC1155, IERC1155Rec } emit Swap( - id, - msg.sender, - delta.amount0(), - delta.amount1(), - state.sqrtPriceX96, - state.liquidity, - state.tick, - totalSwapFee + id, msg.sender, delta.amount0(), delta.amount1(), state.sqrtPriceX96, state.liquidity, state.tick, swapFee ); } @@ -388,6 +374,17 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC1155, IERC1155Rec emit HookFeeUpdated(id, newHookFees); } + function updateDynamicSwapFee(PoolKey memory key) external { + if (key.fee.isDynamicFee()) { + uint24 newDynamicSwapFee = _fetchDynamicSwapFee(key); + PoolId id = key.toId(); + pools[id].setSwapFee(newDynamicSwapFee); + emit DynamicSwapFeeUpdated(id, newDynamicSwapFee); + } else { + revert FeeNotDynamic(); + } + } + function extsload(bytes32 slot) external view returns (bytes32 value) { /// @solidity memory-safe-assembly assembly { diff --git a/contracts/interfaces/IDynamicFeeManager.sol b/contracts/interfaces/IDynamicFeeManager.sol index f56cd6dfe..e3e93736b 100644 --- a/contracts/interfaces/IDynamicFeeManager.sol +++ b/contracts/interfaces/IDynamicFeeManager.sol @@ -7,7 +7,5 @@ import {IPoolManager} from "./IPoolManager.sol"; /// @notice The dynamic fee manager determines fees for pools /// @dev note that this pool is only called if the PoolKey fee value is equal to the DYNAMIC_FEE magic value interface IDynamicFeeManager { - function getFee(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata data) - external - returns (uint24); + function getFee(address sender, PoolKey calldata key) external view returns (uint24); } diff --git a/contracts/interfaces/IFees.sol b/contracts/interfaces/IFees.sol index 4c7ad2d1b..943f6d4cd 100644 --- a/contracts/interfaces/IFees.sol +++ b/contracts/interfaces/IFees.sol @@ -8,6 +8,8 @@ interface IFees { error FeeTooLarge(); /// @notice Thrown when not enough gas is provided to look up the protocol fee error ProtocolFeeCannotBeFetched(); + /// @notice Thrown when a pool does not have a dynamic fee. + error FeeNotDynamic(); event ProtocolFeeControllerUpdated(address protocolFeeController); diff --git a/contracts/interfaces/IPoolManager.sol b/contracts/interfaces/IPoolManager.sol index 9206678b5..4d4160a7d 100644 --- a/contracts/interfaces/IPoolManager.sol +++ b/contracts/interfaces/IPoolManager.sol @@ -82,6 +82,8 @@ interface IPoolManager is IFees, IERC1155 { event HookFeeUpdated(PoolId indexed id, uint24 hookFees); + event DynamicSwapFeeUpdated(PoolId indexed id, uint24 dynamicSwapFee); + /// @notice Returns the constant representing the maximum tickSpacing for an initialized pool key function MAX_TICK_SPACING() external view returns (int24); @@ -187,6 +189,9 @@ interface IPoolManager is IFees, IERC1155 { /// @notice Sets the hook's swap and withdrawal fees for the given pool function setHookFees(PoolKey memory key) external; + /// @notice Updates the pools swap fees for the a pool that has enabled dynamic swap fees. + function updateDynamicSwapFee(PoolKey memory key) external; + /// @notice Called by external contracts to access granular pool state /// @param slot Key of slot to sload /// @return value The value of the slot as bytes32 diff --git a/contracts/libraries/Pool.sol b/contracts/libraries/Pool.sol index 12a87c862..a2786515d 100644 --- a/contracts/libraries/Pool.sol +++ b/contracts/libraries/Pool.sol @@ -74,8 +74,9 @@ library Pool { int24 tick; uint24 protocolFees; uint24 hookFees; + // used for the swap fee, either static at initialize or dynamic via hook + uint24 swapFee; } - // 24 bits left! // info stored for each initialized individual tick struct TickInfo { @@ -107,7 +108,7 @@ library Pool { if (tickUpper > TickMath.MAX_TICK) revert TickUpperOutOfBounds(tickUpper); } - function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFees, uint24 hookFees) + function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFees, uint24 hookFees, uint24 swapFee) internal returns (int24 tick) { @@ -115,7 +116,13 @@ library Pool { tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); - self.slot0 = Slot0({sqrtPriceX96: sqrtPriceX96, tick: tick, protocolFees: protocolFees, hookFees: hookFees}); + self.slot0 = Slot0({ + sqrtPriceX96: sqrtPriceX96, + tick: tick, + protocolFees: protocolFees, + hookFees: hookFees, + swapFee: swapFee + }); } function getSwapFee(uint24 feesStorage) internal pure returns (uint16) { @@ -138,6 +145,12 @@ library Pool { self.slot0.hookFees = hookFees; } + /// @notice Only dynamic fee pools may update the swap fee. + function setSwapFee(State storage self, uint24 swapFee) internal { + if (self.slot0.sqrtPriceX96 == 0) revert PoolNotInitialized(); + self.slot0.swapFee = swapFee; + } + struct ModifyPositionParams { // the address that owns the position address owner; @@ -367,7 +380,6 @@ library Pool { } struct SwapParams { - uint24 fee; int24 tickSpacing; bool zeroForOne; int256 amountSpecified; @@ -377,11 +389,18 @@ library Pool { /// @dev Executes a swap against the state, and returns the amount deltas of the pool function swap(State storage self, SwapParams memory params) internal - returns (BalanceDelta result, uint256 feeForProtocol, uint256 feeForHook, SwapState memory state) + returns ( + BalanceDelta result, + uint256 feeForProtocol, + uint256 feeForHook, + uint24 swapFee, + SwapState memory state + ) { if (params.amountSpecified == 0) revert SwapAmountCannotBeZero(); Slot0 memory slot0Start = self.slot0; + swapFee = slot0Start.swapFee; if (slot0Start.sqrtPriceX96 == 0) revert PoolNotInitialized(); if (params.zeroForOne) { if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96) { @@ -446,7 +465,7 @@ library Pool { ) ? params.sqrtPriceLimitX96 : step.sqrtPriceNextX96, state.liquidity, state.amountSpecifiedRemaining, - params.fee + swapFee ); if (exactInput) { diff --git a/contracts/test/DynamicFeesTest.sol b/contracts/test/DynamicFeesTest.sol new file mode 100644 index 000000000..756bab730 --- /dev/null +++ b/contracts/test/DynamicFeesTest.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {BaseTestHooks} from "./BaseTestHooks.sol"; +import {IDynamicFeeManager} from "../interfaces/IDynamicFeeManager.sol"; +import {PoolKey} from "../types/PoolKey.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {IHooks} from "../interfaces/IHooks.sol"; + +contract DynamicFeesTest is BaseTestHooks, IDynamicFeeManager { + uint24 internal fee; + IPoolManager manager; + + constructor() {} + + function setManager(IPoolManager _manager) external { + manager = _manager; + } + + function setFee(uint24 _fee) external { + fee = _fee; + } + + function getFee(address, PoolKey calldata) public view returns (uint24) { + return fee; + } + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata hookData) + external + override + returns (bytes4) + { + // updates the dynamic fee in the pool if update is true + bool _update; + uint24 _fee; + + if (hookData.length > 0) { + (_update, _fee) = abi.decode(hookData, (bool, uint24)); + } + if (_update == true) { + fee = _fee; + + manager.updateDynamicSwapFee(key); + } + return IHooks.beforeSwap.selector; + } +} diff --git a/test/__snapshots__/PoolManager.gas.spec.ts.snap b/test/__snapshots__/PoolManager.gas.spec.ts.snap index e9eda67fc..a89a29cb4 100644 --- a/test/__snapshots__/PoolManager.gas.spec.ts.snap +++ b/test/__snapshots__/PoolManager.gas.spec.ts.snap @@ -3,7 +3,7 @@ exports[`PoolManager gas tests ERC20 tokens #initialize initialize pool with no hooks and no protocol fee 1`] = ` Object { "calldataByteLength": 292, - "gasUsed": 55149, + "gasUsed": 55267, } `; @@ -73,63 +73,63 @@ Object { exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 first swap in block moves tick, no initialized crossings 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 161498, + "gasUsed": 161519, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 first swap in block with no tick movement 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 155793, + "gasUsed": 155824, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 first swap in block, large swap crossing a single initialized tick 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 182491, + "gasUsed": 182502, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 first swap in block, large swap crossing several initialized ticks 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 223959, + "gasUsed": 223941, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 first swap in block, large swap, no initialized crossings 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 173101, + "gasUsed": 173103, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 second swap in block moves tick, no initialized crossings 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 161498, + "gasUsed": 161519, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 second swap in block with no tick movement 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 155881, + "gasUsed": 155912, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 second swap in block, large swap crossing a single initialized tick 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 174663, + "gasUsed": 174684, } `; exports[`PoolManager gas tests ERC20 tokens #swapExact0For1 second swap in block, large swap crossing several initialized ticks 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 216106, + "gasUsed": 216098, } `; @@ -199,62 +199,62 @@ Object { exports[`PoolManager gas tests Native Tokens #swapExact0For1 first swap in block moves tick, no initialized crossings 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 148865, + "gasUsed": 148886, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 first swap in block with no tick movement 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 143160, + "gasUsed": 143191, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 first swap in block, large swap crossing a single initialized tick 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 169858, + "gasUsed": 169869, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 first swap in block, large swap crossing several initialized ticks 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 211326, + "gasUsed": 211308, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 first swap in block, large swap, no initialized crossings 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 160468, + "gasUsed": 160470, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 second swap in block moves tick, no initialized crossings 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 148865, + "gasUsed": 148886, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 second swap in block with no tick movement 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 143248, + "gasUsed": 143279, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 second swap in block, large swap crossing a single initialized tick 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 162030, + "gasUsed": 162051, } `; exports[`PoolManager gas tests Native Tokens #swapExact0For1 second swap in block, large swap crossing several initialized ticks 1`] = ` Object { "calldataByteLength": 420, - "gasUsed": 203473, + "gasUsed": 203465, } `; diff --git a/test/__snapshots__/PoolManager.spec.ts.snap b/test/__snapshots__/PoolManager.spec.ts.snap index 79c3d9071..b2cef86a7 100644 --- a/test/__snapshots__/PoolManager.spec.ts.snap +++ b/test/__snapshots__/PoolManager.spec.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PoolManager bytecode size 1`] = `28673`; +exports[`PoolManager bytecode size 1`] = `29268`; diff --git a/test/foundry-tests/DynamicFees.t.sol b/test/foundry-tests/DynamicFees.t.sol index cb23e62fa..b86ba6808 100644 --- a/test/foundry-tests/DynamicFees.t.sol +++ b/test/foundry-tests/DynamicFees.t.sol @@ -9,35 +9,29 @@ import {FeeLibrary} from "../../contracts/libraries/FeeLibrary.sol"; import {IPoolManager} from "../../contracts/interfaces/IPoolManager.sol"; import {IFees} from "../../contracts/interfaces/IFees.sol"; import {IHooks} from "../../contracts/interfaces/IHooks.sol"; -import {Currency} from "../../contracts/types/Currency.sol"; import {PoolKey} from "../../contracts/types/PoolKey.sol"; import {PoolManager} from "../../contracts/PoolManager.sol"; import {PoolSwapTest} from "../../contracts/test/PoolSwapTest.sol"; import {Deployers} from "./utils/Deployers.sol"; import {IDynamicFeeManager} from "././../../contracts/interfaces/IDynamicFeeManager.sol"; -import {Fees} from "./../../contracts/Fees.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; - -contract DynamicFees is IDynamicFeeManager { - uint24 internal fee; - - function setFee(uint24 _fee) external { - fee = _fee; - } - - function getFee(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) - public - view - returns (uint24) - { - return fee; - } -} +import {DynamicFeesTest} from "../../contracts/test/DynamicFeesTest.sol"; contract TestDynamicFees is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; - DynamicFees dynamicFees = DynamicFees( + DynamicFeesTest dynamicFees = DynamicFeesTest( + address( + uint160(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF) + & uint160( + ~Hooks.BEFORE_INITIALIZE_FLAG & ~Hooks.AFTER_INITIALIZE_FLAG & ~Hooks.BEFORE_MODIFY_POSITION_FLAG + & ~Hooks.AFTER_MODIFY_POSITION_FLAG & ~Hooks.AFTER_SWAP_FLAG & ~Hooks.BEFORE_DONATE_FLAG + & ~Hooks.AFTER_DONATE_FLAG + ) + ) + ); + + DynamicFeesTest dynamicFeesNoHook = DynamicFeesTest( address( uint160(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF) & uint160( @@ -47,28 +41,41 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { ) ) ); + PoolManager manager; PoolKey key; + PoolKey key2; PoolSwapTest swapRouter; function setUp() public { - DynamicFees impl = new DynamicFees(); + DynamicFeesTest impl = new DynamicFeesTest(); vm.etch(address(dynamicFees), address(impl).code); + vm.etch(address(dynamicFeesNoHook), address(impl).code); (manager, key,) = Deployers.createFreshPool(IHooks(address(dynamicFees)), FeeLibrary.DYNAMIC_FEE_FLAG, SQRT_RATIO_1_1); + dynamicFees.setManager(IPoolManager(manager)); + + PoolId id2; + (key2, id2) = Deployers.createPool( + manager, IHooks(address(dynamicFeesNoHook)), FeeLibrary.DYNAMIC_FEE_FLAG, SQRT_RATIO_1_1 + ); + dynamicFeesNoHook.setManager(IPoolManager(manager)); + swapRouter = new PoolSwapTest(manager); } + function testPoolInitializeFailsWithTooLargeFee() public { + dynamicFees.setFee(1000000); + PoolKey memory key0 = Deployers.createKey(IHooks(address(dynamicFees)), FeeLibrary.DYNAMIC_FEE_FLAG); + vm.expectRevert(IFees.FeeTooLarge.selector); + manager.initialize(key0, SQRT_RATIO_1_1, ZERO_BYTES); + } + function testSwapFailsWithTooLargeFee() public { dynamicFees.setFee(1000000); vm.expectRevert(IFees.FeeTooLarge.selector); - swapRouter.swap( - key, - IPoolManager.SwapParams(false, 1, SQRT_RATIO_1_1 + 1), - PoolSwapTest.TestSettings(false, false), - ZERO_BYTES - ); + manager.updateDynamicSwapFee(key); } event Swap( @@ -84,6 +91,7 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { function testSwapWorks() public { dynamicFees.setFee(123); + manager.updateDynamicSwapFee(key); vm.expectEmit(true, true, true, true, address(manager)); emit Swap(key.toId(), address(swapRouter), 0, 0, SQRT_RATIO_1_1 + 1, 0, 0, 123); snapStart("swap with dynamic fee"); @@ -95,4 +103,52 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { ); snapEnd(); } + + function testCacheDynamicFeeAndSwap() public { + dynamicFees.setFee(123); + manager.updateDynamicSwapFee(key); + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), 0, 0, SQRT_RATIO_1_1 + 1, 0, 0, 456); + snapStart("update dynamic fee in before swap"); + bytes memory data = abi.encode(true, uint24(456)); + swapRouter.swap( + key, IPoolManager.SwapParams(false, 1, SQRT_RATIO_1_1 + 1), PoolSwapTest.TestSettings(false, false), data + ); + snapEnd(); + } + + function testDynamicFeeAndBeforeSwapHook() public { + dynamicFees.setFee(123); + manager.updateDynamicSwapFee(key); + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), 0, 0, SQRT_RATIO_1_1 + 1, 0, 0, 123); + snapStart("before swap hook, already cached dynamic fee"); + bytes memory data = abi.encode(false, uint24(0)); + swapRouter.swap( + key, IPoolManager.SwapParams(false, 1, SQRT_RATIO_1_1 + 1), PoolSwapTest.TestSettings(false, false), data + ); + snapEnd(); + } + + function testUpdateRevertsOnStaticFeePool() public { + (PoolKey memory staticPoolKey, PoolId id) = + Deployers.createPool(manager, IHooks(address(0)), 3000, SQRT_RATIO_1_1); + vm.expectRevert(IFees.FeeNotDynamic.selector); + manager.updateDynamicSwapFee(staticPoolKey); + } + + function testDynamicFeesCacheNoOtherHooks() public { + dynamicFeesNoHook.setFee(123); + manager.updateDynamicSwapFee(key2); + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key2.toId(), address(swapRouter), 0, 0, SQRT_RATIO_1_1 + 1, 0, 0, 123); + snapStart("cached dynamic fee, no hooks"); + swapRouter.swap( + key2, + IPoolManager.SwapParams(false, 1, SQRT_RATIO_1_1 + 1), + PoolSwapTest.TestSettings(false, false), + ZERO_BYTES + ); + snapEnd(); + } } diff --git a/test/foundry-tests/Pool.t.sol b/test/foundry-tests/Pool.t.sol index 1e50e4cbe..4ec7716ab 100644 --- a/test/foundry-tests/Pool.t.sol +++ b/test/foundry-tests/Pool.t.sol @@ -14,7 +14,7 @@ contract PoolTest is Test { Pool.State state; - function testPoolInitialize(uint160 sqrtPriceX96, uint16 protocolFee, uint16 hookFee) public { + function testPoolInitialize(uint160 sqrtPriceX96, uint16 protocolFee, uint16 hookFee, uint24 dynamicFee) public { vm.assume(protocolFee < 2 ** 12 && hookFee < 2 ** 12); if (sqrtPriceX96 < TickMath.MIN_SQRT_RATIO || sqrtPriceX96 >= TickMath.MAX_SQRT_RATIO) { @@ -22,13 +22,15 @@ contract PoolTest is Test { state.initialize( sqrtPriceX96, _formatSwapAndWithdrawFee(protocolFee, protocolFee), - _formatSwapAndWithdrawFee(hookFee, hookFee) + _formatSwapAndWithdrawFee(hookFee, hookFee), + dynamicFee ); } else { state.initialize( sqrtPriceX96, _formatSwapAndWithdrawFee(protocolFee, protocolFee), - _formatSwapAndWithdrawFee(hookFee, hookFee) + _formatSwapAndWithdrawFee(hookFee, hookFee), + dynamicFee ); assertEq(state.slot0.sqrtPriceX96, sqrtPriceX96); assertEq(state.slot0.protocolFees >> 12, protocolFee); @@ -43,7 +45,7 @@ contract PoolTest is Test { vm.assume(params.tickSpacing > 0); vm.assume(params.tickSpacing < 32768); - testPoolInitialize(sqrtPriceX96, 0, 0); + testPoolInitialize(sqrtPriceX96, 0, 0, 0); if (params.tickLower >= params.tickUpper) { vm.expectRevert(abi.encodeWithSelector(Pool.TicksMisordered.selector, params.tickLower, params.tickUpper)); @@ -71,13 +73,13 @@ contract PoolTest is Test { state.modifyPosition(params); } - function testSwap(uint160 sqrtPriceX96, Pool.SwapParams memory params) public { + function testSwap(uint160 sqrtPriceX96, uint24 swapFee, Pool.SwapParams memory params) public { // Assumptions tested in PoolManager.t.sol vm.assume(params.tickSpacing > 0); vm.assume(params.tickSpacing < 32768); - vm.assume(params.fee < 1000000); + vm.assume(swapFee < 1000000); - testPoolInitialize(sqrtPriceX96, 0, 0); + testPoolInitialize(sqrtPriceX96, 0, 0, 0); Pool.Slot0 memory slot0 = state.slot0; if (params.amountSpecified == 0) { diff --git a/test/foundry-tests/utils/Deployers.sol b/test/foundry-tests/utils/Deployers.sol index b4f89de2c..55eadb7d9 100644 --- a/test/foundry-tests/utils/Deployers.sol +++ b/test/foundry-tests/utils/Deployers.sol @@ -37,7 +37,7 @@ contract Deployers { } function createPool(PoolManager manager, IHooks hooks, uint24 fee, uint160 sqrtPriceX96) - private + public returns (PoolKey memory key, PoolId id) { (key, id) = createPool(manager, hooks, fee, sqrtPriceX96, ZERO_BYTES); @@ -54,6 +54,12 @@ contract Deployers { manager.initialize(key, sqrtPriceX96, initData); } + function createKey(IHooks hooks, uint24 fee) internal returns (PoolKey memory key) { + MockERC20[] memory tokens = deployTokens(2, 2 ** 255); + (Currency currency0, Currency currency1) = SortTokens.sort(tokens[0], tokens[1]); + key = PoolKey(currency0, currency1, fee, fee.isDynamicFee() ? int24(60) : int24(fee / 100 * 2), hooks); + } + function createFreshPool(IHooks hooks, uint24 fee, uint160 sqrtPriceX96) internal returns (PoolManager manager, PoolKey memory key, PoolId id)