diff --git a/.forge-snapshots/addLiquidity with empty hook.snap b/.forge-snapshots/addLiquidity with empty hook.snap index 50c70f46e..bff0f6af2 100644 --- a/.forge-snapshots/addLiquidity with empty hook.snap +++ b/.forge-snapshots/addLiquidity with empty hook.snap @@ -1 +1 @@ -264375 \ No newline at end of file +259285 \ 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 76beb54cb..b71e91920 100644 --- a/.forge-snapshots/addLiquidity with native token.snap +++ b/.forge-snapshots/addLiquidity with native token.snap @@ -1 +1 @@ -139216 \ No newline at end of file +136628 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity.snap b/.forge-snapshots/addLiquidity.snap index c18243311..26a19adbd 100644 --- a/.forge-snapshots/addLiquidity.snap +++ b/.forge-snapshots/addLiquidity.snap @@ -1 +1 @@ -144552 \ No newline at end of file +139462 \ 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 8a8adaaba..a64b2af8a 100644 --- a/.forge-snapshots/donate gas with 1 token.snap +++ b/.forge-snapshots/donate gas with 1 token.snap @@ -1 +1 @@ -101147 \ No newline at end of file +98611 \ 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 60b88f450..8ca2e3c48 100644 --- a/.forge-snapshots/donate gas with 2 tokens.snap +++ b/.forge-snapshots/donate gas with 2 tokens.snap @@ -1 +1 @@ -132153 \ No newline at end of file +127061 \ 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 b9fe27a3f..36cf22341 100644 --- a/.forge-snapshots/erc20 collect protocol fees.snap +++ b/.forge-snapshots/erc20 collect protocol fees.snap @@ -1 +1 @@ -24961 \ No newline at end of file +24938 \ 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 984502b79..5d4c1b5c0 100644 --- a/.forge-snapshots/native collect protocol fees.snap +++ b/.forge-snapshots/native collect protocol fees.snap @@ -1 +1 @@ -36634 \ No newline at end of file +36611 \ No newline at end of file diff --git a/.forge-snapshots/poolManager bytecode size.snap b/.forge-snapshots/poolManager bytecode size.snap index e32648bde..72d6bf5b4 100644 --- a/.forge-snapshots/poolManager bytecode size.snap +++ b/.forge-snapshots/poolManager bytecode size.snap @@ -1 +1 @@ -22366 \ No newline at end of file +22541 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity with empty hook.snap b/.forge-snapshots/removeLiquidity with empty hook.snap index f0da7eff8..968a0ecc9 100644 --- a/.forge-snapshots/removeLiquidity with empty hook.snap +++ b/.forge-snapshots/removeLiquidity with empty hook.snap @@ -1 +1 @@ -54720 \ No newline at end of file +53760 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity with native token.snap b/.forge-snapshots/removeLiquidity with native token.snap index db68d84df..d2a55de87 100644 --- a/.forge-snapshots/removeLiquidity with native token.snap +++ b/.forge-snapshots/removeLiquidity with native token.snap @@ -1 +1 @@ -146903 \ No newline at end of file +141532 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity.snap b/.forge-snapshots/removeLiquidity.snap index bb0b110ef..b0757bb34 100644 --- a/.forge-snapshots/removeLiquidity.snap +++ b/.forge-snapshots/removeLiquidity.snap @@ -1 +1 @@ -148370 \ No newline at end of file +137810 \ 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 c7043791c..d38714a78 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -132580 \ No newline at end of file +126802 \ No newline at end of file diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index bf7058159..28983a330 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -146475 \ No newline at end of file +138195 \ 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 dcd3c811e..e9d1cdd2c 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -72112 \ No newline at end of file +71137 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 146ee96a1..5bf73b1bc 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -60134 \ No newline at end of file +61456 \ 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 48cbd7544..8b787012a 100644 --- a/.forge-snapshots/swap burn 6909 for input.snap +++ b/.forge-snapshots/swap burn 6909 for input.snap @@ -1 +1 @@ -80275 \ No newline at end of file +79333 \ 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 1907fad7e..186ee2f0a 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -76142 \ No newline at end of file +75192 \ 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 4b72dd140..ef50d8bcd 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -138642 \ No newline at end of file +135559 \ 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 a2a4548db..65d0e8b8f 100644 --- a/.forge-snapshots/swap mint output as 6909.snap +++ b/.forge-snapshots/swap mint output as 6909.snap @@ -1 +1 @@ -155290 \ No newline at end of file +152198 \ 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 f1f4f5e86..7ec0f499f 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 @@ -155727 \ No newline at end of file +158761 \ 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 6c0e46a45..2a44cb097 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -89365 \ No newline at end of file +90685 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index 17e19c3f4..daab821d2 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -60112 \ No newline at end of file +61435 \ 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 2cdad0da8..1df8bd034 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -139991 \ No newline at end of file +131711 \ No newline at end of file diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 90a8cd004..d81b3a83d 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -21,6 +21,7 @@ import {Lock} from "./libraries/Lock.sol"; import {CurrencyDelta} from "./libraries/CurrencyDelta.sol"; import {NonZeroDeltaCount} from "./libraries/NonZeroDeltaCount.sol"; import {PoolGetters} from "./libraries/PoolGetters.sol"; +import {Reserves} from "./libraries/Reserves.sol"; import {Extsload} from "./Extsload.sol"; /// @notice Holds the state for all pools @@ -35,6 +36,7 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim using CurrencyDelta for Currency; using SwapFeeLibrary for uint24; using PoolGetters for Pool.State; + using Reserves for Currency; /// @inheritdoc IPoolManager int24 public constant MAX_TICK_SPACING = TickMath.MAX_TICK_SPACING; @@ -42,9 +44,6 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim /// @inheritdoc IPoolManager int24 public constant MIN_TICK_SPACING = TickMath.MIN_TICK_SPACING; - /// @inheritdoc IPoolManager - mapping(Currency currency => uint256) public override reservesOf; - mapping(PoolId id => Pool.State) public pools; constructor(uint256 controllerGasLimit) ProtocolFees(controllerGasLimit) {} @@ -146,18 +145,22 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim Lock.lock(); } + /// @inheritdoc IPoolManager + function sync(Currency currency) public returns (uint256 balance) { + balance = currency.balanceOfSelf(); + currency.setReserves(balance); + } + function _accountDelta(Currency currency, int128 delta) internal { if (delta == 0) return; int256 current = currency.getDelta(msg.sender); int256 next = current + delta; - unchecked { - if (next == 0) { - NonZeroDeltaCount.decrement(); - } else if (current == 0) { - NonZeroDeltaCount.increment(); - } + if (next == 0) { + NonZeroDeltaCount.decrement(); + } else if (current == 0) { + NonZeroDeltaCount.increment(); } currency.setDelta(msg.sender, next); @@ -260,10 +263,11 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim /// @inheritdoc IPoolManager function take(Currency currency, address to, uint256 amount) external override onlyWhenUnlocked { - // subtraction must be safe - _accountDelta(currency, -(amount.toInt128())); - if (!currency.isNative()) reservesOf[currency] -= amount; - currency.transfer(to, amount); + unchecked { + // subtraction must be safe + _accountDelta(currency, -(amount.toInt128())); + currency.transfer(to, amount); + } } /// @inheritdoc IPoolManager @@ -272,19 +276,20 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim paid = msg.value; } else { if (msg.value > 0) revert NonZeroNativeValue(); - uint256 reservesBefore = reservesOf[currency]; - reservesOf[currency] = currency.balanceOfSelf(); - paid = reservesOf[currency] - reservesBefore; + uint256 reservesBefore = currency.getReserves(); + uint256 reservesNow = sync(currency); + paid = reservesNow - reservesBefore; } - _accountDelta(currency, paid.toInt128()); } /// @inheritdoc IPoolManager function mint(address to, uint256 id, uint256 amount) external override onlyWhenUnlocked { - // subtraction must be safe - _accountDelta(CurrencyLibrary.fromId(id), -(amount.toInt128())); - _mint(to, id, amount); + unchecked { + // subtraction must be safe + _accountDelta(CurrencyLibrary.fromId(id), -(amount.toInt128())); + _mint(to, id, amount); + } } /// @inheritdoc IPoolManager @@ -312,6 +317,11 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim return pools[id].getPoolBitmapInfo(word); } + /// @notice Temporary view function. Replaceable by transient EXTSLOAD. + function getReserves(Currency currency) external view returns (uint256 balance) { + return currency.getReserves(); + } + function getFeeGrowthGlobals(PoolId id) external view diff --git a/src/ProtocolFees.sol b/src/ProtocolFees.sol index b7d2b062b..8948a20d5 100644 --- a/src/ProtocolFees.sol +++ b/src/ProtocolFees.sol @@ -33,9 +33,9 @@ abstract contract ProtocolFees is IProtocolFees, Owned { } /// @inheritdoc IProtocolFees - function setProtocolFee(PoolKey memory key) external { - (bool success, uint24 newProtocolFee) = _fetchProtocolFee(key); - if (!success) revert ProtocolFeeControllerCallFailedOrInvalidResult(); + function setProtocolFee(PoolKey memory key, uint24 newProtocolFee) external { + if (msg.sender != address(protocolFeeController)) revert InvalidCaller(); + if (!newProtocolFee.validate()) revert InvalidProtocolFee(); PoolId id = key.toId(); _getPool(id).setProtocolFee(newProtocolFee); emit ProtocolFeeUpdated(id, newProtocolFee); diff --git a/src/interfaces/IPoolManager.sol b/src/interfaces/IPoolManager.sol index 1ba40664d..7d9c9f69f 100644 --- a/src/interfaces/IPoolManager.sol +++ b/src/interfaces/IPoolManager.sol @@ -124,8 +124,10 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { view returns (Position.Info memory position); - /// @notice Returns the reserves for a given ERC20 currency - function reservesOf(Currency currency) external view returns (uint256); + /// @notice Writes the current ERC20 balance of the specified currency to transient storage + /// This is used to checkpoint balances for the manager and derive deltas for the caller. + /// @dev This MUST be called before any ERC20 tokens are sent into the contract. + function sync(Currency currency) external returns (uint256 balance); /// @notice Returns whether the contract is unlocked or not function isUnlocked() external view returns (bool); @@ -198,4 +200,6 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { /// @notice Updates the pools swap fees for the a pool that has enabled dynamic swap fees. function updateDynamicSwapFee(PoolKey memory key, uint24 newDynamicSwapFee) external; + + function getReserves(Currency currency) external view returns (uint256 balance); } diff --git a/src/interfaces/IProtocolFees.sol b/src/interfaces/IProtocolFees.sol index aeed1a0e4..0af846a08 100644 --- a/src/interfaces/IProtocolFees.sol +++ b/src/interfaces/IProtocolFees.sol @@ -9,10 +9,10 @@ import {PoolKey} from "../types/PoolKey.sol"; interface IProtocolFees { /// @notice Thrown when not enough gas is provided to look up the protocol fee error ProtocolFeeCannotBeFetched(); - /// @notice Thrown when the call to fetch the protocol fee reverts or returns invalid data. - error ProtocolFeeControllerCallFailedOrInvalidResult(); + /// @notice Thrown when protocol fee is set too high + error InvalidProtocolFee(); - /// @notice Thrown when collectProtocolFees is not called by the controller. + /// @notice Thrown when collectProtocolFees or setProtocolFee is not called by the controller. error InvalidCaller(); event ProtocolFeeControllerUpdated(address protocolFeeController); @@ -22,9 +22,8 @@ interface IProtocolFees { /// @notice Given a currency address, returns the protocol fees accrued in that currency function protocolFeesAccrued(Currency) external view returns (uint256); - /// @notice Sets the protocol's swap fee for the given pool - /// Protocol fees are always a portion of the LP swap fee that is owed. If that fee is 0, no protocol fees will accrue even if it is set to > 0. - function setProtocolFee(PoolKey memory key) external; + /// @notice Sets the protocol fee for the given pool + function setProtocolFee(PoolKey memory key, uint24) external; /// @notice Sets the protocol fee controller function setProtocolFeeController(IProtocolFeeController) external; diff --git a/src/libraries/CurrencySettleTake.sol b/src/libraries/CurrencySettleTake.sol new file mode 100644 index 000000000..8fcc957a8 --- /dev/null +++ b/src/libraries/CurrencySettleTake.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Currency, CurrencyLibrary} from "../types/Currency.sol"; +import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; + +library CurrencySettleTake { + using CurrencyLibrary for Currency; + + /// @notice Settle (pay) a currency to the PoolManager + /// @param currency Currency to settle + /// @param manager IPoolManager to settle to + /// @param payer Address of the payer, the token sender + /// @param amount Amount to send + /// @param burn If true, burn the ERC-6909 token, otherwise ERC20-transfer to the PoolManager + function settle(Currency currency, IPoolManager manager, address payer, uint256 amount, bool burn) internal { + // for native currencies or burns, calling sync is not required + // short circuit for ERC-6909 burns to support ERC-6909-wrapped native tokens + if (burn) { + manager.burn(payer, currency.toId(), amount); + } else if (currency.isNative()) { + manager.settle{value: amount}(currency); + } else { + manager.sync(currency); + IERC20Minimal(Currency.unwrap(currency)).transferFrom(payer, address(manager), amount); + manager.settle(currency); + } + } + + /// @notice Take (receive) a currency from the PoolManager + /// @param currency Currency to take + /// @param manager IPoolManager to take from + /// @param recipient Address of the recipient, the token receiver + /// @param amount Amount to receive + /// @param claims If true, mint the ERC-6909 token, otherwise ERC20-transfer from the PoolManager to recipient + function take(Currency currency, IPoolManager manager, address recipient, uint256 amount, bool claims) internal { + claims ? manager.mint(recipient, currency.toId(), amount) : manager.take(currency, recipient, amount); + } +} diff --git a/src/libraries/Reserves.sol b/src/libraries/Reserves.sol new file mode 100644 index 000000000..4099e4c96 --- /dev/null +++ b/src/libraries/Reserves.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +import {Currency} from "../types/Currency.sol"; + +library Reserves { + /// uint256(keccak256("ReservesOf")) - 1 + uint256 constant RESERVES_OF_SLOT = uint256(0x1e0745a7db1623981f0b2a5d4232364c00787266eb75ad546f190e6cebe9bd95); + /// @notice The transient reserves for pools with no balance is set to the max as a sentinel to track that it has been synced. + uint256 public constant ZERO_BALANCE = type(uint256).max; + + /// @notice Thrown when someone has not called sync before calling settle for the first time. + error ReservesMustBeSynced(); + + function setReserves(Currency currency, uint256 value) internal { + if (value == 0) value = ZERO_BALANCE; + bytes32 key = _getKey(currency); + assembly { + tstore(key, value) + } + } + + function getReserves(Currency currency) internal view returns (uint256 value) { + bytes32 key = _getKey(currency); + assembly { + value := tload(key) + } + if (value == 0) revert ReservesMustBeSynced(); + if (value == ZERO_BALANCE) value = 0; + } + + function _getKey(Currency currency) private pure returns (bytes32 key) { + uint256 slot = RESERVES_OF_SLOT; + assembly { + mstore(0, slot) + mstore(32, currency) + key := keccak256(0, 64) + } + } +} diff --git a/src/test/ActionsRouter.sol b/src/test/ActionsRouter.sol new file mode 100644 index 000000000..aeb6c4eb7 --- /dev/null +++ b/src/test/ActionsRouter.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {IUnlockCallback} from "../interfaces/callback/IUnlockCallback.sol"; +import {Currency, CurrencyLibrary} from "../types/Currency.sol"; +import {PoolKey} from "../types/PoolKey.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +// Supported Actions. +enum Actions { + SETTLE, + TAKE, + SYNC, + MINT, + ASSERT_BALANCE_EQUALS, + ASSERT_RESERVES_EQUALS, + ASSERT_DELTA_EQUALS, + TRANSFER_FROM +} +// TODO: Add other actions as needed. +// BURN, +// MODIFY_POSITION, +// INITIALIZE, +// DONATE + +/// @notice A router that handles an arbitrary input of actions. +/// TODO: Can continue to add functions per action. +contract ActionsRouter is IUnlockCallback, Test { + using CurrencyLibrary for Currency; + + error ActionNotSupported(); + + IPoolManager manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + function unlockCallback(bytes calldata data) external returns (bytes memory) { + (Actions[] memory actions, bytes[] memory params) = abi.decode(data, (Actions[], bytes[])); + for (uint256 i = 0; i < actions.length; i++) { + Actions action = actions[i]; + bytes memory param = params[i]; + if (action == Actions.SETTLE) { + _settle(param); + } else if (action == Actions.TAKE) { + _take(param); + } else if (action == Actions.SYNC) { + _sync(param); + } else if (action == Actions.MINT) { + _mint(param); + } else if (action == Actions.ASSERT_BALANCE_EQUALS) { + _assertBalanceEquals(param); + } else if (action == Actions.ASSERT_RESERVES_EQUALS) { + _assertReservesEquals(param); + } else if (action == Actions.ASSERT_DELTA_EQUALS) { + _assertDeltaEquals(param); + } else if (action == Actions.TRANSFER_FROM) { + _transferFrom(param); + } + } + } + + function executeActions(Actions[] memory actions, bytes[] memory params) external { + manager.unlock(abi.encode(actions, params)); + } + + function _settle(bytes memory params) internal { + Currency currency = abi.decode(params, (Currency)); + manager.settle(currency); + } + + function _take(bytes memory params) internal { + (Currency currency, address recipient, int128 amount) = abi.decode(params, (Currency, address, int128)); + manager.take(currency, recipient, uint128(amount)); + } + + function _sync(bytes memory params) internal { + Currency currency = Currency.wrap(abi.decode(params, (address))); + manager.sync(currency); + } + + function _mint(bytes memory params) internal { + (address recipient, Currency currency, uint256 amount) = abi.decode(params, (address, Currency, uint256)); + manager.mint(recipient, currency.toId(), amount); + } + + function _assertBalanceEquals(bytes memory params) internal { + (Currency currency, address user, uint256 expectedBalance) = abi.decode(params, (Currency, address, uint256)); + assertEq(currency.balanceOf(user), expectedBalance, "usertoken value incorrect"); + } + + function _assertReservesEquals(bytes memory params) internal { + (Currency currency, uint256 expectedReserves) = abi.decode(params, (Currency, uint256)); + assertEq(manager.getReserves(currency), expectedReserves, "reserves value incorrect"); + } + + function _assertDeltaEquals(bytes memory params) internal { + (Currency currency, address caller, int256 expectedDelta) = abi.decode(params, (Currency, address, int256)); + + assertEq(manager.currencyDelta(caller, currency), expectedDelta, "delta value incorrect"); + } + + function _transferFrom(bytes memory params) internal { + (Currency currency, address from, address recipient, uint256 amount) = + abi.decode(params, (Currency, address, address, uint256)); + MockERC20(Currency.unwrap(currency)).transferFrom(from, recipient, uint256(amount)); + } +} diff --git a/src/test/PoolClaimsTest.sol b/src/test/PoolClaimsTest.sol index b5af6b763..1ad2a586b 100644 --- a/src/test/PoolClaimsTest.sol +++ b/src/test/PoolClaimsTest.sol @@ -7,9 +7,11 @@ import {IPoolManager} from "../interfaces/IPoolManager.sol"; import {PoolKey} from "../types/PoolKey.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {SafeCast} from "../libraries/SafeCast.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; contract PoolClaimsTest is PoolTestBase { using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; using SafeCast for uint256; constructor(IPoolManager _manager) PoolTestBase(_manager) {} @@ -38,11 +40,11 @@ contract PoolClaimsTest is PoolTestBase { CallbackData memory data = abi.decode(rawData, (CallbackData)); if (data.deposit) { - manager.mint(data.user, data.currency.toId(), uint128(data.amount)); - _settle(data.currency, data.user, -data.amount.toInt128(), true); + data.currency.take(manager, data.user, data.amount, true); // mint 6909 + data.currency.settle(manager, data.user, data.amount, false); // transfer ERC20 } else { - manager.burn(data.user, data.currency.toId(), uint128(data.amount)); - _take(data.currency, data.user, data.amount.toInt128(), true); + data.currency.settle(manager, data.user, data.amount, true); // burn 6909 + data.currency.take(manager, data.user, data.amount, false); // claim ERC20 } return abi.encode(0); diff --git a/src/test/PoolDonateTest.sol b/src/test/PoolDonateTest.sol index 55d95c392..5b16f7988 100644 --- a/src/test/PoolDonateTest.sol +++ b/src/test/PoolDonateTest.sol @@ -8,9 +8,11 @@ import {BalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {IHooks} from "../interfaces/IHooks.sol"; import {Hooks} from "../libraries/Hooks.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; contract PoolDonateTest is PoolTestBase { using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; using Hooks for IHooks; constructor(IPoolManager _manager) PoolTestBase(_manager) {} @@ -57,10 +59,10 @@ contract PoolDonateTest is PoolTestBase { require(deltaAfter0 == -int256(data.amount0), "deltaAfter0 is not equal to -int256(data.amount0)"); require(deltaAfter1 == -int256(data.amount1), "deltaAfter1 is not equal to -int256(data.amount1)"); - if (deltaAfter0 < 0) _settle(data.key.currency0, data.sender, int128(deltaAfter0), true); - if (deltaAfter1 < 0) _settle(data.key.currency1, data.sender, int128(deltaAfter1), true); - if (deltaAfter0 > 0) _take(data.key.currency0, data.sender, int128(deltaAfter0), true); - if (deltaAfter1 > 0) _take(data.key.currency1, data.sender, int128(deltaAfter1), true); + if (deltaAfter0 < 0) data.key.currency0.settle(manager, data.sender, uint256(-deltaAfter0), false); + if (deltaAfter1 < 0) data.key.currency1.settle(manager, data.sender, uint256(-deltaAfter1), false); + if (deltaAfter0 > 0) data.key.currency0.take(manager, data.sender, uint256(deltaAfter0), false); + if (deltaAfter1 > 0) data.key.currency1.take(manager, data.sender, uint256(deltaAfter1), false); return abi.encode(delta); } diff --git a/src/test/PoolModifyLiquidityTest.sol b/src/test/PoolModifyLiquidityTest.sol index 0ad4f4b11..8a9fc27f5 100644 --- a/src/test/PoolModifyLiquidityTest.sol +++ b/src/test/PoolModifyLiquidityTest.sol @@ -9,9 +9,11 @@ import {PoolTestBase} from "./PoolTestBase.sol"; import {IHooks} from "../interfaces/IHooks.sol"; import {Hooks} from "../libraries/Hooks.sol"; import {SwapFeeLibrary} from "../libraries/SwapFeeLibrary.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; @@ -22,8 +24,8 @@ contract PoolModifyLiquidityTest is PoolTestBase { PoolKey key; IPoolManager.ModifyLiquidityParams params; bytes hookData; - bool settleUsingTransfer; - bool withdrawTokens; + bool settleUsingBurn; + bool takeClaims; } function modifyLiquidity( @@ -31,20 +33,18 @@ contract PoolModifyLiquidityTest is PoolTestBase { IPoolManager.ModifyLiquidityParams memory params, bytes memory hookData ) external payable returns (BalanceDelta delta) { - delta = modifyLiquidity(key, params, hookData, true, true); + delta = modifyLiquidity(key, params, hookData, false, false); } function modifyLiquidity( PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params, bytes memory hookData, - bool settleUsingTransfer, - bool withdrawTokens + bool settleUsingBurn, + bool takeClaims ) public payable returns (BalanceDelta delta) { delta = abi.decode( - manager.unlock( - abi.encode(CallbackData(msg.sender, key, params, hookData, settleUsingTransfer, withdrawTokens)) - ), + manager.unlock(abi.encode(CallbackData(msg.sender, key, params, hookData, settleUsingBurn, takeClaims))), (BalanceDelta) ); @@ -72,10 +72,10 @@ contract PoolModifyLiquidityTest is PoolTestBase { assert(!(delta0 > 0 || delta1 > 0)); } - if (delta0 < 0) _settle(data.key.currency0, data.sender, int128(delta0), data.settleUsingTransfer); - if (delta1 < 0) _settle(data.key.currency1, data.sender, int128(delta1), data.settleUsingTransfer); - if (delta0 > 0) _take(data.key.currency0, data.sender, int128(delta0), data.withdrawTokens); - if (delta1 > 0) _take(data.key.currency1, data.sender, int128(delta1), data.withdrawTokens); + if (delta0 < 0) data.key.currency0.settle(manager, data.sender, uint256(-delta0), data.settleUsingBurn); + if (delta1 < 0) data.key.currency1.settle(manager, data.sender, uint256(-delta1), data.settleUsingBurn); + if (delta0 > 0) data.key.currency0.take(manager, data.sender, uint256(delta0), data.takeClaims); + if (delta1 > 0) data.key.currency1.take(manager, data.sender, uint256(delta1), data.takeClaims); return abi.encode(delta); } diff --git a/src/test/PoolNestedActionsTest.sol b/src/test/PoolNestedActionsTest.sol index 612199231..07fbdd081 100644 --- a/src/test/PoolNestedActionsTest.sol +++ b/src/test/PoolNestedActionsTest.sol @@ -10,6 +10,7 @@ import {Test} from "forge-std/Test.sol"; import {BalanceDelta} from "../types/BalanceDelta.sol"; import {Currency} from "../types/Currency.sol"; import {PoolId, PoolIdLibrary} from "../types/PoolId.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; enum Action { NESTED_SELF_UNLOCK, @@ -58,6 +59,7 @@ contract PoolNestedActionsTest is Test, IUnlockCallback { } contract NestedActionExecutor is Test, PoolTestBase { + using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; PoolKey internal key; @@ -129,8 +131,8 @@ contract NestedActionExecutor is Test, PoolTestBase { assertEq(delta.amount0(), deltaThisAfter0, "Swap delta 0"); assertEq(delta.amount1(), deltaThisAfter1, "Swap delta 1"); - _settle(key.currency0, user, int128(deltaThisAfter0), true); - _take(key.currency1, user, int128(deltaThisAfter1), true); + key.currency0.settle(manager, user, uint256(-deltaThisAfter0), false); + key.currency1.take(manager, user, uint256(deltaThisAfter1), false); } function _addLiquidity(address caller) internal { @@ -153,8 +155,8 @@ contract NestedActionExecutor is Test, PoolTestBase { assertEq(deltaThisBefore0 + delta.amount0(), deltaThisAfter0, "Executor delta 0"); assertEq(deltaThisBefore1 + delta.amount1(), deltaThisAfter1, "Executor delta 1"); - _settle(key.currency0, user, int128(deltaThisAfter0), true); - _settle(key.currency1, user, int128(deltaThisAfter1), true); + key.currency0.settle(manager, user, uint256(-deltaThisAfter0), false); + key.currency1.settle(manager, user, uint256(-deltaThisAfter1), false); } // cannot remove non-existent liquidity - need to perform an add before this removal @@ -178,8 +180,8 @@ contract NestedActionExecutor is Test, PoolTestBase { assertEq(deltaThisBefore0 + delta.amount0(), deltaThisAfter0, "Executor delta 0"); assertEq(deltaThisBefore1 + delta.amount1(), deltaThisAfter1, "Executor delta 1"); - _take(key.currency0, user, int128(deltaThisAfter0), true); - _take(key.currency1, user, int128(deltaThisAfter1), true); + key.currency0.take(manager, user, uint256(deltaThisAfter0), false); + key.currency1.take(manager, user, uint256(deltaThisAfter1), false); } function _donate(address caller) internal { @@ -204,8 +206,8 @@ contract NestedActionExecutor is Test, PoolTestBase { assertEq(-delta.amount0(), int256(DONATE_AMOUNT0), "Donate delta 0"); assertEq(-delta.amount1(), int256(DONATE_AMOUNT1), "Donate delta 1"); - _settle(key.currency0, user, int128(deltaThisAfter0), true); - _settle(key.currency1, user, int128(deltaThisAfter1), true); + key.currency0.settle(manager, user, uint256(-deltaThisAfter0), false); + key.currency1.settle(manager, user, uint256(-deltaThisAfter1), false); } function _initialize() internal { diff --git a/src/test/PoolSwapTest.sol b/src/test/PoolSwapTest.sol index 255fb678a..07484aa02 100644 --- a/src/test/PoolSwapTest.sol +++ b/src/test/PoolSwapTest.sol @@ -10,9 +10,11 @@ import {Hooks} from "../libraries/Hooks.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {Hooks} from "../libraries/Hooks.sol"; import {IHooks} from "../interfaces/IHooks.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; contract PoolSwapTest is PoolTestBase { using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; using Hooks for IHooks; constructor(IPoolManager _manager) PoolTestBase(_manager) {} @@ -28,9 +30,8 @@ contract PoolSwapTest is PoolTestBase { } struct TestSettings { - bool withdrawTokens; - bool settleUsingTransfer; - bool currencyAlreadySent; + bool takeClaims; + bool settleUsingBurn; } function swap( @@ -98,24 +99,16 @@ contract PoolSwapTest is PoolTestBase { } if (deltaAfter0 < 0) { - if (data.testSettings.currencyAlreadySent) { - manager.settle(data.key.currency0); - } else { - _settle(data.key.currency0, data.sender, int128(deltaAfter0), data.testSettings.settleUsingTransfer); - } + data.key.currency0.settle(manager, data.sender, uint256(-deltaAfter0), data.testSettings.settleUsingBurn); } if (deltaAfter1 < 0) { - if (data.testSettings.currencyAlreadySent) { - manager.settle(data.key.currency1); - } else { - _settle(data.key.currency1, data.sender, int128(deltaAfter1), data.testSettings.settleUsingTransfer); - } + data.key.currency1.settle(manager, data.sender, uint256(-deltaAfter1), data.testSettings.settleUsingBurn); } if (deltaAfter0 > 0) { - _take(data.key.currency0, data.sender, int128(deltaAfter0), data.testSettings.withdrawTokens); + data.key.currency0.take(manager, data.sender, uint256(deltaAfter0), data.testSettings.takeClaims); } if (deltaAfter1 > 0) { - _take(data.key.currency1, data.sender, int128(deltaAfter1), data.testSettings.withdrawTokens); + data.key.currency1.take(manager, data.sender, uint256(deltaAfter1), data.testSettings.takeClaims); } return abi.encode(delta); diff --git a/src/test/PoolTakeTest.sol b/src/test/PoolTakeTest.sol index 4f4675b08..f676f78ee 100644 --- a/src/test/PoolTakeTest.sol +++ b/src/test/PoolTakeTest.sol @@ -6,9 +6,11 @@ import {IPoolManager} from "../interfaces/IPoolManager.sol"; import {PoolKey} from "../types/PoolKey.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {SafeCast} from "../libraries/SafeCast.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; contract PoolTakeTest is PoolTestBase { using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; using SafeCast for uint256; constructor(IPoolManager _manager) PoolTestBase(_manager) {} @@ -40,7 +42,7 @@ contract PoolTakeTest is PoolTestBase { _fetchBalances(currency, sender, address(this)); require(deltaBefore == 0, "deltaBefore is not equal to 0"); - _take(currency, sender, amount.toInt128(), true); + currency.take(manager, sender, amount, false); (uint256 userBalAfter, uint256 pmBalAfter, int256 deltaAfter) = _fetchBalances(currency, sender, address(this)); @@ -55,6 +57,6 @@ contract PoolTakeTest is PoolTestBase { "the difference between pmBalBefore and pmBalAfter is not equal to amount" ); - _settle(currency, sender, -amount.toInt128(), true); + currency.settle(manager, sender, amount, false); } } diff --git a/src/test/PoolTestBase.sol b/src/test/PoolTestBase.sol index c75a264a1..c52a5f046 100644 --- a/src/test/PoolTestBase.sol +++ b/src/test/PoolTestBase.sol @@ -16,29 +16,6 @@ abstract contract PoolTestBase is IUnlockCallback { manager = _manager; } - function _take(Currency currency, address recipient, int128 amount, bool withdrawTokens) internal { - require(amount > 0, "amount is not greater than zero"); - if (withdrawTokens) { - manager.take(currency, recipient, uint128(amount)); - } else { - manager.mint(recipient, currency.toId(), uint128(amount)); - } - } - - function _settle(Currency currency, address payer, int128 amount, bool settleUsingTransfer) internal { - require(amount < 0, "amount is not less than zero"); - if (settleUsingTransfer) { - if (currency.isNative()) { - manager.settle{value: uint128(-amount)}(currency); - } else { - IERC20Minimal(Currency.unwrap(currency)).transferFrom(payer, address(manager), uint128(-amount)); - manager.settle(currency); - } - } else { - manager.burn(payer, currency.toId(), uint128(-amount)); - } - } - function _fetchBalances(Currency currency, address user, address deltaHolder) internal view diff --git a/src/test/SkipCallsTestHook.sol b/src/test/SkipCallsTestHook.sol index 0ebc72185..c98a7975c 100644 --- a/src/test/SkipCallsTestHook.sol +++ b/src/test/SkipCallsTestHook.sol @@ -13,8 +13,10 @@ import {CurrencyLibrary, Currency} from "../types/Currency.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {Constants} from "../../test/utils/Constants.sol"; import {Test} from "forge-std/Test.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; contract SkipCallsTestHook is BaseTestHooks, Test { + using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; using Hooks for IHooks; @@ -146,9 +148,8 @@ contract SkipCallsTestHook is BaseTestHooks, Test { assertEq(delta0, params.amountSpecified); int256 delta1 = IPoolManager(manager).currencyDelta(address(this), key.currency1); assert(delta1 > 0); - IERC20Minimal(Currency.unwrap(key.currency0)).transferFrom(payer, address(manager), uint256(-delta0)); - manager.settle(key.currency0); - manager.take(key.currency1, payer, uint256(delta1)); + key.currency0.settle(manager, payer, uint256(-delta0), false); + key.currency1.take(manager, payer, uint256(delta1), false); } function _addLiquidity( @@ -164,10 +165,8 @@ contract SkipCallsTestHook is BaseTestHooks, Test { assert(delta0 < 0 || delta1 < 0); assert(!(delta0 > 0 || delta1 > 0)); - IERC20Minimal(Currency.unwrap(key.currency0)).transferFrom(payer, address(manager), uint256(-delta0)); - manager.settle(key.currency0); - IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(payer, address(manager), uint256(-delta1)); - manager.settle(key.currency1); + key.currency0.settle(manager, payer, uint256(-delta0), false); + key.currency1.settle(manager, payer, uint256(-delta1), false); } function _removeLiquidity( @@ -188,10 +187,8 @@ contract SkipCallsTestHook is BaseTestHooks, Test { assert(delta0 < 0 || delta1 < 0); assert(!(delta0 > 0 || delta1 > 0)); - IERC20Minimal(Currency.unwrap(key.currency0)).transferFrom(payer, address(manager), uint256(-delta0)); - manager.settle(key.currency0); - IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(payer, address(manager), uint256(-delta1)); - manager.settle(key.currency1); + key.currency0.settle(manager, payer, uint256(-delta0), false); + key.currency1.settle(manager, payer, uint256(-delta1), false); } function _donate(PoolKey calldata key, uint256 amt0, uint256 amt1, bytes calldata hookData) public { @@ -199,9 +196,7 @@ contract SkipCallsTestHook is BaseTestHooks, Test { address payer = abi.decode(hookData, (address)); int256 delta0 = IPoolManager(manager).currencyDelta(address(this), key.currency0); int256 delta1 = IPoolManager(manager).currencyDelta(address(this), key.currency1); - IERC20Minimal(Currency.unwrap(key.currency0)).transferFrom(payer, address(manager), uint256(-delta0)); - IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(payer, address(manager), uint256(-delta1)); - manager.settle(key.currency0); - manager.settle(key.currency1); + key.currency0.settle(manager, payer, uint256(-delta0), false); + key.currency1.settle(manager, payer, uint256(-delta1), false); } } diff --git a/test/DynamicFees.t.sol b/test/DynamicFees.t.sol index 9af8a928d..378e5c1c2 100644 --- a/test/DynamicFees.t.sol +++ b/test/DynamicFees.t.sol @@ -123,7 +123,7 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); vm.expectRevert(SwapFeeLibrary.FeeTooLarge.selector); swapRouter.swap(key, params, testSettings, ZERO_BYTES); @@ -137,7 +137,7 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + 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, 123); @@ -161,7 +161,7 @@ contract TestDynamicFees is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + 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, 123); diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 97d14c4f7..94078b257 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -339,8 +339,8 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { // allow liquidity router to burn our 6909 tokens manager.setOperator(address(modifyLiquidityRouter), true); - // add liquidity with 6909: settleUsingTransfer=false, withdrawTokens=false (unused) - modifyLiquidityRouter.modifyLiquidity(key, LIQ_PARAMS, ZERO_BYTES, false, false); + // add liquidity with 6909: settleUsingBurn=true, takeClaims=true (unused) + modifyLiquidityRouter.modifyLiquidity(key, LIQ_PARAMS, ZERO_BYTES, true, true); assertLt(manager.balanceOf(address(this), currency0.toId()), 10_000e18); assertLt(manager.balanceOf(address(this), currency1.toId()), 10_000e18); @@ -365,8 +365,8 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { uint256 currency0PMBalanceBefore = currency0.balanceOf(address(manager)); uint256 currency1PMBalanceBefore = currency1.balanceOf(address(manager)); - // remove liquidity as 6909: settleUsingTransfer=false (unused), withdrawTokens=false - modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQ_PARAMS, ZERO_BYTES, false, false); + // remove liquidity as 6909: settleUsingBurn=true (unused), takeClaims=true + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQ_PARAMS, ZERO_BYTES, true, true); assertTrue(manager.balanceOf(address(this), currency0.toId()) > 0); assertTrue(manager.balanceOf(address(this), currency1.toId()) > 0); @@ -439,7 +439,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: sqrtPriceX96}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); vm.expectRevert(Pool.PoolNotInitialized.selector); swapRouter.swap(key, params, testSettings, ZERO_BYTES); @@ -450,7 +450,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); vm.expectEmit(true, true, true, true); emit Swap( @@ -473,7 +473,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); vm.expectEmit(true, true, true, true); emit Swap( @@ -506,7 +506,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); BalanceDelta balanceDelta = swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES); @@ -535,7 +535,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); mockHooks.setReturnValue(mockHooks.beforeSwap.selector, bytes4(0xdeadbeef)); mockHooks.setReturnValue(mockHooks.afterSwap.selector, bytes4(0xdeadbeef)); @@ -563,7 +563,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -10, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); mockHooks.setReturnValue(mockHooks.beforeSwap.selector, mockHooks.beforeSwap.selector); mockHooks.setReturnValue(mockHooks.afterSwap.selector, mockHooks.afterSwap.selector); @@ -579,7 +579,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("simple swap"); swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES); @@ -591,7 +591,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("simple swap with native"); swapRouter.swap{value: 100}(nativeKey, swapParams, testSettings, ZERO_BYTES); @@ -611,14 +611,13 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES); swapParams = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_4}); - testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("swap with hooks"); swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES); @@ -630,7 +629,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); vm.expectEmit(); emit Transfer(address(swapRouter), address(0), address(this), CurrencyLibrary.toId(currency1), 98); @@ -647,7 +646,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: false, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_2_1}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); vm.expectEmit(); emit Transfer(address(swapRouter), address(0), address(this), CurrencyLibrary.toId(CurrencyLibrary.NATIVE), 98); @@ -664,7 +663,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); vm.expectEmit(); emit Transfer(address(swapRouter), address(0), address(this), CurrencyLibrary.toId(currency1), 98); @@ -678,8 +677,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { // swap from currency1 to currency0 again, using 6909s as input tokens params = IPoolManager.SwapParams({zeroForOne: false, amountSpecified: 25, sqrtPriceLimitX96: SQRT_RATIO_4_1}); - testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: false, currencyAlreadySent: false}); + testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: true}); vm.expectEmit(); emit Transfer(address(swapRouter), address(this), address(0), CurrencyLibrary.toId(currency1), 27); @@ -696,7 +694,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: false, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_2_1}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false}); vm.expectEmit(); emit Transfer(address(swapRouter), address(0), address(this), CurrencyLibrary.toId(CurrencyLibrary.NATIVE), 98); @@ -710,8 +708,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { // swap from currency0 to currency1, using 6909s as input tokens params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 25, sqrtPriceLimitX96: SQRT_RATIO_1_4}); - testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: false, currencyAlreadySent: false}); + testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: true}); vm.expectEmit(); emit Transfer(address(swapRouter), address(this), address(0), CurrencyLibrary.toId(CurrencyLibrary.NATIVE), 27); @@ -729,7 +726,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); swapRouter.swap(key, params, testSettings, ZERO_BYTES); @@ -745,7 +742,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); swapRouter.swap{value: 1 ether}(nativeKey, params, testSettings, ZERO_BYTES); @@ -762,8 +759,8 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { uint24 protocolFee = (uint24(protocolFee1) << 12) | uint24(protocolFee0); - feeController.setProtocolFeeForPool(key.toId(), protocolFee); - manager.setProtocolFee(key); + vm.prank(address(feeController)); + manager.setProtocolFee(key, protocolFee); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); assertEq(slot0ProtocolFee, protocolFee); @@ -787,7 +784,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { 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(true, true, false), ZERO_BYTES); + swapRouter.swap(key, swapParams, PoolSwapTest.TestSettings(false, false), ZERO_BYTES); uint256 expectedTotalSwapFee = uint256(-swapParams.amountSpecified) * key.fee / 1e6; uint256 expectedProtocolFee = expectedTotalSwapFee * protocolFee1 / 1e4; @@ -954,42 +951,38 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { function test_setProtocolFee_updatesProtocolFeeForInitializedPool(uint24 protocolFee) public { (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); assertEq(slot0ProtocolFee, 0); - feeController.setProtocolFeeForPool(key.toId(), protocolFee); uint16 fee0 = protocolFee.getZeroForOneFee(); uint16 fee1 = protocolFee.getOneForZeroFee(); + vm.prank(address(feeController)); if ((fee0 > 2500) || (fee1 > 2500)) { - vm.expectRevert(IProtocolFees.ProtocolFeeControllerCallFailedOrInvalidResult.selector); - manager.setProtocolFee(key); + vm.expectRevert(IProtocolFees.InvalidProtocolFee.selector); + manager.setProtocolFee(key, protocolFee); } else { vm.expectEmit(false, false, false, true); emit IProtocolFees.ProtocolFeeUpdated(key.toId(), protocolFee); - manager.setProtocolFee(key); + manager.setProtocolFee(key, protocolFee); (,, slot0ProtocolFee,) = manager.getSlot0(key.toId()); assertEq(slot0ProtocolFee, protocolFee); } } - function test_setProtocolFee_failsWithInvalidProtocolFeeControllers() public { + function test_setProtocolFee_failsWithInvalidFee() public { (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); assertEq(slot0ProtocolFee, 0); - manager.setProtocolFeeController(revertingFeeController); - vm.expectRevert(IProtocolFees.ProtocolFeeControllerCallFailedOrInvalidResult.selector); - manager.setProtocolFee(key); - - manager.setProtocolFeeController(outOfBoundsFeeController); - vm.expectRevert(IProtocolFees.ProtocolFeeControllerCallFailedOrInvalidResult.selector); - manager.setProtocolFee(key); + vm.prank(address(feeController)); + vm.expectRevert(IProtocolFees.InvalidProtocolFee.selector); + manager.setProtocolFee(key, MAX_FEE_BOTH_TOKENS + 1); + } - manager.setProtocolFeeController(overflowFeeController); - vm.expectRevert(IProtocolFees.ProtocolFeeControllerCallFailedOrInvalidResult.selector); - manager.setProtocolFee(key); + function test_setProtocolFee_failsWithInvalidCaller() public { + (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); + assertEq(slot0ProtocolFee, 0); - manager.setProtocolFeeController(invalidReturnSizeFeeController); - vm.expectRevert(IProtocolFees.ProtocolFeeControllerCallFailedOrInvalidResult.selector); - manager.setProtocolFee(key); + vm.expectRevert(IProtocolFees.InvalidCaller.selector); + manager.setProtocolFee(key, MAX_FEE_BOTH_TOKENS); } function test_collectProtocolFees_initializesWithProtocolFeeIfCalled() public { @@ -1008,8 +1001,9 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { function test_collectProtocolFees_ERC20_accumulateFees_gas() public { uint256 expectedFees = 7; - feeController.setProtocolFeeForPool(key.toId(), MAX_FEE_BOTH_TOKENS); - manager.setProtocolFee(key); + uint24 protocolFee = MAX_FEE_BOTH_TOKENS; + vm.prank(address(feeController)); + manager.setProtocolFee(key, protocolFee); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); @@ -1017,7 +1011,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { swapRouter.swap( key, IPoolManager.SwapParams(true, -10000, SQRT_RATIO_1_2), - PoolSwapTest.TestSettings(true, true, false), + PoolSwapTest.TestSettings(false, false), ZERO_BYTES ); @@ -1035,8 +1029,9 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { function test_collectProtocolFees_ERC20_returnsAllFeesIf0IsProvidedAsParameter() public { uint256 expectedFees = 7; - feeController.setProtocolFeeForPool(key.toId(), MAX_FEE_BOTH_TOKENS); - manager.setProtocolFee(key); + uint24 protocolFee = MAX_FEE_BOTH_TOKENS; + vm.prank(address(feeController)); + manager.setProtocolFee(key, protocolFee); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId()); assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); @@ -1044,7 +1039,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { swapRouter.swap( key, IPoolManager.SwapParams(false, -10000, TickMath.MAX_SQRT_RATIO - 1), - PoolSwapTest.TestSettings(true, true, false), + PoolSwapTest.TestSettings(false, false), ZERO_BYTES ); @@ -1062,8 +1057,9 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { Currency nativeCurrency = CurrencyLibrary.NATIVE; // set protocol fee before initializing the pool as it is fetched on initialization - feeController.setProtocolFeeForPool(nativeKey.toId(), MAX_FEE_BOTH_TOKENS); - manager.setProtocolFee(nativeKey); + uint24 protocolFee = MAX_FEE_BOTH_TOKENS; + vm.prank(address(feeController)); + manager.setProtocolFee(nativeKey, protocolFee); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId()); assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); @@ -1071,7 +1067,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { swapRouter.swap{value: 10000}( nativeKey, IPoolManager.SwapParams(true, -10000, SQRT_RATIO_1_2), - PoolSwapTest.TestSettings(true, true, false), + PoolSwapTest.TestSettings(false, false), ZERO_BYTES ); @@ -1090,8 +1086,9 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { uint256 expectedFees = 7; Currency nativeCurrency = CurrencyLibrary.NATIVE; - feeController.setProtocolFeeForPool(nativeKey.toId(), MAX_FEE_BOTH_TOKENS); - manager.setProtocolFee(nativeKey); + uint24 protocolFee = MAX_FEE_BOTH_TOKENS; + vm.prank(address(feeController)); + manager.setProtocolFee(nativeKey, protocolFee); (,, uint24 slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId()); assertEq(slot0ProtocolFee, MAX_FEE_BOTH_TOKENS); @@ -1099,7 +1096,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { swapRouter.swap{value: 10000}( nativeKey, IPoolManager.SwapParams(true, -10000, SQRT_RATIO_1_2), - PoolSwapTest.TestSettings(true, true, false), + PoolSwapTest.TestSettings(false, false), ZERO_BYTES ); @@ -1170,12 +1167,12 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { // swapRouter.swap( // key, // IPoolManager.SwapParams(false, 1 ether, TickMath.MAX_SQRT_RATIO - 1), - // PoolSwapTest.TestSettings(true, true, false) + // PoolSwapTest.TestSettings(true, true) // ); // swapRouter.swap( // key, // IPoolManager.SwapParams(true, 5 ether, TickMath.MIN_SQRT_RATIO + 1), - // PoolSwapTest.TestSettings(true, true, false) + // PoolSwapTest.TestSettings(true, true) // ); // PoolId poolId = key.toId(); diff --git a/test/PoolManagerInitialize.t.sol b/test/PoolManagerInitialize.t.sol index 9d47632eb..0365bceb8 100644 --- a/test/PoolManagerInitialize.t.sol +++ b/test/PoolManagerInitialize.t.sol @@ -321,9 +321,6 @@ contract PoolManagerInitializeTest is Test, Deployers, GasSnapshot { // protocol fees should default to 0 (,, uint24 slot0ProtocolFee,) = manager.getSlot0(uninitializedKey.toId()); assertEq(slot0ProtocolFee, 0); - // call to setProtocolFee should also revert - vm.expectRevert(IProtocolFees.ProtocolFeeControllerCallFailedOrInvalidResult.selector); - manager.setProtocolFee(uninitializedKey); } function test_initialize_succeedsWithRevertingFeeController(uint160 sqrtPriceX96) public { diff --git a/test/Reserves.t.sol b/test/Reserves.t.sol new file mode 100644 index 000000000..6465e3380 --- /dev/null +++ b/test/Reserves.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Reserves} from "../src/libraries/Reserves.sol"; +import {Test} from "forge-std/Test.sol"; +import {Currency} from "../src/types/Currency.sol"; + +contract ReservesTest is Test { + using Reserves for Currency; + + Currency currency0; + + function setUp() public { + currency0 = Currency.wrap(address(0xbeef)); + } + + function test_getReserves_reverts_withoutSet() public { + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + currency0.getReserves(); + } + + function test_getReserves_returns0AfterSet() public { + currency0.setReserves(0); + uint256 value = currency0.getReserves(); + assertEq(value, 0); + } + + function test_getReserves_returns_set() public { + currency0.setReserves(100); + uint256 value = currency0.getReserves(); + assertEq(value, 100); + } + + function test_set_twice_returns_correct_value() public { + currency0.setReserves(100); + currency0.setReserves(200); + uint256 value = currency0.getReserves(); + assertEq(value, 200); + } + + function test_reservesOfSlot() public { + assertEq(uint256(keccak256("ReservesOf")) - 1, Reserves.RESERVES_OF_SLOT); + } + + function test_fuzz_get_set(Currency currency, uint256 value) public { + vm.assume(value != type(uint256).max); + currency.setReserves(value); + assertEq(currency.getReserves(), value); + } +} diff --git a/test/SkipCallsTestHook.t.sol b/test/SkipCallsTestHook.t.sol index 317e2aaf4..f95e76814 100644 --- a/test/SkipCallsTestHook.t.sol +++ b/test/SkipCallsTestHook.t.sol @@ -25,8 +25,7 @@ contract SkipCallsTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams swapParams = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); - PoolSwapTest.TestSettings testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); uint160 clearAllHookPermisssionsMask; uint256 hookPermissionCount = 10; diff --git a/test/Sync.t.sol b/test/Sync.t.sol new file mode 100644 index 000000000..853a4c1c3 --- /dev/null +++ b/test/Sync.t.sol @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import {Deployers} from "./utils/Deployers.sol"; +import {IHooks} from "../src/interfaces/IHooks.sol"; +import {Currency, CurrencyLibrary} from "../src/types/Currency.sol"; +import {IPoolManager} from "../src/interfaces/IPoolManager.sol"; +import {PoolSwapTest} from "../src/test/PoolSwapTest.sol"; +import {IUnlockCallback} from "../src/interfaces/callback/IUnlockCallback.sol"; +import {PoolKey} from "../src/types/PoolKey.sol"; +import {ActionsRouter, Actions} from "../src/test/ActionsRouter.sol"; +import {SafeCast} from "../src/libraries/SafeCast.sol"; +import {Reserves} from "../src/libraries/Reserves.sol"; + +contract SyncTest is Test, Deployers, GasSnapshot { + using CurrencyLibrary for Currency; + + // PoolManager has no balance of currency2. + Currency currency2; + ActionsRouter router; + + function setUp() public { + initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0))); + currency2 = deployMintAndApproveCurrency(); + router = new ActionsRouter(manager); + } + + function test_sync_balanceIsZero() public { + assertEq(currency2.balanceOf(address(manager)), uint256(0)); + uint256 balance = manager.sync(currency2); + + assertEq(uint256(balance), 0); + assertEq(manager.getReserves(currency2), 0); + } + + function test_sync_balanceIsNonZero() public { + uint256 currency0Balance = currency0.balanceOf(address(manager)); + assertGt(currency0Balance, uint256(0)); + + // Without calling sync, getReserves should revert. + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + manager.getReserves(currency0); + + uint256 balance = manager.sync(currency0); + assertEq(balance, currency0Balance, "balance not equal"); + assertEq(manager.getReserves(currency0), balance); + } + + function test_settle_withStartingBalance() public { + assertGt(currency0.balanceOf(address(manager)), uint256(0)); + + 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}); + + // Sync has not been called. + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + manager.getReserves(currency0); + + swapRouter.swap(key, params, testSettings, new bytes(0)); + (uint256 balanceCurrency0) = currency0.balanceOf(address(manager)); + assertEq(manager.getReserves(currency0), balanceCurrency0); // Reserves are up to date since settle was called. + } + + function test_settle_withNoStartingBalance() public { + assertEq(currency2.balanceOf(address(manager)), uint256(0)); + + (Currency cur0, Currency cur1) = currency0 < currency2 ? (currency0, currency2) : (currency2, currency0); + PoolKey memory key2 = + PoolKey({currency0: cur0, currency1: cur1, fee: 3000, tickSpacing: 60, hooks: IHooks(address(0))}); + + manager.initialize(key2, SQRT_RATIO_1_1, new bytes(0)); + + // Sync has not been called. + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + manager.getReserves(currency2); + modifyLiquidityRouter.modifyLiquidity(key2, IPoolManager.ModifyLiquidityParams(-60, 60, 100), new bytes(0)); + (uint256 balanceCurrency2) = currency2.balanceOf(address(manager)); + assertEq(manager.getReserves(currency2), balanceCurrency2); + } + + function test_settle_revertsIfSyncNotCalled() public { + Actions[] memory actions = new Actions[](1); + bytes[] memory params = new bytes[](1); + + actions[0] = Actions.SETTLE; + params[0] = abi.encode(currency0); + + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + router.executeActions(actions, params); + } + + /// @notice When there is no balance and reserves are set to type(uint256).max, no delta should be applied. + function test_settle_noBalanceInPool_shouldNotApplyDelta() public { + assertEq(currency2.balanceOf(address(manager)), uint256(0)); + + // Sync has not been called. + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + manager.getReserves(currency2); + + manager.sync(currency2); + assertEq(manager.getReserves(currency2), 0); + + Actions[] memory actions = new Actions[](2); + bytes[] memory params = new bytes[](2); + + actions[0] = Actions.SETTLE; + params[0] = abi.encode(currency2); + + actions[1] = Actions.ASSERT_DELTA_EQUALS; + params[1] = abi.encode(currency2, address(router), 0); + + router.executeActions(actions, params); + } + + /// @notice When there is a balance, no delta should be applied. + function test_settle_balanceInPool_shouldNotApplyDelta() public { + uint256 currency0Balance = currency0.balanceOf(address(manager)); + + // Sync has not been called. + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + manager.getReserves(currency0); + + manager.sync(currency0); + assertEq(manager.getReserves(currency0), currency0Balance); + + Actions[] memory actions = new Actions[](2); + bytes[] memory params = new bytes[](2); + + actions[0] = Actions.SETTLE; + params[0] = abi.encode(currency0); + + actions[1] = Actions.ASSERT_DELTA_EQUALS; + params[1] = abi.encode(currency0, address(router), 0); + + router.executeActions(actions, params); + } + + /// @notice When there is no actual balance in the pool, the ZERO_BALANCE stored in transient reserves should never actually used in calculating the amount paid in settle. + /// This tests check that the reservesNow value is set to 0 not ZERO_BALANCE, by checking that an underflow happens when + /// a) the contract balance is 0 and b) the reservesBefore value is out of date (sync isn't called again before settle). + /// ie because paid = reservesNow - reservesBefore, and because reservesNow < reservesBefore an underflow should happen. + function test_settle_afterTake_doesNotApplyDelta() public { + Currency currency3 = deployMintAndApproveCurrency(); + + // Approve the router for a transfer. + MockERC20(Currency.unwrap(currency3)).approve(address(router), type(uint256).max); + + // Sync has not been called on currency3. + vm.expectRevert(Reserves.ReservesMustBeSynced.selector); + manager.getReserves(currency3); + + manager.sync(currency3); + // Sync has been called. + assertEq(manager.getReserves(currency3), 0); + + uint256 maxBalanceCurrency3 = uint256(int256(type(int128).max)); + + Actions[] memory actions = new Actions[](10); + bytes[] memory params = new bytes[](10); + + // 1. First supply a large amount of currency3 to the pool, by minting and transfering. + // Encode a MINT. + actions[0] = Actions.MINT; + params[0] = abi.encode(address(this), currency3, maxBalanceCurrency3); + + // Encode a TRANSFER. + actions[1] = Actions.TRANSFER_FROM; + params[1] = abi.encode(currency3, address(this), address(manager), maxBalanceCurrency3); + + // Encode a SETTLE. + actions[2] = Actions.SETTLE; + params[2] = abi.encode(currency3); + + // 2. Second check that the balances (token balances, reserves balance, and delta balance) are as expected. + // The token balance of the pool should be the full balance. + // The reserves balance should have been updated to the full balance in settle. + // And the delta balance should be 0, because it has been fully settled. + + // Assert that the manager balance is the full balance. + actions[3] = Actions.ASSERT_BALANCE_EQUALS; + params[3] = abi.encode(currency3, address(manager), maxBalanceCurrency3); + + // Assert that the reserves balance is the full balance. + actions[4] = Actions.ASSERT_RESERVES_EQUALS; + params[4] = abi.encode(currency3, maxBalanceCurrency3); + + // Assert that the delta is settled. + actions[5] = Actions.ASSERT_DELTA_EQUALS; + params[5] = abi.encode(currency3, address(router), 0); + + // 3. Take the full balance from the pool, but do not call sync. + // Thus reservesBefore stays > 0. And the next reserves call will be 0 causing a revert. + + // Encode a TAKE. + actions[6] = Actions.TAKE; + params[6] = abi.encode(currency3, address(this), maxBalanceCurrency3); + + // Assert that the actual balance of the pool is 0. + actions[7] = Actions.ASSERT_BALANCE_EQUALS; + params[7] = abi.encode(currency3, address(manager), 0); + + // Assert that the reserves balance is the old pool balance because sync has not been called. + actions[8] = Actions.ASSERT_RESERVES_EQUALS; + params[8] = abi.encode(currency3, maxBalanceCurrency3); + + // Encode a SETTLE. + actions[9] = Actions.SETTLE; + params[9] = abi.encode(currency3); + + // Expect an underflow/overflow because reservesBefore > reservesNow since sync() had not been called before settle. + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); + + router.executeActions(actions, params); + } + + // @notice This tests expected behavior if you DO NOT call sync. (ie. Do not interact with the pool manager properly. You can lose funds.) + function test_settle_withoutSync_doesNotRevert_takesUserBalance() public { + MockERC20(Currency.unwrap(currency0)).approve(address(router), type(uint256).max); + uint256 managerCurrency0BalanceBefore = currency0.balanceOf(address(manager)); + uint256 userCurrency0BalanceBefore = currency0.balanceOf(address(this)); + + Actions[] memory actions = new Actions[](8); + bytes[] memory params = new bytes[](8); + + manager.sync(currency0); + assertEq(manager.getReserves(currency0), managerCurrency0BalanceBefore); // reserves are 100. + + actions[0] = Actions.TAKE; + params[0] = abi.encode(currency0, address(this), 10); + + // Assert that the delta open on the router is -10. (The user owes 10 to the pool). + actions[1] = Actions.ASSERT_DELTA_EQUALS; + params[1] = abi.encode(currency0, address(router), -10); + + actions[2] = Actions.TRANSFER_FROM; + params[2] = abi.encode(currency0, address(this), manager, 10); + + actions[3] = Actions.SETTLE; + params[3] = abi.encode(currency0); // Since reserves now == reserves, paid = 0 and the delta owed by the user will still be -10 after settle. + + actions[4] = Actions.ASSERT_DELTA_EQUALS; + params[4] = abi.encode(currency0, address(router), -10); + + // To now settle the delta, the user owes 10 to the pool. + // Because sync is called in settle we can transfer + settle. + actions[5] = Actions.TRANSFER_FROM; + params[5] = abi.encode(currency0, address(this), manager, 10); + + actions[6] = Actions.SETTLE; + params[6] = abi.encode(currency0); + + actions[7] = Actions.ASSERT_DELTA_EQUALS; + params[7] = abi.encode(currency0, address(router), 0); + + router.executeActions(actions, params); + + // The manager gained 10 currency0. + assertEq(currency0.balanceOf(address(manager)), managerCurrency0BalanceBefore + 10); + // The user lost 10 currency0, and can never claim it back. + assertEq(currency0.balanceOf(address(this)), userCurrency0BalanceBefore - 10); + } +} diff --git a/test/libraries/Hooks.t.sol b/test/libraries/Hooks.t.sol index 2dc57cc12..4f8ea6e40 100644 --- a/test/libraries/Hooks.t.sol +++ b/test/libraries/Hooks.t.sol @@ -146,7 +146,7 @@ contract HooksTest is Test, Deployers, GasSnapshot { IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); swapRouter.swap(key, swapParams, testSettings, new bytes(222)); assertEq(mockHooks.beforeSwapData(), new bytes(222)); @@ -159,7 +159,7 @@ contract HooksTest is Test, Deployers, GasSnapshot { swapRouter.swap( key, IPoolManager.SwapParams(false, 100, SQRT_RATIO_1_1 + 60), - PoolSwapTest.TestSettings(false, false, false), + PoolSwapTest.TestSettings(true, true), ZERO_BYTES ); } @@ -170,7 +170,7 @@ contract HooksTest is Test, Deployers, GasSnapshot { swapRouter.swap( key, IPoolManager.SwapParams(false, 100, SQRT_RATIO_1_1 + 60), - PoolSwapTest.TestSettings(false, false, false), + PoolSwapTest.TestSettings(true, true), ZERO_BYTES ); } diff --git a/test/utils/Deployers.sol b/test/utils/Deployers.sol index b4ece252d..adccc9660 100644 --- a/test/utils/Deployers.sol +++ b/test/utils/Deployers.sol @@ -98,7 +98,16 @@ contract Deployers { // You must have first initialised the routers with deployFreshManagerAndRouters // If you only need the currencies (and not approvals) call deployAndMint2Currencies function deployMintAndApprove2Currencies() internal returns (Currency, Currency) { - MockERC20[] memory tokens = deployTokens(2, 2 ** 255); + Currency _currencyA = deployMintAndApproveCurrency(); + Currency _currencyB = deployMintAndApproveCurrency(); + + (currency0, currency1) = + SortTokens.sort(MockERC20(Currency.unwrap(_currencyA)), MockERC20(Currency.unwrap(_currencyB))); + return (currency0, currency1); + } + + function deployMintAndApproveCurrency() internal returns (Currency currency) { + MockERC20 token = deployTokens(1, 2 ** 255)[0]; address[6] memory toApprove = [ address(swapRouter), @@ -110,12 +119,10 @@ contract Deployers { ]; for (uint256 i = 0; i < toApprove.length; i++) { - tokens[0].approve(toApprove[i], Constants.MAX_UINT256); - tokens[1].approve(toApprove[i], Constants.MAX_UINT256); + token.approve(toApprove[i], Constants.MAX_UINT256); } - (currency0, currency1) = SortTokens.sort(tokens[0], tokens[1]); - return (currency0, currency1); + return Currency.wrap(address(token)); } function deployAndMint2Currencies() internal returns (Currency, Currency) { @@ -203,7 +210,7 @@ contract Deployers { amountSpecified: amountSpecified, sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT }), - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}), + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), hookData ); } @@ -226,7 +233,7 @@ contract Deployers { amountSpecified: amountSpecified, sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT }), - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}), + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), hookData ); }