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..3e292380f 100644 --- a/.forge-snapshots/initialize.snap +++ b/.forge-snapshots/initialize.snap @@ -1 +1 @@ -38009 \ No newline at end of file +38123 \ 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 02d56c5e2..6e6ab8db2 100644 --- a/contracts/PoolManager.sol +++ b/contracts/PoolManager.sol @@ -109,7 +109,7 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC1155, IERC1155Rec // see TickBitmap.sol for overflow conditions that can arise from tick spacing being too large if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge(); if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall(); - if (key.currency0 > key.currency1) revert CurrenciesInitializedOutOfOrder(); + if (key.currency0 >= key.currency1) revert CurrenciesInitializedOutOfOrder(); if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks)); if (key.hooks.shouldCallBeforeInitialize()) { @@ -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 ); } @@ -368,11 +354,8 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC1155, IERC1155Rec returns (bytes4) { if (msg.sender != address(this)) revert NotPoolManagerToken(); - // unchecked to save gas on incrementations of i - unchecked { - for (uint256 i; i < ids.length; i++) { - _burnAndAccount(CurrencyLibrary.fromId(ids[i]), values[i]); - } + for (uint256 i; i < ids.length; i++) { + _burnAndAccount(CurrencyLibrary.fromId(ids[i]), values[i]); } return IERC1155Receiver.onERC1155BatchReceived.selector; } @@ -391,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 f9ddc640e..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); @@ -137,8 +139,8 @@ interface IPoolManager is IFees, IERC1155 { function currencyDelta(address locker, Currency currency) external view returns (int256); /// @notice All operations go through this function - /// @param data Any data to pass to the callback, via `ILockCallback(msg.sender).lockCallback(data)` - /// @return The data returned by the call to `ILockCallback(msg.sender).lockCallback(data)` + /// @param data Any data to pass to the callback, via `ILockCallback(msg.sender).lockAcquired(data)` + /// @return The data returned by the call to `ILockCallback(msg.sender).lockAcquired(data)` function lock(bytes calldata data) external returns (bytes memory); struct ModifyPositionParams { @@ -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/BaseTestHooks.sol b/contracts/test/BaseTestHooks.sol new file mode 100644 index 000000000..7404f5851 --- /dev/null +++ b/contracts/test/BaseTestHooks.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {IHooks} from "../interfaces/IHooks.sol"; +import {PoolKey} from "../types/PoolKey.sol"; +import {BalanceDelta} from "../types/BalanceDelta.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; + +contract BaseTestHooks is IHooks { + error HookNotImplemented(); + + function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, bytes calldata hookData) + external + virtual + returns (bytes4) + { + revert HookNotImplemented(); + } + + function afterInitialize( + address sender, + PoolKey calldata key, + uint160 sqrtPriceX96, + int24 tick, + bytes calldata hookData + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function beforeModifyPosition( + address sender, + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + bytes calldata hookData + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterModifyPosition( + address sender, + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function beforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata hookData + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function beforeDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } +} 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/contracts/types/Currency.sol b/contracts/types/Currency.sol index 9b28d2bca..5d04541c2 100644 --- a/contracts/types/Currency.sol +++ b/contracts/types/Currency.sol @@ -5,7 +5,7 @@ import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; type Currency is address; -using {greaterThan as >, lessThan as <, equals as ==} for Currency global; +using {greaterThan as >, lessThan as <, greaterThanOrEqualTo as >=, equals as ==} for Currency global; function equals(Currency currency, Currency other) pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(other); @@ -19,6 +19,10 @@ function lessThan(Currency currency, Currency other) pure returns (bool) { return Currency.unwrap(currency) < Currency.unwrap(other); } +function greaterThanOrEqualTo(Currency currency, Currency other) pure returns (bool) { + return Currency.unwrap(currency) >= Currency.unwrap(other); +} + /// @title CurrencyLibrary /// @dev This library allows for transferring and holding native tokens and ERC20 tokens library CurrencyLibrary { diff --git a/foundry.toml b/foundry.toml index 04fac2eec..a12de45f7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,11 +1,12 @@ [profile.default] src = 'contracts' out = 'foundry-out' -solc_version = '0.8.20' +solc_version = '0.8.22' optimizer_runs = 800 via_ir = false ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}, { access = "read", path = "./foundry-out"}] +evm_version = 'paris' [profile.ci] fuzz_runs = 100000 diff --git a/justfile b/justfile index 7c66df5a3..d92342ae8 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ build-forge: install-forge build-hardhat: install-hardhat yarn build -snapshots-forge: install-forge +snapshots-forge: install-forge test-forge forge snapshot snapshots-hardhat: install-hardhat diff --git a/test/__snapshots__/PoolManager.gas.spec.ts.snap b/test/__snapshots__/PoolManager.gas.spec.ts.snap index e9eda67fc..bb054b010 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": 55264, } `; @@ -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 6ae7c5cdc..a9f9fa5c0 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`] = `28706`; +exports[`PoolManager bytecode size 1`] = `29267`; 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/PoolManager.t.sol b/test/foundry-tests/PoolManager.t.sol index f900ad747..1f12de125 100644 --- a/test/foundry-tests/PoolManager.t.sol +++ b/test/foundry-tests/PoolManager.t.sol @@ -227,6 +227,19 @@ contract PoolManagerTest is Test, Deployers, TokenFixture, GasSnapshot, IERC1155 assertEq(slot0.sqrtPriceX96, sqrtPriceX96); } + function testPoolManagerInitializeRevertsWithIdenticalTokens(uint160 sqrtPriceX96) public { + // Assumptions tested in Pool.t.sol + vm.assume(sqrtPriceX96 >= TickMath.MIN_SQRT_RATIO); + vm.assume(sqrtPriceX96 < TickMath.MAX_SQRT_RATIO); + + // Both currencies are currency0 + PoolKey memory key = + PoolKey({currency0: currency0, currency1: currency0, fee: 3000, hooks: IHooks(address(0)), tickSpacing: 60}); + + vm.expectRevert(IPoolManager.CurrenciesInitializedOutOfOrder.selector); + manager.initialize(key, sqrtPriceX96, ZERO_BYTES); + } + function testPoolManagerInitializeRevertsWithSameTokenCombo(uint160 sqrtPriceX96) public { // Assumptions tested in Pool.t.sol vm.assume(sqrtPriceX96 >= TickMath.MIN_SQRT_RATIO); diff --git a/test/foundry-tests/utils/Deployers.sol b/test/foundry-tests/utils/Deployers.sol index 658f02617..02b1dcd29 100644 --- a/test/foundry-tests/utils/Deployers.sol +++ b/test/foundry-tests/utils/Deployers.sol @@ -38,7 +38,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); @@ -55,6 +55,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)