diff --git a/.forge-snapshots/SwapMath_oneForZero_exactInCapped.snap b/.forge-snapshots/SwapMath_oneForZero_exactInCapped.snap index 27d48e9a4..19261c267 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactInCapped.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactInCapped.snap @@ -1 +1 @@ -1947 \ No newline at end of file +2248 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap b/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap index 9560ba822..38ef83141 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap @@ -1 +1 @@ -3238 \ No newline at end of file +3038 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_oneForZero_exactOutCapped.snap b/.forge-snapshots/SwapMath_oneForZero_exactOutCapped.snap index 758e03adc..01a646754 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactOutCapped.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactOutCapped.snap @@ -1 +1 @@ -2208 \ No newline at end of file +1987 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap b/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap index 9560ba822..e4da9ac19 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap @@ -1 +1 @@ -3238 \ No newline at end of file +3278 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_zeroForOne_exactInCapped.snap b/.forge-snapshots/SwapMath_zeroForOne_exactInCapped.snap index 12057ad70..b038f6888 100644 --- a/.forge-snapshots/SwapMath_zeroForOne_exactInCapped.snap +++ b/.forge-snapshots/SwapMath_zeroForOne_exactInCapped.snap @@ -1 +1 @@ -1937 \ No newline at end of file +2238 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_zeroForOne_exactInPartial.snap b/.forge-snapshots/SwapMath_zeroForOne_exactInPartial.snap index c173e591f..3832db4f7 100644 --- a/.forge-snapshots/SwapMath_zeroForOne_exactInPartial.snap +++ b/.forge-snapshots/SwapMath_zeroForOne_exactInPartial.snap @@ -1 +1 @@ -2826 \ No newline at end of file +3191 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_zeroForOne_exactOutCapped.snap b/.forge-snapshots/SwapMath_zeroForOne_exactOutCapped.snap index c802063a0..ca9836afa 100644 --- a/.forge-snapshots/SwapMath_zeroForOne_exactOutCapped.snap +++ b/.forge-snapshots/SwapMath_zeroForOne_exactOutCapped.snap @@ -1 +1 @@ -2198 \ No newline at end of file +1977 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_zeroForOne_exactOutPartial.snap b/.forge-snapshots/SwapMath_zeroForOne_exactOutPartial.snap index c173e591f..d872354f5 100644 --- a/.forge-snapshots/SwapMath_zeroForOne_exactOutPartial.snap +++ b/.forge-snapshots/SwapMath_zeroForOne_exactOutPartial.snap @@ -1 +1 @@ -2826 \ No newline at end of file +2866 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity with empty hook.snap b/.forge-snapshots/addLiquidity with empty hook.snap index 20a22caec..628c91c63 100644 --- a/.forge-snapshots/addLiquidity with empty hook.snap +++ b/.forge-snapshots/addLiquidity with empty hook.snap @@ -1 +1 @@ -259318 \ No newline at end of file +259212 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity with native token.snap b/.forge-snapshots/addLiquidity with native token.snap index ae1c48a5d..884af7457 100644 --- a/.forge-snapshots/addLiquidity with native token.snap +++ b/.forge-snapshots/addLiquidity with native token.snap @@ -1 +1 @@ -136661 \ No newline at end of file +136577 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity.snap b/.forge-snapshots/addLiquidity.snap index 6631fc9dc..8505ea3ff 100644 --- a/.forge-snapshots/addLiquidity.snap +++ b/.forge-snapshots/addLiquidity.snap @@ -1 +1 @@ -139495 \ No newline at end of file +139411 \ 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 ba7ceb742..c830a56ed 100644 --- a/.forge-snapshots/donate gas with 1 token.snap +++ b/.forge-snapshots/donate gas with 1 token.snap @@ -1 +1 @@ -98620 \ No newline at end of file +98578 \ 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 ce0b88f83..751929834 100644 --- a/.forge-snapshots/donate gas with 2 tokens.snap +++ b/.forge-snapshots/donate gas with 2 tokens.snap @@ -1 +1 @@ -127073 \ No newline at end of file +126989 \ No newline at end of file diff --git a/.forge-snapshots/erc20 collect protocol fees.snap b/.forge-snapshots/erc20 collect protocol fees.snap index 36cf22341..7cfa73c37 100644 --- a/.forge-snapshots/erc20 collect protocol fees.snap +++ b/.forge-snapshots/erc20 collect protocol fees.snap @@ -1 +1 @@ -24938 \ No newline at end of file +24960 \ No newline at end of file diff --git a/.forge-snapshots/initialize.snap b/.forge-snapshots/initialize.snap index dcb952425..066e657ef 100644 --- a/.forge-snapshots/initialize.snap +++ b/.forge-snapshots/initialize.snap @@ -1 +1 @@ -51159 \ No newline at end of file +51181 \ No newline at end of file diff --git a/.forge-snapshots/native collect protocol fees.snap b/.forge-snapshots/native collect protocol fees.snap index 5d4c1b5c0..af4d450da 100644 --- a/.forge-snapshots/native collect protocol fees.snap +++ b/.forge-snapshots/native collect protocol fees.snap @@ -1 +1 @@ -36611 \ No newline at end of file +36633 \ No newline at end of file diff --git a/.forge-snapshots/poolManager bytecode size.snap b/.forge-snapshots/poolManager bytecode size.snap index 5a9de0185..d7dcb55cc 100644 --- a/.forge-snapshots/poolManager bytecode size.snap +++ b/.forge-snapshots/poolManager bytecode size.snap @@ -1 +1 @@ -22646 \ No newline at end of file +22732 \ No newline at end of file diff --git a/.forge-snapshots/simple swap with native.snap b/.forge-snapshots/simple swap with native.snap index 65d59457a..502a3b11f 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -126742 \ No newline at end of file +126824 \ No newline at end of file diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index a7ec372db..7f82535ca 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -138135 \ No newline at end of file +138217 \ 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 6a1c46e3d..3ac27d2af 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -71077 \ No newline at end of file +71119 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 02dcb6b47..c3e7ce594 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -61396 \ No newline at end of file +61438 \ No newline at end of file diff --git a/.forge-snapshots/swap burn 6909 for input.snap b/.forge-snapshots/swap burn 6909 for input.snap index 9dbc65ab0..0c7863315 100644 --- a/.forge-snapshots/swap burn 6909 for input.snap +++ b/.forge-snapshots/swap burn 6909 for input.snap @@ -1 +1 @@ -79289 \ No newline at end of file +79427 \ No newline at end of file diff --git a/.forge-snapshots/swap burn native 6909 for input.snap b/.forge-snapshots/swap burn native 6909 for input.snap index b4c8b2ad1..63c0bac0c 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -75132 \ No newline at end of file +75270 \ No newline at end of file diff --git a/.forge-snapshots/swap mint native output as 6909.snap b/.forge-snapshots/swap mint native output as 6909.snap index a2fb49618..39a0cd464 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -135515 \ No newline at end of file +135557 \ No newline at end of file diff --git a/.forge-snapshots/swap mint output as 6909.snap b/.forge-snapshots/swap mint output as 6909.snap index e7fba55b3..24bc1d2f4 100644 --- a/.forge-snapshots/swap mint output as 6909.snap +++ b/.forge-snapshots/swap mint output as 6909.snap @@ -1 +1 @@ -152138 \ No newline at end of file +152220 \ No newline at end of file diff --git a/.forge-snapshots/swap skips hook call if hook is caller.snap b/.forge-snapshots/swap skips hook call if hook is caller.snap index 2eea862e0..8adb3d3ea 100644 --- a/.forge-snapshots/swap skips hook call if hook is caller.snap +++ b/.forge-snapshots/swap skips hook call if hook is caller.snap @@ -1 +1 @@ -158641 \ No newline at end of file +158759 \ 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 ffc0b43ea..8e5da1f34 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -90622 \ No newline at end of file +90704 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index 959298d59..b750ea8c7 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -61375 \ No newline at end of file +61417 \ No newline at end of file diff --git a/.forge-snapshots/swap with lp fee and protocol fee.snap b/.forge-snapshots/swap with lp fee and protocol fee.snap new file mode 100644 index 000000000..fef304d99 --- /dev/null +++ b/.forge-snapshots/swap with lp fee and protocol fee.snap @@ -0,0 +1 @@ +151034 \ 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 index 7ff318fbd..d3875f3d7 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -131648 \ No newline at end of file +131731 \ No newline at end of file diff --git a/src/PoolManager.sol b/src/PoolManager.sol index d81b3a83d..f452ffd72 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -5,7 +5,7 @@ import {Hooks} from "./libraries/Hooks.sol"; import {Pool} from "./libraries/Pool.sol"; import {SafeCast} from "./libraries/SafeCast.sol"; import {Position} from "./libraries/Position.sol"; -import {SwapFeeLibrary} from "./libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "./libraries/LPFeeLibrary.sol"; import {Currency, CurrencyLibrary} from "./types/Currency.sol"; import {PoolKey} from "./types/PoolKey.sol"; import {TickMath} from "./libraries/TickMath.sol"; @@ -34,7 +34,7 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim using Position for mapping(bytes32 => Position.Info); using CurrencyLibrary for Currency; using CurrencyDelta for Currency; - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; using PoolGetters for Pool.State; using Reserves for Currency; @@ -57,11 +57,11 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim external view override - returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 swapFee) + returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) { Pool.Slot0 memory slot0 = pools[id].slot0; - return (slot0.sqrtPriceX96, slot0.tick, slot0.protocolFee, slot0.swapFee); + return (slot0.sqrtPriceX96, slot0.tick, slot0.protocolFee, slot0.lpFee); } /// @inheritdoc IPoolManager @@ -117,14 +117,14 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual(); if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks)); - uint24 swapFee = key.fee.getInitialSwapFee(); + uint24 lpFee = key.fee.getInitialLPFee(); key.hooks.beforeInitialize(key, sqrtPriceX96, hookData); PoolId id = key.toId(); (, uint24 protocolFee) = _fetchProtocolFee(key); - tick = pools[id].initialize(sqrtPriceX96, protocolFee, swapFee); + tick = pools[id].initialize(sqrtPriceX96, protocolFee, lpFee); key.hooks.afterInitialize(key, sqrtPriceX96, tick, hookData); @@ -298,11 +298,11 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim _burnFrom(from, id, amount); } - function updateDynamicSwapFee(PoolKey memory key, uint24 newDynamicSwapFee) external { - if (!key.fee.isDynamicFee() || msg.sender != address(key.hooks)) revert UnauthorizedDynamicSwapFeeUpdate(); - newDynamicSwapFee.validate(); + function updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee) external { + if (!key.fee.isDynamicFee() || msg.sender != address(key.hooks)) revert UnauthorizedDynamicLPFeeUpdate(); + newDynamicLPFee.validate(); PoolId id = key.toId(); - pools[id].setSwapFee(newDynamicSwapFee); + pools[id].setLPFee(newDynamicLPFee); } function getNonzeroDeltaCount() external view returns (uint256 _nonzeroDeltaCount) { diff --git a/src/interfaces/IPoolManager.sol b/src/interfaces/IPoolManager.sol index 8070d4fc6..1ba47b813 100644 --- a/src/interfaces/IPoolManager.sol +++ b/src/interfaces/IPoolManager.sol @@ -33,9 +33,9 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { /// @notice PoolKey must have currencies where address(currency0) < address(currency1) error CurrenciesOutOfOrderOrEqual(); - /// @notice Thrown when a call to updateDynamicSwapFee is made by an address that is not the hook, + /// @notice Thrown when a call to updateDynamicLPFee is made by an address that is not the hook, /// or on a pool that does not have a dynamic swap fee. - error UnauthorizedDynamicSwapFeeUpdate(); + error UnauthorizedDynamicLPFeeUpdate(); ///@notice Thrown when native currency is passed to a non native settlement error NonZeroNativeValue(); @@ -69,6 +69,7 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { /// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96 /// @param liquidity The liquidity of the pool after the swap /// @param tick The log base 1.0001 of the price of the pool after the swap + /// @param fee The swap fee in hundredths of a bip event Swap( PoolId indexed id, address sender, @@ -90,7 +91,7 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { function getSlot0(PoolId id) external view - returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 swapFee); + returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee); /// @notice Get the current value of liquidity of the given pool function getLiquidity(PoolId id) external view returns (uint128 liquidity); @@ -193,8 +194,8 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { /// @notice Called by the user to pay what is owed function settle(Currency token) external payable returns (uint256 paid); - /// @notice Updates the pools swap fees for the a pool that has enabled dynamic swap fees. - function updateDynamicSwapFee(PoolKey memory key, uint24 newDynamicSwapFee) external; + /// @notice Updates the pools lp fees for the a pool that has enabled dynamic lp fees. + function updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee) external; function getReserves(Currency currency) external view returns (uint256 balance); } diff --git a/src/libraries/Hooks.sol b/src/libraries/Hooks.sol index 895876a2a..6d83b9152 100644 --- a/src/libraries/Hooks.sol +++ b/src/libraries/Hooks.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {PoolKey} from "../types/PoolKey.sol"; import {IHooks} from "../interfaces/IHooks.sol"; -import {SwapFeeLibrary} from "./SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "./LPFeeLibrary.sol"; import {BalanceDelta} from "../types/BalanceDelta.sol"; import {IPoolManager} from "../interfaces/IPoolManager.sol"; @@ -12,7 +12,7 @@ import {IPoolManager} from "../interfaces/IPoolManager.sol"; /// For example, a hooks contract deployed to address: 0x9000000000000000000000000000000000000000 /// has leading bits '1001' which would cause the 'before initialize' and 'after add liquidity' hooks to be used. library Hooks { - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; using Hooks for IHooks; uint256 internal constant BEFORE_INITIALIZE_FLAG = 1 << 159; diff --git a/src/libraries/SwapFeeLibrary.sol b/src/libraries/LPFeeLibrary.sol similarity index 60% rename from src/libraries/SwapFeeLibrary.sol rename to src/libraries/LPFeeLibrary.sol index 7fec6095b..ac9ab507a 100644 --- a/src/libraries/SwapFeeLibrary.sol +++ b/src/libraries/LPFeeLibrary.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.20; import {PoolKey} from "../types/PoolKey.sol"; -library SwapFeeLibrary { - using SwapFeeLibrary for uint24; +library LPFeeLibrary { + using LPFeeLibrary for uint24; /// @notice Thrown when the static or dynamic fee on a pool exceeds 100%. error FeeTooLarge(); @@ -12,21 +12,21 @@ library SwapFeeLibrary { uint24 public constant STATIC_FEE_MASK = 0x7FFFFF; uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; - // the swap fee is represented in hundredths of a bip, so the max is 100% - uint24 public constant MAX_SWAP_FEE = 1000000; + // the lp fee is represented in hundredths of a bip, so the max is 100% + uint24 public constant MAX_LP_FEE = 1000000; function isDynamicFee(uint24 self) internal pure returns (bool) { return self & DYNAMIC_FEE_FLAG != 0; } function validate(uint24 self) internal pure { - if (self > MAX_SWAP_FEE) revert FeeTooLarge(); + if (self > MAX_LP_FEE) revert FeeTooLarge(); } - function getInitialSwapFee(uint24 self) internal pure returns (uint24 swapFee) { + function getInitialLPFee(uint24 self) internal pure returns (uint24 lpFee) { // the initial fee for a dynamic fee pool is 0 if (self.isDynamicFee()) return 0; - swapFee = self & STATIC_FEE_MASK; - swapFee.validate(); + lpFee = self & STATIC_FEE_MASK; + lpFee.validate(); } } diff --git a/src/libraries/NonZeroDeltaCount.sol b/src/libraries/NonZeroDeltaCount.sol index d9af3d222..e52df5300 100644 --- a/src/libraries/NonZeroDeltaCount.sol +++ b/src/libraries/NonZeroDeltaCount.sol @@ -8,17 +8,18 @@ import {IHooks} from "../interfaces/IHooks.sol"; /// TODO: This library can be deleted when we have the transient keyword support in solidity. library NonZeroDeltaCount { // The slot holding the number of nonzero deltas. uint256(keccak256("NonzeroDeltaCount")) - 1 - uint256 constant NONZERO_DELTA_COUNT = uint256(0x7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b); + uint256 constant NONZERO_DELTA_COUNT_SLOT = + uint256(0x7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b); function read() internal view returns (uint256 count) { - uint256 slot = NONZERO_DELTA_COUNT; + uint256 slot = NONZERO_DELTA_COUNT_SLOT; assembly { count := tload(slot) } } function increment() internal { - uint256 slot = NONZERO_DELTA_COUNT; + uint256 slot = NONZERO_DELTA_COUNT_SLOT; assembly { let count := tload(slot) count := add(count, 1) @@ -29,7 +30,7 @@ library NonZeroDeltaCount { /// @notice Potential to underflow. /// Current usage ensures this will not happen because we call decrement with known boundaries (only up to the number of times we call increment). function decrement() internal { - uint256 slot = NONZERO_DELTA_COUNT; + uint256 slot = NONZERO_DELTA_COUNT_SLOT; assembly { let count := tload(slot) count := sub(count, 1) diff --git a/src/libraries/Pool.sol b/src/libraries/Pool.sol index d63e8e5a6..2adb15166 100644 --- a/src/libraries/Pool.sol +++ b/src/libraries/Pool.sol @@ -12,6 +12,7 @@ import {SwapMath} from "./SwapMath.sol"; import {BalanceDelta, toBalanceDelta} from "../types/BalanceDelta.sol"; import {ProtocolFeeLibrary} from "./ProtocolFeeLibrary.sol"; import {LiquidityMath} from "./LiquidityMath.sol"; +import {LPFeeLibrary} from "./LPFeeLibrary.sol"; library Pool { using SafeCast for *; @@ -62,17 +63,21 @@ library Pool { /// @notice Thrown by donate if there is currently 0 liquidity, since the fees will not go to any liquidity providers error NoLiquidityToReceiveFees(); + /// @notice Thrown when trying to swap with max lp fee and specifying an output amount + error InvalidFeeForExactOut(); + struct Slot0 { // the current price uint160 sqrtPriceX96; // the current tick int24 tick; - // protocol swap fee, taken as a % of the LP swap fee + // protocol fee, expressed in hundredths of a bip // upper 12 bits are for 1->0, and the lower 12 are for 0->1 - // the maximum is 2500 - meaning the maximum protocol fee is 25% + // the maximum is 1000 - meaning the maximum protocol fee is 0.1% + // the protocolFee is taken from the input first, then the lpFee is taken from the remaining input uint24 protocolFee; - // used for the swap fee, either static at initialize or dynamic via hook - uint24 swapFee; + // used for the lp fee, either static at initialize or dynamic via hook + uint24 lpFee; } // info stored for each initialized individual tick @@ -105,7 +110,7 @@ library Pool { if (tickUpper > TickMath.MAX_TICK) revert TickUpperOutOfBounds(tickUpper); } - function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFee, uint24 swapFee) + function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFee, uint24 lpFee) internal returns (int24 tick) { @@ -113,7 +118,7 @@ library Pool { tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); - self.slot0 = Slot0({sqrtPriceX96: sqrtPriceX96, tick: tick, protocolFee: protocolFee, swapFee: swapFee}); + self.slot0 = Slot0({sqrtPriceX96: sqrtPriceX96, tick: tick, protocolFee: protocolFee, lpFee: lpFee}); } function setProtocolFee(State storage self, uint24 protocolFee) internal { @@ -122,10 +127,10 @@ library Pool { self.slot0.protocolFee = protocolFee; } - /// @notice Only dynamic fee pools may update the swap fee. - function setSwapFee(State storage self, uint24 swapFee) internal { + /// @notice Only dynamic fee pools may update the lp fee. + function setLPFee(State storage self, uint24 lpFee) internal { if (self.isNotInitialized()) revert PoolNotInitialized(); - self.slot0.swapFee = swapFee; + self.slot0.lpFee = lpFee; } struct ModifyLiquidityParams { @@ -303,7 +308,6 @@ library Pool { if (params.amountSpecified == 0) revert SwapAmountCannotBeZero(); Slot0 memory slot0Start = self.slot0; - swapFee = slot0Start.swapFee; bool zeroForOne = params.zeroForOne; if (zeroForOne) { if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96) { @@ -338,6 +342,13 @@ library Pool { }); StepComputations memory step; + swapFee = + cache.protocolFee == 0 ? slot0Start.lpFee : uint24(cache.protocolFee).calculateSwapFee(slot0Start.lpFee); + + if (!exactInput && (swapFee == LPFeeLibrary.MAX_LP_FEE)) { + revert InvalidFeeForExactOut(); + } + // continue swapping as long as we haven't used the entire input/output and haven't reached the price limit while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != params.sqrtPriceLimitX96) { step.sqrtPriceStartX96 = state.sqrtPriceX96; @@ -383,10 +394,12 @@ library Pool { // if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee if (cache.protocolFee > 0) { - // calculate the amount of the fee that should go to the protocol - uint256 delta = step.feeAmount * cache.protocolFee / ProtocolFeeLibrary.BIPS_DENOMINATOR; - // subtract it from the regular fee and add it to the protocol fee unchecked { + // step.amountIn does not include the swap fee, as it's already been taken from it, + // so add it back to get the total amountIn and use that to calculate the amount of fees owed to the protocol + uint256 delta = + (step.amountIn + step.feeAmount) * cache.protocolFee / ProtocolFeeLibrary.PIPS_DENOMINATOR; + // subtract it from the total fee and add it to the protocol fee step.feeAmount -= delta; feeForProtocol += delta; } @@ -403,12 +416,17 @@ library Pool { if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { // if the tick is initialized, run the tick transition if (step.initialized) { - int128 liquidityNet = Pool.crossTick( - self, - step.tickNext, - (zeroForOne ? state.feeGrowthGlobalX128 : self.feeGrowthGlobal0X128), - (zeroForOne ? self.feeGrowthGlobal1X128 : state.feeGrowthGlobalX128) - ); + uint256 feeGrowthGlobal0X128; + uint256 feeGrowthGlobal1X128; + if (!zeroForOne) { + feeGrowthGlobal0X128 = self.feeGrowthGlobal0X128; + feeGrowthGlobal1X128 = state.feeGrowthGlobalX128; + } else { + feeGrowthGlobal0X128 = state.feeGrowthGlobalX128; + feeGrowthGlobal1X128 = self.feeGrowthGlobal1X128; + } + int128 liquidityNet = + Pool.crossTick(self, step.tickNext, feeGrowthGlobal0X128, feeGrowthGlobal1X128); // if we're moving leftward, we interpret liquidityNet as the opposite sign // safe because liquidityNet cannot be type(int128).min unchecked { diff --git a/src/libraries/ProtocolFeeLibrary.sol b/src/libraries/ProtocolFeeLibrary.sol index 28d75dc3d..c70755d84 100644 --- a/src/libraries/ProtocolFeeLibrary.sol +++ b/src/libraries/ProtocolFeeLibrary.sol @@ -1,12 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.20; +import "./UnsafeMath.sol"; + library ProtocolFeeLibrary { - // Max protocol fee is 25% (2500 bips) - uint16 public constant MAX_PROTOCOL_FEE = 2500; + // Max protocol fee is 0.1% (1000 pips) + uint16 public constant MAX_PROTOCOL_FEE = 1000; - // Total bips - uint16 internal constant BIPS_DENOMINATOR = 10_000; + // the protocol fee is represented in hundredths of a bip + uint256 internal constant PIPS_DENOMINATOR = 1_000_000; function getZeroForOneFee(uint24 self) internal pure returns (uint16) { return uint16(self & (4096 - 1)); @@ -20,11 +22,21 @@ library ProtocolFeeLibrary { if (self != 0) { uint16 fee0 = getZeroForOneFee(self); uint16 fee1 = getOneForZeroFee(self); - // The fee is represented in bips so it cannot be GREATER than the MAX_PROTOCOL_FEE. + // The fee is represented in pips and it cannot be greater than the MAX_PROTOCOL_FEE. if ((fee0 > MAX_PROTOCOL_FEE) || (fee1 > MAX_PROTOCOL_FEE)) { return false; } } return true; } + + // The protocol fee is taken from the input amount first and then the LP fee is taken from the remaining + // The swap fee is capped at 100% + // equivalent to protocolFee + lpFee(1_000_000 - protocolFee) / 1_000_000 + function calculateSwapFee(uint24 self, uint24 lpFee) internal pure returns (uint24) { + unchecked { + uint256 numerator = uint256(self) * uint256(lpFee); + return uint24(uint256(self) + lpFee - UnsafeMath.divRoundingUp(numerator, PIPS_DENOMINATOR)); + } + } } diff --git a/src/libraries/SwapMath.sol b/src/libraries/SwapMath.sol index d7ba4405d..de8e672f8 100644 --- a/src/libraries/SwapMath.sol +++ b/src/libraries/SwapMath.sol @@ -82,7 +82,7 @@ library SwapMath { // we didn't reach the target, so take the remainder of the maximum input as fee feeAmount = uint256(-amountRemaining) - amountIn; } else { - feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips); + feeAmount = feePips == 1e6 ? amountIn : FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips); } } } diff --git a/src/test/DynamicFeesTestHook.sol b/src/test/DynamicFeesTestHook.sol index 18c70c55d..ecc1b459b 100644 --- a/src/test/DynamicFeesTestHook.sol +++ b/src/test/DynamicFeesTestHook.sol @@ -23,7 +23,7 @@ contract DynamicFeesTestHook is BaseTestHooks { override returns (bytes4) { - manager.updateDynamicSwapFee(key, fee); + manager.updateDynamicLPFee(key, fee); return IHooks.afterInitialize.selector; } @@ -32,11 +32,11 @@ contract DynamicFeesTestHook is BaseTestHooks { override returns (bytes4) { - manager.updateDynamicSwapFee(key, fee); + manager.updateDynamicLPFee(key, fee); return IHooks.beforeSwap.selector; } function forcePoolFeeUpdate(PoolKey calldata _key, uint24 _fee) external { - manager.updateDynamicSwapFee(_key, _fee); + manager.updateDynamicLPFee(_key, _fee); } } diff --git a/src/test/MockHooks.sol b/src/test/MockHooks.sol index 499eca30d..1da0f160f 100644 --- a/src/test/MockHooks.sol +++ b/src/test/MockHooks.sol @@ -25,7 +25,7 @@ contract MockHooks is IHooks { mapping(bytes4 => bytes4) public returnValues; - mapping(PoolId => uint16) public swapFees; + mapping(PoolId => uint16) public lpFees; function beforeInitialize(address, PoolKey calldata, uint160, bytes calldata hookData) external @@ -139,7 +139,7 @@ contract MockHooks is IHooks { returnValues[key] = value; } - function setSwapFee(PoolKey calldata key, uint16 value) external { - swapFees[key.toId()] = value; + function setlpFee(PoolKey calldata key, uint16 value) external { + lpFees[key.toId()] = value; } } diff --git a/src/test/PoolModifyLiquidityTest.sol b/src/test/PoolModifyLiquidityTest.sol index 8a9fc27f5..c3c41ff42 100644 --- a/src/test/PoolModifyLiquidityTest.sol +++ b/src/test/PoolModifyLiquidityTest.sol @@ -8,14 +8,14 @@ import {PoolKey} from "../types/PoolKey.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {IHooks} from "../interfaces/IHooks.sol"; import {Hooks} from "../libraries/Hooks.sol"; -import {SwapFeeLibrary} from "../libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "../libraries/LPFeeLibrary.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; contract PoolModifyLiquidityTest is PoolTestBase { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using Hooks for IHooks; - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; constructor(IPoolManager _manager) PoolTestBase(_manager) {} diff --git a/src/test/PoolSwapTest.sol b/src/test/PoolSwapTest.sol index 07484aa02..46a70d4c1 100644 --- a/src/test/PoolSwapTest.sol +++ b/src/test/PoolSwapTest.sol @@ -68,32 +68,32 @@ contract PoolSwapTest is PoolTestBase { if (data.params.amountSpecified < 0) { // exact input, 0 for 1 require( - deltaAfter0 == data.params.amountSpecified, - "deltaAfter0 is not equal to data.params.amountSpecified" + deltaAfter0 >= data.params.amountSpecified, + "deltaAfter0 is not greater than or equal to data.params.amountSpecified" ); - require(deltaAfter1 > 0, "deltaAfter1 is not greater than 0"); + require(deltaAfter1 >= 0, "deltaAfter1 is not greater than or equal to 0"); } else { // exact output, 0 for 1 require(deltaAfter0 < 0, "deltaAfter0 is not less than zero"); require( - deltaAfter1 == data.params.amountSpecified, - "deltaAfter1 is not equal to data.params.amountSpecified" + deltaAfter1 <= data.params.amountSpecified, + "deltaAfter1 is not less than or equal to data.params.amountSpecified" ); } } else { if (data.params.amountSpecified < 0) { // exact input, 1 for 0 require( - deltaAfter1 == data.params.amountSpecified, - "deltaAfter1 is not equal to data.params.amountSpecified" + deltaAfter1 >= data.params.amountSpecified, + "deltaAfter1 is not greater than or equal to data.params.amountSpecified" ); - require(deltaAfter0 > 0, "deltaAfter0 is not greater than 0"); + require(deltaAfter0 >= 0, "deltaAfter0 is not greater than or equal to 0"); } else { // exact output, 1 for 0 require(deltaAfter1 < 0, "deltaAfter1 is not less than 0"); require( - deltaAfter0 == data.params.amountSpecified, - "deltaAfter0 is not equal to data.params.amountSpecified" + deltaAfter0 <= data.params.amountSpecified, + "deltaAfter0 is not less than or equal to data.params.amountSpecified" ); } } diff --git a/src/test/ProtocolFeeControllerTest.sol b/src/test/ProtocolFeeControllerTest.sol index 30aec61a7..13813083b 100644 --- a/src/test/ProtocolFeeControllerTest.sol +++ b/src/test/ProtocolFeeControllerTest.sol @@ -31,8 +31,8 @@ contract RevertingProtocolFeeControllerTest is IProtocolFeeController { /// @notice Returns an out of bounds protocol fee contract OutOfBoundsProtocolFeeControllerTest is IProtocolFeeController { function protocolFeeForPool(PoolKey memory /* key */ ) external pure returns (uint24) { - // set both swap fees to 2501, which is greater than MAX_PROTOCOL_FEE - return 0x9C59C5; + // set both protocol fees to 1001, which is greater than MAX_PROTOCOL_FEE + return (1001 << 12) | 1001; } } diff --git a/test/DynamicFees.t.sol b/test/DynamicFees.t.sol index 373876359..778b9a366 100644 --- a/test/DynamicFees.t.sol +++ b/test/DynamicFees.t.sol @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {PoolId, PoolIdLibrary} from "../src/types/PoolId.sol"; import {Hooks} from "../src/libraries/Hooks.sol"; -import {SwapFeeLibrary} from "../src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "../src/libraries/LPFeeLibrary.sol"; import {IPoolManager} from "../src/interfaces/IPoolManager.sol"; import {IProtocolFees} from "../src/interfaces/IProtocolFees.sol"; import {IHooks} from "../src/interfaces/IHooks.sol"; @@ -17,6 +17,8 @@ import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {DynamicFeesTestHook} from "../src/test/DynamicFeesTestHook.sol"; import {Currency, CurrencyLibrary} from "../src/types/Currency.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {Pool} from "../src/libraries/Pool.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../src/types/BalanceDelta.sol"; contract TestDynamicFees is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; @@ -69,17 +71,17 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { currency0, currency1, IHooks(address(dynamicFeesHooks)), - SwapFeeLibrary.DYNAMIC_FEE_FLAG, + LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_RATIO_1_1, ZERO_BYTES ); } - function test_updateDynamicSwapFee_afterInitialize_failsWithTooLargeFee() public { + function test_updateDynamicLPFee_afterInitialize_failsWithTooLargeFee() public { key.tickSpacing = 30; dynamicFeesHooks.setFee(1000001); - vm.expectRevert(SwapFeeLibrary.FeeTooLarge.selector); + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); } @@ -90,33 +92,33 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { dynamicFeesNoHooks.setFee(1000000); manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - assertEq(_fetchPoolSwapFee(key), 0); + assertEq(_fetchPoolLPFee(key), 0); } - function test_updateDynamicSwapFee_afterInitialize_initializesFee() public { + function test_updateDynamicLPFee_afterInitialize_initializesFee() public { key.tickSpacing = 30; dynamicFeesHooks.setFee(123); manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - assertEq(_fetchPoolSwapFee(key), 123); + assertEq(_fetchPoolLPFee(key), 123); } - function test_updateDynamicSwapFee_revertsIfCallerIsntHook() public { - vm.expectRevert(IPoolManager.UnauthorizedDynamicSwapFeeUpdate.selector); - manager.updateDynamicSwapFee(key, 123); + function test_updateDynamicLPFee_revertsIfCallerIsntHook() public { + vm.expectRevert(IPoolManager.UnauthorizedDynamicLPFeeUpdate.selector); + manager.updateDynamicLPFee(key, 123); } - function test_updateDynamicSwapFee_revertsIfPoolHasStaticFee() public { + function test_updateDynamicLPFee_revertsIfPoolHasStaticFee() public { key.fee = 3000; // static fee dynamicFeesHooks.setFee(123); // afterInitialize will try to update the fee, and fail - vm.expectRevert(IPoolManager.UnauthorizedDynamicSwapFeeUpdate.selector); + vm.expectRevert(IPoolManager.UnauthorizedDynamicLPFeeUpdate.selector); manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); } - function test_updateDynamicSwapFee_beforeSwap_failsWithTooLargeFee() public { - assertEq(_fetchPoolSwapFee(key), 0); + function test_updateDynamicLPFee_beforeSwap_failsWithTooLargeFee() public { + assertEq(_fetchPoolLPFee(key), 0); dynamicFeesHooks.setFee(1000001); @@ -125,12 +127,12 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { PoolSwapTest.TestSettings memory testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); - vm.expectRevert(SwapFeeLibrary.FeeTooLarge.selector); + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); swapRouter.swap(key, params, testSettings, ZERO_BYTES); } - function test_updateDynamicSwapFee_beforeSwap_succeeds_gas() public { - assertEq(_fetchPoolSwapFee(key), 0); + function test_updateDynamicLPFee_beforeSwap_succeeds_gas() public { + assertEq(_fetchPoolLPFee(key), 0); dynamicFeesHooks.setFee(123); @@ -146,17 +148,184 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { swapRouter.swap(key, params, testSettings, ZERO_BYTES); snapEnd(); - assertEq(_fetchPoolSwapFee(key), 123); + assertEq(_fetchPoolLPFee(key), 123); + } + + function test_swap_100PercentLPFee_AmountIn_NoProtocol() public { + assertEq(_fetchPoolLPFee(key), 0); + + dynamicFeesHooks.setFee(1000000); + + IPoolManager.SwapParams memory params = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), -100, 0, SQRT_RATIO_1_1, 1e18, -1, 1000000); + + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + + assertEq(_fetchPoolLPFee(key), 1000000); + } + + function test_swap_50PercentLPFee_AmountIn_NoProtocol() public { + assertEq(_fetchPoolLPFee(key), 0); + + dynamicFeesHooks.setFee(500000); + + IPoolManager.SwapParams memory params = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), -100, 49, 79228162514264333632135824623, 1e18, -1, 500000); + + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + + assertEq(_fetchPoolLPFee(key), 500000); + } + + function test_swap_50PercentLPFee_AmountOut_NoProtocol() public { + assertEq(_fetchPoolLPFee(key), 0); + + dynamicFeesHooks.setFee(500000); + + IPoolManager.SwapParams memory params = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), -202, 100, 79228162514264329670727698909, 1e18, -1, 500000); + + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + + assertEq(_fetchPoolLPFee(key), 500000); + } + + function test_swap_revertsWith_InvalidFeeForExactOut_whenFeeIsMax() public { + assertEq(_fetchPoolLPFee(key), 0); + + dynamicFeesHooks.setFee(1000000); + + IPoolManager.SwapParams memory params = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + vm.expectRevert(Pool.InvalidFeeForExactOut.selector); + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + } + + function test_swap_99PercentFee_AmountOut_WithProtocol() public { + assertEq(_fetchPoolLPFee(key), 0); + + dynamicFeesHooks.setFee(999999); + + vm.prank(address(feeController)); + manager.setProtocolFee(key, 1000); + + IPoolManager.SwapParams memory params = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), -101000000, 100, 79228162514264329670727698909, 1e18, -1, 999999); + + snapStart("swap with lp fee and protocol fee"); + BalanceDelta delta = swapRouter.swap(key, params, testSettings, ZERO_BYTES); + snapEnd(); + + uint256 expectedProtocolFee = uint256(uint128(-delta.amount0())) * 1000 / 1e6; + assertEq(manager.protocolFeesAccrued(currency0), expectedProtocolFee); + + assertEq(_fetchPoolLPFee(key), 999999); + } + + function test_swap_100PercentFee_AmountIn_WithProtocol() public { + assertEq(_fetchPoolLPFee(key), 0); + + dynamicFeesHooks.setFee(1000000); + + vm.prank(address(feeController)); + manager.setProtocolFee(key, 1000); + + IPoolManager.SwapParams memory params = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -1000, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), -1000, 0, SQRT_RATIO_1_1, 1e18, -1, 1000000); + + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + + uint256 expectedProtocolFee = uint256(-params.amountSpecified) * 1000 / 1e6; + assertEq(manager.protocolFeesAccrued(currency0), expectedProtocolFee); + } + + function test_emitsSwapFee() public { + assertEq(_fetchPoolLPFee(key), 0); + + dynamicFeesHooks.setFee(123); + + vm.prank(address(feeController)); + manager.setProtocolFee(key, 1000); + + IPoolManager.SwapParams memory params = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + vm.expectEmit(true, true, true, true, address(manager)); + emit Swap(key.toId(), address(swapRouter), -100, 98, 79228162514264329749955861424, 1e18, -1, 1122); + + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + + assertEq(_fetchPoolLPFee(key), 123); + } + + function test_fuzz_ProtocolAndLPFee(uint24 lpFee, uint16 protocolFee0, uint16 protocolFee1, int256 amountSpecified) + public + { + assertEq(_fetchPoolLPFee(key), 0); + + lpFee = uint16(bound(lpFee, 0, 1000000)); + protocolFee0 = uint16(bound(protocolFee0, 0, 1000)); + protocolFee1 = uint16(bound(protocolFee1, 0, 1000)); + vm.assume(amountSpecified != 0); + + uint24 protocolFee = (uint24(protocolFee1) << 12) | uint24(protocolFee0); + dynamicFeesHooks.setFee(lpFee); + + vm.prank(address(feeController)); + manager.setProtocolFee(key, protocolFee); + + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: SQRT_RATIO_1_2 + }); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + BalanceDelta delta = swapRouter.swap(key, params, testSettings, ZERO_BYTES); + + uint256 expectedProtocolFee = uint256(uint128(-delta.amount0())) * protocolFee0 / 1e6; + assertEq(manager.protocolFeesAccrued(currency0), expectedProtocolFee); } function test_swap_withDynamicFee_gas() public { (key,) = initPoolAndAddLiquidity( - currency0, currency1, dynamicFeesNoHooks, SwapFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_RATIO_1_1, ZERO_BYTES + currency0, currency1, dynamicFeesNoHooks, LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_RATIO_1_1, ZERO_BYTES ); - assertEq(_fetchPoolSwapFee(key), 0); + assertEq(_fetchPoolLPFee(key), 0); dynamicFeesNoHooks.forcePoolFeeUpdate(key, 123); - assertEq(_fetchPoolSwapFee(key), 123); + assertEq(_fetchPoolLPFee(key), 123); IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); @@ -171,8 +340,8 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { snapEnd(); } - function _fetchPoolSwapFee(PoolKey memory _key) internal view returns (uint256 swapFee) { + function _fetchPoolLPFee(PoolKey memory _key) internal view returns (uint256 lpFee) { PoolId id = _key.toId(); - (,,, swapFee) = manager.getSlot0(id); + (,,, lpFee) = manager.getSlot0(id); } } diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 28b48de76..5d2f8786a 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -25,7 +25,7 @@ import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolEmptyUnlockTest} from "../src/test/PoolEmptyUnlockTest.sol"; import {Action} from "../src/test/PoolNestedActionsTest.sol"; import {PoolId, PoolIdLibrary} from "../src/types/PoolId.sol"; -import {SwapFeeLibrary} from "../src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "../src/libraries/LPFeeLibrary.sol"; import {Position} from "../src/libraries/Position.sol"; import {Constants} from "./utils/Constants.sol"; import {SafeCast} from "../src/libraries/SafeCast.sol"; @@ -36,7 +36,7 @@ import {IProtocolFees} from "../src/interfaces/IProtocolFees.sol"; contract PoolManagerTest is Test, Deployers, GasSnapshot { using Hooks for IHooks; using PoolIdLibrary for PoolKey; - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; using CurrencyLibrary for Currency; using ProtocolFeeLibrary for uint24; @@ -62,7 +62,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { PoolEmptyUnlockTest emptyUnlockRouter; - uint24 constant MAX_FEE_BOTH_TOKENS = (2500 << 12) | 2500; // 2500 2500 + uint24 constant MAX_PROTOCOL_FEE_BOTH_TOKENS = (1000 << 12) | 1000; // 1000 1000 function setUp() public { initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0))); @@ -753,9 +753,10 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { snapEnd(); } - function test_swap_accruesProtocolFees(uint16 protocolFee0, uint16 protocolFee1) public { - protocolFee0 = uint16(bound(protocolFee0, 1, 2500)); - protocolFee1 = uint16(bound(protocolFee1, 1, 2500)); + function test_swap_accruesProtocolFees(uint16 protocolFee0, uint16 protocolFee1, int256 amountSpecified) public { + protocolFee0 = uint16(bound(protocolFee0, 0, 1000)); + protocolFee1 = uint16(bound(protocolFee1, 0, 1000)); + vm.assume(amountSpecified != 0); uint24 protocolFee = (uint24(protocolFee1) << 12) | uint24(protocolFee0); @@ -783,11 +784,11 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { params.liquidityDelta = LIQ_PARAMS.liquidityDelta; modifyLiquidityRouter.modifyLiquidity(key, params, ZERO_BYTES); - IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams(false, -10000, TickMath.MAX_SQRT_RATIO - 1); - swapRouter.swap(key, swapParams, PoolSwapTest.TestSettings(false, false), ZERO_BYTES); - - uint256 expectedTotalSwapFee = uint256(-swapParams.amountSpecified) * key.fee / 1e6; - uint256 expectedProtocolFee = expectedTotalSwapFee * protocolFee1 / 1e4; + IPoolManager.SwapParams memory swapParams = + IPoolManager.SwapParams(false, amountSpecified, TickMath.MAX_SQRT_RATIO - 1); + BalanceDelta delta = swapRouter.swap(key, swapParams, PoolSwapTest.TestSettings(false, false), ZERO_BYTES); + uint256 expectedProtocolFee = + uint256(uint128(-delta.amount1())) * protocolFee1 / ProtocolFeeLibrary.PIPS_DENOMINATOR; assertEq(manager.protocolFeesAccrued(currency0), 0); assertEq(manager.protocolFeesAccrued(currency1), expectedProtocolFee); } @@ -955,7 +956,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { uint16 fee0 = protocolFee.getZeroForOneFee(); uint16 fee1 = protocolFee.getOneForZeroFee(); vm.prank(address(feeController)); - if ((fee0 > 2500) || (fee1 > 2500)) { + if ((fee0 > 1000) || (fee1 > 1000)) { vm.expectRevert(IProtocolFees.InvalidProtocolFee.selector); manager.setProtocolFee(key, protocolFee); } else { @@ -974,7 +975,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { vm.prank(address(feeController)); vm.expectRevert(IProtocolFees.InvalidProtocolFee.selector); - manager.setProtocolFee(key, MAX_FEE_BOTH_TOKENS + 1); + manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS + 1); } function test_setProtocolFee_failsWithInvalidCaller() public { @@ -982,15 +983,15 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { assertEq(slot0ProtocolFee, 0); vm.expectRevert(IProtocolFees.InvalidCaller.selector); - manager.setProtocolFee(key, MAX_FEE_BOTH_TOKENS); + manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS); } function test_collectProtocolFees_initializesWithProtocolFeeIfCalled() public { - feeController.setProtocolFeeForPool(uninitializedKey.toId(), MAX_FEE_BOTH_TOKENS); + feeController.setProtocolFeeForPool(uninitializedKey.toId(), MAX_PROTOCOL_FEE_BOTH_TOKENS); manager.initialize(uninitializedKey, SQRT_RATIO_1_1, ZERO_BYTES); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(uninitializedKey.toId()); - assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); + assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS); } function test_collectProtocolFees_revertsIfCallerIsNotController() public { @@ -999,14 +1000,13 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { } function test_collectProtocolFees_ERC20_accumulateFees_gas() public { - uint256 expectedFees = 7; + uint256 expectedFees = 10; - uint24 protocolFee = MAX_FEE_BOTH_TOKENS; vm.prank(address(feeController)); - manager.setProtocolFee(key, protocolFee); + manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); - assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); + assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS); swapRouter.swap( key, @@ -1026,15 +1026,41 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { assertEq(manager.protocolFeesAccrued(currency0), 0); } + function test_collectProtocolFees_ERC20_accumulateFees_exactOutput() public { + uint256 expectedFees = 10; + + vm.prank(address(feeController)); + manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS); + + (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); + assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS); + + swapRouter.swap( + key, + IPoolManager.SwapParams(true, 10000, SQRT_RATIO_1_2), + PoolSwapTest.TestSettings(false, false), + ZERO_BYTES + ); + + assertEq(manager.protocolFeesAccrued(currency0), expectedFees); + assertEq(manager.protocolFeesAccrued(currency1), 0); + assertEq(currency0.balanceOf(address(1)), 0); + vm.prank(address(feeController)); + snapStart("erc20 collect protocol fees"); + manager.collectProtocolFees(address(1), currency0, expectedFees); + snapEnd(); + assertEq(currency0.balanceOf(address(1)), expectedFees); + assertEq(manager.protocolFeesAccrued(currency0), 0); + } + function test_collectProtocolFees_ERC20_returnsAllFeesIf0IsProvidedAsParameter() public { - uint256 expectedFees = 7; + uint256 expectedFees = 10; - uint24 protocolFee = MAX_FEE_BOTH_TOKENS; vm.prank(address(feeController)); - manager.setProtocolFee(key, protocolFee); + manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); - assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); + assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS); swapRouter.swap( key, @@ -1053,16 +1079,14 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { } function test_collectProtocolFees_nativeToken_accumulateFees_gas() public { - uint256 expectedFees = 7; + uint256 expectedFees = 10; Currency nativeCurrency = CurrencyLibrary.NATIVE; - // set protocol fee before initializing the pool as it is fetched on initialization - uint24 protocolFee = MAX_FEE_BOTH_TOKENS; vm.prank(address(feeController)); - manager.setProtocolFee(nativeKey, protocolFee); + manager.setProtocolFee(nativeKey, MAX_PROTOCOL_FEE_BOTH_TOKENS); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId()); - assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); + assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS); swapRouter.swap{value: 10000}( nativeKey, @@ -1083,15 +1107,14 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { } function test_collectProtocolFees_nativeToken_returnsAllFeesIf0IsProvidedAsParameter() public { - uint256 expectedFees = 7; + uint256 expectedFees = 10; Currency nativeCurrency = CurrencyLibrary.NATIVE; - uint24 protocolFee = MAX_FEE_BOTH_TOKENS; vm.prank(address(feeController)); - manager.setProtocolFee(nativeKey, protocolFee); + manager.setProtocolFee(nativeKey, MAX_PROTOCOL_FEE_BOTH_TOKENS); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId()); - assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); + assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS); swapRouter.swap{value: 10000}( nativeKey, diff --git a/test/PoolManagerInitialize.t.sol b/test/PoolManagerInitialize.t.sol index 86cfbd61a..afa60ea6d 100644 --- a/test/PoolManagerInitialize.t.sol +++ b/test/PoolManagerInitialize.t.sol @@ -18,7 +18,7 @@ import {EmptyTestHooks} from "../src/test/EmptyTestHooks.sol"; import {PoolKey} from "../src/types/PoolKey.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolId, PoolIdLibrary} from "../src/types/PoolId.sol"; -import {SwapFeeLibrary} from "../src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "../src/libraries/LPFeeLibrary.sol"; import {ProtocolFeeControllerTest} from "../src/test/ProtocolFeeControllerTest.sol"; import {IProtocolFeeController} from "../src/interfaces/IProtocolFeeController.sol"; import {ProtocolFeeLibrary} from "../src/libraries/ProtocolFeeLibrary.sol"; @@ -26,7 +26,7 @@ import {ProtocolFeeLibrary} from "../src/libraries/ProtocolFeeLibrary.sol"; contract PoolManagerInitializeTest is Test, Deployers, GasSnapshot { using Hooks for IHooks; using PoolIdLibrary for PoolKey; - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; using ProtocolFeeLibrary for uint24; event Initialize( @@ -71,9 +71,9 @@ contract PoolManagerInitializeTest is Test, Deployers, GasSnapshot { vm.expectRevert(abi.encodeWithSelector(Hooks.HookAddressNotValid.selector, address(key0.hooks))); manager.initialize(key0, sqrtPriceX96, ZERO_BYTES); } else if ( - (key0.fee & SwapFeeLibrary.DYNAMIC_FEE_FLAG == 0) && (key0.fee & SwapFeeLibrary.STATIC_FEE_MASK > 1000000) + (key0.fee & LPFeeLibrary.DYNAMIC_FEE_FLAG == 0) && (key0.fee & LPFeeLibrary.STATIC_FEE_MASK > 1000000) ) { - vm.expectRevert(abi.encodeWithSelector(SwapFeeLibrary.FeeTooLarge.selector)); + vm.expectRevert(abi.encodeWithSelector(LPFeeLibrary.FeeTooLarge.selector)); manager.initialize(key0, sqrtPriceX96, ZERO_BYTES); } else { vm.expectEmit(true, true, true, true); @@ -209,7 +209,7 @@ contract PoolManagerInitializeTest is Test, Deployers, GasSnapshot { (uint160 slot0SqrtPriceX96,, uint24 slot0ProtocolFee,) = manager.getSlot0(uninitializedKey.toId()); assertEq(slot0SqrtPriceX96, SQRT_RATIO_1_1); - if ((fee0 > 2500) || (fee1 > 2500)) { + if ((fee0 > 1000) || (fee1 > 1000)) { assertEq(slot0ProtocolFee, 0); } else { assertEq(slot0ProtocolFee, protocolFee); diff --git a/test/SkipCallsTestHook.t.sol b/test/SkipCallsTestHook.t.sol index f95e76814..fb1156823 100644 --- a/test/SkipCallsTestHook.t.sol +++ b/test/SkipCallsTestHook.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {PoolId, PoolIdLibrary} from "../src/types/PoolId.sol"; import {Hooks} from "../src/libraries/Hooks.sol"; -import {SwapFeeLibrary} from "../src/libraries/SwapFeeLibrary.sol"; import {IPoolManager} from "../src/interfaces/IPoolManager.sol"; import {IProtocolFees} from "../src/interfaces/IProtocolFees.sol"; import {IHooks} from "../src/interfaces/IHooks.sol"; diff --git a/test/libraries/Hooks.t.sol b/test/libraries/Hooks.t.sol index 4f8ea6e40..e1686d5a6 100644 --- a/test/libraries/Hooks.t.sol +++ b/test/libraries/Hooks.t.sol @@ -5,7 +5,7 @@ import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {Hooks} from "src/libraries/Hooks.sol"; -import {SwapFeeLibrary} from "src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "src/libraries/LPFeeLibrary.sol"; import {MockHooks} from "src/test/MockHooks.sol"; import {IPoolManager} from "src/interfaces/IPoolManager.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; @@ -809,13 +809,11 @@ contract HooksTest is Test, Deployers, GasSnapshot { function test_isValidIfDynamicFee() public { assertTrue( - Hooks.isValidHookAddress( - IHooks(0x0000000000000000000000000000000000000001), SwapFeeLibrary.DYNAMIC_FEE_FLAG - ) + Hooks.isValidHookAddress(IHooks(0x0000000000000000000000000000000000000001), LPFeeLibrary.DYNAMIC_FEE_FLAG) ); assertTrue( Hooks.isValidHookAddress( - IHooks(0x0000000000000000000000000000000000000001), SwapFeeLibrary.DYNAMIC_FEE_FLAG | uint24(3000) + IHooks(0x0000000000000000000000000000000000000001), LPFeeLibrary.DYNAMIC_FEE_FLAG | uint24(3000) ) ); assertTrue(Hooks.isValidHookAddress(IHooks(0x8000000000000000000000000000000000000000), 3000)); diff --git a/test/libraries/Lock.t.sol b/test/libraries/Lock.t.sol index 9fa8b126b..2b8fd0a14 100644 --- a/test/libraries/Lock.t.sol +++ b/test/libraries/Lock.t.sol @@ -16,4 +16,8 @@ contract LockTest is Test { assertFalse(Lock.isUnlocked()); } + + function test_unlockedSlot() public { + assertEq(uint256(keccak256("Unlocked")) - 1, Lock.IS_UNLOCKED_SLOT); + } } diff --git a/test/libraries/NonZeroDeltaCount.t.sol b/test/libraries/NonZeroDeltaCount.t.sol index 523324f01..128a5964e 100644 --- a/test/libraries/NonZeroDeltaCount.t.sol +++ b/test/libraries/NonZeroDeltaCount.t.sol @@ -39,4 +39,8 @@ contract NonZeroDeltaCountTest is Test { assertEq(NonZeroDeltaCount.read(), expectedCount); } } + + function test_nonZeroDeltaCountSlot() public { + assertEq(uint256(keccak256("NonzeroDeltaCount")) - 1, NonZeroDeltaCount.NONZERO_DELTA_COUNT_SLOT); + } } diff --git a/test/libraries/Pool.t.sol b/test/libraries/Pool.t.sol index 104ab9696..25f9bd7f2 100644 --- a/test/libraries/Pool.t.sol +++ b/test/libraries/Pool.t.sol @@ -11,13 +11,18 @@ import {TickBitmap} from "src/libraries/TickBitmap.sol"; import {LiquidityAmounts} from "test/utils/LiquidityAmounts.sol"; import {Constants} from "test/utils/Constants.sol"; import {SafeCast} from "src/libraries/SafeCast.sol"; +import {ProtocolFeeLibrary} from "src/libraries/ProtocolFeeLibrary.sol"; +import {LPFeeLibrary} from "src/libraries/LPFeeLibrary.sol"; contract PoolTest is Test { using Pool for Pool.State; Pool.State state; - function testPoolInitialize(uint160 sqrtPriceX96, uint16 protocolFee, uint24 dynamicFee) public { + uint24 constant MAX_PROTOCOL_FEE = ProtocolFeeLibrary.MAX_PROTOCOL_FEE; // 0.1% + uint24 constant MAX_LP_FEE = LPFeeLibrary.MAX_LP_FEE; // 100% + + function testPoolInitialize(uint160 sqrtPriceX96, uint24 protocolFee, uint24 dynamicFee) public { if (sqrtPriceX96 < TickMath.MIN_SQRT_RATIO || sqrtPriceX96 >= TickMath.MAX_SQRT_RATIO) { vm.expectRevert(TickMath.InvalidSqrtRatio.selector); state.initialize(sqrtPriceX96, protocolFee, dynamicFee); @@ -31,11 +36,16 @@ contract PoolTest is Test { } } - function testModifyLiquidity(uint160 sqrtPriceX96, Pool.ModifyLiquidityParams memory params) public { + function testModifyLiquidity( + uint160 sqrtPriceX96, + uint24 protocolFee, + uint24 lpFee, + Pool.ModifyLiquidityParams memory params + ) public { // Assumptions tested in PoolManager.t.sol params.tickSpacing = int24(bound(params.tickSpacing, TickMath.MIN_TICK_SPACING, TickMath.MAX_TICK_SPACING)); - testPoolInitialize(sqrtPriceX96, 0, 0); + testPoolInitialize(sqrtPriceX96, protocolFee, lpFee); if (params.tickLower >= params.tickUpper) { vm.expectRevert(abi.encodeWithSelector(Pool.TicksMisordered.selector, params.tickLower, params.tickUpper)); @@ -76,14 +86,25 @@ contract PoolTest is Test { state.modifyLiquidity(params); } - function testSwap(uint160 sqrtPriceX96, uint24 swapFee, Pool.SwapParams memory params) public { + function testSwap( + uint160 sqrtPriceX96, + uint24 lpFee, + uint16 protocolFee0, + uint16 protocolFee1, + Pool.SwapParams memory params + ) public { // Assumptions tested in PoolManager.t.sol params.tickSpacing = int24(bound(params.tickSpacing, TickMath.MIN_TICK_SPACING, TickMath.MAX_TICK_SPACING)); - swapFee = uint24(bound(swapFee, 0, 999999)); + lpFee = uint24(bound(lpFee, 0, MAX_LP_FEE)); + protocolFee0 = uint16(bound(protocolFee0, 0, MAX_PROTOCOL_FEE)); + protocolFee1 = uint16(bound(protocolFee1, 0, MAX_PROTOCOL_FEE)); + uint24 protocolFee = protocolFee1 << 12 | protocolFee0; // initialize and add liquidity testModifyLiquidity( sqrtPriceX96, + protocolFee, + lpFee, Pool.ModifyLiquidityParams({ owner: address(this), tickLower: -120, @@ -116,6 +137,10 @@ contract PoolTest is Test { } else if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_RATIO) { vm.expectRevert(abi.encodeWithSelector(Pool.PriceLimitOutOfBounds.selector, params.sqrtPriceLimitX96)); } + } else if (params.amountSpecified > 0) { + if (lpFee == MAX_LP_FEE) { + vm.expectRevert(Pool.InvalidFeeForExactOut.selector); + } } state.swap(params); diff --git a/test/libraries/ProtocolFeeLibrary.t.sol b/test/libraries/ProtocolFeeLibrary.t.sol new file mode 100644 index 000000000..82cb139d9 --- /dev/null +++ b/test/libraries/ProtocolFeeLibrary.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "src/libraries/ProtocolFeeLibrary.sol"; +import "src/libraries/LPFeeLibrary.sol"; +import "forge-std/Test.sol"; + +contract ProtocolFeeLibraryTest is Test { + function test_zeroForOne() public { + uint24 fee = uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE - 1) << 12 | ProtocolFeeLibrary.MAX_PROTOCOL_FEE; + assertEq(ProtocolFeeLibrary.getZeroForOneFee(fee), uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE)); + } + + function test_oneForZero() public { + uint24 fee = uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE - 1) << 12 | ProtocolFeeLibrary.MAX_PROTOCOL_FEE; + assertEq(ProtocolFeeLibrary.getOneForZeroFee(fee), uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE - 1)); + } + + function test_fuzz_validate_protocolFee(uint24 fee) public { + if ( + (fee >> 12 > ProtocolFeeLibrary.MAX_PROTOCOL_FEE) + || (fee & (4096 - 1) > ProtocolFeeLibrary.MAX_PROTOCOL_FEE) + ) { + assertFalse(ProtocolFeeLibrary.validate(fee)); + } else { + assertTrue(ProtocolFeeLibrary.validate(fee)); + } + } + + function test_validate() public { + uint24 fee = uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE + 1) << 12 | ProtocolFeeLibrary.MAX_PROTOCOL_FEE; + assertFalse(ProtocolFeeLibrary.validate(fee)); + + fee = uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE) << 12 | (ProtocolFeeLibrary.MAX_PROTOCOL_FEE + 1); + assertFalse(ProtocolFeeLibrary.validate(fee)); + + fee = uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE + 1) << 12 | (ProtocolFeeLibrary.MAX_PROTOCOL_FEE + 1); + assertFalse(ProtocolFeeLibrary.validate(fee)); + + fee = uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE) << 12 | ProtocolFeeLibrary.MAX_PROTOCOL_FEE; + assertTrue(ProtocolFeeLibrary.validate(fee)); + } + + function test_fuzz_calculateSwapFeeDoesNotOverflow(uint24 self, uint24 lpFee) public { + lpFee = uint24(bound(lpFee, 0, LPFeeLibrary.MAX_LP_FEE)); + self = uint24(bound(self, 0, ProtocolFeeLibrary.MAX_PROTOCOL_FEE)); + assertGe(ProtocolFeeLibrary.calculateSwapFee(self, lpFee), lpFee); + } + + function test_fuzz_calculateSwapFeeNeverEqualsMax(uint24 self, uint24 lpFee) public { + lpFee = uint24(bound(lpFee, 0, LPFeeLibrary.MAX_LP_FEE - 1)); + self = uint24(bound(self, 0, ProtocolFeeLibrary.MAX_PROTOCOL_FEE)); + assertLt(ProtocolFeeLibrary.calculateSwapFee(self, lpFee), LPFeeLibrary.MAX_LP_FEE); + } + + function test_calculateSwapFee() public { + uint24 self = uint24(ProtocolFeeLibrary.MAX_PROTOCOL_FEE); + uint24 lpFee = LPFeeLibrary.MAX_LP_FEE; + assertEq(ProtocolFeeLibrary.calculateSwapFee(self, lpFee), LPFeeLibrary.MAX_LP_FEE); + + lpFee = 3000; + assertEq(ProtocolFeeLibrary.calculateSwapFee(self, lpFee), 3997); + + lpFee = 0; + assertEq(ProtocolFeeLibrary.calculateSwapFee(self, lpFee), ProtocolFeeLibrary.MAX_PROTOCOL_FEE); + + self = 0; + assertEq(ProtocolFeeLibrary.calculateSwapFee(self, lpFee), 0); + + lpFee = 1000; + assertEq(ProtocolFeeLibrary.calculateSwapFee(self, lpFee), 1000); + } +} diff --git a/test/libraries/SwapFeeLibrary.t.sol b/test/libraries/SwapFeeLibrary.t.sol index a1976e1dd..67eca377c 100644 --- a/test/libraries/SwapFeeLibrary.t.sol +++ b/test/libraries/SwapFeeLibrary.t.sol @@ -1,81 +1,81 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; -import "src/libraries/SwapFeeLibrary.sol"; +import "src/libraries/LPFeeLibrary.sol"; import "forge-std/Test.sol"; -contract SwapFeeLibraryTest is Test { +contract LPFeeLibraryTest is Test { function test_isDynamicFee_returnsTrue() public { uint24 dynamicFee = 0x800000; - assertTrue(SwapFeeLibrary.isDynamicFee(dynamicFee)); + assertTrue(LPFeeLibrary.isDynamicFee(dynamicFee)); } function test_isDynamicFee_returnsTrue_forMaxValue() public { uint24 dynamicFee = 0xFFFFFF; - assertTrue(SwapFeeLibrary.isDynamicFee(dynamicFee)); + assertTrue(LPFeeLibrary.isDynamicFee(dynamicFee)); } function test_isDynamicFee_returnsFalse() public { uint24 dynamicFee = 0x7FFFFF; - assertFalse(SwapFeeLibrary.isDynamicFee(dynamicFee)); + assertFalse(LPFeeLibrary.isDynamicFee(dynamicFee)); } function test_fuzz_isDynamicFee(uint24 fee) public { - assertEq((fee >> 23 == 1), SwapFeeLibrary.isDynamicFee(fee)); + assertEq((fee >> 23 == 1), LPFeeLibrary.isDynamicFee(fee)); } function test_validate_doesNotRevertWithNoFee() public pure { uint24 fee = 0; - SwapFeeLibrary.validate(fee); + LPFeeLibrary.validate(fee); } function test_validate_doesNotRevert() public pure { uint24 fee = 500000; // 50% - SwapFeeLibrary.validate(fee); + LPFeeLibrary.validate(fee); } function test_validate_doesNotRevertWithMaxFee() public pure { uint24 maxFee = 1000000; // 100% - SwapFeeLibrary.validate(maxFee); + LPFeeLibrary.validate(maxFee); } function test_validate_revertsWithFeeTooLarge() public { uint24 fee = 1000001; - vm.expectRevert(SwapFeeLibrary.FeeTooLarge.selector); - SwapFeeLibrary.validate(fee); + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + LPFeeLibrary.validate(fee); } function test_fuzz_validate(uint24 fee) public { if (fee > 1000000) { - vm.expectRevert(SwapFeeLibrary.FeeTooLarge.selector); + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); } - SwapFeeLibrary.validate(fee); + LPFeeLibrary.validate(fee); } - function test_getInitialSwapFee_forStaticFeeIsCorrect() public { + function test_getInitialLPFee_forStaticFeeIsCorrect() public { uint24 staticFee = 3000; // 30 bps - assertEq(SwapFeeLibrary.getInitialSwapFee(staticFee), staticFee); + assertEq(LPFeeLibrary.getInitialLPFee(staticFee), staticFee); } - function test_getInitialSwapFee_revertsWithFeeTooLarge_forStaticFee() public { + function test_getInitialLPFee_revertsWithFeeTooLarge_forStaticFee() public { uint24 staticFee = 1000001; - vm.expectRevert(SwapFeeLibrary.FeeTooLarge.selector); - SwapFeeLibrary.getInitialSwapFee(staticFee); + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + LPFeeLibrary.getInitialLPFee(staticFee); } - function test_getInitialSwapFee_forDynamicFeeIsZero() public { + function test_getInitialLPFee_forDynamicFeeIsZero() public { uint24 dynamicFee = 0x800BB8; - assertEq(SwapFeeLibrary.getInitialSwapFee(dynamicFee), 0); + assertEq(LPFeeLibrary.getInitialLPFee(dynamicFee), 0); } - function test_fuzz_getInitialSwapFee(uint24 fee) public { + function test_fuzz_getInitialLPFee(uint24 fee) public { if (fee >> 23 == 1) { - assertEq(SwapFeeLibrary.getInitialSwapFee(fee), 0); + assertEq(LPFeeLibrary.getInitialLPFee(fee), 0); } else if (fee > 1000000) { - vm.expectRevert(SwapFeeLibrary.FeeTooLarge.selector); - SwapFeeLibrary.getInitialSwapFee(fee); + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + LPFeeLibrary.getInitialLPFee(fee); } else { - assertEq(SwapFeeLibrary.getInitialSwapFee(fee), fee); + assertEq(LPFeeLibrary.getInitialLPFee(fee), fee); } } } diff --git a/test/libraries/SwapMath.t.sol b/test/libraries/SwapMath.t.sol index 2c17f41f6..7810c4ec7 100644 --- a/test/libraries/SwapMath.t.sol +++ b/test/libraries/SwapMath.t.sol @@ -16,16 +16,16 @@ contract SwapMathTest is Test, GasSnapshot { uint160 private constant SQRT_RATIO_1010_100 = 251791039410471229173201122529; uint160 private constant SQRT_RATIO_10000_100 = 792281625142643375935439503360; - function test_exactAmountIn_oneForZero_thatGetsCappedAtPriceTargetIn() public { + function test_exactAmountOut_oneForZero_thatGetsCappedAtPriceTargetIn() public { uint160 priceTarget = SQRT_RATIO_101_100; uint160 price = SQRT_RATIO_1_1; uint128 liquidity = 2 ether; int256 amount = 1 ether; - uint24 fee = 600; + uint24 lpFee = 600; bool zeroForOne = false; (uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = - SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, fee); + SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee); assertEq(amountIn, 9975124224178055); assertEq(amountOut, 9925619580021728); @@ -39,16 +39,16 @@ contract SwapMathTest is Test, GasSnapshot { assert(sqrtQ < priceAfterWholeInputAmount); } - function test_exactAmountOut_oneForZero_thatGetsCappedAtPriceTargetIn() public { + function test_exactAmountIn_oneForZero_thatGetsCappedAtPriceTargetIn() public { uint160 priceTarget = SQRT_RATIO_101_100; uint160 price = SQRT_RATIO_1_1; uint128 liquidity = 2 ether; int256 amount = (1 ether) * -1; - uint24 fee = 600; + uint24 lpFee = 600; bool zeroForOne = false; (uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = - SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, fee); + SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee); assertEq(amountIn, 9975124224178055); assertEq(amountOut, 9925619580021728); @@ -67,11 +67,11 @@ contract SwapMathTest is Test, GasSnapshot { uint160 price = SQRT_RATIO_1_1; uint128 liquidity = 2 ether; int256 amount = 1 ether * -1; - uint24 fee = 600; + uint24 lpFee = 600; bool zeroForOne = false; (uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = - SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, fee); + SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee); assertEq(amountIn, 999400000000000000); assertEq(amountOut, 666399946655997866); @@ -90,11 +90,11 @@ contract SwapMathTest is Test, GasSnapshot { uint160 price = SQRT_RATIO_1_1; uint128 liquidity = 2 ether; int256 amount = (1 ether); - uint24 fee = 600; + uint24 lpFee = 600; bool zeroForOne = false; (uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = - SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, fee); + SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee); assertEq(amountIn, 2000000000000000000); assertEq(feeAmount, 1200720432259356); @@ -224,37 +224,37 @@ contract SwapMathTest is Test, GasSnapshot { function test_swapOneForZero_exactInCapped() public { snapStart("SwapMath_oneForZero_exactInCapped"); - SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_101_100, 2 ether, 1 ether, 600); + SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_101_100, 2 ether, (1 ether) * -1, 600); snapEnd(); } function test_swapZeroForOne_exactInCapped() public { snapStart("SwapMath_zeroForOne_exactInCapped"); - SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_99_100, 2 ether, 1 ether, 600); + SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_99_100, 2 ether, (1 ether) * -1, 600); snapEnd(); } function test_swapOneForZero_exactOutCapped() public { snapStart("SwapMath_oneForZero_exactOutCapped"); - SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_101_100, 2 ether, (1 ether) * -1, 600); + SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_101_100, 2 ether, 1 ether, 600); snapEnd(); } function test_swapZeroForOne_exactOutCapped() public { snapStart("SwapMath_zeroForOne_exactOutCapped"); - SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_99_100, 2 ether, (1 ether) * -1, 600); + SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_99_100, 2 ether, 1 ether, 600); snapEnd(); } function test_swapOneForZero_exactInPartial() public { snapStart("SwapMath_oneForZero_exactInPartial"); - SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_1010_100, 2 ether, 1_000, 600); + SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_1010_100, 2 ether, 1_000 * -1, 600); snapEnd(); } function test_swapZeroForOne_exactInPartial() public { snapStart("SwapMath_zeroForOne_exactInPartial"); - SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_99_1000, 2 ether, 1_000, 600); + SwapMath.computeSwapStep(SQRT_RATIO_1_1, SQRT_RATIO_99_1000, 2 ether, 1_000 * -1, 600); snapEnd(); } diff --git a/test/utils/Deployers.sol b/test/utils/Deployers.sol index adccc9660..a2666ab48 100644 --- a/test/utils/Deployers.sol +++ b/test/utils/Deployers.sol @@ -8,7 +8,7 @@ import {IHooks} from "../../src/interfaces/IHooks.sol"; import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; import {PoolManager} from "../../src/PoolManager.sol"; import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; -import {SwapFeeLibrary} from "../../src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "../../src/libraries/LPFeeLibrary.sol"; import {PoolKey} from "../../src/types/PoolKey.sol"; import {BalanceDelta} from "../../src/types/BalanceDelta.sol"; import {TickMath} from "../../src/libraries/TickMath.sol"; @@ -30,7 +30,7 @@ import { } from "../../src/test/ProtocolFeeControllerTest.sol"; contract Deployers { - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; diff --git a/test/utils/SwapHelper.t.sol b/test/utils/SwapHelper.t.sol index c13ee82a6..a7765e3ec 100644 --- a/test/utils/SwapHelper.t.sol +++ b/test/utils/SwapHelper.t.sol @@ -5,7 +5,6 @@ import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {Hooks} from "../../src/libraries/Hooks.sol"; -import {SwapFeeLibrary} from "../../src/libraries/SwapFeeLibrary.sol"; import {MockHooks} from "../../src/test/MockHooks.sol"; import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol";