From 6c63e1fc19d302a2169d76b2152648b2eb954aeb Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Tue, 5 Dec 2023 12:53:29 -0500 Subject: [PATCH 01/98] initial thoughts lock and batch --- README.md | 2 +- contracts/BaseHook.sol | 16 ++--- contracts/base/LockAndBatchCall.sol | 83 ++++++++++++++++++++++ contracts/base/SafeCallback.sol | 19 +++++ contracts/hooks/examples/FullRange.sol | 9 +-- contracts/hooks/examples/GeomeanOracle.sol | 8 +-- contracts/hooks/examples/LimitOrder.sol | 4 +- contracts/hooks/examples/TWAMM.sol | 8 +-- 8 files changed, 122 insertions(+), 27 deletions(-) create mode 100644 contracts/base/LockAndBatchCall.sol create mode 100644 contracts/base/SafeCallback.sol diff --git a/README.md b/README.md index b931bd6a..12f0a651 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ contract CoolHook is BaseHook { address, IPoolManager.PoolKey calldata key, IPoolManager.ModifyPositionParams calldata params - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { // hook logic return BaseHook.beforeModifyPosition.selector; } diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 8d463807..93dc7d18 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -6,9 +6,9 @@ import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.s import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {SafeCallback} from "./base/SafeCallback.sol"; -abstract contract BaseHook is IHooks { - error NotPoolManager(); +abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); error InvalidPool(); error LockFailure(); @@ -17,17 +17,15 @@ abstract contract BaseHook is IHooks { /// @notice The address of the pool manager IPoolManager public immutable poolManager; + function manager() public view override returns (IPoolManager) { + return poolManager; + } + constructor(IPoolManager _poolManager) { poolManager = _poolManager; validateHookAddress(this); } - /// @dev Only the pool manager may call this function - modifier poolManagerOnly() { - if (msg.sender != address(poolManager)) revert NotPoolManager(); - _; - } - /// @dev Only this address may call this function modifier selfOnly() { if (msg.sender != address(this)) revert NotSelf(); @@ -49,7 +47,7 @@ abstract contract BaseHook is IHooks { Hooks.validateHookAddress(_this, getHooksCalls()); } - function lockAcquired(bytes calldata data) external virtual poolManagerOnly returns (bytes memory) { + function lockAcquired(bytes calldata data) external virtual override onlyByManager returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol new file mode 100644 index 00000000..85ce255c --- /dev/null +++ b/contracts/base/LockAndBatchCall.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {SafeCallback} from "./SafeCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; + +abstract contract LockAndBatchCall is SafeCallback { + error NotSelf(); + error OnlyExternal(); + error CallFail(bytes reason); + + modifier onlyBySelf() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + modifier onlyByExternalCaller() { + if (msg.sender == address(this)) revert OnlyExternal(); + _; + } + + function execute(bytes memory executeData, bytes memory settleData) external { + (bytes memory lockReturnData) = manager().lock(abi.encode(executeData, settleData)); + (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); + _handleAfterExecute(executeReturnData, settleReturnData); + } + + /// @param data Data passed from the top-level execute function to the internal (and overrideable) _executeWithLockCalls and _settle function. + /// @dev lockAcquired is responsible for executing the internal calls under the lock and settling open deltas left on the pool + function lockAcquired(bytes calldata data) external override onlyByManager returns (bytes memory) { + (bytes memory executeData, bytes memory settleData) = abi.decode(data, (bytes, bytes)); + bytes memory executeReturnData = _executeWithLockCalls(executeData); + bytes memory settleReturnData = _settle(settleData); + return abi.encode(executeReturnData, settleReturnData); + } + + function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(manager().initialize(key, sqrtPriceX96, hookData)); + } + + function modifyPositionWithLock( + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + bytes calldata hookData + ) external onlyBySelf returns (bytes memory) { + return abi.encode(manager().modifyPosition(key, params, hookData)); + } + + function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(manager().swap(key, params, hookData)); + } + + function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(manager().donate(key, amount0, amount1, hookData)); + } + + function _executeWithLockCalls(bytes memory data) internal virtual returns (bytes memory) { + bytes[] memory calls = abi.decode(data, (bytes[])); + bytes[] memory callsReturnData = new bytes[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory returnData) = address(this).call(calls[i]); + if (!success) revert(string(returnData)); + callsReturnData[i] = returnData; + } + return abi.encode(callsReturnData); + } + + function _settle(bytes memory data) internal virtual returns (bytes memory settleData); + function _handleAfterExecute(bytes memory callReturnData, bytes memory settleReturnData) internal virtual; +} diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol new file mode 100644 index 00000000..7f9c4e09 --- /dev/null +++ b/contracts/base/SafeCallback.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; + +abstract contract SafeCallback is ILockCallback { + error NotManager(); + + function manager() public view virtual returns (IPoolManager); + + modifier onlyByManager() { + if (msg.sender != address(manager())) revert NotManager(); + _; + } + + /// @dev There is no way to force the onlyByManager modifier but for this callback to be safe, it MUST check that the msg.sender is the pool manager. + function lockAcquired(bytes calldata data) external virtual returns (bytes memory); +} diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol index 6c5b08ec..92641a9f 100644 --- a/contracts/hooks/examples/FullRange.sol +++ b/contracts/hooks/examples/FullRange.sol @@ -23,7 +23,7 @@ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import "../../libraries/LiquidityAmounts.sol"; -contract FullRange is BaseHook, ILockCallback { +contract FullRange is BaseHook { using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; using SafeCast for uint256; @@ -295,12 +295,7 @@ contract FullRange is BaseHook, ILockCallback { pool.hasAccruedFees = false; } - function lockAcquired(bytes calldata rawData) - external - override(ILockCallback, BaseHook) - poolManagerOnly - returns (bytes memory) - { + function lockAcquired(bytes calldata rawData) external override(BaseHook) onlyByManager returns (bytes memory) { CallbackData memory data = abi.decode(rawData, (CallbackData)); BalanceDelta delta; diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol index 5c78e785..e19245e2 100644 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ b/contracts/hooks/examples/GeomeanOracle.sol @@ -77,7 +77,7 @@ contract GeomeanOracle is BaseHook { external view override - poolManagerOnly + onlyByManager returns (bytes4) { // This is to limit the fragmentation of pools using this oracle hook. In other words, @@ -90,7 +90,7 @@ contract GeomeanOracle is BaseHook { function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { PoolId id = key.toId(); @@ -115,7 +115,7 @@ contract GeomeanOracle is BaseHook { PoolKey calldata key, IPoolManager.ModifyPositionParams calldata params, bytes calldata - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { if (params.liquidityDelta < 0) revert OraclePoolMustLockLiquidity(); int24 maxTickSpacing = poolManager.MAX_TICK_SPACING(); if ( @@ -129,7 +129,7 @@ contract GeomeanOracle is BaseHook { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { _updatePool(key); diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol index 8eff6c68..16cf008f 100644 --- a/contracts/hooks/examples/LimitOrder.sol +++ b/contracts/hooks/examples/LimitOrder.sol @@ -119,7 +119,7 @@ contract LimitOrder is BaseHook { function afterInitialize(address, PoolKey calldata key, uint160, int24 tick, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { setTickLowerLast(key.toId(), getTickLower(tick, key.tickSpacing)); @@ -132,7 +132,7 @@ contract LimitOrder is BaseHook { IPoolManager.SwapParams calldata params, BalanceDelta, bytes calldata - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { (int24 tickLower, int24 lower, int24 upper) = _getCrossedTicks(key.toId(), key.tickSpacing); if (lower > upper) return LimitOrder.afterSwap.selector; diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index 55d44888..3940c3c0 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -77,7 +77,7 @@ contract TWAMM is BaseHook, ITWAMM { external virtual override - poolManagerOnly + onlyByManager returns (bytes4) { // one-time initialization enforced in PoolManager @@ -90,7 +90,7 @@ contract TWAMM is BaseHook, ITWAMM { PoolKey calldata key, IPoolManager.ModifyPositionParams calldata, bytes calldata - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { executeTWAMMOrders(key); return BaseHook.beforeModifyPosition.selector; } @@ -98,7 +98,7 @@ contract TWAMM is BaseHook, ITWAMM { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { executeTWAMMOrders(key); @@ -302,7 +302,7 @@ contract TWAMM is BaseHook, ITWAMM { IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); } - function lockAcquired(bytes calldata rawData) external override poolManagerOnly returns (bytes memory) { + function lockAcquired(bytes calldata rawData) external override onlyByManager returns (bytes memory) { (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); From ad39d198b3c20558d34d43b4187f189d9b65e660 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Tue, 5 Dec 2023 15:48:14 -0500 Subject: [PATCH 02/98] update safecallback with constructor --- contracts/BaseHook.sol | 12 ++---------- contracts/base/LockAndBatchCall.sol | 14 +++++++------- contracts/base/SafeCallback.sol | 14 +++++++++++--- contracts/hooks/examples/FullRange.sol | 2 +- contracts/hooks/examples/TWAMM.sol | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 93dc7d18..0deff29b 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -14,15 +14,7 @@ abstract contract BaseHook is IHooks, SafeCallback { error LockFailure(); error HookNotImplemented(); - /// @notice The address of the pool manager - IPoolManager public immutable poolManager; - - function manager() public view override returns (IPoolManager) { - return poolManager; - } - - constructor(IPoolManager _poolManager) { - poolManager = _poolManager; + constructor(IPoolManager _poolManager) SafeCallback(_poolManager) { validateHookAddress(this); } @@ -47,7 +39,7 @@ abstract contract BaseHook is IHooks, SafeCallback { Hooks.validateHookAddress(_this, getHooksCalls()); } - function lockAcquired(bytes calldata data) external virtual override onlyByManager returns (bytes memory) { + function _lockAcquired(bytes calldata data) internal virtual override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol index 85ce255c..b97a60dc 100644 --- a/contracts/base/LockAndBatchCall.sol +++ b/contracts/base/LockAndBatchCall.sol @@ -21,14 +21,14 @@ abstract contract LockAndBatchCall is SafeCallback { } function execute(bytes memory executeData, bytes memory settleData) external { - (bytes memory lockReturnData) = manager().lock(abi.encode(executeData, settleData)); + (bytes memory lockReturnData) = poolManager.lock(abi.encode(executeData, settleData)); (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); _handleAfterExecute(executeReturnData, settleReturnData); } /// @param data Data passed from the top-level execute function to the internal (and overrideable) _executeWithLockCalls and _settle function. /// @dev lockAcquired is responsible for executing the internal calls under the lock and settling open deltas left on the pool - function lockAcquired(bytes calldata data) external override onlyByManager returns (bytes memory) { + function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { (bytes memory executeData, bytes memory settleData) = abi.decode(data, (bytes, bytes)); bytes memory executeReturnData = _executeWithLockCalls(executeData); bytes memory settleReturnData = _settle(settleData); @@ -40,7 +40,7 @@ abstract contract LockAndBatchCall is SafeCallback { onlyBySelf returns (bytes memory) { - return abi.encode(manager().initialize(key, sqrtPriceX96, hookData)); + return abi.encode(poolManager.initialize(key, sqrtPriceX96, hookData)); } function modifyPositionWithLock( @@ -48,7 +48,7 @@ abstract contract LockAndBatchCall is SafeCallback { IPoolManager.ModifyPositionParams calldata params, bytes calldata hookData ) external onlyBySelf returns (bytes memory) { - return abi.encode(manager().modifyPosition(key, params, hookData)); + return abi.encode(poolManager.modifyPosition(key, params, hookData)); } function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) @@ -56,7 +56,7 @@ abstract contract LockAndBatchCall is SafeCallback { onlyBySelf returns (bytes memory) { - return abi.encode(manager().swap(key, params, hookData)); + return abi.encode(poolManager.swap(key, params, hookData)); } function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) @@ -64,10 +64,10 @@ abstract contract LockAndBatchCall is SafeCallback { onlyBySelf returns (bytes memory) { - return abi.encode(manager().donate(key, amount0, amount1, hookData)); + return abi.encode(poolManager.donate(key, amount0, amount1, hookData)); } - function _executeWithLockCalls(bytes memory data) internal virtual returns (bytes memory) { + function _executeWithLockCalls(bytes memory data) internal returns (bytes memory) { bytes[] memory calls = abi.decode(data, (bytes[])); bytes[] memory callsReturnData = new bytes[](calls.length); for (uint256 i = 0; i < calls.length; i++) { diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol index 7f9c4e09..220d6b64 100644 --- a/contracts/base/SafeCallback.sol +++ b/contracts/base/SafeCallback.sol @@ -7,13 +7,21 @@ import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.s abstract contract SafeCallback is ILockCallback { error NotManager(); - function manager() public view virtual returns (IPoolManager); + IPoolManager public immutable poolManager; + + constructor(IPoolManager _manager) { + poolManager = _manager; + } modifier onlyByManager() { - if (msg.sender != address(manager())) revert NotManager(); + if (msg.sender != address(poolManager)) revert NotManager(); _; } /// @dev There is no way to force the onlyByManager modifier but for this callback to be safe, it MUST check that the msg.sender is the pool manager. - function lockAcquired(bytes calldata data) external virtual returns (bytes memory); + function lockAcquired(bytes calldata data) external onlyByManager returns (bytes memory) { + return _lockAcquired(data); + } + + function _lockAcquired(bytes calldata data) internal virtual returns (bytes memory); } diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol index 92641a9f..662fd90b 100644 --- a/contracts/hooks/examples/FullRange.sol +++ b/contracts/hooks/examples/FullRange.sol @@ -295,7 +295,7 @@ contract FullRange is BaseHook { pool.hasAccruedFees = false; } - function lockAcquired(bytes calldata rawData) external override(BaseHook) onlyByManager returns (bytes memory) { + function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory) { CallbackData memory data = abi.decode(rawData, (CallbackData)); BalanceDelta delta; diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index 3940c3c0..28cae61f 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -302,7 +302,7 @@ contract TWAMM is BaseHook, ITWAMM { IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); } - function lockAcquired(bytes calldata rawData) external override onlyByManager returns (bytes memory) { + function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory) { (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); From 64fc40a2957e3877a87a96989c25da6903770eba Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Tue, 5 Dec 2023 19:56:04 -0500 Subject: [PATCH 03/98] simple batch under lock --- .../FullRangeAddInitialLiquidity.snap | 2 +- .forge-snapshots/FullRangeAddLiquidity.snap | 2 +- .forge-snapshots/FullRangeFirstSwap.snap | 2 +- .forge-snapshots/FullRangeInitialize.snap | 2 +- .../FullRangeRemoveLiquidity.snap | 2 +- .../FullRangeRemoveLiquidityAndRebalance.snap | 2 +- .forge-snapshots/FullRangeSecondSwap.snap | 2 +- .forge-snapshots/FullRangeSwap.snap | 2 +- .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- contracts/BaseHook.sol | 3 +- contracts/SimpleBatchCall.sol | 62 +++++++++++++++ contracts/base/CallsWithLock.sol | 50 ++++++++++++ contracts/base/ImmutableState.sol | 12 +++ contracts/base/LockAndBatchCall.sol | 66 +++------------- contracts/base/SafeCallback.sol | 9 +-- contracts/interfaces/ICallsWithLock.sol | 25 ++++++ test/SimpleBatchCallTest.t.sol | 78 +++++++++++++++++++ 17 files changed, 252 insertions(+), 71 deletions(-) create mode 100644 contracts/SimpleBatchCall.sol create mode 100644 contracts/base/CallsWithLock.sol create mode 100644 contracts/base/ImmutableState.sol create mode 100644 contracts/interfaces/ICallsWithLock.sol create mode 100644 test/SimpleBatchCallTest.t.sol diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index 2d5250a5..64c72f4e 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -412696 \ No newline at end of file +412756 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index 032a6a3b..eb5dc38b 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -206962 \ No newline at end of file +207022 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index 9d59ac16..276ad91c 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -154763 \ No newline at end of file +154767 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index e0b3ab13..0362b78a 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -879542 \ No newline at end of file +879546 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 920384a4..9c0e04d2 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -200095 \ No newline at end of file +200159 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index 5ee38978..c91b8f4f 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -379287 \ No newline at end of file +379355 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index 436848b5..7314abe0 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -112303 \ No newline at end of file +112307 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index d48620c7..43c7c6b8 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -153038 \ No newline at end of file +153042 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index 9adc49a6..194502b1 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -123576 \ No newline at end of file +123580 \ No newline at end of file diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 0deff29b..3e135dd5 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -7,6 +7,7 @@ import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; import {SafeCallback} from "./base/SafeCallback.sol"; +import {ImmutableState} from "./base/ImmutableState.sol"; abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); @@ -14,7 +15,7 @@ abstract contract BaseHook is IHooks, SafeCallback { error LockFailure(); error HookNotImplemented(); - constructor(IPoolManager _poolManager) SafeCallback(_poolManager) { + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) { validateHookAddress(this); } diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol new file mode 100644 index 00000000..a8d587f1 --- /dev/null +++ b/contracts/SimpleBatchCall.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {LockAndBatchCall} from "./base/LockAndBatchCall.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {ImmutableState} from "./base/ImmutableState.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @title SimpleBatchCall +/// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. +contract SimpleBatchCall is LockAndBatchCall { + using CurrencyLibrary for Currency; + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + struct SettleConfig { + bool withdrawTokens; // If true, takes the underlying ERC20s. + bool settleUsingTransfer; // If true, sends the underlying ERC20s. + } + + mapping(address locker => mapping(Currency currency => int256 currencyDelta)) public currencyDelta; + + /// @notice We naively settle all currencies that are touched by the batch call. This data is passed in intially to `execute`. + function _settle(address sender, bytes memory data) internal override returns (bytes memory settleData) { + if (data.length != 0) { + (Currency[] memory currenciesTouched, SettleConfig memory config) = + abi.decode(data, (Currency[], SettleConfig)); + + for (uint256 i = 0; i < currenciesTouched.length; i++) { + Currency currency = currenciesTouched[i]; + int256 delta = poolManager.currencyDelta(address(this), currenciesTouched[i]); + + if (delta > 0) { + if (config.settleUsingTransfer) { + ERC20(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), uint256(delta)); + poolManager.settle(currency); + } else { + poolManager.safeTransferFrom( + address(this), address(poolManager), currency.toId(), uint256(delta), new bytes(0) + ); + } + } + if (delta < 0) { + if (config.withdrawTokens) { + poolManager.mint(currency, address(this), uint256(-delta)); + } else { + poolManager.take(currency, address(this), uint256(-delta)); + } + } + } + } + } + + function _handleAfterExecute(bytes memory, /*callReturnData*/ bytes memory /*settleReturnData*/ ) + internal + pure + override + { + return; + } +} diff --git a/contracts/base/CallsWithLock.sol b/contracts/base/CallsWithLock.sol new file mode 100644 index 00000000..55b3694f --- /dev/null +++ b/contracts/base/CallsWithLock.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {ImmutableState} from "./ImmutableState.sol"; +import {ICallsWithLock} from "../interfaces/ICallsWithLock.sol"; + +/// @title CallsWithLock +/// @notice Handles all the calls to the pool manager contract. Assumes the integrating contract has already acquired a lock. +abstract contract CallsWithLock is ICallsWithLock, ImmutableState { + error NotSelf(); + + modifier onlyBySelf() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(poolManager.initialize(key, sqrtPriceX96, hookData)); + } + + function modifyPositionWithLock( + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + bytes calldata hookData + ) external onlyBySelf returns (bytes memory) { + return abi.encode(poolManager.modifyPosition(key, params, hookData)); + } + + function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(poolManager.swap(key, params, hookData)); + } + + function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(poolManager.donate(key, amount0, amount1, hookData)); + } +} diff --git a/contracts/base/ImmutableState.sol b/contracts/base/ImmutableState.sol new file mode 100644 index 00000000..3917b35d --- /dev/null +++ b/contracts/base/ImmutableState.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; + +contract ImmutableState { + IPoolManager public immutable poolManager; + + constructor(IPoolManager _manager) { + poolManager = _manager; + } +} diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol index b97a60dc..6785290b 100644 --- a/contracts/base/LockAndBatchCall.sol +++ b/contracts/base/LockAndBatchCall.sol @@ -4,72 +4,33 @@ pragma solidity ^0.8.19; import {SafeCallback} from "./SafeCallback.sol"; import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {CallsWithLock} from "./CallsWithLock.sol"; -abstract contract LockAndBatchCall is SafeCallback { - error NotSelf(); - error OnlyExternal(); +abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { error CallFail(bytes reason); - modifier onlyBySelf() { - if (msg.sender != address(this)) revert NotSelf(); - _; - } - - modifier onlyByExternalCaller() { - if (msg.sender == address(this)) revert OnlyExternal(); - _; - } + function _settle(address sender, bytes memory data) internal virtual returns (bytes memory settleData); + function _handleAfterExecute(bytes memory callReturnData, bytes memory settleReturnData) internal virtual; + /// @param executeData The function selectors and calldata for any of the function selectors in ICallsWithLock encoded as an array of bytes. function execute(bytes memory executeData, bytes memory settleData) external { - (bytes memory lockReturnData) = poolManager.lock(abi.encode(executeData, settleData)); + (bytes memory lockReturnData) = poolManager.lock(abi.encode(executeData, abi.encode(msg.sender, settleData))); (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); _handleAfterExecute(executeReturnData, settleReturnData); } - /// @param data Data passed from the top-level execute function to the internal (and overrideable) _executeWithLockCalls and _settle function. - /// @dev lockAcquired is responsible for executing the internal calls under the lock and settling open deltas left on the pool + /// @param data This data is passed from the top-level execute function to the internal _executeWithLockCalls and _settle function. It is decoded as two separate dynamic bytes parameters. + /// @dev _lockAcquired is responsible for executing the internal calls under the lock and settling open deltas left on the pool function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { - (bytes memory executeData, bytes memory settleData) = abi.decode(data, (bytes, bytes)); - bytes memory executeReturnData = _executeWithLockCalls(executeData); - bytes memory settleReturnData = _settle(settleData); - return abi.encode(executeReturnData, settleReturnData); - } - - function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) - external - onlyBySelf - returns (bytes memory) - { - return abi.encode(poolManager.initialize(key, sqrtPriceX96, hookData)); - } - - function modifyPositionWithLock( - PoolKey calldata key, - IPoolManager.ModifyPositionParams calldata params, - bytes calldata hookData - ) external onlyBySelf returns (bytes memory) { - return abi.encode(poolManager.modifyPosition(key, params, hookData)); - } - - function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) - external - onlyBySelf - returns (bytes memory) - { - return abi.encode(poolManager.swap(key, params, hookData)); - } - - function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) - external - onlyBySelf - returns (bytes memory) - { - return abi.encode(poolManager.donate(key, amount0, amount1, hookData)); + (bytes memory executeData, bytes memory settleDataWithSender) = abi.decode(data, (bytes, bytes)); + (address sender, bytes memory settleData) = abi.decode(settleDataWithSender, (address, bytes)); + return abi.encode(_executeWithLockCalls(executeData), _settle(sender, settleData)); } function _executeWithLockCalls(bytes memory data) internal returns (bytes memory) { bytes[] memory calls = abi.decode(data, (bytes[])); bytes[] memory callsReturnData = new bytes[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { (bool success, bytes memory returnData) = address(this).call(calls[i]); if (!success) revert(string(returnData)); @@ -77,7 +38,4 @@ abstract contract LockAndBatchCall is SafeCallback { } return abi.encode(callsReturnData); } - - function _settle(bytes memory data) internal virtual returns (bytes memory settleData); - function _handleAfterExecute(bytes memory callReturnData, bytes memory settleReturnData) internal virtual; } diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol index 220d6b64..46cbb640 100644 --- a/contracts/base/SafeCallback.sol +++ b/contracts/base/SafeCallback.sol @@ -3,16 +3,11 @@ pragma solidity ^0.8.19; import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {ImmutableState} from "./ImmutableState.sol"; -abstract contract SafeCallback is ILockCallback { +abstract contract SafeCallback is ImmutableState, ILockCallback { error NotManager(); - IPoolManager public immutable poolManager; - - constructor(IPoolManager _manager) { - poolManager = _manager; - } - modifier onlyByManager() { if (msg.sender != address(poolManager)) revert NotManager(); _; diff --git a/contracts/interfaces/ICallsWithLock.sol b/contracts/interfaces/ICallsWithLock.sol new file mode 100644 index 00000000..564dd1ca --- /dev/null +++ b/contracts/interfaces/ICallsWithLock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; + +interface ICallsWithLock { + function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) + external + returns (bytes memory); + + function modifyPositionWithLock( + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + bytes calldata hookData + ) external returns (bytes memory); + + function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) + external + returns (bytes memory); + + function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) + external + returns (bytes memory); +} diff --git a/test/SimpleBatchCallTest.t.sol b/test/SimpleBatchCallTest.t.sol new file mode 100644 index 00000000..8792ab08 --- /dev/null +++ b/test/SimpleBatchCallTest.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {SimpleBatchCall} from "../contracts/SimpleBatchCall.sol"; +import {ICallsWithLock} from "../contracts/interfaces/ICallsWithLock.sol"; + +import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; +import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; +import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {Pool} from "@uniswap/v4-core/contracts/libraries/Pool.sol"; +import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; + +/// @title SimpleBatchCall +/// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. +contract SimpleBatchCallTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + + SimpleBatchCall batchCall; + Currency currency0; + Currency currency1; + PoolKey key; + IPoolManager poolManager; + + function setUp() public { + poolManager = createFreshManager(); + (currency0, currency1) = deployCurrencies(2 ** 255); + key = + PoolKey({currency0: currency0, currency1: currency1, fee: 3000, tickSpacing: 60, hooks: IHooks(address(0))}); + + batchCall = new SimpleBatchCall(poolManager); + ERC20(Currency.unwrap(currency0)).approve(address(batchCall), 2 ** 255); + ERC20(Currency.unwrap(currency1)).approve(address(batchCall), 2 ** 255); + } + + function test_initialize() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_RATIO_1_1, ZERO_BYTES); + bytes memory settleData = + abi.encode(SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true})); + batchCall.execute(abi.encode(calls), ZERO_BYTES); + + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId()); + assertEq(sqrtPriceX96, SQRT_RATIO_1_1); + } + + function test_initialize_modifyPosition() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_RATIO_1_1, ZERO_BYTES); + calls[1] = abi.encodeWithSelector( + ICallsWithLock.modifyPositionWithLock.selector, + key, + IPoolManager.ModifyPositionParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18}), + ZERO_BYTES + ); + Currency[] memory currenciesTouched = new Currency[](2); + currenciesTouched[0] = currency0; + currenciesTouched[1] = currency1; + bytes memory settleData = abi.encode( + currenciesTouched, SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true}) + ); + uint256 balance0 = ERC20(Currency.unwrap(currency0)).balanceOf(address(poolManager)); + uint256 balance1 = ERC20(Currency.unwrap(currency1)).balanceOf(address(poolManager)); + batchCall.execute(abi.encode(calls), settleData); + uint256 balance0After = ERC20(Currency.unwrap(currency0)).balanceOf(address(poolManager)); + uint256 balance1After = ERC20(Currency.unwrap(currency1)).balanceOf(address(poolManager)); + + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId()); + + assertGt(balance0After, balance0); + assertGt(balance1After, balance1); + assertEq(sqrtPriceX96, SQRT_RATIO_1_1); + } +} From a707c2089bb647561c02a43ec051e7745cbf6645 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Tue, 5 Dec 2023 20:01:35 -0500 Subject: [PATCH 04/98] oops --- contracts/SimpleBatchCall.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol index a8d587f1..0c7a64db 100644 --- a/contracts/SimpleBatchCall.sol +++ b/contracts/SimpleBatchCall.sol @@ -19,8 +19,6 @@ contract SimpleBatchCall is LockAndBatchCall { bool settleUsingTransfer; // If true, sends the underlying ERC20s. } - mapping(address locker => mapping(Currency currency => int256 currencyDelta)) public currencyDelta; - /// @notice We naively settle all currencies that are touched by the batch call. This data is passed in intially to `execute`. function _settle(address sender, bytes memory data) internal override returns (bytes memory settleData) { if (data.length != 0) { From 1d0e566ee5c20699dcc7ec3748a1a1fecb0ef705 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 22 Feb 2024 22:23:15 -0500 Subject: [PATCH 05/98] misc version bump; will conflict but can resolve later --- contracts/NonfungiblePositionManager.sol | 8 ++++++++ foundry.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 contracts/NonfungiblePositionManager.sol diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol new file mode 100644 index 00000000..a8d9f5c5 --- /dev/null +++ b/contracts/NonfungiblePositionManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; + +contract NonfungiblePositionManager is ERC721 { + constructor() ERC721("Uniswap V4 LPT", "UV4LPT") {} +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index b3132187..6450c8f6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,7 +1,7 @@ [profile.default] src = 'contracts' out = 'foundry-out' -solc_version = '0.8.20' +solc_version = '0.8.24' optimizer_runs = 800 ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] From 4f8bbd20a22c6cee24fff0d3dec4ba4d89fb7704 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 22 Feb 2024 22:39:35 -0500 Subject: [PATCH 06/98] defining types and different levels of abstractions --- contracts/NonfungiblePositionManager.sol | 37 ++++++++++-- contracts/base/BaseLiquidityManagement.sol | 59 +++++++++++++++++++ .../IAdvancedLiquidityManagement.sol | 20 +++++++ .../interfaces/IBaseLiquidityManagement.sol | 21 +++++++ .../INonfungiblePositionManager.sol | 30 ++++++++++ contracts/types/LiquidityPositionId.sol | 21 +++++++ 6 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 contracts/base/BaseLiquidityManagement.sol create mode 100644 contracts/interfaces/IAdvancedLiquidityManagement.sol create mode 100644 contracts/interfaces/IBaseLiquidityManagement.sol create mode 100644 contracts/interfaces/INonfungiblePositionManager.sol create mode 100644 contracts/types/LiquidityPositionId.sol diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index a8d9f5c5..f2572961 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -1,8 +1,37 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.24; import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; +import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; -contract NonfungiblePositionManager is ERC721 { - constructor() ERC721("Uniswap V4 LPT", "UV4LPT") {} -} \ No newline at end of file +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {LiquidityPosition} from "./types/LiquidityPositionId.sol"; + +contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { + constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} + + // NOTE: more gas efficient as LiquidityAmounts is used offchain + function mint(LiquidityPosition memory position, uint256 liquidity, uint256 deadline) + external + payable + returns (uint256 tokenId) + {} + + // NOTE: more expensive since LiquidityAmounts is used onchain + function mint( + PoolKey memory key, + uint256 amount0Desired, + uint256 amount1Desired, + uint256 amount0Min, + uint256 amount1Min, + address recipient, + uint256 deadline + ) external payable returns (uint256 tokenId) {} + + function burn(uint256 tokenId) external {} + + // TODO: in v3, we can partially collect fees, but what was the usecase here? + function collect(uint256 tokenId, address recipient) external {} +} diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol new file mode 100644 index 00000000..ef75e349 --- /dev/null +++ b/contracts/base/BaseLiquidityManagement.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {LiquidityPosition, LiquidityPositionId, LiquidityPositionIdLibrary} from "../types/LiquidityPositionId.sol"; +import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; +import {SafeCallback} from "./SafeCallback.sol"; +import {ImmutableState} from "./ImmutableState.sol"; + +abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagement { + using LiquidityPositionIdLibrary for LiquidityPosition; + + struct CallbackData { + address sender; + PoolKey key; + IPoolManager.ModifyPositionParams params; + bytes hookData; + } + + mapping(address owner => mapping(LiquidityPositionId positionId => uint256 liquidity)) public liquidityOf; + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + // NOTE: handles add/remove/collect + function modifyLiquidity( + PoolKey memory key, + IPoolManager.ModifyPositionParams memory params, + bytes calldata hookData, + address owner + ) external payable override returns (BalanceDelta delta) { + // if removing liquidity, check that the owner is the sender? + if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); + + delta = + abi.decode(poolManager.lock(abi.encode(CallbackData(msg.sender, key, params, hookData))), (BalanceDelta)); + + params.liquidityDelta < 0 + ? liquidityOf[owner][LiquidityPosition(key, params.tickLower, params.tickUpper).toId()] -= + uint256(-params.liquidityDelta) + : liquidityOf[owner][LiquidityPosition(key, params.tickLower, params.tickUpper).toId()] += + uint256(params.liquidityDelta); + + // TODO: handle & test + // uint256 ethBalance = address(this).balance; + // if (ethBalance > 0) { + // CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); + // } + } + + function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory result) { + CallbackData memory data = abi.decode(rawData, (CallbackData)); + + result = abi.encode(poolManager.modifyPosition(data.key, data.params, data.hookData)); + + // TODO: pay balances + } +} diff --git a/contracts/interfaces/IAdvancedLiquidityManagement.sol b/contracts/interfaces/IAdvancedLiquidityManagement.sol new file mode 100644 index 00000000..58b02853 --- /dev/null +++ b/contracts/interfaces/IAdvancedLiquidityManagement.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; +import {LiquidityPosition} from "../types/LiquidityPositionId.sol"; + +interface IAdvancedLiquidityManagement is IBaseLiquidityManagement { + /// @notice Move an existing liquidity position into a new range + function rebalanceLiquidity( + LiquidityPosition memory position, + int24 tickLowerNew, + int24 tickUpperNew, + int256 liquidityDelta + ) external; + + /// @notice Move an existing liquidity position into a new pool, keeping the same range + function migrateLiquidity(LiquidityPosition memory position, PoolKey memory newKey) external; +} diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol new file mode 100644 index 00000000..6dfdca5a --- /dev/null +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; + +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; +import {LiquidityPosition, LiquidityPositionId} from "../types/LiquidityPositionId.sol"; + +interface IBaseLiquidityManagement is ILockCallback { + function liquidityOf(address owner, LiquidityPositionId positionId) external view returns (uint256 liquidity); + + // NOTE: handles add/remove/collect + function modifyLiquidity( + PoolKey memory key, + IPoolManager.ModifyPositionParams memory params, + bytes calldata hookData, + address owner + ) external payable returns (BalanceDelta delta); +} diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol new file mode 100644 index 00000000..b3e9a2a6 --- /dev/null +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {LiquidityPosition} from "../types/LiquidityPositionId.sol"; +import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; + +interface INonfungiblePositionManager is IBaseLiquidityManagement { + // NOTE: more gas efficient as LiquidityAmounts is used offchain + function mint(LiquidityPosition memory position, uint256 liquidity, uint256 deadline) + external + payable + returns (uint256 tokenId); + + // NOTE: more expensive since LiquidityAmounts is used onchain + function mint( + PoolKey memory key, + uint256 amount0Desired, + uint256 amount1Desired, + uint256 amount0Min, + uint256 amount1Min, + address recipient, + uint256 deadline + ) external payable returns (uint256 tokenId); + + function burn(uint256 tokenId) external; + + // TODO: in v3, we can partially collect fees, but what was the usecase here? + function collect(uint256 tokenId, address recipient) external; +} diff --git a/contracts/types/LiquidityPositionId.sol b/contracts/types/LiquidityPositionId.sol new file mode 100644 index 00000000..7b2e88a4 --- /dev/null +++ b/contracts/types/LiquidityPositionId.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; + +// TODO: move into core? some of the mappings / pool.state seem to hash position id's +struct LiquidityPosition { + PoolKey key; + int24 tickLower; + int24 tickUpper; +} + +type LiquidityPositionId is bytes32; + +/// @notice Library for computing the ID of a pool +library LiquidityPositionIdLibrary { + function toId(LiquidityPosition memory position) internal pure returns (LiquidityPositionId) { + // TODO: gas, is it better to encodePacked? + return LiquidityPositionId.wrap(keccak256(abi.encode(position))); + } +} From c4c9dcd68382c2e2ccc0c779076dbce99f1924e3 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 1 Mar 2024 12:20:59 -0700 Subject: [PATCH 07/98] merge in main; resolve conflicts --- .env | 7 + .../FullOracleObserve0After5Seconds.snap | 2 +- .../FullOracleObserve200By13.snap | 2 +- .../FullOracleObserve200By13Plus5.snap | 2 +- .../FullOracleObserve5After5Seconds.snap | 2 +- .forge-snapshots/FullOracleObserveOldest.snap | 2 +- .../FullOracleObserveOldestAfter5Seconds.snap | 2 +- .forge-snapshots/FullOracleObserveZero.snap | 2 +- .../FullRangeAddInitialLiquidity.snap | 2 +- .forge-snapshots/FullRangeAddLiquidity.snap | 2 +- .forge-snapshots/FullRangeFirstSwap.snap | 2 +- .forge-snapshots/FullRangeInitialize.snap | 2 +- .../FullRangeRemoveLiquidity.snap | 2 +- .../FullRangeRemoveLiquidityAndRebalance.snap | 2 +- .forge-snapshots/FullRangeSecondSwap.snap | 2 +- .forge-snapshots/FullRangeSwap.snap | 2 +- .forge-snapshots/OracleGrow10Slots.snap | 2 +- .../OracleGrow10SlotsCardinalityGreater.snap | 2 +- .forge-snapshots/OracleGrow1Slot.snap | 2 +- .../OracleGrow1SlotCardinalityGreater.snap | 2 +- .forge-snapshots/OracleInitialize.snap | 2 +- ...eObserveBetweenOldestAndOldestPlusOne.snap | 2 +- .../OracleObserveCurrentTime.snap | 2 +- ...racleObserveCurrentTimeCounterfactual.snap | 2 +- .../OracleObserveLast20Seconds.snap | 2 +- .../OracleObserveLatestEqual.snap | 2 +- .../OracleObserveLatestTransform.snap | 2 +- .forge-snapshots/OracleObserveMiddle.snap | 2 +- .forge-snapshots/OracleObserveOldest.snap | 2 +- .../OracleObserveSinceMostRecent.snap | 2 +- .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- .gitignore | 3 +- README.md | 2 +- contracts/BaseHook.sol | 41 +- contracts/SimpleBatchCall.sol | 10 +- contracts/base/CallsWithLock.sol | 8 +- contracts/base/ImmutableState.sol | 2 +- contracts/base/LockAndBatchCall.sol | 6 +- contracts/base/PeripheryPayments.sol | 2 +- contracts/base/SafeCallback.sol | 6 +- contracts/hooks/examples/FullRange.sol | 80 ++- contracts/hooks/examples/GeomeanOracle.sol | 43 +- contracts/hooks/examples/LimitOrder.sol | 98 +-- contracts/hooks/examples/TWAMM.sol | 60 +- contracts/hooks/examples/VolatilityOracle.sol | 30 +- contracts/interfaces/ICallsWithLock.sol | 6 +- contracts/interfaces/IPeripheryPayments.sol | 2 +- contracts/interfaces/IQuoter.sol | 106 +++ contracts/interfaces/ITWAMM.sol | 10 +- contracts/lens/Quoter.sol | 340 +++++++++ contracts/libraries/LiquidityAmounts.sol | 6 +- contracts/libraries/PathKey.sol | 30 + contracts/libraries/PoolGetters.sol | 13 +- contracts/libraries/PoolTicksCounter.sol | 107 +++ contracts/libraries/TWAMM/TwammMath.sol | 6 +- contracts/libraries/TransferHelper.sol | 2 +- foundry.toml | 5 +- lib/v4-core | 2 +- test/FullRange.t.sol | 146 ++-- test/GeomeanOracle.t.sol | 95 +-- test/LimitOrder.t.sol | 69 +- test/Quoter.t.sol | 666 ++++++++++++++++++ test/SimpleBatchCallTest.t.sol | 40 +- test/TWAMM.t.sol | 85 +-- .../FullRangeImplementation.sol | 6 +- .../GeomeanOracleImplementation.sol | 6 +- .../LimitOrderImplementation.sol | 6 +- .../implementation/TWAMMImplementation.sol | 6 +- test/utils/HookEnabledSwapRouter.sol | 71 ++ 69 files changed, 1829 insertions(+), 460 deletions(-) create mode 100644 .env create mode 100644 contracts/interfaces/IQuoter.sol create mode 100644 contracts/lens/Quoter.sol create mode 100644 contracts/libraries/PathKey.sol create mode 100644 contracts/libraries/PoolTicksCounter.sol create mode 100644 test/Quoter.t.sol create mode 100644 test/utils/HookEnabledSwapRouter.sol diff --git a/.env b/.env new file mode 100644 index 00000000..7859e840 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +FOUNDRY_FUZZ_SEED=0x4444 + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + export FOUNDRY_SOLC="./lib/v4-core/bin/solc-static-linux" +elif [[ "$OSTYPE" == "darwin"* ]]; then + export FOUNDRY_SOLC="./lib/v4-core/bin/solc-mac" +fi diff --git a/.forge-snapshots/FullOracleObserve0After5Seconds.snap b/.forge-snapshots/FullOracleObserve0After5Seconds.snap index 9463411b..f5b9e8bf 100644 --- a/.forge-snapshots/FullOracleObserve0After5Seconds.snap +++ b/.forge-snapshots/FullOracleObserve0After5Seconds.snap @@ -1 +1 @@ -2000 \ No newline at end of file +1912 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13.snap b/.forge-snapshots/FullOracleObserve200By13.snap index 638f8744..b47b8dc4 100644 --- a/.forge-snapshots/FullOracleObserve200By13.snap +++ b/.forge-snapshots/FullOracleObserve200By13.snap @@ -1 +1 @@ -21068 \ No newline at end of file +20210 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13Plus5.snap b/.forge-snapshots/FullOracleObserve200By13Plus5.snap index 1bc3059d..46616951 100644 --- a/.forge-snapshots/FullOracleObserve200By13Plus5.snap +++ b/.forge-snapshots/FullOracleObserve200By13Plus5.snap @@ -1 +1 @@ -21318 \ No newline at end of file +20443 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve5After5Seconds.snap b/.forge-snapshots/FullOracleObserve5After5Seconds.snap index a5bb2393..dba60802 100644 --- a/.forge-snapshots/FullOracleObserve5After5Seconds.snap +++ b/.forge-snapshots/FullOracleObserve5After5Seconds.snap @@ -1 +1 @@ -2076 \ No newline at end of file +2024 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldest.snap b/.forge-snapshots/FullOracleObserveOldest.snap index db768f3a..c90bb2fe 100644 --- a/.forge-snapshots/FullOracleObserveOldest.snap +++ b/.forge-snapshots/FullOracleObserveOldest.snap @@ -1 +1 @@ -20164 \ No newline at end of file +19279 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap b/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap index c04b75bb..1d23504b 100644 --- a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap +++ b/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap @@ -1 +1 @@ -20458 \ No newline at end of file +19555 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveZero.snap b/.forge-snapshots/FullOracleObserveZero.snap index 7f966954..3559f242 100644 --- a/.forge-snapshots/FullOracleObserveZero.snap +++ b/.forge-snapshots/FullOracleObserveZero.snap @@ -1 +1 @@ -1525 \ No newline at end of file +1477 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index 64c72f4e..253abc39 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -412756 \ No newline at end of file +392801 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index eb5dc38b..19f279ca 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -207022 \ No newline at end of file +187168 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index 276ad91c..029a908d 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -154767 \ No newline at end of file +136542 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 0362b78a..631d5a68 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -879546 \ No newline at end of file +1041059 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 9c0e04d2..d20f1db8 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -200159 \ No newline at end of file +175928 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index c91b8f4f..0df1c54f 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -379355 \ No newline at end of file +364024 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index 7314abe0..c02e1eae 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -112307 \ No newline at end of file +97295 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index 43c7c6b8..8adf5f54 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -153042 \ No newline at end of file +134817 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10Slots.snap b/.forge-snapshots/OracleGrow10Slots.snap index 61763356..3dada479 100644 --- a/.forge-snapshots/OracleGrow10Slots.snap +++ b/.forge-snapshots/OracleGrow10Slots.snap @@ -1 +1 @@ -233028 \ No newline at end of file +232960 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap index 4f1264df..f623cfa5 100644 --- a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap @@ -1 +1 @@ -223717 \ No newline at end of file +223649 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1Slot.snap b/.forge-snapshots/OracleGrow1Slot.snap index 3d85d6d7..137baa16 100644 --- a/.forge-snapshots/OracleGrow1Slot.snap +++ b/.forge-snapshots/OracleGrow1Slot.snap @@ -1 +1 @@ -32886 \ No newline at end of file +32845 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap index bc6dc069..e6dc42ce 100644 --- a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap @@ -1 +1 @@ -23586 \ No newline at end of file +23545 \ No newline at end of file diff --git a/.forge-snapshots/OracleInitialize.snap b/.forge-snapshots/OracleInitialize.snap index da81ec04..e4e9e6b2 100644 --- a/.forge-snapshots/OracleInitialize.snap +++ b/.forge-snapshots/OracleInitialize.snap @@ -1 +1 @@ -51411 \ No newline at end of file +51310 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap b/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap index f61a3565..5996d53e 100644 --- a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap +++ b/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap @@ -1 +1 @@ -5571 \ No newline at end of file +5368 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTime.snap b/.forge-snapshots/OracleObserveCurrentTime.snap index 7f966954..3559f242 100644 --- a/.forge-snapshots/OracleObserveCurrentTime.snap +++ b/.forge-snapshots/OracleObserveCurrentTime.snap @@ -1 +1 @@ -1525 \ No newline at end of file +1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap b/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap index 7f966954..3559f242 100644 --- a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap +++ b/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap @@ -1 +1 @@ -1525 \ No newline at end of file +1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLast20Seconds.snap b/.forge-snapshots/OracleObserveLast20Seconds.snap index 41599c5d..24efe8f4 100644 --- a/.forge-snapshots/OracleObserveLast20Seconds.snap +++ b/.forge-snapshots/OracleObserveLast20Seconds.snap @@ -1 +1 @@ -75965 \ No newline at end of file +73037 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestEqual.snap b/.forge-snapshots/OracleObserveLatestEqual.snap index 7f966954..3559f242 100644 --- a/.forge-snapshots/OracleObserveLatestEqual.snap +++ b/.forge-snapshots/OracleObserveLatestEqual.snap @@ -1 +1 @@ -1525 \ No newline at end of file +1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestTransform.snap b/.forge-snapshots/OracleObserveLatestTransform.snap index 9463411b..f5b9e8bf 100644 --- a/.forge-snapshots/OracleObserveLatestTransform.snap +++ b/.forge-snapshots/OracleObserveLatestTransform.snap @@ -1 +1 @@ -2000 \ No newline at end of file +1912 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveMiddle.snap b/.forge-snapshots/OracleObserveMiddle.snap index 0b1caa8d..76e5b53e 100644 --- a/.forge-snapshots/OracleObserveMiddle.snap +++ b/.forge-snapshots/OracleObserveMiddle.snap @@ -1 +1 @@ -5746 \ No newline at end of file +5541 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveOldest.snap b/.forge-snapshots/OracleObserveOldest.snap index bee097af..f124ce2d 100644 --- a/.forge-snapshots/OracleObserveOldest.snap +++ b/.forge-snapshots/OracleObserveOldest.snap @@ -1 +1 @@ -5277 \ No newline at end of file +5092 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveSinceMostRecent.snap b/.forge-snapshots/OracleObserveSinceMostRecent.snap index a51f76e9..9dab3404 100644 --- a/.forge-snapshots/OracleObserveSinceMostRecent.snap +++ b/.forge-snapshots/OracleObserveSinceMostRecent.snap @@ -1 +1 @@ -2615 \ No newline at end of file +2522 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index 194502b1..1ac55f85 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -123580 \ No newline at end of file +122753 \ No newline at end of file diff --git a/.gitignore b/.gitignore index de5c2c73..785fb393 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ cache/ -foundry-out/ \ No newline at end of file +foundry-out/ +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 12f0a651..5ad350a7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ contract CoolHook is BaseHook { function beforeModifyPosition( address, IPoolManager.PoolKey calldata key, - IPoolManager.ModifyPositionParams calldata params + IPoolManager.ModifyLiquidityParams calldata params ) external override onlyByManager returns (bytes4) { // hook logic return BaseHook.beforeModifyPosition.selector; diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 3e135dd5..72bff2c4 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.24; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {SafeCallback} from "./base/SafeCallback.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; @@ -31,13 +31,13 @@ abstract contract BaseHook is IHooks, SafeCallback { _; } - function getHooksCalls() public pure virtual returns (Hooks.Calls memory); + function getHookPermissions() public pure virtual returns (Hooks.Permissions memory); // this function is virtual so that we can override it during testing, // which allows us to deploy an implementation to any address // and then etch the bytecode into the correct address function validateHookAddress(BaseHook _this) internal pure virtual { - Hooks.validateHookAddress(_this, getHooksCalls()); + Hooks.validateHookPermissions(_this, getHookPermissions()); } function _lockAcquired(bytes calldata data) internal virtual override returns (bytes memory) { @@ -63,7 +63,7 @@ abstract contract BaseHook is IHooks, SafeCallback { revert HookNotImplemented(); } - function beforeModifyPosition(address, PoolKey calldata, IPoolManager.ModifyPositionParams calldata, bytes calldata) + function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) external virtual returns (bytes4) @@ -71,10 +71,29 @@ abstract contract BaseHook is IHooks, SafeCallback { revert HookNotImplemented(); } - function afterModifyPosition( + function beforeRemoveLiquidity( address, PoolKey calldata, - IPoolManager.ModifyPositionParams calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterAddLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata ) external virtual returns (bytes4) { diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol index 0c7a64db..9e6e8c71 100644 --- a/contracts/SimpleBatchCall.sol +++ b/contracts/SimpleBatchCall.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.19; import {LockAndBatchCall} from "./base/LockAndBatchCall.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /// @title SimpleBatchCall @@ -34,14 +34,14 @@ contract SimpleBatchCall is LockAndBatchCall { ERC20(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), uint256(delta)); poolManager.settle(currency); } else { - poolManager.safeTransferFrom( - address(this), address(poolManager), currency.toId(), uint256(delta), new bytes(0) + poolManager.transferFrom( + address(poolManager), address(this), currency.toId(), uint256(delta) ); } } if (delta < 0) { if (config.withdrawTokens) { - poolManager.mint(currency, address(this), uint256(-delta)); + poolManager.mint(address(this), currency.toId(), uint256(-delta)); } else { poolManager.take(currency, address(this), uint256(-delta)); } diff --git a/contracts/base/CallsWithLock.sol b/contracts/base/CallsWithLock.sol index 55b3694f..c871c797 100644 --- a/contracts/base/CallsWithLock.sol +++ b/contracts/base/CallsWithLock.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.19; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {ICallsWithLock} from "../interfaces/ICallsWithLock.sol"; @@ -26,10 +26,10 @@ abstract contract CallsWithLock is ICallsWithLock, ImmutableState { function modifyPositionWithLock( PoolKey calldata key, - IPoolManager.ModifyPositionParams calldata params, + IPoolManager.ModifyLiquidityParams calldata params, bytes calldata hookData ) external onlyBySelf returns (bytes memory) { - return abi.encode(poolManager.modifyPosition(key, params, hookData)); + return abi.encode(poolManager.modifyLiquidity(key, params, hookData)); } function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) diff --git a/contracts/base/ImmutableState.sol b/contracts/base/ImmutableState.sol index 3917b35d..7208c302 100644 --- a/contracts/base/ImmutableState.sol +++ b/contracts/base/ImmutableState.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.19; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; contract ImmutableState { IPoolManager public immutable poolManager; diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol index 6785290b..7855ff2b 100644 --- a/contracts/base/LockAndBatchCall.sol +++ b/contracts/base/LockAndBatchCall.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.19; import {SafeCallback} from "./SafeCallback.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {CallsWithLock} from "./CallsWithLock.sol"; abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { @@ -14,7 +14,7 @@ abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { /// @param executeData The function selectors and calldata for any of the function selectors in ICallsWithLock encoded as an array of bytes. function execute(bytes memory executeData, bytes memory settleData) external { - (bytes memory lockReturnData) = poolManager.lock(abi.encode(executeData, abi.encode(msg.sender, settleData))); + (bytes memory lockReturnData) = poolManager.lock(address(this), abi.encode(executeData, abi.encode(msg.sender, settleData))); (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); _handleAfterExecute(executeReturnData, settleReturnData); } diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index f272da34..24466924 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol index 46cbb640..ac5eb720 100644 --- a/contracts/base/SafeCallback.sol +++ b/contracts/base/SafeCallback.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./ImmutableState.sol"; abstract contract SafeCallback is ImmutableState, ILockCallback { @@ -14,7 +14,7 @@ abstract contract SafeCallback is ImmutableState, ILockCallback { } /// @dev There is no way to force the onlyByManager modifier but for this callback to be safe, it MUST check that the msg.sender is the pool manager. - function lockAcquired(bytes calldata data) external onlyByManager returns (bytes memory) { + function lockAcquired(address, bytes calldata data) external onlyByManager returns (bytes memory) { return _lockAcquired(data); } diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol index 662fd90b..b74cfb92 100644 --- a/contracts/hooks/examples/FullRange.sol +++ b/contracts/hooks/examples/FullRange.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {BaseHook} from "../../BaseHook.sol"; -import {SafeCast} from "@uniswap/v4-core/contracts/libraries/SafeCast.sol"; -import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; -import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; +import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {UniswapV4ERC20} from "../../libraries/UniswapV4ERC20.sol"; -import {FixedPoint96} from "@uniswap/v4-core/contracts/libraries/FixedPoint96.sol"; +import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; @@ -50,7 +50,7 @@ contract FullRange is BaseHook { struct CallbackData { address sender; PoolKey key; - IPoolManager.ModifyPositionParams params; + IPoolManager.ModifyLiquidityParams params; } struct PoolInfo { @@ -87,16 +87,20 @@ contract FullRange is BaseHook { _; } - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: true, afterInitialize: false, - beforeModifyPosition: true, - afterModifyPosition: false, + beforeAddLiquidity: true, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + noOp: false, + accessLock: false }); } @@ -115,7 +119,7 @@ contract FullRange is BaseHook { PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,) = poolManager.getSlot0(poolId); if (sqrtPriceX96 == 0) revert PoolNotInitialized(); @@ -136,7 +140,7 @@ contract FullRange is BaseHook { } BalanceDelta addedDelta = modifyPosition( key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: liquidity.toInt256() @@ -172,7 +176,7 @@ contract FullRange is BaseHook { PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,) = poolManager.getSlot0(poolId); if (sqrtPriceX96 == 0) revert PoolNotInitialized(); @@ -180,7 +184,7 @@ contract FullRange is BaseHook { delta = modifyPosition( key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: -(params.liquidity.toInt256()) @@ -217,15 +221,15 @@ contract FullRange is BaseHook { return FullRange.beforeInitialize.selector; } - function beforeModifyPosition( + function beforeAddLiquidity( address sender, PoolKey calldata, - IPoolManager.ModifyPositionParams calldata, + IPoolManager.ModifyLiquidityParams calldata, bytes calldata ) external view override returns (bytes4) { if (sender != address(this)) revert SenderMustBeHook(); - return FullRange.beforeModifyPosition.selector; + return FullRange.beforeAddLiquidity.selector; } function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) @@ -243,11 +247,13 @@ contract FullRange is BaseHook { return IHooks.beforeSwap.selector; } - function modifyPosition(PoolKey memory key, IPoolManager.ModifyPositionParams memory params) + function modifyPosition(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) internal returns (BalanceDelta delta) { - delta = abi.decode(poolManager.lock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); + delta = abi.decode( + poolManager.lock(address(this), abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta) + ); } function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { @@ -273,7 +279,7 @@ contract FullRange is BaseHook { poolManager.take(key.currency1, sender, uint256(uint128(-delta.amount1()))); } - function _removeLiquidity(PoolKey memory key, IPoolManager.ModifyPositionParams memory params) + function _removeLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) internal returns (BalanceDelta delta) { @@ -291,7 +297,7 @@ contract FullRange is BaseHook { ); params.liquidityDelta = -(liquidityToRemove.toInt256()); - delta = poolManager.modifyPosition(key, params, ZERO_BYTES); + delta = poolManager.modifyLiquidity(key, params, ZERO_BYTES); pool.hasAccruedFees = false; } @@ -303,7 +309,7 @@ contract FullRange is BaseHook { delta = _removeLiquidity(data.key, data.params); _takeDeltas(data.sender, data.key, delta); } else { - delta = poolManager.modifyPosition(data.key, data.params, ZERO_BYTES); + delta = poolManager.modifyLiquidity(data.key, data.params, ZERO_BYTES); _settleDeltas(data.sender, data.key, delta); } return abi.encode(delta); @@ -311,9 +317,9 @@ contract FullRange is BaseHook { function _rebalance(PoolKey memory key) public { PoolId poolId = key.toId(); - BalanceDelta balanceDelta = poolManager.modifyPosition( + BalanceDelta balanceDelta = poolManager.modifyLiquidity( key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()) @@ -327,7 +333,7 @@ contract FullRange is BaseHook { ) * FixedPointMathLib.sqrt(FixedPoint96.Q96) ).toUint160(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,) = poolManager.getSlot0(poolId); poolManager.swap( key, @@ -347,9 +353,9 @@ contract FullRange is BaseHook { uint256(uint128(-balanceDelta.amount1())) ); - BalanceDelta balanceDeltaAfter = poolManager.modifyPosition( + BalanceDelta balanceDeltaAfter = poolManager.modifyLiquidity( key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: liquidity.toInt256() diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol index e19245e2..8181ca1d 100644 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ b/contracts/hooks/examples/GeomeanOracle.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Oracle} from "../../libraries/Oracle.sol"; import {BaseHook} from "../../BaseHook.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; /// @notice A hook for a pool that allows a Uniswap pool to act as an oracle. Pools that use this hook must have full range /// tick spacing and liquidity is always permanently locked in these pools. This is the suggested configuration @@ -60,16 +60,20 @@ contract GeomeanOracle is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: true, afterInitialize: true, - beforeModifyPosition: true, - afterModifyPosition: false, + beforeAddLiquidity: true, + beforeRemoveLiquidity: true, + afterAddLiquidity: false, + afterRemoveLiquidity: false, beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + noOp: false, + accessLock: false }); } @@ -101,7 +105,7 @@ contract GeomeanOracle is BaseHook { /// @dev Called before any action that potentially modifies pool price or liquidity, such as swap or modify position function _updatePool(PoolKey calldata key) private { PoolId id = key.toId(); - (, int24 tick,,) = poolManager.getSlot0(id); + (, int24 tick,) = poolManager.getSlot0(id); uint128 liquidity = poolManager.getLiquidity(id); @@ -110,10 +114,10 @@ contract GeomeanOracle is BaseHook { ); } - function beforeModifyPosition( + function beforeAddLiquidity( address, PoolKey calldata key, - IPoolManager.ModifyPositionParams calldata params, + IPoolManager.ModifyLiquidityParams calldata params, bytes calldata ) external override onlyByManager returns (bytes4) { if (params.liquidityDelta < 0) revert OraclePoolMustLockLiquidity(); @@ -123,7 +127,16 @@ contract GeomeanOracle is BaseHook { || params.tickUpper != TickMath.maxUsableTick(maxTickSpacing) ) revert OraclePositionsMustBeFullRange(); _updatePool(key); - return GeomeanOracle.beforeModifyPosition.selector; + return GeomeanOracle.beforeAddLiquidity.selector; + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external view override onlyByManager returns (bytes4) { + revert OraclePoolMustLockLiquidity(); } function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) @@ -146,7 +159,7 @@ contract GeomeanOracle is BaseHook { ObservationState memory state = states[id]; - (, int24 tick,,) = poolManager.getSlot0(id); + (, int24 tick,) = poolManager.getSlot0(id); uint128 liquidity = poolManager.getLiquidity(id); diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol index 16cf008f..a854ae01 100644 --- a/contracts/hooks/examples/LimitOrder.sol +++ b/contracts/hooks/examples/LimitOrder.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol"; -import {SafeCast} from "@uniswap/v4-core/contracts/libraries/SafeCast.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {BaseHook} from "../../BaseHook.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; type Epoch is uint232; @@ -73,16 +73,20 @@ contract LimitOrder is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: false, afterInitialize: true, - beforeModifyPosition: false, - afterModifyPosition: false, + beforeAddLiquidity: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, beforeSwap: false, afterSwap: true, beforeDonate: false, - afterDonate: false + afterDonate: false, + noOp: false, + accessLock: false }); } @@ -107,7 +111,7 @@ contract LimitOrder is BaseHook { } function getTick(PoolId poolId) private view returns (int24 tick) { - (, tick,,) = poolManager.getSlot0(poolId); + (, tick,) = poolManager.getSlot0(poolId); } function getTickLower(int24 tick, int24 tickSpacing) private pure returns (int24) { @@ -156,6 +160,7 @@ contract LimitOrder is BaseHook { (uint256 amount0, uint256 amount1) = abi.decode( poolManager.lock( + address(this), abi.encodeCall(this.lockAcquiredFill, (key, lower, -int256(uint256(epochInfo.liquidityTotal)))) ), (uint256, uint256) @@ -194,9 +199,9 @@ contract LimitOrder is BaseHook { selfOnly returns (uint128 amount0, uint128 amount1) { - BalanceDelta delta = poolManager.modifyPosition( + BalanceDelta delta = poolManager.modifyLiquidity( key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickLower + key.tickSpacing, liquidityDelta: liquidityDelta @@ -204,8 +209,12 @@ contract LimitOrder is BaseHook { ZERO_BYTES ); - if (delta.amount0() < 0) poolManager.mint(key.currency0, address(this), amount0 = uint128(-delta.amount0())); - if (delta.amount1() < 0) poolManager.mint(key.currency1, address(this), amount1 = uint128(-delta.amount1())); + if (delta.amount0() < 0) { + poolManager.mint(address(this), key.currency0.toId(), amount0 = uint128(-delta.amount0())); + } + if (delta.amount1() < 0) { + poolManager.mint(address(this), key.currency1.toId(), amount1 = uint128(-delta.amount1())); + } } function place(PoolKey calldata key, int24 tickLower, bool zeroForOne, uint128 liquidity) @@ -215,6 +224,7 @@ contract LimitOrder is BaseHook { if (liquidity == 0) revert ZeroLiquidity(); poolManager.lock( + address(this), abi.encodeCall(this.lockAcquiredPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender)) ); @@ -250,9 +260,9 @@ contract LimitOrder is BaseHook { int256 liquidityDelta, address owner ) external selfOnly { - BalanceDelta delta = poolManager.modifyPosition( + BalanceDelta delta = poolManager.modifyLiquidity( key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickLower + key.tickSpacing, liquidityDelta: liquidityDelta @@ -291,21 +301,20 @@ contract LimitOrder is BaseHook { uint128 liquidity = epochInfo.liquidity[msg.sender]; if (liquidity == 0) revert ZeroLiquidity(); delete epochInfo.liquidity[msg.sender]; - uint128 liquidityTotal = epochInfo.liquidityTotal; - epochInfo.liquidityTotal = liquidityTotal - liquidity; uint256 amount0Fee; uint256 amount1Fee; (amount0, amount1, amount0Fee, amount1Fee) = abi.decode( poolManager.lock( + address(this), abi.encodeCall( this.lockAcquiredKill, - (key, tickLower, -int256(uint256(liquidity)), to, liquidity == liquidityTotal) + (key, tickLower, -int256(uint256(liquidity)), to, liquidity == epochInfo.liquidityTotal) ) ), (uint256, uint256, uint256, uint256) ); - + epochInfo.liquidityTotal -= liquidity; unchecked { epochInfo.token0Total += amount0Fee; epochInfo.token1Total += amount1Fee; @@ -328,23 +337,23 @@ contract LimitOrder is BaseHook { // could be unfairly diluted by a user sychronously placing then killing a limit order to skim off fees. // to prevent this, we allocate all fee revenue to remaining limit order placers, unless this is the last order. if (!removingAllLiquidity) { - BalanceDelta deltaFee = poolManager.modifyPosition( + BalanceDelta deltaFee = poolManager.modifyLiquidity( key, - IPoolManager.ModifyPositionParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: 0}), + IPoolManager.ModifyLiquidityParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: 0}), ZERO_BYTES ); if (deltaFee.amount0() < 0) { - poolManager.mint(key.currency0, address(this), amount0Fee = uint128(-deltaFee.amount0())); + poolManager.mint(address(this), key.currency0.toId(), amount0Fee = uint128(-deltaFee.amount0())); } if (deltaFee.amount1() < 0) { - poolManager.mint(key.currency1, address(this), amount1Fee = uint128(-deltaFee.amount1())); + poolManager.mint(address(this), key.currency1.toId(), amount1Fee = uint128(-deltaFee.amount1())); } } - BalanceDelta delta = poolManager.modifyPosition( + BalanceDelta delta = poolManager.modifyLiquidity( key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: liquidityDelta @@ -352,8 +361,12 @@ contract LimitOrder is BaseHook { ZERO_BYTES ); - if (delta.amount0() < 0) poolManager.take(key.currency0, to, amount0 = uint128(-delta.amount0())); - if (delta.amount1() < 0) poolManager.take(key.currency1, to, amount1 = uint128(-delta.amount1())); + if (delta.amount0() < 0) { + poolManager.take(key.currency0, to, amount0 = uint128(-delta.amount0())); + } + if (delta.amount1() < 0) { + poolManager.take(key.currency1, to, amount1 = uint128(-delta.amount1())); + } } function withdraw(Epoch epoch, address to) external returns (uint256 amount0, uint256 amount1) { @@ -365,18 +378,17 @@ contract LimitOrder is BaseHook { if (liquidity == 0) revert ZeroLiquidity(); delete epochInfo.liquidity[msg.sender]; - uint256 token0Total = epochInfo.token0Total; - uint256 token1Total = epochInfo.token1Total; uint128 liquidityTotal = epochInfo.liquidityTotal; - amount0 = FullMath.mulDiv(token0Total, liquidity, liquidityTotal); - amount1 = FullMath.mulDiv(token1Total, liquidity, liquidityTotal); + amount0 = FullMath.mulDiv(epochInfo.token0Total, liquidity, liquidityTotal); + amount1 = FullMath.mulDiv(epochInfo.token1Total, liquidity, liquidityTotal); - epochInfo.token0Total = token0Total - amount0; - epochInfo.token1Total = token1Total - amount1; + epochInfo.token0Total -= amount0; + epochInfo.token1Total -= amount1; epochInfo.liquidityTotal = liquidityTotal - liquidity; poolManager.lock( + address(this), abi.encodeCall(this.lockAcquiredWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to)) ); @@ -391,15 +403,11 @@ contract LimitOrder is BaseHook { address to ) external selfOnly { if (token0Amount > 0) { - poolManager.safeTransferFrom( - address(this), address(poolManager), uint256(uint160(Currency.unwrap(currency0))), token0Amount, "" - ); + poolManager.burn(address(this), currency0.toId(), token0Amount); poolManager.take(currency0, to, token0Amount); } if (token1Amount > 0) { - poolManager.safeTransferFrom( - address(this), address(poolManager), uint256(uint160(Currency.unwrap(currency1))), token1Amount, "" - ); + poolManager.burn(address(this), currency1.toId(), token1Amount); poolManager.take(currency1, to, token1Amount); } } diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index 28cae61f..a7de52d1 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -1,24 +1,24 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.15; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {TickBitmap} from "@uniswap/v4-core/contracts/libraries/TickBitmap.sol"; -import {SqrtPriceMath} from "@uniswap/v4-core/contracts/libraries/SqrtPriceMath.sol"; -import {FixedPoint96} from "@uniswap/v4-core/contracts/libraries/FixedPoint96.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {SafeCast} from "@uniswap/v4-core/contracts/libraries/SafeCast.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TickBitmap} from "@uniswap/v4-core/src/libraries/TickBitmap.sol"; +import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol"; +import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {BaseHook} from "../../BaseHook.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ITWAMM} from "../../interfaces/ITWAMM.sol"; -import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {TransferHelper} from "../../libraries/TransferHelper.sol"; import {TwammMath} from "../../libraries/TWAMM/TwammMath.sol"; import {OrderPool} from "../../libraries/TWAMM/OrderPool.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolGetters} from "../../libraries/PoolGetters.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; contract TWAMM is BaseHook, ITWAMM { using TransferHelper for IERC20Minimal; @@ -60,16 +60,20 @@ contract TWAMM is BaseHook, ITWAMM { expirationInterval = _expirationInterval; } - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: true, afterInitialize: false, - beforeModifyPosition: true, - afterModifyPosition: false, + beforeAddLiquidity: true, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + noOp: false, + accessLock: false }); } @@ -85,14 +89,14 @@ contract TWAMM is BaseHook, ITWAMM { return BaseHook.beforeInitialize.selector; } - function beforeModifyPosition( + function beforeAddLiquidity( address, PoolKey calldata key, - IPoolManager.ModifyPositionParams calldata, + IPoolManager.ModifyLiquidityParams calldata, bytes calldata ) external override onlyByManager returns (bytes4) { executeTWAMMOrders(key); - return BaseHook.beforeModifyPosition.selector; + return BaseHook.beforeAddLiquidity.selector; } function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) @@ -129,16 +133,10 @@ contract TWAMM is BaseHook, ITWAMM { self.lastVirtualOrderTimestamp = block.timestamp; } - struct CallbackData { - address sender; - PoolKey key; - IPoolManager.SwapParams params; - } - /// @inheritdoc ITWAMM function executeTWAMMOrders(PoolKey memory key) public { PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,) = poolManager.getSlot0(poolId); State storage twamm = twammStates[poolId]; (bool zeroForOne, uint160 sqrtPriceLimitX96) = _executeTWAMMOrders( @@ -146,7 +144,9 @@ contract TWAMM is BaseHook, ITWAMM { ); if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - poolManager.lock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96))); + poolManager.lock( + address(this), abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96)) + ); } } @@ -516,7 +516,7 @@ contract TWAMM is BaseHook, ITWAMM { _isCrossingInitializedTick(params.pool, poolManager, poolKey, finalSqrtPriceX96); if (crossingInitializedTick) { - int128 liquidityNetAtTick = poolManager.getNetLiquidityAtTick(poolKey.toId(), tick); + int128 liquidityNetAtTick = poolManager.getPoolTickInfo(poolKey.toId(), tick).liquidityNet; uint160 initializedSqrtPrice = TickMath.getSqrtRatioAtTick(tick); uint256 swapDelta0 = SqrtPriceMath.getAmount0Delta( @@ -600,7 +600,7 @@ contract TWAMM is BaseHook, ITWAMM { unchecked { // update pool - int128 liquidityNet = poolManager.getNetLiquidityAtTick(poolKey.toId(), params.initializedTick); + int128 liquidityNet = poolManager.getPoolTickInfo(poolKey.toId(), params.initializedTick).liquidityNet; if (initializedSqrtPrice < params.pool.sqrtPriceX96) liquidityNet = -liquidityNet; params.pool.liquidity = liquidityNet < 0 ? params.pool.liquidity - uint128(-liquidityNet) diff --git a/contracts/hooks/examples/VolatilityOracle.sol b/contracts/hooks/examples/VolatilityOracle.sol index 0a7e696d..df8bdde5 100644 --- a/contracts/hooks/examples/VolatilityOracle.sol +++ b/contracts/hooks/examples/VolatilityOracle.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {IDynamicFeeManager} from "@uniswap/v4-core/contracts/interfaces/IDynamicFeeManager.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {FeeLibrary} from "@uniswap/v4-core/contracts/libraries/FeeLibrary.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IDynamicFeeManager} from "@uniswap/v4-core/src/interfaces/IDynamicFeeManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {FeeLibrary} from "@uniswap/v4-core/src/libraries/FeeLibrary.sol"; import {BaseHook} from "../../BaseHook.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; contract VolatilityOracle is BaseHook, IDynamicFeeManager { using FeeLibrary for uint24; @@ -15,11 +15,7 @@ contract VolatilityOracle is BaseHook, IDynamicFeeManager { uint32 deployTimestamp; - function getFee(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) - external - view - returns (uint24) - { + function getFee(address, PoolKey calldata) external view returns (uint24) { uint24 startingFee = 3000; uint32 lapsed = _blockTimestamp() - deployTimestamp; return startingFee + (uint24(lapsed) * 100) / 60; // 100 bps a minute @@ -34,16 +30,20 @@ contract VolatilityOracle is BaseHook, IDynamicFeeManager { deployTimestamp = _blockTimestamp(); } - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return Hooks.Calls({ + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ beforeInitialize: true, afterInitialize: false, - beforeModifyPosition: false, - afterModifyPosition: false, + beforeAddLiquidity: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, beforeSwap: false, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + noOp: false, + accessLock: false }); } diff --git a/contracts/interfaces/ICallsWithLock.sol b/contracts/interfaces/ICallsWithLock.sol index 564dd1ca..26017356 100644 --- a/contracts/interfaces/ICallsWithLock.sol +++ b/contracts/interfaces/ICallsWithLock.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; interface ICallsWithLock { function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) @@ -11,7 +11,7 @@ interface ICallsWithLock { function modifyPositionWithLock( PoolKey calldata key, - IPoolManager.ModifyPositionParams calldata params, + IPoolManager.ModifyLiquidityParams calldata params, bytes calldata hookData ) external returns (bytes memory); diff --git a/contracts/interfaces/IPeripheryPayments.sol b/contracts/interfaces/IPeripheryPayments.sol index 765b980f..f3c24660 100644 --- a/contracts/interfaces/IPeripheryPayments.sol +++ b/contracts/interfaces/IPeripheryPayments.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; /// @title Periphery Payments /// @notice Functions to ease deposits and withdrawals of ETH diff --git a/contracts/interfaces/IQuoter.sol b/contracts/interfaces/IQuoter.sol new file mode 100644 index 00000000..90a390fc --- /dev/null +++ b/contracts/interfaces/IQuoter.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PathKey} from "../libraries/PathKey.sol"; + +/// @title Quoter Interface +/// @notice Supports quoting the delta amounts from exact input or exact output swaps. +/// @notice For each pool also tells you the number of initialized ticks loaded and the sqrt price of the pool after the swap. +/// @dev These functions are not marked view because they rely on calling non-view functions and reverting +/// to compute the result. They are also not gas efficient and should not be called on-chain. +interface IQuoter { + error InvalidLockAcquiredSender(); + error InvalidLockCaller(); + error InvalidQuoteBatchParams(); + error InsufficientAmountOut(); + error LockFailure(); + error NotSelf(); + error UnexpectedRevertBytes(bytes revertData); + + struct PoolDeltas { + int128 currency0Delta; + int128 currency1Delta; + } + + struct QuoteExactSingleParams { + PoolKey poolKey; + bool zeroForOne; + address recipient; + uint128 exactAmount; + uint160 sqrtPriceLimitX96; + bytes hookData; + } + + struct QuoteExactParams { + Currency exactCurrency; + PathKey[] path; + address recipient; + uint128 exactAmount; + } + + /// @notice Returns the delta amounts for a given exact input swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` + /// poolKey The key for identifying a V4 pool + /// zeroForOne If the swap is from currency0 to currency1 + /// recipient The intended recipient of the output tokens + /// exactAmount The desired input amount + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// hookData arbitrary hookData to pass into the associated hooks + /// @return deltaAmounts Delta amounts resulted from the swap + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksLoaded The number of initialized ticks that the swap loaded + function quoteExactInputSingle(QuoteExactSingleParams calldata params) + external + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded); + + /// @notice Returns the delta amounts along the swap path for a given exact input swap + /// @param params the params for the quote, encoded as 'QuoteExactInputParams' + /// currencyIn The input currency of the swap + /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info + /// recipient The intended recipient of the output tokens + /// exactAmount The desired input amount + /// @return deltaAmounts Delta amounts along the path resulted from the swap + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksLoadedList List of the initialized ticks that the swap loaded for each pool in the path + function quoteExactInput(QuoteExactParams memory params) + external + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ); + + /// @notice Returns the delta amounts for a given exact output swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` + /// poolKey The key for identifying a V4 pool + /// zeroForOne If the swap is from currency0 to currency1 + /// recipient The intended recipient of the output tokens + /// exactAmount The desired input amount + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// hookData arbitrary hookData to pass into the associated hooks + /// @return deltaAmounts Delta amounts resulted from the swap + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksLoaded The number of initialized ticks that the swap loaded + function quoteExactOutputSingle(QuoteExactSingleParams calldata params) + external + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded); + + /// @notice Returns the delta amounts along the swap path for a given exact output swap + /// @param params the params for the quote, encoded as 'QuoteExactOutputParams' + /// currencyOut The output currency of the swap + /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info + /// recipient The intended recipient of the output tokens + /// exactAmount The desired output amount + /// @return deltaAmounts Delta amounts along the path resulted from the swap + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksLoadedList List of the initialized ticks that the swap loaded for each pool in the path + function quoteExactOutput(QuoteExactParams memory params) + external + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ); +} diff --git a/contracts/interfaces/ITWAMM.sol b/contracts/interfaces/ITWAMM.sol index 570617b6..3b932d3c 100644 --- a/contracts/interfaces/ITWAMM.sol +++ b/contracts/interfaces/ITWAMM.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.15; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; interface ITWAMM { /// @notice Thrown when account other than owner attempts to interact with an order diff --git a/contracts/lens/Quoter.sol b/contracts/lens/Quoter.sol new file mode 100644 index 00000000..1f9350a8 --- /dev/null +++ b/contracts/lens/Quoter.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {IQuoter} from "../interfaces/IQuoter.sol"; +import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; +import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; + +contract Quoter is IQuoter, ILockCallback { + using Hooks for IHooks; + using PoolIdLibrary for PoolKey; + using PathKeyLib for PathKey; + + /// @dev cache used to check a safety condition in exact output swaps. + uint128 private amountOutCached; + + // v4 Singleton contract + IPoolManager public immutable manager; + + /// @dev min valid reason is 3-words long + /// @dev int128[2] + sqrtPriceX96After padded to 32bytes + intializeTicksLoaded padded to 32bytes + uint256 internal constant MINIMUM_VALID_RESPONSE_LENGTH = 96; + + struct QuoteResult { + int128[] deltaAmounts; + uint160[] sqrtPriceX96AfterList; + uint32[] initializedTicksLoadedList; + } + + struct QuoteCache { + BalanceDelta curDeltas; + uint128 prevAmount; + int128 deltaIn; + int128 deltaOut; + int24 tickBefore; + int24 tickAfter; + Currency prevCurrency; + uint160 sqrtPriceX96After; + } + + /// @dev Only this address may call this function + modifier selfOnly() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + constructor(address _poolManager) { + manager = IPoolManager(_poolManager); + } + + /// @inheritdoc IQuoter + function quoteExactInputSingle(QuoteExactSingleParams memory params) + public + override + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} + catch (bytes memory reason) { + return _handleRevertSingle(reason); + } + } + + /// @inheritdoc IQuoter + function quoteExactInput(QuoteExactParams memory params) + external + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} + catch (bytes memory reason) { + return _handleRevert(reason); + } + } + + /// @inheritdoc IQuoter + function quoteExactOutputSingle(QuoteExactSingleParams memory params) + public + override + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} + catch (bytes memory reason) { + if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; + return _handleRevertSingle(reason); + } + } + + /// @inheritdoc IQuoter + function quoteExactOutput(QuoteExactParams memory params) + public + override + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} + catch (bytes memory reason) { + return _handleRevert(reason); + } + } + + /// @inheritdoc ILockCallback + function lockAcquired(address lockCaller, bytes calldata data) external returns (bytes memory) { + if (msg.sender != address(manager)) { + revert InvalidLockAcquiredSender(); + } + if (lockCaller != address(this)) { + revert InvalidLockCaller(); + } + + (bool success, bytes memory returnData) = address(this).call(data); + if (success) return returnData; + if (returnData.length == 0) revert LockFailure(); + // if the call failed, bubble up the reason + /// @solidity memory-safe-assembly + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + + /// @dev check revert bytes and pass through if considered valid; otherwise revert with different message + function validateRevertReason(bytes memory reason) private pure returns (bytes memory) { + if (reason.length < MINIMUM_VALID_RESPONSE_LENGTH) { + revert UnexpectedRevertBytes(reason); + } + return reason; + } + + /// @dev parse revert bytes from a single-pool quote + function _handleRevertSingle(bytes memory reason) + private + pure + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + { + reason = validateRevertReason(reason); + (deltaAmounts, sqrtPriceX96After, initializedTicksLoaded) = abi.decode(reason, (int128[], uint160, uint32)); + } + + /// @dev parse revert bytes from a potentially multi-hop quote and return the delta amounts, sqrtPriceX96After, and initializedTicksLoaded + function _handleRevert(bytes memory reason) + private + pure + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) + { + reason = validateRevertReason(reason); + (deltaAmounts, sqrtPriceX96AfterList, initializedTicksLoadedList) = + abi.decode(reason, (int128[], uint160[], uint32[])); + } + + /// @dev quote an ExactInput swap along a path of tokens, then revert with the result + function _quoteExactInput(QuoteExactParams memory params) public selfOnly returns (bytes memory) { + uint256 pathLength = params.path.length; + + QuoteResult memory result = QuoteResult({ + deltaAmounts: new int128[](pathLength + 1), + sqrtPriceX96AfterList: new uint160[](pathLength), + initializedTicksLoadedList: new uint32[](pathLength) + }); + QuoteCache memory cache; + + for (uint256 i = 0; i < pathLength; i++) { + (PoolKey memory poolKey, bool zeroForOne) = + params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : cache.prevCurrency); + (, cache.tickBefore,) = manager.getSlot0(poolKey.toId()); + + (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap( + poolKey, + zeroForOne, + int256(int128(i == 0 ? params.exactAmount : cache.prevAmount)), + 0, + params.path[i].hookData + ); + + (cache.deltaIn, cache.deltaOut) = zeroForOne + ? (cache.curDeltas.amount0(), cache.curDeltas.amount1()) + : (cache.curDeltas.amount1(), cache.curDeltas.amount0()); + result.deltaAmounts[i] += cache.deltaIn; + result.deltaAmounts[i + 1] += cache.deltaOut; + + cache.prevAmount = zeroForOne ? uint128(-cache.curDeltas.amount1()) : uint128(-cache.curDeltas.amount0()); + cache.prevCurrency = params.path[i].intermediateCurrency; + result.sqrtPriceX96AfterList[i] = cache.sqrtPriceX96After; + result.initializedTicksLoadedList[i] = + PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter); + } + bytes memory r = + abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); + assembly { + revert(add(0x20, r), mload(r)) + } + } + + /// @dev quote an ExactInput swap on a pool, then revert with the result + function _quoteExactInputSingle(QuoteExactSingleParams memory params) public selfOnly returns (bytes memory) { + (, int24 tickBefore,) = manager.getSlot0(params.poolKey.toId()); + + (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( + params.poolKey, + params.zeroForOne, + int256(int128(params.exactAmount)), + params.sqrtPriceLimitX96, + params.hookData + ); + + int128[] memory deltaAmounts = new int128[](2); + + deltaAmounts[0] = deltas.amount0(); + deltaAmounts[1] = deltas.amount1(); + + uint32 initializedTicksLoaded = + PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); + assembly { + revert(add(0x20, result), mload(result)) + } + } + + /// @dev quote an ExactOutput swap along a path of tokens, then revert with the result + function _quoteExactOutput(QuoteExactParams memory params) public selfOnly returns (bytes memory) { + uint256 pathLength = params.path.length; + + QuoteResult memory result = QuoteResult({ + deltaAmounts: new int128[](pathLength + 1), + sqrtPriceX96AfterList: new uint160[](pathLength), + initializedTicksLoadedList: new uint32[](pathLength) + }); + QuoteCache memory cache; + uint128 curAmountOut; + + for (uint256 i = pathLength; i > 0; i--) { + curAmountOut = i == pathLength ? params.exactAmount : cache.prevAmount; + amountOutCached = curAmountOut; + + (PoolKey memory poolKey, bool oneForZero) = PathKeyLib.getPoolAndSwapDirection( + params.path[i - 1], i == pathLength ? params.exactCurrency : cache.prevCurrency + ); + + (, cache.tickBefore,) = manager.getSlot0(poolKey.toId()); + + (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = + _swap(poolKey, !oneForZero, -int256(uint256(curAmountOut)), 0, params.path[i - 1].hookData); + + // always clear because sqrtPriceLimitX96 is set to 0 always + delete amountOutCached; + (cache.deltaIn, cache.deltaOut) = !oneForZero + ? (cache.curDeltas.amount0(), cache.curDeltas.amount1()) + : (cache.curDeltas.amount1(), cache.curDeltas.amount0()); + result.deltaAmounts[i - 1] += cache.deltaIn; + result.deltaAmounts[i] += cache.deltaOut; + + cache.prevAmount = !oneForZero ? uint128(cache.curDeltas.amount0()) : uint128(cache.curDeltas.amount1()); + cache.prevCurrency = params.path[i - 1].intermediateCurrency; + result.sqrtPriceX96AfterList[i - 1] = cache.sqrtPriceX96After; + result.initializedTicksLoadedList[i - 1] = + PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter); + } + bytes memory r = + abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); + assembly { + revert(add(0x20, r), mload(r)) + } + } + + /// @dev quote an ExactOutput swap on a pool, then revert with the result + function _quoteExactOutputSingle(QuoteExactSingleParams memory params) public selfOnly returns (bytes memory) { + // if no price limit has been specified, cache the output amount for comparison in the swap callback + if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.exactAmount; + + (, int24 tickBefore,) = manager.getSlot0(params.poolKey.toId()); + (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( + params.poolKey, + params.zeroForOne, + -int256(uint256(params.exactAmount)), + params.sqrtPriceLimitX96, + params.hookData + ); + + if (amountOutCached != 0) delete amountOutCached; + int128[] memory deltaAmounts = new int128[](2); + + deltaAmounts[0] = deltas.amount0(); + deltaAmounts[1] = deltas.amount1(); + + uint32 initializedTicksLoaded = + PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); + assembly { + revert(add(0x20, result), mload(result)) + } + } + + /// @dev Execute a swap and return the amounts delta, as well as relevant pool state + /// @notice if amountSpecified > 0, the swap is exactInput, otherwise exactOutput + function _swap( + PoolKey memory poolKey, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes memory hookData + ) private returns (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) { + deltas = manager.swap( + poolKey, + IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: _sqrtPriceLimitOrDefault(sqrtPriceLimitX96, zeroForOne) + }), + hookData + ); + // only exactOut case + if (amountOutCached != 0 && amountOutCached != uint128(zeroForOne ? -deltas.amount1() : -deltas.amount0())) { + revert InsufficientAmountOut(); + } + (sqrtPriceX96After, tickAfter,) = manager.getSlot0(poolKey.toId()); + } + + /// @dev return either the sqrtPriceLimit from user input, or the max/min value possible depending on trade direction + function _sqrtPriceLimitOrDefault(uint160 sqrtPriceLimitX96, bool zeroForOne) private pure returns (uint160) { + return sqrtPriceLimitX96 == 0 + ? zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 + : sqrtPriceLimitX96; + } +} diff --git a/contracts/libraries/LiquidityAmounts.sol b/contracts/libraries/LiquidityAmounts.sol index b2c8b54c..742e48f5 100644 --- a/contracts/libraries/LiquidityAmounts.sol +++ b/contracts/libraries/LiquidityAmounts.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; -import "@uniswap/v4-core/contracts/libraries/FullMath.sol"; -import "@uniswap/v4-core/contracts/libraries/FixedPoint96.sol"; +import "@uniswap/v4-core/src/libraries/FullMath.sol"; +import "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; /// @title Liquidity amount functions /// @notice Provides functions for computing liquidity amounts from token amounts and prices diff --git a/contracts/libraries/PathKey.sol b/contracts/libraries/PathKey.sol new file mode 100644 index 00000000..f9d5da33 --- /dev/null +++ b/contracts/libraries/PathKey.sol @@ -0,0 +1,30 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.20; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +struct PathKey { + Currency intermediateCurrency; + uint24 fee; + int24 tickSpacing; + IHooks hooks; + bytes hookData; +} + +library PathKeyLib { + function getPoolAndSwapDirection(PathKey memory params, Currency currencyIn) + internal + pure + returns (PoolKey memory poolKey, bool zeroForOne) + { + (Currency currency0, Currency currency1) = currencyIn < params.intermediateCurrency + ? (currencyIn, params.intermediateCurrency) + : (params.intermediateCurrency, currencyIn); + + zeroForOne = currencyIn == currency0; + poolKey = PoolKey(currency0, currency1, params.fee, params.tickSpacing, params.hooks); + } +} diff --git a/contracts/libraries/PoolGetters.sol b/contracts/libraries/PoolGetters.sol index d2c7fbf2..e3cb318b 100644 --- a/contracts/libraries/PoolGetters.sol +++ b/contracts/libraries/PoolGetters.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Pool} from "@uniswap/v4-core/contracts/libraries/Pool.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {BitMath} from "@uniswap/v4-core/contracts/libraries/BitMath.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol"; /// @title Helper functions to access pool information +/// TODO: Expose other getters on core with extsload. Only use when extsload is available and storage layout is frozen. library PoolGetters { uint256 constant POOL_SLOT = 10; uint256 constant TICKS_OFFSET = 4; @@ -62,7 +63,7 @@ library PoolGetters { // all the 1s at or to the right of the current bitPos uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); // uint256 masked = self[wordPos] & mask; - uint256 masked = getTickBitmapAtWord(poolManager, poolId, wordPos) & mask; + uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word initialized = masked != 0; @@ -75,7 +76,7 @@ library PoolGetters { (int16 wordPos, uint8 bitPos) = position(compressed + 1); // all the 1s at or to the left of the bitPos uint256 mask = ~((1 << bitPos) - 1); - uint256 masked = getTickBitmapAtWord(poolManager, poolId, wordPos) & mask; + uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; // if there are no initialized ticks to the left of the current tick, return leftmost in the word initialized = masked != 0; diff --git a/contracts/libraries/PoolTicksCounter.sol b/contracts/libraries/PoolTicksCounter.sol new file mode 100644 index 00000000..077ef4a6 --- /dev/null +++ b/contracts/libraries/PoolTicksCounter.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.20; + +import {PoolGetters} from "./PoolGetters.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; + +library PoolTicksCounter { + using PoolIdLibrary for PoolKey; + + struct TickCache { + int16 wordPosLower; + int16 wordPosHigher; + uint8 bitPosLower; + uint8 bitPosHigher; + bool tickBeforeInitialized; + bool tickAfterInitialized; + } + + /// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore and tickAfter. + /// When tickBefore and/or tickAfter themselves are initialized, the logic over whether we should count them depends on the + /// direction of the swap. If we are swapping upwards (tickAfter > tickBefore) we don't want to count tickBefore but we do + /// want to count tickAfter. The opposite is true if we are swapping downwards. + function countInitializedTicksLoaded(IPoolManager self, PoolKey memory key, int24 tickBefore, int24 tickAfter) + internal + view + returns (uint32 initializedTicksLoaded) + { + TickCache memory cache; + + { + // Get the key and offset in the tick bitmap of the active tick before and after the swap. + int16 wordPos = int16((tickBefore / key.tickSpacing) >> 8); + uint8 bitPos = uint8(uint24((tickBefore / key.tickSpacing) % 256)); + + int16 wordPosAfter = int16((tickAfter / key.tickSpacing) >> 8); + uint8 bitPosAfter = uint8(uint24((tickAfter / key.tickSpacing) % 256)); + + // In the case where tickAfter is initialized, we only want to count it if we are swapping downwards. + // If the initializable tick after the swap is initialized, our original tickAfter is a + // multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized + // and we shouldn't count it. + uint256 bmAfter = self.getPoolBitmapInfo(key.toId(), wordPosAfter); + //uint256 bmAfter = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPosAfter); + cache.tickAfterInitialized = + ((bmAfter & (1 << bitPosAfter)) > 0) && ((tickAfter % key.tickSpacing) == 0) && (tickBefore > tickAfter); + + // In the case where tickBefore is initialized, we only want to count it if we are swapping upwards. + // Use the same logic as above to decide whether we should count tickBefore or not. + uint256 bmBefore = self.getPoolBitmapInfo(key.toId(), wordPos); + //uint256 bmBefore = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPos); + cache.tickBeforeInitialized = + ((bmBefore & (1 << bitPos)) > 0) && ((tickBefore % key.tickSpacing) == 0) && (tickBefore < tickAfter); + + if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) { + cache.wordPosLower = wordPos; + cache.bitPosLower = bitPos; + cache.wordPosHigher = wordPosAfter; + cache.bitPosHigher = bitPosAfter; + } else { + cache.wordPosLower = wordPosAfter; + cache.bitPosLower = bitPosAfter; + cache.wordPosHigher = wordPos; + cache.bitPosHigher = bitPos; + } + } + + // Count the number of initialized ticks crossed by iterating through the tick bitmap. + // Our first mask should include the lower tick and everything to its left. + uint256 mask = type(uint256).max << cache.bitPosLower; + while (cache.wordPosLower <= cache.wordPosHigher) { + // If we're on the final tick bitmap page, ensure we only count up to our + // ending tick. + if (cache.wordPosLower == cache.wordPosHigher) { + mask = mask & (type(uint256).max >> (255 - cache.bitPosHigher)); + } + + //uint256 bmLower = PoolGetters.getTickBitmapAtWord(self, key.toId(), cache.wordPosLower); + uint256 bmLower = self.getPoolBitmapInfo(key.toId(), cache.wordPosLower); + uint256 masked = bmLower & mask; + initializedTicksLoaded += countOneBits(masked); + cache.wordPosLower++; + // Reset our mask so we consider all bits on the next iteration. + mask = type(uint256).max; + } + + if (cache.tickAfterInitialized) { + initializedTicksLoaded -= 1; + } + + if (cache.tickBeforeInitialized) { + initializedTicksLoaded -= 1; + } + + return initializedTicksLoaded; + } + + function countOneBits(uint256 x) private pure returns (uint16) { + uint16 bits = 0; + while (x != 0) { + bits++; + x &= (x - 1); + } + return bits; + } +} diff --git a/contracts/libraries/TWAMM/TwammMath.sol b/contracts/libraries/TWAMM/TwammMath.sol index 133a68c7..a5994b51 100644 --- a/contracts/libraries/TWAMM/TwammMath.sol +++ b/contracts/libraries/TWAMM/TwammMath.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.15; import {ABDKMathQuad} from "./ABDKMathQuad.sol"; -import {FixedPoint96} from "@uniswap/v4-core/contracts/libraries/FixedPoint96.sol"; -import {SafeCast} from "@uniswap/v4-core/contracts/libraries/SafeCast.sol"; -import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; +import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; /// @title TWAMM Math - Pure functions for TWAMM math calculations library TwammMath { diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol index 5b1833a7..9ab40d9e 100644 --- a/contracts/libraries/TransferHelper.sol +++ b/contracts/libraries/TransferHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.15; -import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; /// @title TransferHelper /// @notice Contains helper methods for interacting with ERC20 tokens that do not consistently return true/false diff --git a/foundry.toml b/foundry.toml index b3132187..4e95a213 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,10 +1,11 @@ [profile.default] src = 'contracts' out = 'foundry-out' -solc_version = '0.8.20' -optimizer_runs = 800 +solc_version = '0.8.24' +optimizer_runs = 1000000 ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] +evm_version = "cancun" [profile.ci] fuzz_runs = 100000 diff --git a/lib/v4-core b/lib/v4-core index 0095e084..4a13732d 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit 0095e0848098c3e32e016eac6d2537b67aa47358 +Subproject commit 4a13732dc0b9a8c516d3639a78c54af3fc3db8d4 diff --git a/test/FullRange.t.sol b/test/FullRange.t.sol index fa9d13ed..076abab3 100644 --- a/test/FullRange.t.sol +++ b/test/FullRange.t.sol @@ -3,22 +3,23 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {FullRange} from "../contracts/hooks/examples/FullRange.sol"; import {FullRangeImplementation} from "./shared/implementation/FullRangeImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; -import {MockERC20} from "@uniswap/v4-core/test/foundry-tests/utils/MockERC20.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {PoolModifyPositionTest} from "@uniswap/v4-core/contracts/test/PoolModifyPositionTest.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/contracts/test/PoolSwapTest.sol"; -import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {UniswapV4ERC20} from "../contracts/libraries/UniswapV4ERC20.sol"; -import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol"; -import {SafeCast} from "@uniswap/v4-core/contracts/libraries/SafeCast.sol"; +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; contract TestFullRange is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; @@ -47,6 +48,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { uint24 fee ); + HookEnabledSwapRouter router; /// @dev Min tick for full range with tick spacing of 60 int24 internal constant MIN_TICK = -887220; /// @dev Max tick for full range with tick spacing of 60 @@ -62,15 +64,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { MockERC20 token1; MockERC20 token2; - Currency currency0; - Currency currency1; - - PoolManager manager; FullRangeImplementation fullRange = FullRangeImplementation( - address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG | Hooks.BEFORE_SWAP_FLAG)) + address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG)) ); - PoolKey key; PoolId id; PoolKey key2; @@ -80,15 +77,13 @@ contract TestFullRange is Test, Deployers, GasSnapshot { PoolKey keyWithLiq; PoolId idWithLiq; - PoolModifyPositionTest modifyPositionRouter; - PoolSwapTest swapRouter; - function setUp() public { - token0 = new MockERC20("TestA", "A", 18, 2 ** 128); - token1 = new MockERC20("TestB", "B", 18, 2 ** 128); - token2 = new MockERC20("TestC", "C", 18, 2 ** 128); - - manager = new PoolManager(500000); + deployFreshManagerAndRouters(); + router = new HookEnabledSwapRouter(manager); + MockERC20[] memory tokens = deployTokens(3, 2 ** 128); + token0 = tokens[0]; + token1 = tokens[1]; + token2 = tokens[2]; FullRangeImplementation impl = new FullRangeImplementation(manager, fullRange); vm.etch(address(fullRange), address(impl).code); @@ -102,17 +97,14 @@ contract TestFullRange is Test, Deployers, GasSnapshot { keyWithLiq = createPoolKey(token0, token2); idWithLiq = keyWithLiq.toId(); - modifyPositionRouter = new PoolModifyPositionTest(manager); - swapRouter = new PoolSwapTest(manager); - token0.approve(address(fullRange), type(uint256).max); token1.approve(address(fullRange), type(uint256).max); token2.approve(address(fullRange), type(uint256).max); - token0.approve(address(swapRouter), type(uint256).max); - token1.approve(address(swapRouter), type(uint256).max); - token2.approve(address(swapRouter), type(uint256).max); + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + token2.approve(address(router), type(uint256).max); - manager.initialize(keyWithLiq, SQRT_RATIO_1_1, ZERO_BYTES); + initPool(keyWithLiq.currency0, keyWithLiq.currency1, fullRange, 3000, SQRT_RATIO_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( keyWithLiq.currency0, @@ -135,7 +127,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { emit Initialize(id, testKey.currency0, testKey.currency1, testKey.fee, testKey.tickSpacing, testKey.hooks); snapStart("FullRangeInitialize"); - manager.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); snapEnd(); (, address liquidityToken) = fullRange.poolInfo(id); @@ -147,11 +139,11 @@ contract TestFullRange is Test, Deployers, GasSnapshot { PoolKey memory wrongKey = PoolKey(key.currency0, key.currency1, 0, TICK_SPACING + 1, fullRange); vm.expectRevert(FullRange.TickSpacingNotDefault.selector); - manager.initialize(wrongKey, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(wrongKey, SQRT_RATIO_1_1, ZERO_BYTES); } function testFullRange_addLiquidity_InitialAddSucceeds() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -177,7 +169,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_InitialAddFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); if (amount < LOCKED_LIQUIDITY) { vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); fullRange.addLiquidity( @@ -252,7 +244,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_SwapThenAddSucceeds() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -273,16 +265,16 @@ contract TestFullRange is Test, Deployers, GasSnapshot { vm.expectEmit(true, true, true, true); emit Swap( - id, address(swapRouter), 1 ether, -906610893880149131, 72045250990510446115798809072, 10 ether, -1901, 3000 + id, address(router), 1 ether, -906610893880149131, 72045250990510446115798809072, 10 ether, -1901, 3000 ); IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); - PoolSwapTest.TestSettings memory settings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings memory settings = + HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); snapStart("FullRangeSwap"); - swapRouter.swap(key, params, settings, ZERO_BYTES); + router.swap(key, params, settings, ZERO_BYTES); snapEnd(); (bool hasAccruedFees,) = fullRange.poolInfo(id); @@ -306,7 +298,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_FailsIfTooMuchSlippage() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -316,10 +308,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1000 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); - PoolSwapTest.TestSettings memory settings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings memory settings = + HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); - swapRouter.swap(key, params, settings, ZERO_BYTES); + router.swap(key, params, settings, ZERO_BYTES); vm.expectRevert(FullRange.TooMuchSlippage.selector); fullRange.addLiquidity( @@ -331,7 +323,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { function testFullRange_swap_TwoSwaps() public { PoolKey memory testKey = key; - manager.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -341,18 +333,18 @@ contract TestFullRange is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); - PoolSwapTest.TestSettings memory settings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings memory settings = + HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); snapStart("FullRangeFirstSwap"); - swapRouter.swap(testKey, params, settings, ZERO_BYTES); + router.swap(testKey, params, settings, ZERO_BYTES); snapEnd(); (bool hasAccruedFees,) = fullRange.poolInfo(id); assertEq(hasAccruedFees, true); snapStart("FullRangeSecondSwap"); - swapRouter.swap(testKey, params, settings, ZERO_BYTES); + router.swap(testKey, params, settings, ZERO_BYTES); snapEnd(); (hasAccruedFees,) = fullRange.poolInfo(id); @@ -360,8 +352,8 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_swap_TwoPools() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - manager.initialize(key2, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key2, SQRT_RATIO_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -377,11 +369,11 @@ contract TestFullRange is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10000000, sqrtPriceLimitX96: SQRT_RATIO_1_2}); - PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings memory testSettings = + HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); - swapRouter.swap(key, params, testSettings, ZERO_BYTES); - swapRouter.swap(key2, params, testSettings, ZERO_BYTES); + router.swap(key, params, testSettings, ZERO_BYTES); + router.swap(key2, params, testSettings, ZERO_BYTES); (bool hasAccruedFees,) = fullRange.poolInfo(id); assertEq(hasAccruedFees, true); @@ -416,7 +408,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_InitialRemoveFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -464,7 +456,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_FailsIfNoLiquidity() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); @@ -476,7 +468,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_SucceedsWithPartial() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOfSelf(); uint256 prevBalance1 = key.currency1.balanceOfSelf(); @@ -511,7 +503,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_DiffRatios() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -560,10 +552,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); - PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings memory testSettings = + HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); - swapRouter.swap(keyWithLiq, params, testSettings, ZERO_BYTES); + router.swap(keyWithLiq, params, testSettings, ZERO_BYTES); UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); @@ -579,7 +571,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_RemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); if (amount <= LOCKED_LIQUIDITY) { @@ -634,7 +626,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { vm.prank(address(2)); token1.approve(address(fullRange), type(uint256).max); - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); // Test contract adds liquidity @@ -687,10 +679,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: SQRT_RATIO_1_4}); - PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings memory testSettings = + HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); - swapRouter.swap(key, params, testSettings, ZERO_BYTES); + router.swap(key, params, testSettings, ZERO_BYTES); (bool hasAccruedFees,) = fullRange.poolInfo(id); assertEq(hasAccruedFees, true); @@ -712,7 +704,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_SwapRemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); if (amount <= LOCKED_LIQUIDITY) { @@ -742,10 +734,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { sqrtPriceLimitX96: SQRT_RATIO_1_4 }); - PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings memory testSettings = + HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); - swapRouter.swap(key, params, testSettings, ZERO_BYTES); + router.swap(key, params, testSettings, ZERO_BYTES); // Test contract removes liquidity, succeeds UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); @@ -761,12 +753,12 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_BeforeModifyPositionFailsWithWrongMsgSender() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); vm.expectRevert(FullRange.SenderMustBeHook.selector); - modifyPositionRouter.modifyPosition( + modifyLiquidityRouter.modifyLiquidity( key, - IPoolManager.ModifyPositionParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100}), + IPoolManager.ModifyLiquidityParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100}), ZERO_BYTES ); } diff --git a/test/GeomeanOracle.t.sol b/test/GeomeanOracle.t.sol index bd0e0c05..ec74affc 100644 --- a/test/GeomeanOracle.t.sol +++ b/test/GeomeanOracle.t.sol @@ -3,50 +3,44 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {GetSender} from "./shared/GetSender.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {GeomeanOracle} from "../contracts/hooks/examples/GeomeanOracle.sol"; import {GeomeanOracleImplementation} from "./shared/implementation/GeomeanOracleImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; -import {TokenFixture} from "@uniswap/v4-core/test/foundry-tests/utils/TokenFixture.sol"; -import {TestERC20} from "@uniswap/v4-core/contracts/test/TestERC20.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {PoolModifyPositionTest} from "@uniswap/v4-core/contracts/test/PoolModifyPositionTest.sol"; -import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Oracle} from "../contracts/libraries/Oracle.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -contract TestGeomeanOracle is Test, Deployers, TokenFixture { +contract TestGeomeanOracle is Test, Deployers { using PoolIdLibrary for PoolKey; int24 constant MAX_TICK_SPACING = 32767; - uint160 constant SQRT_RATIO_2_1 = 112045541949572279837463876454; TestERC20 token0; TestERC20 token1; - PoolManager manager; GeomeanOracleImplementation geomeanOracle = GeomeanOracleImplementation( address( uint160( - Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG - | Hooks.BEFORE_SWAP_FLAG + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG ) ) ); - PoolKey key; PoolId id; - PoolModifyPositionTest modifyPositionRouter; - function setUp() public { - initializeTokens(); + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + token0 = TestERC20(Currency.unwrap(currency0)); token1 = TestERC20(Currency.unwrap(currency1)); - manager = new PoolManager(500000); - vm.record(); GeomeanOracleImplementation impl = new GeomeanOracleImplementation(manager, geomeanOracle); (, bytes32[] memory writes) = vm.accesses(address(impl)); @@ -62,21 +56,21 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { key = PoolKey(currency0, currency1, 0, MAX_TICK_SPACING, geomeanOracle); id = key.toId(); - modifyPositionRouter = new PoolModifyPositionTest(manager); + modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); token0.approve(address(geomeanOracle), type(uint256).max); token1.approve(address(geomeanOracle), type(uint256).max); - token0.approve(address(modifyPositionRouter), type(uint256).max); - token1.approve(address(modifyPositionRouter), type(uint256).max); + token0.approve(address(modifyLiquidityRouter), type(uint256).max); + token1.approve(address(modifyLiquidityRouter), type(uint256).max); } function testBeforeInitializeAllowsPoolCreation() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); } function testBeforeInitializeRevertsIfFee() public { vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); - manager.initialize( + initializeRouter.initialize( PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 1, MAX_TICK_SPACING, geomeanOracle), SQRT_RATIO_1_1, ZERO_BYTES @@ -85,7 +79,7 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { function testBeforeInitializeRevertsIfNotMaxTickSpacing() public { vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); - manager.initialize( + initializeRouter.initialize( PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 0, 60, geomeanOracle), SQRT_RATIO_1_1, ZERO_BYTES @@ -93,7 +87,7 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { } function testAfterInitializeState() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); assertEq(observationState.index, 0); assertEq(observationState.cardinality, 1); @@ -101,7 +95,7 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { } function testAfterInitializeObservation() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); assertTrue(observation.initialized); assertEq(observation.blockTimestamp, 1); @@ -110,7 +104,7 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { } function testAfterInitializeObserve0() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); uint32[] memory secondsAgo = new uint32[](1); secondsAgo[0] = 0; (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = @@ -122,10 +116,10 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { } function testBeforeModifyPositionNoObservations() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); - modifyPositionRouter.modifyPosition( + initializeRouter.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity( key, - IPoolManager.ModifyPositionParams( + IPoolManager.ModifyLiquidityParams( TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 ), ZERO_BYTES @@ -144,11 +138,11 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { } function testBeforeModifyPositionObservation() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds - modifyPositionRouter.modifyPosition( + modifyLiquidityRouter.modifyLiquidity( key, - IPoolManager.ModifyPositionParams( + IPoolManager.ModifyLiquidityParams( TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 ), ZERO_BYTES @@ -167,7 +161,7 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { } function testBeforeModifyPositionObservationAndCardinality() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + initializeRouter.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds geomeanOracle.increaseCardinalityNext(key, 2); GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); @@ -175,9 +169,9 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { assertEq(observationState.cardinality, 1); assertEq(observationState.cardinalityNext, 2); - modifyPositionRouter.modifyPosition( + modifyLiquidityRouter.modifyLiquidity( key, - IPoolManager.ModifyPositionParams( + IPoolManager.ModifyLiquidityParams( TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 ), ZERO_BYTES @@ -203,4 +197,25 @@ contract TestGeomeanOracle is Test, Deployers, TokenFixture { assertEq(observation.tickCumulative, 13862); assertEq(observation.secondsPerLiquidityCumulativeX128, 680564733841876926926749214863536422912); } + + function testPermanentLiquidity() public { + initializeRouter.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + geomeanOracle.setTime(3); // advance 2 seconds + modifyLiquidityRouter.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams( + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + ), + ZERO_BYTES + ); + + vm.expectRevert(GeomeanOracle.OraclePoolMustLockLiquidity.selector); + modifyLiquidityRouter.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams( + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), -1000 + ), + ZERO_BYTES + ); + } } diff --git a/test/LimitOrder.t.sol b/test/LimitOrder.t.sol index 27613654..94cca602 100644 --- a/test/LimitOrder.t.sol +++ b/test/LimitOrder.t.sol @@ -3,41 +3,38 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {GetSender} from "./shared/GetSender.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {LimitOrder, Epoch, EpochLibrary} from "../contracts/hooks/examples/LimitOrder.sol"; import {LimitOrderImplementation} from "./shared/implementation/LimitOrderImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; -import {TokenFixture} from "@uniswap/v4-core/test/foundry-tests/utils/TokenFixture.sol"; -import {TestERC20} from "@uniswap/v4-core/contracts/test/TestERC20.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/contracts/test/PoolSwapTest.sol"; -import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; - -contract TestLimitOrder is Test, Deployers, TokenFixture { +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; + +contract TestLimitOrder is Test, Deployers { using PoolIdLibrary for PoolKey; uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + HookEnabledSwapRouter router; TestERC20 token0; TestERC20 token1; - PoolManager manager; LimitOrder limitOrder = LimitOrder(address(uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.AFTER_SWAP_FLAG))); - PoolKey key; PoolId id; - PoolSwapTest swapRouter; - function setUp() public { - initializeTokens(); + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); token0 = TestERC20(Currency.unwrap(currency0)); token1 = TestERC20(Currency.unwrap(currency1)); - manager = new PoolManager(500000); - vm.record(); LimitOrderImplementation impl = new LimitOrderImplementation(manager, limitOrder); (, bytes32[] memory writes) = vm.accesses(address(impl)); @@ -50,16 +47,13 @@ contract TestLimitOrder is Test, Deployers, TokenFixture { } } - key = PoolKey(currency0, currency1, 3000, 60, limitOrder); - id = key.toId(); - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - - swapRouter = new PoolSwapTest(manager); + // key = PoolKey(currency0, currency1, 3000, 60, limitOrder); + (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_RATIO_1_1, ZERO_BYTES); token0.approve(address(limitOrder), type(uint256).max); token1.approve(address(limitOrder), type(uint256).max); - token0.approve(address(swapRouter), type(uint256).max); - token1.approve(address(swapRouter), type(uint256).max); + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); } function testGetTickLowerLast() public { @@ -69,7 +63,7 @@ contract TestLimitOrder is Test, Deployers, TokenFixture { function testGetTickLowerLastWithDifferentPrice() public { PoolKey memory differentKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 61, limitOrder); - manager.initialize(differentKey, SQRT_RATIO_10_1, ZERO_BYTES); + initializeRouter.initialize(differentKey, SQRT_RATIO_10_1, ZERO_BYTES); assertEq(limitOrder.getTickLowerLast(differentKey.toId()), 22997); } @@ -107,10 +101,10 @@ contract TestLimitOrder is Test, Deployers, TokenFixture { function testZeroForOneInRangeRevert() public { // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei - swapRouter.swap( + router.swap( key, - IPoolManager.SwapParams(false, 1, SQRT_RATIO_1_1 + 1), - PoolSwapTest.TestSettings(true, true), + IPoolManager.SwapParams(false, 1 ether, SQRT_RATIO_1_1 + 1), + HookEnabledSwapRouter.TestSettings(true, true), ZERO_BYTES ); vm.expectRevert(LimitOrder.InRange.selector); @@ -133,8 +127,11 @@ contract TestLimitOrder is Test, Deployers, TokenFixture { function testNotZeroForOneInRangeRevert() public { // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei - swapRouter.swap( - key, IPoolManager.SwapParams(true, 1, SQRT_RATIO_1_1 - 1), PoolSwapTest.TestSettings(true, true), ZERO_BYTES + router.swap( + key, + IPoolManager.SwapParams(true, 1 ether, SQRT_RATIO_1_1 - 1), + HookEnabledSwapRouter.TestSettings(true, true), + ZERO_BYTES ); vm.expectRevert(LimitOrder.InRange.selector); limitOrder.place(key, -60, false, 1000000); @@ -192,15 +189,15 @@ contract TestLimitOrder is Test, Deployers, TokenFixture { uint128 liquidity = 1000000; limitOrder.place(key, tickLower, zeroForOne, liquidity); - swapRouter.swap( + router.swap( key, IPoolManager.SwapParams(false, 1e18, TickMath.getSqrtRatioAtTick(60)), - PoolSwapTest.TestSettings(true, true), + HookEnabledSwapRouter.TestSettings(true, true), ZERO_BYTES ); assertEq(limitOrder.getTickLowerLast(id), 60); - (, int24 tick,,) = manager.getSlot0(id); + (, int24 tick,) = manager.getSlot0(id); assertEq(tick, 60); (bool filled,,, uint256 token0Total, uint256 token1Total,) = limitOrder.epochInfos(Epoch.wrap(1)); diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol new file mode 100644 index 00000000..87de52d5 --- /dev/null +++ b/test/Quoter.t.sol @@ -0,0 +1,666 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {PathKey} from "../contracts/libraries/PathKey.sol"; +import {IQuoter} from "../contracts/interfaces/IQuoter.sol"; +import {Quoter} from "../contracts/lens/Quoter.sol"; +import {LiquidityAmounts} from "../contracts/libraries/LiquidityAmounts.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; + +contract QuoterTest is Test, Deployers { + using SafeCast for *; + using PoolIdLibrary for PoolKey; + + // Min tick for full range with tick spacing of 60 + int24 internal constant MIN_TICK = -887220; + // Max tick for full range with tick spacing of 60 + int24 internal constant MAX_TICK = -MIN_TICK; + + uint160 internal constant SQRT_RATIO_100_102 = 78447570448055484695608110440; + uint160 internal constant SQRT_RATIO_102_100 = 80016521857016594389520272648; + + uint256 internal constant CONTROLLER_GAS_LIMIT = 500000; + + Quoter quoter; + + PoolModifyLiquidityTest positionManager; + + MockERC20 token0; + MockERC20 token1; + MockERC20 token2; + + PoolKey key01; + PoolKey key02; + PoolKey key12; + + MockERC20[] tokenPath; + + function setUp() public { + deployFreshManagerAndRouters(); + quoter = new Quoter(address(manager)); + positionManager = new PoolModifyLiquidityTest(manager); + + // salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2) + token0 = new MockERC20("Test0", "0", 18); + vm.etch(address(0x1111), address(token0).code); + token0 = MockERC20(address(0x1111)); + token0.mint(address(this), 2 ** 128); + + vm.etch(address(0x2222), address(token0).code); + token1 = MockERC20(address(0x2222)); + token1.mint(address(this), 2 ** 128); + + vm.etch(address(0x3333), address(token0).code); + token2 = MockERC20(address(0x3333)); + token2.mint(address(this), 2 ** 128); + + key01 = createPoolKey(token0, token1, address(0)); + key02 = createPoolKey(token0, token2, address(0)); + key12 = createPoolKey(token1, token2, address(0)); + setupPool(key01); + setupPool(key12); + setupPoolMultiplePositions(key02); + } + + function testQuoter_quoteExactInputSingle_ZeroForOne_MultiplePositions() public { + uint256 amountIn = 10000; + uint256 expectedAmountOut = 9871; + uint160 expectedSqrtPriceX96After = 78461846509168490764501028180; + + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactInputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key02, + zeroForOne: true, + recipient: address(this), + exactAmount: uint128(amountIn), + sqrtPriceLimitX96: 0, + hookData: ZERO_BYTES + }) + ); + + assertEq(uint128(-deltaAmounts[1]), expectedAmountOut); + assertEq(sqrtPriceX96After, expectedSqrtPriceX96After); + assertEq(initializedTicksLoaded, 2); + } + + function testQuoter_quoteExactInputSingle_OneForZero_MultiplePositions() public { + uint256 amountIn = 10000; + uint256 expectedAmountOut = 9871; + uint160 expectedSqrtPriceX96After = 80001962924147897865541384515; + + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactInputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key02, + zeroForOne: false, + recipient: address(this), + exactAmount: uint128(amountIn), + sqrtPriceLimitX96: 0, + hookData: ZERO_BYTES + }) + ); + + assertEq(uint128(-deltaAmounts[0]), expectedAmountOut); + assertEq(sqrtPriceX96After, expectedSqrtPriceX96After); + assertEq(initializedTicksLoaded, 2); + } + + // nested self-call into lockAcquired reverts + function testQuoter_callLockAcquired_reverts() public { + vm.expectRevert(IQuoter.InvalidLockAcquiredSender.selector); + vm.prank(address(manager)); + quoter.lockAcquired(address(quoter), abi.encodeWithSelector(quoter.lockAcquired.selector, address(this), "0x")); + } + + function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 9871); + assertEq(sqrtPriceX96AfterList[0], 78461846509168490764501028180); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactInput_0to2_2TicksLoaded_initialiedAfter() public { + tokenPath.push(token0); + tokenPath.push(token2); + + // The swap amount is set such that the active tick after the swap is -120. + // -120 is an initialized tick for this pool. We check that we don't count it. + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6200); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 6143); + assertEq(sqrtPriceX96AfterList[0], 78757224507315167622282810783); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactInput_0to2_1TickLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + + // The swap amount is set such that the active tick after the swap is -60. + // -60 is an initialized tick for this pool. We check that we don't count it. + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 4000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 3971); + assertEq(sqrtPriceX96AfterList[0], 78926452400586371254602774705); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public { + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 8); + assertEq(sqrtPriceX96AfterList[0], 79227483487511329217250071027); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactInput_0to2_0TickLoaded_startingInitialized() public { + setupPoolWithZeroTickInitialized(key02); + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 8); + assertEq(sqrtPriceX96AfterList[0], 79227817515327498931091950511); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactInput_2to0_2TicksLoaded() public { + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 9871); + assertEq(sqrtPriceX96AfterList[0], 80001962924147897865541384515); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactInput_2to0_2TicksLoaded_initialiedAfter() public { + tokenPath.push(token2); + tokenPath.push(token0); + + // The swap amount is set such that the active tick after the swap is 120. + // 120 is an initialized tick for this pool. We check that we don't count it. + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6250); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 6190); + assertEq(sqrtPriceX96AfterList[0], 79705728824507063507279123685); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactInput_2to0_0TickLoaded_startingInitialized() public { + setupPoolWithZeroTickInitialized(key02); + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 200); + + // Tick 0 initialized. Tick after = 1 + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 198); + assertEq(sqrtPriceX96AfterList[0], 79235729830182478001034429156); + assertEq(initializedTicksLoadedList[0], 0); + } + + // 2->0 starting not initialized + function testQuoter_quoteExactInput_2to0_0TickLoaded_startingNotInitialized() public { + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 101); + assertEq(sqrtPriceX96AfterList[0], 79235858216754624215638319723); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactInput_2to1() public { + tokenPath.push(token2); + tokenPath.push(token1); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + assertEq(-deltaAmounts[1], 9871); + assertEq(sqrtPriceX96AfterList[0], 80018067294531553039351583520); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactInput_0to2to1() public { + tokenPath.push(token0); + tokenPath.push(token2); + tokenPath.push(token1); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[2], 9745); + assertEq(sqrtPriceX96AfterList[0], 78461846509168490764501028180); + assertEq(sqrtPriceX96AfterList[1], 80007846861567212939802016351); + assertEq(initializedTicksLoadedList[0], 2); + assertEq(initializedTicksLoadedList[1], 0); + } + + function testQuoter_quoteExactOutputSingle_0to1() public { + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactOutputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key01, + zeroForOne: true, + recipient: address(this), + exactAmount: type(uint128).max, + sqrtPriceLimitX96: SQRT_RATIO_100_102, + hookData: ZERO_BYTES + }) + ); + + assertEq(deltaAmounts[0], 9981); + assertEq(sqrtPriceX96After, SQRT_RATIO_100_102); + assertEq(initializedTicksLoaded, 0); + } + + function testQuoter_quoteExactOutputSingle_1to0() public { + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactOutputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key01, + zeroForOne: false, + recipient: address(this), + exactAmount: type(uint128).max, + sqrtPriceLimitX96: SQRT_RATIO_102_100, + hookData: ZERO_BYTES + }) + ); + + assertEq(deltaAmounts[1], 9981); + assertEq(sqrtPriceX96After, SQRT_RATIO_102_100); + assertEq(initializedTicksLoaded, 0); + } + + function testQuoter_quoteExactOutput_0to2_2TicksLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 15273); + assertEq(sqrtPriceX96AfterList[0], 78055527257643669242286029831); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactOutput_0to2_1TickLoaded_initialiedAfter() public { + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 6200); + assertEq(sqrtPriceX96AfterList[0], 78757225449310403327341205211); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_0to2_1TickLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 4029); + assertEq(sqrtPriceX96AfterList[0], 78924219757724709840818372098); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingInitialized() public { + setupPoolWithZeroTickInitialized(key02); + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 100); + + // Tick 0 initialized. Tick after = 1 + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 102); + assertEq(sqrtPriceX96AfterList[0], 79224329176051641448521403903); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingNotInitialized() public { + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 12); + assertEq(sqrtPriceX96AfterList[0], 79227408033628034983534698435); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactOutput_2to0_2TicksLoaded() public { + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 15273); + assertEq(sqrtPriceX96AfterList[0], 80418414376567919517220409857); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactOutput_2to0_2TicksLoaded_initialiedAfter() public { + tokenPath.push(token2); + tokenPath.push(token0); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 6283); + assertEq(sqrtPriceX96AfterList[0], 79708304437530892332449657932); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactOutput_2to0_1TickLoaded() public { + tokenPath.push(token2); + tokenPath.push(token0); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000); + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 6055); + assertEq(sqrtPriceX96AfterList[0], 79690640184021170956740081887); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_2to1() public { + tokenPath.push(token2); + tokenPath.push(token1); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 10000); + assertEq(sqrtPriceX96AfterList[0], 80018020393569259756601362385); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactOutput_0to2to1() public { + tokenPath.push(token0); + tokenPath.push(token2); + tokenPath.push(token1); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 10000); + assertEq(deltaAmounts[1], 0); + assertEq(deltaAmounts[2], -9745); + assertEq(sqrtPriceX96AfterList[0], 78461888503179331029803316753); + assertEq(sqrtPriceX96AfterList[1], 80007838904387594703933785072); + assertEq(initializedTicksLoadedList.length, 2); + assertEq(initializedTicksLoadedList[0], 2); + assertEq(initializedTicksLoadedList[1], 0); + } + + function createPoolKey(MockERC20 tokenA, MockERC20 tokenB, address hookAddr) + internal + pure + returns (PoolKey memory) + { + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey(Currency.wrap(address(tokenA)), Currency.wrap(address(tokenB)), 3000, 60, IHooks(hookAddr)); + } + + function setupPool(PoolKey memory poolKey) internal { + initializeRouter.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); + MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); + positionManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + ), + ZERO_BYTES + ); + } + + function setupPoolMultiplePositions(PoolKey memory poolKey) internal { + initializeRouter.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); + MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); + positionManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams( + -60, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -60, 60, 100, 100).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams( + -120, 120, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 120, 100, 100).toInt256() + ), + ZERO_BYTES + ); + } + + function setupPoolWithZeroTickInitialized(PoolKey memory poolKey) internal { + PoolId poolId = poolKey.toId(); + (uint160 sqrtPriceX96,,) = manager.getSlot0(poolId); + if (sqrtPriceX96 == 0) { + initializeRouter.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + } + + MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); + MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); + positionManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams( + 0, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, 0, 60, 100, 100).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams( + -120, 0, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 0, 100, 100).toInt256() + ), + ZERO_BYTES + ); + } + + function calculateLiquidityFromAmounts( + uint160 sqrtRatioX96, + int24 tickLower, + int24 tickUpper, + uint256 amount0, + uint256 amount1 + ) internal pure returns (uint128 liquidity) { + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); + liquidity = + LiquidityAmounts.getLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1); + } + + function getExactInputParams(MockERC20[] memory _tokenPath, uint256 amountIn) + internal + view + returns (IQuoter.QuoteExactParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = 0; i < _tokenPath.length - 1; i++) { + path[i] = PathKey(Currency.wrap(address(_tokenPath[i + 1])), 3000, 60, IHooks(address(0)), bytes("")); + } + + params.exactCurrency = Currency.wrap(address(_tokenPath[0])); + params.path = path; + params.recipient = address(this); + params.exactAmount = uint128(amountIn); + } + + function getExactOutputParams(MockERC20[] memory _tokenPath, uint256 amountOut) + internal + view + returns (IQuoter.QuoteExactParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = _tokenPath.length - 1; i > 0; i--) { + path[i - 1] = PathKey(Currency.wrap(address(_tokenPath[i - 1])), 3000, 60, IHooks(address(0)), bytes("")); + } + + params.exactCurrency = Currency.wrap(address(_tokenPath[_tokenPath.length - 1])); + params.path = path; + params.recipient = address(this); + params.exactAmount = uint128(amountOut); + } +} diff --git a/test/SimpleBatchCallTest.t.sol b/test/SimpleBatchCallTest.t.sol index 8792ab08..c4cadc43 100644 --- a/test/SimpleBatchCallTest.t.sol +++ b/test/SimpleBatchCallTest.t.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.19; import {SimpleBatchCall} from "../contracts/SimpleBatchCall.sol"; import {ICallsWithLock} from "../contracts/interfaces/ICallsWithLock.sol"; -import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; -import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; -import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Pool} from "@uniswap/v4-core/contracts/libraries/Pool.sol"; -import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Test} from "forge-std/Test.sol"; @@ -21,18 +21,14 @@ contract SimpleBatchCallTest is Test, Deployers { using PoolIdLibrary for PoolKey; SimpleBatchCall batchCall; - Currency currency0; - Currency currency1; - PoolKey key; - IPoolManager poolManager; function setUp() public { - poolManager = createFreshManager(); - (currency0, currency1) = deployCurrencies(2 ** 255); + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); key = PoolKey({currency0: currency0, currency1: currency1, fee: 3000, tickSpacing: 60, hooks: IHooks(address(0))}); - batchCall = new SimpleBatchCall(poolManager); + batchCall = new SimpleBatchCall(manager); ERC20(Currency.unwrap(currency0)).approve(address(batchCall), 2 ** 255); ERC20(Currency.unwrap(currency1)).approve(address(batchCall), 2 ** 255); } @@ -44,7 +40,7 @@ contract SimpleBatchCallTest is Test, Deployers { abi.encode(SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true})); batchCall.execute(abi.encode(calls), ZERO_BYTES); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId()); + (uint160 sqrtPriceX96,,) = manager.getSlot0(key.toId()); assertEq(sqrtPriceX96, SQRT_RATIO_1_1); } @@ -54,7 +50,7 @@ contract SimpleBatchCallTest is Test, Deployers { calls[1] = abi.encodeWithSelector( ICallsWithLock.modifyPositionWithLock.selector, key, - IPoolManager.ModifyPositionParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18}), + IPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18}), ZERO_BYTES ); Currency[] memory currenciesTouched = new Currency[](2); @@ -63,13 +59,13 @@ contract SimpleBatchCallTest is Test, Deployers { bytes memory settleData = abi.encode( currenciesTouched, SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true}) ); - uint256 balance0 = ERC20(Currency.unwrap(currency0)).balanceOf(address(poolManager)); - uint256 balance1 = ERC20(Currency.unwrap(currency1)).balanceOf(address(poolManager)); + uint256 balance0 = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); + uint256 balance1 = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); batchCall.execute(abi.encode(calls), settleData); - uint256 balance0After = ERC20(Currency.unwrap(currency0)).balanceOf(address(poolManager)); - uint256 balance1After = ERC20(Currency.unwrap(currency1)).balanceOf(address(poolManager)); + uint256 balance0After = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); + uint256 balance1After = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId()); + (uint160 sqrtPriceX96,,) = manager.getSlot0(key.toId()); assertGt(balance0After, balance0); assertGt(balance1After, balance1); diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol index 84ed9716..fdcf81d2 100644 --- a/test/TWAMM.t.sol +++ b/test/TWAMM.t.sol @@ -3,26 +3,25 @@ pragma solidity ^0.8.15; import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {MockERC20} from "@uniswap/v4-core/test/foundry-tests/utils/MockERC20.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; import {TWAMMImplementation} from "./shared/implementation/TWAMMImplementation.sol"; -import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; -import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {PoolModifyPositionTest} from "@uniswap/v4-core/contracts/test/PoolModifyPositionTest.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/contracts/test/PoolSwapTest.sol"; -import {PoolDonateTest} from "@uniswap/v4-core/contracts/test/PoolDonateTest.sol"; -import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; -import {TokenFixture} from "@uniswap/v4-core/test/foundry-tests/utils/TokenFixture.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {PoolDonateTest} from "@uniswap/v4-core/src/test/PoolDonateTest.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {TWAMM} from "../contracts/hooks/examples/TWAMM.sol"; import {ITWAMM} from "../contracts/interfaces/ITWAMM.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { +contract TWAMMTest is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; @@ -44,15 +43,8 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { uint256 earningsFactorLast ); - // address constant TWAMMAddr = address(uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG)); - TWAMM twamm = TWAMM( - address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG)) - ); - // TWAMM twamm; - PoolManager manager; - PoolModifyPositionTest modifyPositionRouter; - PoolSwapTest swapRouter; - PoolDonateTest donateRouter; + TWAMM twamm = + TWAMM(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG))); address hookAddress; MockERC20 token0; MockERC20 token1; @@ -60,10 +52,11 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { PoolId poolId; function setUp() public { - initializeTokens(); + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + token0 = MockERC20(Currency.unwrap(currency0)); token1 = MockERC20(Currency.unwrap(currency1)); - manager = new PoolManager(500000); TWAMMImplementation impl = new TWAMMImplementation(manager, 10_000, twamm); (, bytes32[] memory writes) = vm.accesses(address(impl)); @@ -76,22 +69,21 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { } } - modifyPositionRouter = new PoolModifyPositionTest(IPoolManager(address(manager))); - swapRouter = new PoolSwapTest(IPoolManager(address(manager))); - - poolKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, twamm); - poolId = poolKey.toId(); - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_RATIO_1_1, ZERO_BYTES); - token0.approve(address(modifyPositionRouter), 100 ether); - token1.approve(address(modifyPositionRouter), 100 ether); + token0.approve(address(modifyLiquidityRouter), 100 ether); + token1.approve(address(modifyLiquidityRouter), 100 ether); token0.mint(address(this), 100 ether); token1.mint(address(this), 100 ether); - modifyPositionRouter.modifyPosition(poolKey, IPoolManager.ModifyPositionParams(-60, 60, 10 ether), ZERO_BYTES); - modifyPositionRouter.modifyPosition(poolKey, IPoolManager.ModifyPositionParams(-120, 120, 10 ether), ZERO_BYTES); - modifyPositionRouter.modifyPosition( + modifyLiquidityRouter.modifyLiquidity( + poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES + ); + modifyLiquidityRouter.modifyLiquidity( + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + ); + modifyLiquidityRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyPositionParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), ZERO_BYTES ); } @@ -100,7 +92,8 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { (PoolKey memory initKey, PoolId initId) = newPoolKeyWithTWAMM(twamm); assertEq(twamm.lastVirtualOrderTimestamp(initId), 0); vm.warp(10000); - manager.initialize(initKey, SQRT_RATIO_1_1, ZERO_BYTES); + + initializeRouter.initialize(initKey, SQRT_RATIO_1_1, ZERO_BYTES); assertEq(twamm.lastVirtualOrderTimestamp(initId), 10000); } @@ -242,7 +235,7 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, originalSellRate * 80 / 100); + assertEq(updatedSellRate, (originalSellRate * 80) / 100); assertEq(token0Owed, uint256(-amountDelta)); assertEq(token1Owed, orderAmount / 2); } @@ -267,7 +260,7 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, originalSellRate * 80 / 100); + assertEq(updatedSellRate, (originalSellRate * 80) / 100); assertEq(token0Owed, orderAmount / 2); assertEq(token1Owed, uint256(-amountDelta)); } @@ -369,8 +362,8 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { token0.approve(address(twamm), 100e18); token1.approve(address(twamm), 100e18); - modifyPositionRouter.modifyPosition( - poolKey, IPoolManager.ModifyPositionParams(-2400, 2400, 10 ether), ZERO_BYTES + modifyLiquidityRouter.modifyLiquidity( + poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether), ZERO_BYTES ); vm.warp(10000); @@ -416,8 +409,8 @@ contract TWAMMTest is Test, Deployers, TokenFixture, GasSnapshot { } function newPoolKeyWithTWAMM(IHooks hooks) public returns (PoolKey memory, PoolId) { - MockERC20[] memory tokens = deployTokens(2, 2 ** 255); - PoolKey memory key = PoolKey(Currency.wrap(address(tokens[0])), Currency.wrap(address(tokens[1])), 0, 60, hooks); + (Currency _token0, Currency _token1) = deployMintAndApprove2Currencies(); + PoolKey memory key = PoolKey(_token0, _token1, 0, 60, hooks); return (key, key.toId()); } diff --git a/test/shared/implementation/FullRangeImplementation.sol b/test/shared/implementation/FullRangeImplementation.sol index fcd8ae3f..2d4ce3cc 100644 --- a/test/shared/implementation/FullRangeImplementation.sol +++ b/test/shared/implementation/FullRangeImplementation.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; import {FullRange} from "../../../contracts/hooks/examples/FullRange.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; contract FullRangeImplementation is FullRange { constructor(IPoolManager _poolManager, FullRange addressToEtch) FullRange(_poolManager) { - Hooks.validateHookAddress(addressToEtch, getHooksCalls()); + Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } // make this a no-op in testing diff --git a/test/shared/implementation/GeomeanOracleImplementation.sol b/test/shared/implementation/GeomeanOracleImplementation.sol index 06a95fa2..b953a3b6 100644 --- a/test/shared/implementation/GeomeanOracleImplementation.sol +++ b/test/shared/implementation/GeomeanOracleImplementation.sol @@ -3,14 +3,14 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; import {GeomeanOracle} from "../../../contracts/hooks/examples/GeomeanOracle.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; contract GeomeanOracleImplementation is GeomeanOracle { uint32 public time; constructor(IPoolManager _poolManager, GeomeanOracle addressToEtch) GeomeanOracle(_poolManager) { - Hooks.validateHookAddress(addressToEtch, getHooksCalls()); + Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } // make this a no-op in testing diff --git a/test/shared/implementation/LimitOrderImplementation.sol b/test/shared/implementation/LimitOrderImplementation.sol index 340cfc42..11625771 100644 --- a/test/shared/implementation/LimitOrderImplementation.sol +++ b/test/shared/implementation/LimitOrderImplementation.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; import {LimitOrder} from "../../../contracts/hooks/examples/LimitOrder.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; contract LimitOrderImplementation is LimitOrder { constructor(IPoolManager _poolManager, LimitOrder addressToEtch) LimitOrder(_poolManager) { - Hooks.validateHookAddress(addressToEtch, getHooksCalls()); + Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } // make this a no-op in testing diff --git a/test/shared/implementation/TWAMMImplementation.sol b/test/shared/implementation/TWAMMImplementation.sol index 012ca541..f217db8c 100644 --- a/test/shared/implementation/TWAMMImplementation.sol +++ b/test/shared/implementation/TWAMMImplementation.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; import {TWAMM} from "../../../contracts/hooks/examples/TWAMM.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; contract TWAMMImplementation is TWAMM { constructor(IPoolManager poolManager, uint256 interval, TWAMM addressToEtch) TWAMM(poolManager, interval) { - Hooks.validateHookAddress(addressToEtch, getHooksCalls()); + Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } // make this a no-op in testing diff --git a/test/utils/HookEnabledSwapRouter.sol b/test/utils/HookEnabledSwapRouter.sol new file mode 100644 index 00000000..54832b4a --- /dev/null +++ b/test/utils/HookEnabledSwapRouter.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolTestBase} from "@uniswap/v4-core/src/test/PoolTestBase.sol"; +import {Test} from "forge-std/Test.sol"; + +contract HookEnabledSwapRouter is PoolTestBase { + using CurrencyLibrary for Currency; + + error NoSwapOccurred(); + + constructor(IPoolManager _manager) PoolTestBase(_manager) {} + + struct CallbackData { + address sender; + TestSettings testSettings; + PoolKey key; + IPoolManager.SwapParams params; + bytes hookData; + } + + struct TestSettings { + bool withdrawTokens; + bool settleUsingTransfer; + } + + function swap( + PoolKey memory key, + IPoolManager.SwapParams memory params, + TestSettings memory testSettings, + bytes memory hookData + ) external payable returns (BalanceDelta delta) { + delta = abi.decode( + manager.lock(address(this), abi.encode(CallbackData(msg.sender, testSettings, key, params, hookData))), + (BalanceDelta) + ); + + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); + } + + function lockAcquired(address, /*sender*/ bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + + CallbackData memory data = abi.decode(rawData, (CallbackData)); + + BalanceDelta delta = manager.swap(data.key, data.params, data.hookData); + + // Make sure youve added liquidity to the test pool! + if (BalanceDelta.unwrap(delta) == 0) revert NoSwapOccurred(); + + if (data.params.zeroForOne) { + _settle(data.key.currency0, data.sender, delta.amount0(), data.testSettings.settleUsingTransfer); + if (delta.amount1() < 0) { + _take(data.key.currency1, data.sender, delta.amount1(), data.testSettings.withdrawTokens); + } + } else { + _settle(data.key.currency1, data.sender, delta.amount1(), data.testSettings.settleUsingTransfer); + if (delta.amount0() < 0) { + _take(data.key.currency0, data.sender, delta.amount0(), data.testSettings.withdrawTokens); + } + } + + return abi.encode(delta); + } +} From ee27f4fa05e5560725b3f670efe8e1bd6f049c10 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sat, 2 Mar 2024 12:26:32 -0700 Subject: [PATCH 08/98] wip --- .forge-snapshots/FullRangeInitialize.snap | 2 +- contracts/BaseHook.sol | 10 +-- contracts/NonfungiblePositionManager.sol | 69 +++++++++++++++++-- contracts/SimpleBatchCall.sol | 4 +- contracts/base/BaseLiquidityManagement.sol | 28 +++++++- .../INonfungiblePositionManager.sol | 11 +-- .../NonfungiblePositionManager.t.sol | 64 +++++++++++++++++ 7 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 test/position-managers/NonfungiblePositionManager.t.sol diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 0362b78a..e470ab62 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -879546 \ No newline at end of file +880149 \ No newline at end of file diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 3e135dd5..b2a36e1a 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {SafeCallback} from "./base/SafeCallback.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index f2572961..b5d330e5 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -5,19 +5,74 @@ import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {LiquidityPosition} from "./types/LiquidityPositionId.sol"; contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { + /// @dev The ID of the next token that will be minted. Skips 0 + uint256 private _nextId = 1; + constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} + // details about the uniswap position + struct Position { + // the nonce for permits + uint96 nonce; + // the address that is approved for spending this token + address operator; + LiquidityPosition position; + // the liquidity of the position + // NOTE: this value will be less than BaseLiquidityManagement.liquidityOf, if the user + // owns multiple positions with the same range + uint128 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } + + mapping(uint256 tokenId => Position position) public positions; + // NOTE: more gas efficient as LiquidityAmounts is used offchain - function mint(LiquidityPosition memory position, uint256 liquidity, uint256 deadline) - external - payable - returns (uint256 tokenId) - {} + // TODO: deadline check + function mint( + LiquidityPosition memory position, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes calldata hookData + ) external payable returns (uint256 tokenId) { + BaseLiquidityManagement.modifyLiquidity( + position.key, + IPoolManager.ModifyPositionParams({ + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidityDelta: int256(liquidity) + }), + hookData, + recipient + ); + + // mint receipt token + // GAS: uncheck this mf + _mint(recipient, (tokenId = _nextId++)); + + positions[tokenId] = Position({ + nonce: 0, + operator: address(0), + position: position, + liquidity: uint128(liquidity), + feeGrowthInside0LastX128: 0, // TODO: + feeGrowthInside1LastX128: 0, // TODO: + tokensOwed0: 0, + tokensOwed1: 0 + }); + + // TODO: event + } // NOTE: more expensive since LiquidityAmounts is used onchain function mint( diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol index 0c7a64db..b6fe4df7 100644 --- a/contracts/SimpleBatchCall.sol +++ b/contracts/SimpleBatchCall.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.19; import {LockAndBatchCall} from "./base/LockAndBatchCall.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /// @title SimpleBatchCall diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index ef75e349..6ca89320 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -4,13 +4,16 @@ pragma solidity ^0.8.24; import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; import {LiquidityPosition, LiquidityPositionId, LiquidityPositionIdLibrary} from "../types/LiquidityPositionId.sol"; import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagement { using LiquidityPositionIdLibrary for LiquidityPosition; + using CurrencyLibrary for Currency; struct CallbackData { address sender; @@ -29,7 +32,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem IPoolManager.ModifyPositionParams memory params, bytes calldata hookData, address owner - ) external payable override returns (BalanceDelta delta) { + ) public payable override returns (BalanceDelta delta) { // if removing liquidity, check that the owner is the sender? if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); @@ -52,8 +55,27 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory result) { CallbackData memory data = abi.decode(rawData, (CallbackData)); - result = abi.encode(poolManager.modifyPosition(data.key, data.params, data.hookData)); + BalanceDelta delta = poolManager.modifyPosition(data.key, data.params, data.hookData); - // TODO: pay balances + if (data.params.liquidityDelta <= 0) { + // removing liquidity/fees so take tokens + poolManager.take(data.key.currency0, data.sender, uint128(-delta.amount0())); + poolManager.take(data.key.currency1, data.sender, uint128(-delta.amount1())); + } else { + // adding liquidity so pay tokens + _settle(data.sender, data.key.currency0, uint128(delta.amount0())); + _settle(data.sender, data.key.currency1, uint128(delta.amount1())); + } + + result = abi.encode(delta); + } + + function _settle(address payer, Currency currency, uint256 amount) internal { + if (currency.isNative()) { + poolManager.settle{value: uint128(amount)}(currency); + } else { + IERC20(Currency.unwrap(currency)).transferFrom(payer, address(poolManager), uint128(amount)); + poolManager.settle(currency); + } } } diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index b3e9a2a6..d56d6733 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -7,10 +7,13 @@ import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; interface INonfungiblePositionManager is IBaseLiquidityManagement { // NOTE: more gas efficient as LiquidityAmounts is used offchain - function mint(LiquidityPosition memory position, uint256 liquidity, uint256 deadline) - external - payable - returns (uint256 tokenId); + function mint( + LiquidityPosition memory position, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes calldata hookData + ) external payable returns (uint256 tokenId); // NOTE: more expensive since LiquidityAmounts is used onchain function mint( diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol new file mode 100644 index 00000000..8ad23a68 --- /dev/null +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; +import {MockERC20} from "@uniswap/v4-core/test/foundry-tests/utils/MockERC20.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/contracts/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import { + LiquidityPosition, + LiquidityPositionId, + LiquidityPositionIdLibrary +} from "../../contracts/types/LiquidityPositionId.sol"; + +contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { + using CurrencyLibrary for Currency; + using LiquidityPositionIdLibrary for LiquidityPosition; + + NonfungiblePositionManager lpm; + Currency currency0; + Currency currency1; + PoolKey key; + PoolId poolId; + IPoolManager poolManager; + + function setUp() public { + poolManager = createFreshManager(); + (currency0, currency1) = deployCurrencies(2 ** 255); + + (key, poolId) = + createPool(PoolManager(payable(address(poolManager))), IHooks(address(0x0)), uint24(3000), SQRT_RATIO_1_1); + + lpm = new NonfungiblePositionManager(poolManager); + + MockERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + MockERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + } + + function test_mint() public { + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: -600, tickUpper: 600}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + console2.log(balance0Before); + console2.log(balance1Before); + console2.log(address(this)); + console2.log(IERC20(Currency.unwrap(currency0)).allowance(address(this), address(lpm))); + uint256 tokenId = lpm.mint(position, 1_00 ether, block.timestamp + 1, address(this), ZERO_BYTES); + assertEq(tokenId, 1); + uint256 balance0After = currency0.balanceOfSelf(); + uint256 balance1After = currency0.balanceOfSelf(); + } +} From d1c58971cffa9cea50c112b88a6cdb1f3bf6caa3 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sat, 2 Mar 2024 12:48:06 -0700 Subject: [PATCH 09/98] misc fixes with main:latest --- .forge-snapshots/FullRangeInitialize.snap | 2 +- contracts/NonfungiblePositionManager.sol | 2 +- contracts/base/BaseLiquidityManagement.sol | 19 +++++----- .../IAdvancedLiquidityManagement.sol | 4 +-- .../interfaces/IBaseLiquidityManagement.sol | 10 +++--- .../INonfungiblePositionManager.sol | 2 +- contracts/types/LiquidityPositionId.sol | 2 +- .../NonfungiblePositionManager.t.sol | 35 ++++++++----------- 8 files changed, 36 insertions(+), 40 deletions(-) diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index c7563c71..631d5a68 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1041059 +1041059 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index b5d330e5..37461ff6 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -47,7 +47,7 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi ) external payable returns (uint256 tokenId) { BaseLiquidityManagement.modifyLiquidity( position.key, - IPoolManager.ModifyPositionParams({ + IPoolManager.ModifyLiquidityParams({ tickLower: position.tickLower, tickUpper: position.tickUpper, liquidityDelta: int256(liquidity) diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 6ca89320..f9911045 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityPosition, LiquidityPositionId, LiquidityPositionIdLibrary} from "../types/LiquidityPositionId.sol"; import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {SafeCallback} from "./SafeCallback.sol"; @@ -18,7 +18,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem struct CallbackData { address sender; PoolKey key; - IPoolManager.ModifyPositionParams params; + IPoolManager.ModifyLiquidityParams params; bytes hookData; } @@ -29,15 +29,16 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem // NOTE: handles add/remove/collect function modifyLiquidity( PoolKey memory key, - IPoolManager.ModifyPositionParams memory params, + IPoolManager.ModifyLiquidityParams memory params, bytes calldata hookData, address owner ) public payable override returns (BalanceDelta delta) { // if removing liquidity, check that the owner is the sender? if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); - delta = - abi.decode(poolManager.lock(abi.encode(CallbackData(msg.sender, key, params, hookData))), (BalanceDelta)); + delta = abi.decode( + poolManager.lock(address(this), abi.encode(CallbackData(msg.sender, key, params, hookData))), (BalanceDelta) + ); params.liquidityDelta < 0 ? liquidityOf[owner][LiquidityPosition(key, params.tickLower, params.tickUpper).toId()] -= @@ -55,7 +56,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory result) { CallbackData memory data = abi.decode(rawData, (CallbackData)); - BalanceDelta delta = poolManager.modifyPosition(data.key, data.params, data.hookData); + BalanceDelta delta = poolManager.modifyLiquidity(data.key, data.params, data.hookData); if (data.params.liquidityDelta <= 0) { // removing liquidity/fees so take tokens diff --git a/contracts/interfaces/IAdvancedLiquidityManagement.sol b/contracts/interfaces/IAdvancedLiquidityManagement.sol index 58b02853..3c944641 100644 --- a/contracts/interfaces/IAdvancedLiquidityManagement.sol +++ b/contracts/interfaces/IAdvancedLiquidityManagement.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; import {LiquidityPosition} from "../types/LiquidityPositionId.sol"; diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol index 6dfdca5a..2b27f8e0 100644 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; import {LiquidityPosition, LiquidityPositionId} from "../types/LiquidityPositionId.sol"; interface IBaseLiquidityManagement is ILockCallback { @@ -14,7 +14,7 @@ interface IBaseLiquidityManagement is ILockCallback { // NOTE: handles add/remove/collect function modifyLiquidity( PoolKey memory key, - IPoolManager.ModifyPositionParams memory params, + IPoolManager.ModifyLiquidityParams memory params, bytes calldata hookData, address owner ) external payable returns (BalanceDelta delta); diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index d56d6733..9e68fc7d 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {LiquidityPosition} from "../types/LiquidityPositionId.sol"; import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; diff --git a/contracts/types/LiquidityPositionId.sol b/contracts/types/LiquidityPositionId.sol index 7b2e88a4..063db61b 100644 --- a/contracts/types/LiquidityPositionId.sol +++ b/contracts/types/LiquidityPositionId.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.24; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; // TODO: move into core? some of the mappings / pool.state seem to hash position id's struct LiquidityPosition { diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 8ad23a68..ae93f61d 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -3,15 +3,14 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; -import {MockERC20} from "@uniswap/v4-core/test/foundry-tests/utils/MockERC20.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/contracts/test/PoolSwapTest.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; @@ -28,23 +27,19 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { using LiquidityPositionIdLibrary for LiquidityPosition; NonfungiblePositionManager lpm; - Currency currency0; - Currency currency1; - PoolKey key; + PoolId poolId; - IPoolManager poolManager; function setUp() public { - poolManager = createFreshManager(); - (currency0, currency1) = deployCurrencies(2 ** 255); + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = - createPool(PoolManager(payable(address(poolManager))), IHooks(address(0x0)), uint24(3000), SQRT_RATIO_1_1); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); - lpm = new NonfungiblePositionManager(poolManager); + lpm = new NonfungiblePositionManager(manager); - MockERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - MockERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); } function test_mint() public { From 7a134a51233b7f79965b4ac2309c9e1b0e71f857 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 3 Mar 2024 13:41:50 -0700 Subject: [PATCH 10/98] basic mint --- contracts/NonfungiblePositionManager.sol | 38 ++++++--- .../INonfungiblePositionManager.sol | 26 +++--- .../NonfungiblePositionManager.t.sol | 79 +++++++++++++++++-- 3 files changed, 114 insertions(+), 29 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 37461ff6..003bfc92 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -7,10 +7,17 @@ import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {LiquidityPosition} from "./types/LiquidityPositionId.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { + using PoolIdLibrary for PoolKey; /// @dev The ID of the next token that will be minted. Skips 0 + uint256 private _nextId = 1; constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} @@ -39,13 +46,13 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi // NOTE: more gas efficient as LiquidityAmounts is used offchain // TODO: deadline check function mint( - LiquidityPosition memory position, + LiquidityPosition calldata position, uint256 liquidity, uint256 deadline, address recipient, bytes calldata hookData - ) external payable returns (uint256 tokenId) { - BaseLiquidityManagement.modifyLiquidity( + ) public payable returns (uint256 tokenId, BalanceDelta delta) { + delta = BaseLiquidityManagement.modifyLiquidity( position.key, IPoolManager.ModifyLiquidityParams({ tickLower: position.tickLower, @@ -75,15 +82,22 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi } // NOTE: more expensive since LiquidityAmounts is used onchain - function mint( - PoolKey memory key, - uint256 amount0Desired, - uint256 amount1Desired, - uint256 amount0Min, - uint256 amount1Min, - address recipient, - uint256 deadline - ) external payable returns (uint256 tokenId) {} + function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { + (uint160 sqrtPriceX96,,) = poolManager.getSlot0(params.position.key.toId()); + (tokenId, delta) = mint( + params.position, + LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtRatioAtTick(params.position.tickLower), + TickMath.getSqrtRatioAtTick(params.position.tickUpper), + params.amount0Desired, + params.amount1Desired + ), + params.deadline, + params.recipient, + params.hookData + ); + } function burn(uint256 tokenId) external {} diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 9e68fc7d..f45c2dd5 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -2,29 +2,33 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityPosition} from "../types/LiquidityPositionId.sol"; import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; interface INonfungiblePositionManager is IBaseLiquidityManagement { + struct MintParams { + LiquidityPosition position; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + address recipient; + bytes hookData; + } + // NOTE: more gas efficient as LiquidityAmounts is used offchain function mint( - LiquidityPosition memory position, + LiquidityPosition calldata position, uint256 liquidity, uint256 deadline, address recipient, bytes calldata hookData - ) external payable returns (uint256 tokenId); + ) external payable returns (uint256 tokenId, BalanceDelta delta); // NOTE: more expensive since LiquidityAmounts is used onchain - function mint( - PoolKey memory key, - uint256 amount0Desired, - uint256 amount1Desired, - uint256 amount0Min, - uint256 amount1Min, - address recipient, - uint256 deadline - ) external payable returns (uint256 tokenId); + function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); function burn(uint256 tokenId) external; diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index ae93f61d..9eccf31c 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -10,11 +10,13 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import { LiquidityPosition, @@ -29,6 +31,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { NonfungiblePositionManager lpm; PoolId poolId; + address alice = makeAddr("ALICE"); function setUp() public { Deployers.deployFreshManagerAndRouters(); @@ -42,18 +45,82 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); } - function test_mint() public { + function test_mint_withLiquidityDelta() public { LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: -600, tickUpper: 600}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - console2.log(balance0Before); - console2.log(balance1Before); - console2.log(address(this)); - console2.log(IERC20(Currency.unwrap(currency0)).allowance(address(this), address(lpm))); - uint256 tokenId = lpm.mint(position, 1_00 ether, block.timestamp + 1, address(this), ZERO_BYTES); + (uint256 tokenId, BalanceDelta delta) = + lpm.mint(position, 1_00 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 balance0After = currency0.balanceOfSelf(); + uint256 balance1After = currency0.balanceOfSelf(); + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + assertEq(balance0Before - balance0After, uint256(int256(delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(delta.amount1()))); + } + + function test_mint() public { + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: -600, tickUpper: 600}); + + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + position: position, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: address(this), + hookData: ZERO_BYTES + }); + (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency0.balanceOfSelf(); + + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + assertEq(uint256(int256(delta.amount0())), amount0Desired); + assertEq(uint256(int256(delta.amount1())), amount1Desired); + assertEq(balance0Before - balance0After, uint256(int256(delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(delta.amount1()))); } + + function test_mint_recipient() public { + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: -600, tickUpper: 600}); + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + position: position, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: alice, + hookData: ZERO_BYTES + }); + (uint256 tokenId,) = lpm.mint(params); + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(tokenId), alice); + } + + function test_mint_withLiquidityDelta_recipient() public {} + + function test_mint_slippageRevert() public {} + + function test_burn() public {} + function test_collect() public {} + function test_increaseLiquidity() public {} + function test_decreaseLiquidity() public {} + + function test_mintTransferBurn() public {} + function test_mintTransferCollect() public {} + function test_mintTransferIncrease() public {} + function test_mintTransferDecrease() public {} } From 7bd299611673e4660b3281a93953c5107e67ff35 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 4 Mar 2024 09:06:36 -0700 Subject: [PATCH 11/98] begin moving tests to fuzz --- .../NonfungiblePositionManager.t.sol | 71 +++++++++++--- test/shared/fuzz/LiquidityFuzzers.sol | 96 +++++++++++++++++++ 2 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 test/shared/fuzz/LiquidityFuzzers.sol diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 9eccf31c..2c79e476 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -13,6 +13,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; @@ -24,7 +25,9 @@ import { LiquidityPositionIdLibrary } from "../../contracts/types/LiquidityPositionId.sol"; -contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { using CurrencyLibrary for Currency; using LiquidityPositionIdLibrary for LiquidityPosition; @@ -33,6 +36,9 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { PoolId poolId; address alice = makeAddr("ALICE"); + // unused value for the fuzz helper functions + uint128 constant DEAD_VALUE = 6969.6969 ether; + function setUp() public { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); @@ -45,15 +51,46 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); } - function test_mint_withLiquidityDelta() public { - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: -600, tickUpper: 600}); + function test_mint_withLiquidityDelta(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + (tickLower, tickUpper, liquidityDelta) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); (uint256 tokenId, BalanceDelta delta) = - lpm.mint(position, 1_00 ether, block.timestamp + 1, address(this), ZERO_BYTES); + lpm.mint(position, liquidityDelta, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 balance0After = currency0.balanceOfSelf(); + uint256 balance1After = currency1.balanceOfSelf(); + + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); + assertEq(balance0Before - balance0After, uint256(int256(delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - balance1After, uint256(int256(delta.amount1())), "incorrect amount1"); + } + + function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { + (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + (amount0Desired, amount1Desired) = + createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + position: position, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: address(this), + hookData: ZERO_BYTES + }); + (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency0.balanceOfSelf(); + uint256 balance1After = currency1.balanceOfSelf(); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); @@ -61,11 +98,13 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { assertEq(balance1Before - balance1After, uint256(int256(delta.amount1()))); } - function test_mint() public { - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: -600, tickUpper: 600}); - + // minting with perfect token ratios will use all of the tokens + function test_mint_perfect() public { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); uint256 amount0Desired = 100e18; uint256 amount1Desired = 100e18; + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); @@ -81,7 +120,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { }); (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency0.balanceOfSelf(); + uint256 balance1After = currency1.balanceOfSelf(); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); @@ -91,10 +130,14 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { assertEq(balance1Before - balance1After, uint256(int256(delta.amount1()))); } - function test_mint_recipient() public { - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: -600, tickUpper: 600}); - uint256 amount0Desired = 100e18; - uint256 amount1Desired = 100e18; + function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + public + { + (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + (amount0Desired, amount1Desired) = + createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ position: position, amount0Desired: amount0Desired, @@ -110,8 +153,6 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot { assertEq(lpm.ownerOf(tokenId), alice); } - function test_mint_withLiquidityDelta_recipient() public {} - function test_mint_slippageRevert() public {} function test_burn() public {} diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol new file mode 100644 index 00000000..1491abeb --- /dev/null +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Vm} from "forge-std/Vm.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; +import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {LiquidityPosition} from "../../../contracts/types/LiquidityPositionId.sol"; + +contract LiquidityFuzzers is StdUtils { + Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + /// @dev Obtain fuzzed parameters for creating liquidity + /// @param key The pool key + /// @param tickLower The lower tick + /// @param tickUpper The upper tick + /// @param liquidityDelta The liquidity delta + + function createFuzzyLiquidityParams(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidityDelta) + internal + view + returns (int24 _tickLower, int24 _tickUpper, uint128 _liquidityDelta) + { + _vm.assume(0.0000001e18 < liquidityDelta); + + _vm.assume(liquidityDelta < Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)); + + tickLower = int24( + bound( + int256(tickLower), + int256(TickMath.minUsableTick(key.tickSpacing)), + int256(TickMath.maxUsableTick(key.tickSpacing)) + ) + ); + tickUpper = int24( + bound( + int256(tickUpper), + int256(TickMath.minUsableTick(key.tickSpacing)), + int256(TickMath.maxUsableTick(key.tickSpacing)) + ) + ); + + // round down ticks + tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; + tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; + _vm.assume(tickLower < tickUpper); + + _tickLower = tickLower; + _tickUpper = tickUpper; + _liquidityDelta = liquidityDelta; + } + + function createFuzzyLiquidity( + INonfungiblePositionManager lpm, + address recipient, + PoolKey memory key, + int24 tickLower, + int24 tickUpper, + uint128 liquidityDelta, + bytes memory hookData + ) + internal + returns (uint256 _tokenId, int24 _tickLower, int24 _tickUpper, uint128 _liquidityDelta, BalanceDelta _delta) + { + (_tickLower, _tickUpper, _liquidityDelta) = + createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); + (_tokenId, _delta) = lpm.mint( + LiquidityPosition({key: key, tickLower: _tickLower, tickUpper: _tickUpper}), + _liquidityDelta, + block.timestamp, + recipient, + hookData + ); + } + + function createFuzzyAmountDesired( + PoolKey memory key, + int24 tickLower, + int24 tickUpper, + uint256 amount0, + uint256 amount1 + ) internal view returns (uint256 _amount0, uint256 _amount1) { + // fuzzing amount desired is a nice to have instead of using liquidityDelta, however we often violate TickOverflow + // (too many tokens in a tight range) -- need to figure out how to bound it better + bool tight = (tickUpper - tickLower) < 100 * key.tickSpacing; + uint256 maxAmount0 = tight ? 1_000e18 : 10_000e18; + uint256 maxAmount1 = tight ? 1_000e18 : 10_000e18; + _amount0 = bound(amount0, 0, maxAmount0); + _amount1 = bound(amount1, 0, maxAmount1); + _vm.assume(_amount0 != 0 && _amount1 != 0); + } +} From 307f4bb7e5799d58acc0bd033d4d71b98dd32730 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 5 Mar 2024 18:36:05 -0500 Subject: [PATCH 12/98] test for slippage --- contracts/NonfungiblePositionManager.sol | 2 + .../NonfungiblePositionManager.t.sol | 50 +++++++++++++++++-- test/shared/fuzz/LiquidityFuzzers.sol | 6 +-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 003bfc92..4e388599 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -97,6 +97,8 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi params.recipient, params.hookData ); + require(params.amount0Min <= uint256(uint128(delta.amount0())), "INSUFFICIENT_AMOUNT0"); + require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); } function burn(uint256 tokenId) external {} diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 2c79e476..c90c6f99 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -112,8 +112,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi position: position, amount0Desired: amount0Desired, amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, + amount0Min: amount0Desired, + amount1Min: amount1Desired, deadline: block.timestamp + 1, recipient: address(this), hookData: ZERO_BYTES @@ -153,7 +153,51 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(lpm.ownerOf(tokenId), alice); } - function test_mint_slippageRevert() public {} + function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + public + { + (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + vm.assume(tickLower < 0); + vm.assume(tickUpper > 0); + + (amount0Desired, amount1Desired) = + createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + vm.assume(0.00001e18 < amount0Desired); + vm.assume(0.00001e18 < amount1Desired); + + uint256 amount0Min = amount0Desired - 1; + uint256 amount1Min = amount1Desired - 1; + + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + position: position, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: block.timestamp + 1, + recipient: address(this), + hookData: ZERO_BYTES + }); + + // seed some liquidity so we can move the price + modifyLiquidityRouter.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams({ + tickLower: TickMath.minUsableTick(key.tickSpacing), + tickUpper: TickMath.maxUsableTick(key.tickSpacing), + liquidityDelta: 100_000e18 + }), + ZERO_BYTES + ); + + // swap to move the price + swap(key, true, 1000e18, ZERO_BYTES); + + // will revert because amount0Min and amount1Min are very strict + vm.expectRevert(); + lpm.mint(params); + } function test_burn() public {} function test_collect() public {} diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 1491abeb..f9939a0a 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -86,9 +86,9 @@ contract LiquidityFuzzers is StdUtils { ) internal view returns (uint256 _amount0, uint256 _amount1) { // fuzzing amount desired is a nice to have instead of using liquidityDelta, however we often violate TickOverflow // (too many tokens in a tight range) -- need to figure out how to bound it better - bool tight = (tickUpper - tickLower) < 100 * key.tickSpacing; - uint256 maxAmount0 = tight ? 1_000e18 : 10_000e18; - uint256 maxAmount1 = tight ? 1_000e18 : 10_000e18; + bool tight = (tickUpper - tickLower) < 200 * key.tickSpacing; + uint256 maxAmount0 = tight ? 100e18 : 1_000e18; + uint256 maxAmount1 = tight ? 100e18 : 1_000e18; _amount0 = bound(amount0, 0, maxAmount0); _amount1 = bound(amount1, 0, maxAmount1); _vm.assume(_amount0 != 0 && _amount1 != 0); From 109caf42cc2efab8dd98d050a696fc26847897b8 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 6 Mar 2024 15:54:18 -0500 Subject: [PATCH 13/98] burning --- contracts/NonfungiblePositionManager.sol | 78 ++++++++++++++++++- .../INonfungiblePositionManager.sol | 14 +++- .../NonfungiblePositionManager.t.sol | 34 +++++++- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 4e388599..eca1c538 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -101,8 +101,84 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); } - function burn(uint256 tokenId) external {} + function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData) + public + isAuthorizedForToken(params.tokenId) + returns (BalanceDelta delta) + { + require(params.liquidityDelta != 0, "Must decrease liquidity"); + Position storage position = positions[params.tokenId]; + delta = BaseLiquidityManagement.modifyLiquidity( + position.position.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: position.position.tickLower, + tickUpper: position.position.tickUpper, + liquidityDelta: -int256(uint256(params.liquidityDelta)) + }), + hookData, + ownerOf(params.tokenId) + ); + require(params.amount0Min <= uint256(uint128(-delta.amount0())), "INSUFFICIENT_AMOUNT0"); + require(params.amount1Min <= uint256(uint128(-delta.amount1())), "INSUFFICIENT_AMOUNT1"); + + // position.tokensOwed0 += + // uint128(amount0) + + // uint128( + // FullMath.mulDiv( + // feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, + // positionLiquidity, + // FixedPoint128.Q128 + // ) + // ); + // position.tokensOwed1 += + // uint128(amount1) + + // uint128( + // FullMath.mulDiv( + // feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, + // positionLiquidity, + // FixedPoint128.Q128 + // ) + // ); + + // position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + // position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + + // update the position + position.liquidity -= params.liquidityDelta; + } + + function burn(uint256 tokenId, bytes calldata hookData) + external + isAuthorizedForToken(tokenId) + returns (BalanceDelta delta) + { + // remove liquidity + Position storage position = positions[tokenId]; + if (0 < position.liquidity) { + decreaseLiquidity( + DecreaseLiquidityParams({ + tokenId: tokenId, + liquidityDelta: position.liquidity, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }), + hookData + ); + } + + require(position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "NOT_EMPTY"); + delete positions[tokenId]; + + // burn the token + _burn(tokenId); + } // TODO: in v3, we can partially collect fees, but what was the usecase here? function collect(uint256 tokenId, address recipient) external {} + + modifier isAuthorizedForToken(uint256 tokenId) { + require(_isApprovedOrOwner(msg.sender, tokenId), "Not approved"); + _; + } } diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index f45c2dd5..18177b49 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -30,7 +30,19 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { // NOTE: more expensive since LiquidityAmounts is used onchain function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); - function burn(uint256 tokenId) external; + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidityDelta; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData) + external + returns (BalanceDelta delta); + + function burn(uint256 tokenId, bytes calldata hookData) external returns (BalanceDelta delta); // TODO: in v3, we can partially collect fees, but what was the usecase here? function collect(uint256 tokenId, address recipient) external; diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index c90c6f99..c25e09f5 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -16,6 +16,7 @@ import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; @@ -199,7 +200,38 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi lpm.mint(params); } - function test_burn() public {} + function test_burn(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + uint256 balance0Start = currency0.balanceOfSelf(); + uint256 balance1Start = currency1.balanceOfSelf(); + + // create liquidity we can burn + uint256 tokenId; + (tokenId, tickLower, tickUpper, liquidityDelta,) = + createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); + + // burn liquidity + uint256 balance0BeforeBurn = currency0.balanceOfSelf(); + uint256 balance1BeforeBurn = currency1.balanceOfSelf(); + BalanceDelta delta = lpm.burn(tokenId, ZERO_BYTES); + assertEq(lpm.liquidityOf(address(this), position.toId()), 0); + + // TODO: slightly off by 1 bip (0.0001%) + assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(-delta.amount0())), 0.0001e18); + assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(-delta.amount1())), 0.0001e18); + + // OZ 721 will revert if the token does not exist + vm.expectRevert(); + lpm.ownerOf(1); + + // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); + assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); + } + function test_collect() public {} function test_increaseLiquidity() public {} function test_decreaseLiquidity() public {} From 1bf080f434287f9f449f0dd0db8665f5811d8c33 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 6 Mar 2024 17:44:07 -0500 Subject: [PATCH 14/98] decrease liquidity --- .../NonfungiblePositionManager.t.sol | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index c25e09f5..a951f809 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -234,7 +234,30 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_collect() public {} function test_increaseLiquidity() public {} - function test_decreaseLiquidity() public {} + + function test_decreaseLiquidity(int24 tickLower, int24 tickUpper, uint128 liquidityDelta, uint128 decreaseLiquidityDelta) public { + uint256 tokenId; + (tokenId, tickLower, tickUpper, liquidityDelta,) = + createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); + vm.assume(0 < decreaseLiquidityDelta); + vm.assume(decreaseLiquidityDelta <= liquidityDelta); + + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: tokenId, + liquidityDelta: decreaseLiquidityDelta, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1 + }); + BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES); + assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta - decreaseLiquidityDelta); + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0()))); + assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1()))); + } function test_mintTransferBurn() public {} function test_mintTransferCollect() public {} From 40f042ca882026ab552787be63a04fab7f44b2a9 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 7 Mar 2024 10:55:32 -0500 Subject: [PATCH 15/98] mint transfer burn, liquidityOf accounting --- contracts/NonfungiblePositionManager.sol | 10 +++- .../NonfungiblePositionManager.t.sol | 49 +++++++++++++++++-- test/shared/fuzz/LiquidityFuzzers.sol | 2 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index eca1c538..fefdb3b6 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -8,7 +8,7 @@ import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {LiquidityPosition} from "./types/LiquidityPositionId.sol"; +import {LiquidityPosition, LiquidityPositionIdLibrary} from "./types/LiquidityPositionId.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; @@ -16,6 +16,7 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { using PoolIdLibrary for PoolKey; + using LiquidityPositionIdLibrary for LiquidityPosition; /// @dev The ID of the next token that will be minted. Skips 0 uint256 private _nextId = 1; @@ -177,6 +178,13 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi // TODO: in v3, we can partially collect fees, but what was the usecase here? function collect(uint256 tokenId, address recipient) external {} + function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { + Position storage position = positions[firstTokenId]; + position.operator = address(0x0); + liquidityOf[from][position.position.toId()] -= position.liquidity; + liquidityOf[to][position.position.toId()] += position.liquidity; + } + modifier isAuthorizedForToken(uint256 tokenId) { require(_isApprovedOrOwner(msg.sender, tokenId), "Not approved"); _; diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index a951f809..eb0329db 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -234,8 +234,13 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_collect() public {} function test_increaseLiquidity() public {} - - function test_decreaseLiquidity(int24 tickLower, int24 tickUpper, uint128 liquidityDelta, uint128 decreaseLiquidityDelta) public { + + function test_decreaseLiquidity( + int24 tickLower, + int24 tickUpper, + uint128 liquidityDelta, + uint128 decreaseLiquidityDelta + ) public { uint256 tokenId; (tokenId, tickLower, tickUpper, liquidityDelta,) = createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); @@ -246,7 +251,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager.DecreaseLiquidityParams({ + INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ tokenId: tokenId, liquidityDelta: decreaseLiquidityDelta, amount0Min: 0, @@ -259,7 +265,42 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1()))); } - function test_mintTransferBurn() public {} + function test_mintTransferBurn(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + public + { + (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + (amount0Desired, amount1Desired) = + createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + position: position, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: address(this), + hookData: ZERO_BYTES + }); + (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + uint256 liquidity = lpm.liquidityOf(address(this), position.toId()); + + // transfer to Alice + lpm.transferFrom(address(this), alice, tokenId); + + assertEq(lpm.liquidityOf(address(this), position.toId()), 0); + assertEq(lpm.ownerOf(tokenId), alice); + assertEq(lpm.liquidityOf(alice, position.toId()), liquidity); + + // Alice can burn the token + vm.prank(alice); + lpm.burn(tokenId, ZERO_BYTES); + } + function test_mintTransferCollect() public {} function test_mintTransferIncrease() public {} function test_mintTransferDecrease() public {} diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index f9939a0a..7710299d 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -86,7 +86,7 @@ contract LiquidityFuzzers is StdUtils { ) internal view returns (uint256 _amount0, uint256 _amount1) { // fuzzing amount desired is a nice to have instead of using liquidityDelta, however we often violate TickOverflow // (too many tokens in a tight range) -- need to figure out how to bound it better - bool tight = (tickUpper - tickLower) < 200 * key.tickSpacing; + bool tight = (tickUpper - tickLower) < 300 * key.tickSpacing; uint256 maxAmount0 = tight ? 100e18 : 1_000e18; uint256 maxAmount1 = tight ? 100e18 : 1_000e18; _amount0 = bound(amount0, 0, maxAmount0); From 6b1c7cbd3acc6720c97c47d50b73bbde04053ab3 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 8 Mar 2024 12:04:35 -0500 Subject: [PATCH 16/98] wip --- contracts/NonfungiblePositionManager.sol | 15 ++++++++++++++- .../interfaces/INonfungiblePositionManager.sol | 2 +- .../NonfungiblePositionManager.t.sol | 17 ++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index fefdb3b6..d9fe6ea0 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -176,7 +176,20 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi } // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient) external {} + function collect(uint256 tokenId, address recipient) external returns (BalanceDelta delta) { + Position memory position = positions[tokenId]; + BaseLiquidityManagement.modifyLiquidity( + position.position.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: position.position.tickLower, + tickUpper: position.position.tickUpper, + liquidityDelta: 0 + }), + "", + recipient + ); + ) + } function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { Position storage position = positions[firstTokenId]; diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 18177b49..cdb47722 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -45,5 +45,5 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { function burn(uint256 tokenId, bytes calldata hookData) external returns (BalanceDelta delta); // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient) external; + function collect(uint256 tokenId, address recipient) external returns (BalanceDelta delta); } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index eb0329db..1484eb39 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -232,7 +232,22 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_collect() public {} + function test_collect(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + uint256 tokenId; + liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity + (tokenId, tickLower, tickUpper, liquidityDelta,) = + createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); + vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + + // swap to create fees + swap(key, false, 0.01e18, ZERO_BYTES); + + // collect fees + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + BalanceDelta delta = lpm.collect(tokenId, address(this)); + } + function test_increaseLiquidity() public {} function test_decreaseLiquidity( From fa511d093d4117c4036e2520573032f4a66fcaae Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 12 Mar 2024 12:17:26 -0400 Subject: [PATCH 17/98] refactor to use CurrencySettleTake --- contracts/base/BaseLiquidityManagement.sol | 24 +++++++++------------ contracts/libraries/CurrencySettleTake.sol | 25 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 contracts/libraries/CurrencySettleTake.sol diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index f9911045..d3a33fa5 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -11,14 +11,18 @@ import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; + abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagement { using LiquidityPositionIdLibrary for LiquidityPosition; using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; struct CallbackData { address sender; PoolKey key; IPoolManager.ModifyLiquidityParams params; + bool claims; bytes hookData; } @@ -37,7 +41,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); delta = abi.decode( - poolManager.lock(address(this), abi.encode(CallbackData(msg.sender, key, params, hookData))), (BalanceDelta) + poolManager.lock(address(this), abi.encode(CallbackData(msg.sender, key, params, false, hookData))), (BalanceDelta) ); params.liquidityDelta < 0 @@ -60,23 +64,15 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem if (data.params.liquidityDelta <= 0) { // removing liquidity/fees so take tokens - poolManager.take(data.key.currency0, data.sender, uint128(-delta.amount0())); - poolManager.take(data.key.currency1, data.sender, uint128(-delta.amount1())); + data.key.currency0.take(poolManager, data.sender, uint128(-delta.amount0()), data.claims); + data.key.currency1.take(poolManager, data.sender, uint128(-delta.amount1()), data.claims); + } else { // adding liquidity so pay tokens - _settle(data.sender, data.key.currency0, uint128(delta.amount0())); - _settle(data.sender, data.key.currency1, uint128(delta.amount1())); + data.key.currency0.settle(poolManager, data.sender, uint128(delta.amount0()), data.claims); + data.key.currency1.settle(poolManager, data.sender, uint128(delta.amount1()), data.claims); } result = abi.encode(delta); } - - function _settle(address payer, Currency currency, uint256 amount) internal { - if (currency.isNative()) { - poolManager.settle{value: uint128(amount)}(currency); - } else { - IERC20(Currency.unwrap(currency)).transferFrom(payer, address(poolManager), uint128(amount)); - poolManager.settle(currency); - } - } } diff --git a/contracts/libraries/CurrencySettleTake.sol b/contracts/libraries/CurrencySettleTake.sol new file mode 100644 index 00000000..858963bf --- /dev/null +++ b/contracts/libraries/CurrencySettleTake.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; + +library CurrencySettleTake { + using CurrencyLibrary for Currency; + + function settle(Currency currency, IPoolManager manager, address payer, uint256 amount, bool burn) internal { + if (currency.isNative()) { + manager.settle{value: uint128(amount)}(currency); + } else if (burn) { + manager.burn(payer, currency.toId(), amount); + } else { + IERC20Minimal(Currency.unwrap(currency)).transferFrom(payer, address(manager), uint128(amount)); + manager.settle(currency); + } + } + + 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); + } +} From a0e0a44317cb3bec90da71e1a10391cb71837169 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 12 Mar 2024 13:29:15 -0400 Subject: [PATCH 18/98] basic fee collection --- contracts/NonfungiblePositionManager.sol | 10 ++++++---- contracts/base/BaseLiquidityManagement.sol | 4 ++-- .../interfaces/INonfungiblePositionManager.sol | 4 +++- .../NonfungiblePositionManager.t.sol | 15 ++++++++++++--- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index d9fe6ea0..b15452b3 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -176,19 +176,21 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi } // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient) external returns (BalanceDelta delta) { + function collect(uint256 tokenId, address recipient, bytes calldata hookData) + external + returns (BalanceDelta delta) + { Position memory position = positions[tokenId]; - BaseLiquidityManagement.modifyLiquidity( + delta = BaseLiquidityManagement.modifyLiquidity( position.position.key, IPoolManager.ModifyLiquidityParams({ tickLower: position.position.tickLower, tickUpper: position.position.tickUpper, liquidityDelta: 0 }), - "", + hookData, recipient ); - ) } function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index d3a33fa5..7d34c45d 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -41,7 +41,8 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); delta = abi.decode( - poolManager.lock(address(this), abi.encode(CallbackData(msg.sender, key, params, false, hookData))), (BalanceDelta) + poolManager.lock(address(this), abi.encode(CallbackData(msg.sender, key, params, false, hookData))), + (BalanceDelta) ); params.liquidityDelta < 0 @@ -66,7 +67,6 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem // removing liquidity/fees so take tokens data.key.currency0.take(poolManager, data.sender, uint128(-delta.amount0()), data.claims); data.key.currency1.take(poolManager, data.sender, uint128(-delta.amount1()), data.claims); - } else { // adding liquidity so pay tokens data.key.currency0.settle(poolManager, data.sender, uint128(delta.amount0()), data.claims); diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index cdb47722..995c0862 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -45,5 +45,7 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { function burn(uint256 tokenId, bytes calldata hookData) external returns (BalanceDelta delta); // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient) external returns (BalanceDelta delta); + function collect(uint256 tokenId, address recipient, bytes calldata hookData) + external + returns (BalanceDelta delta); } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 1484eb39..958fb7af 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -14,6 +14,7 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; @@ -29,6 +30,7 @@ import { import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { + using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityPositionIdLibrary for LiquidityPosition; @@ -238,14 +240,21 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi (tokenId, tickLower, tickUpper, liquidityDelta,) = createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity - + + uint256 swapAmount = 0.01e18; // swap to create fees - swap(key, false, 0.01e18, ZERO_BYTES); + swap(key, false, int256(swapAmount), ZERO_BYTES); // collect fees uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.collect(tokenId, address(this)); + BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES); + + assertEq(delta.amount0(), 0, "a"); + + // express key.fee as wad (i.e. 3000 = 0.003e18) + uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + assertApproxEqAbs(uint256(int256(-delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); } function test_increaseLiquidity() public {} From 0d936d40d831c54a26498c5f15389db3d061a66d Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sat, 16 Mar 2024 14:30:32 +0000 Subject: [PATCH 19/98] wip --- contracts/NonfungiblePositionManager.sol | 30 ++ contracts/libraries/PoolStateLibrary.sol | 336 ++++++++++++++++++ test/position-managers/FeeCollection.t.sol | 121 +++++++ .../NonfungiblePositionManager.t.sol | 23 -- 4 files changed, 487 insertions(+), 23 deletions(-) create mode 100644 contracts/libraries/PoolStateLibrary.sol create mode 100644 test/position-managers/FeeCollection.t.sol diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index b15452b3..07a2467b 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -13,10 +13,14 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; +import {PoolStateLibrary} from "./libraries/PoolStateLibrary.sol"; contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { using PoolIdLibrary for PoolKey; using LiquidityPositionIdLibrary for LiquidityPosition; + using PoolStateLibrary for IPoolManager; /// @dev The ID of the next token that will be minted. Skips 0 uint256 private _nextId = 1; @@ -191,6 +195,32 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi hookData, recipient ); + + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( + position.position.key.toId(), + position.position.tickLower, + position.position.tickUpper + ); + + // TODO: for now we'll assume user always collects the totality of their fees + uint128 tokensOwed0 = uint128( + FullMath.mulDiv( + feeGrowthInside0X128 - position.feeGrowthInside0LastX128, + position.liquidity, + FixedPoint128.Q128 + ) + ); + uint128 tokens1Owed = uint128( + FullMath.mulDiv( + feeGrowthInside1X128 - position.feeGrowthInside1LastX128, + position.liquidity, + FixedPoint128.Q128 + ) + ); + position.feeGrowthInside0LastX128 = feeGrowthInside0X128; + position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + + // TODO: event } function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { diff --git a/contracts/libraries/PoolStateLibrary.sol b/contracts/libraries/PoolStateLibrary.sol new file mode 100644 index 00000000..63b36ac9 --- /dev/null +++ b/contracts/libraries/PoolStateLibrary.sol @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {PoolId} from "v4-core/src/types/PoolId.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; + +library PoolStateLibrary { + // forge inspect lib/v4-core/src/PoolManager.sol:PoolManager storage --pretty + // | Name | Type | Slot | Offset | Bytes | Contract | + // |-----------------------|---------------------------------------------------------------------|------|--------|-------|---------------------------------------------| + // | pools | mapping(PoolId => struct Pool.State) | 8 | 0 | 32 | lib/v4-core/src/PoolManager.sol:PoolManager | + uint256 public constant POOLS_SLOT = 8; + + // index of feeGrowthGlobal0X128 in Pool.State + uint256 public constant FEE_GROWTH_GLOBAL0_OFFSET = 1; + // index of feeGrowthGlobal1X128 in Pool.State + uint256 public constant FEE_GROWTH_GLOBAL1_OFFSET = 2; + + // index of liquidity in Pool.State + uint256 public constant LIQUIDITY_OFFSET = 3; + + // index of TicksInfo mapping in Pool.State + uint256 public constant TICK_INFO_OFFSET = 4; + + // index of tickBitmap mapping in Pool.State + uint256 public constant TICK_BITMAP_OFFSET = 5; + + // index of Position.Info mapping in Pool.State + uint256 public constant POSITION_INFO_OFFSET = 6; + + /** + * @notice Get Slot0 of the pool: sqrtPriceX96, tick, protocolFee, swapFee + * @dev Corresponds to pools[poolId].slot0 + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @return sqrtPriceX96 The square root of the price of the pool, in Q96 precision. + * @return tick The current tick of the pool. + * @return protocolFee The protocol fee of the pool. + * @return swapFee The swap fee of the pool. + */ + function getSlot0(IPoolManager manager, PoolId poolId) + internal + view + returns (uint160 sqrtPriceX96, int24 tick, uint16 protocolFee, uint24 swapFee) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + bytes32 data = manager.extsload(stateSlot); + + // 32 bits |24bits|16bits |24 bits|160 bits + // 0x00000000 000bb8 0000 ffff75 0000000000000000fe3aa841ba359daa0ea9eff7 + // ---------- | fee |protocolfee | tick | sqrtPriceX96 + assembly { + // bottom 160 bits of data + sqrtPriceX96 := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + // next 24 bits of data + tick := and(shr(160, data), 0xFFFFFF) + // next 16 bits of data + protocolFee := and(shr(184, data), 0xFFFF) + // last 24 bits of data + swapFee := and(shr(200, data), 0xFFFFFF) + } + } + + /** + * @notice Retrieves the tick information of a pool at a specific tick. + * @dev Corresponds to pools[poolId].ticks[tick] + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve information for. + * @return liquidityGross The total position liquidity that references this tick + * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) + * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + */ + function getTickInfo(IPoolManager manager, PoolId poolId, int24 tick) + internal + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128 + ) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State: `mapping(int24 => TickInfo) ticks` + bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); + + // slot key of the tick key: `pools[poolId].ticks[tick] + bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); + + // read all 3 words of the TickInfo struct + bytes memory data = manager.extsload(slot, 3); + assembly { + liquidityGross := shr(128, mload(add(data, 32))) + liquidityNet := and(mload(add(data, 32)), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + feeGrowthOutside0X128 := mload(add(data, 64)) + feeGrowthOutside1X128 := mload(add(data, 96)) + } + } + + /** + * @notice Retrieves the liquidity information of a pool at a specific tick. + * @dev Corresponds to pools[poolId].ticks[tick].liquidityGross and pools[poolId].ticks[tick].liquidityNet. A more gas efficient version of getTickInfo + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve liquidity for. + * @return liquidityGross The total position liquidity that references this tick + * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) + */ + function getTickLiquidity(IPoolManager manager, PoolId poolId, int24 tick) + internal + view + returns (uint128 liquidityGross, int128 liquidityNet) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State: `mapping(int24 => TickInfo) ticks` + bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); + + // slot key of the tick key: `pools[poolId].ticks[tick] + bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); + + bytes32 value = manager.extsload(slot); + assembly { + liquidityNet := shr(128, value) + liquidityGross := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + } + } + + /** + * @notice Retrieves the fee growth outside a tick range of a pool + * @dev Corresponds to pools[poolId].ticks[tick].feeGrowthOutside0X128 and pools[poolId].ticks[tick].feeGrowthOutside1X128. A more gas efficient version of getTickInfo + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve fee growth for. + * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + */ + function getTickFeeGrowthOutside(IPoolManager manager, PoolId poolId, int24 tick) + internal + view + returns (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State: `mapping(int24 => TickInfo) ticks` + bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); + + // slot key of the tick key: `pools[poolId].ticks[tick] + bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); + + // TODO: offset to feeGrowth, to avoid 3-word read + bytes memory data = manager.extsload(slot, 3); + assembly { + feeGrowthOutside0X128 := mload(add(data, 64)) + feeGrowthOutside1X128 := mload(add(data, 96)) + } + } + + /** + * @notice Retrieves the global fee growth of a pool. + * @dev Corresponds to pools[poolId].feeGrowthGlobal0X128 and pools[poolId].feeGrowthGlobal1X128 + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @return feeGrowthGlobal0 The global fee growth for token0. + * @return feeGrowthGlobal1 The global fee growth for token1. + */ + function getFeeGrowthGlobal(IPoolManager manager, PoolId poolId) + internal + view + returns (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State, `uint256 feeGrowthGlobal0X128` + bytes32 slot_feeGrowthGlobal0X128 = bytes32(uint256(stateSlot) + FEE_GROWTH_GLOBAL0_OFFSET); + + // reads 3rd word of Pool.State, `uint256 feeGrowthGlobal1X128` + // bytes32 slot_feeGrowthGlobal1X128 = bytes32(uint256(stateSlot) + uint256(FEE_GROWTH_GLOBAL1_OFFSET)); + + // feeGrowthGlobal0 = uint256(manager.extsload(slot_feeGrowthGlobal0X128)); + // feeGrowthGlobal1 = uint256(manager.extsload(slot_feeGrowthGlobal1X128)); + + // read the 2 words of feeGrowthGlobal + bytes memory data = manager.extsload(slot_feeGrowthGlobal0X128, 2); + assembly { + feeGrowthGlobal0 := mload(add(data, 32)) + feeGrowthGlobal1 := mload(add(data, 64)) + } + } + + /** + * @notice Retrieves total the liquidity of a pool. + * @dev Corresponds to pools[poolId].liquidity + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @return liquidity The liquidity of the pool. + */ + function getLiquidity(IPoolManager manager, PoolId poolId) internal view returns (uint128 liquidity) { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State: `uint128 liquidity` + bytes32 slot = bytes32(uint256(stateSlot) + LIQUIDITY_OFFSET); + + liquidity = uint128(uint256(manager.extsload(slot))); + } + + /** + * @notice Retrieves the tick bitmap of a pool at a specific tick. + * @dev Corresponds to pools[poolId].tickBitmap[tick] + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve the bitmap for. + * @return tickBitmap The bitmap of the tick. + */ + function getTickBitmap(IPoolManager manager, PoolId poolId, int16 tick) + internal + view + returns (uint256 tickBitmap) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State: `mapping(int16 => uint256) tickBitmap;` + bytes32 tickBitmapMapping = bytes32(uint256(stateSlot) + TICK_BITMAP_OFFSET); + + // slot id of the mapping key: `pools[poolId].tickBitmap[tick] + bytes32 slot = keccak256(abi.encodePacked(int256(tick), tickBitmapMapping)); + + tickBitmap = uint256(manager.extsload(slot)); + } + + /** + * @notice Retrieves the position information of a pool at a specific position ID. + * @dev Corresponds to pools[poolId].positions[positionId] + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param positionId The ID of the position. + * @return liquidity The liquidity of the position. + * @return feeGrowthInside0LastX128 The fee growth inside the position for token0. + * @return feeGrowthInside1LastX128 The fee growth inside the position for token1. + */ + function getPositionInfo(IPoolManager manager, PoolId poolId, bytes32 positionId) + internal + view + returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State: `mapping(bytes32 => Position.Info) positions;` + bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITION_INFO_OFFSET); + + // first value slot of the mapping key: `pools[poolId].positions[positionId] (liquidity) + bytes32 slot = keccak256(abi.encodePacked(positionId, positionMapping)); + + // read all 3 words of the Position.Info struct + bytes memory data = manager.extsload(slot, 3); + + assembly { + liquidity := mload(add(data, 32)) + feeGrowthInside0LastX128 := mload(add(data, 64)) + feeGrowthInside1LastX128 := mload(add(data, 96)) + } + } + + /** + * @notice Retrieves the liquidity of a position. + * @dev Corresponds to pools[poolId].positions[positionId].liquidity. A more gas efficient version of getPositionInfo + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param positionId The ID of the position. + * @return liquidity The liquidity of the position. + */ + function getPositionLiquidity(IPoolManager manager, PoolId poolId, bytes32 positionId) + internal + view + returns (uint128 liquidity) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + + // Pool.State: `mapping(bytes32 => Position.Info) positions;` + bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITION_INFO_OFFSET); + + // first value slot of the mapping key: `pools[poolId].positions[positionId] (liquidity) + bytes32 slot = keccak256(abi.encodePacked(positionId, positionMapping)); + + liquidity = uint128(uint256(manager.extsload(slot))); + } + + /** + * @notice Live calculate the fee growth inside a tick range of a pool + * @dev pools[poolId].feeGrowthInside0LastX128 in Position.Info is cached and can become stale. This function will live calculate the feeGrowthInside + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tickLower The lower tick of the range. + * @param tickUpper The upper tick of the range. + * @return feeGrowthInside0X128 The fee growth inside the tick range for token0. + * @return feeGrowthInside1X128 The fee growth inside the tick range for token1. + */ + function getFeeGrowthInside(IPoolManager manager, PoolId poolId, int24 tickLower, int24 tickUpper) + internal + view + returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) + { + (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = getFeeGrowthGlobal(manager, poolId); + + (uint256 lowerFeeGrowthOutside0X128, uint256 lowerFeeGrowthOutside1X128) = + getTickFeeGrowthOutside(manager, poolId, tickLower); + (uint256 upperFeeGrowthOutside0X128, uint256 upperFeeGrowthOutside1X128) = + getTickFeeGrowthOutside(manager, poolId, tickUpper); + (, int24 tickCurrent,,) = getSlot0(manager, poolId); + unchecked { + if (tickCurrent < tickLower) { + feeGrowthInside0X128 = lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; + feeGrowthInside1X128 = lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; + } else if (tickCurrent >= tickUpper) { + feeGrowthInside0X128 = upperFeeGrowthOutside0X128 - lowerFeeGrowthOutside0X128; + feeGrowthInside1X128 = upperFeeGrowthOutside1X128 - lowerFeeGrowthOutside1X128; + } else { + feeGrowthInside0X128 = feeGrowthGlobal0X128 - lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; + feeGrowthInside1X128 = feeGrowthGlobal1X128 - lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; + } + } + } +} diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol new file mode 100644 index 00000000..51e1a005 --- /dev/null +++ b/test/position-managers/FeeCollection.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import { + LiquidityPosition, + LiquidityPositionId, + LiquidityPositionIdLibrary +} from "../../contracts/types/LiquidityPositionId.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityPositionIdLibrary for LiquidityPosition; + + NonfungiblePositionManager lpm; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + // unused value for the fuzz helper functions + uint128 constant DEAD_VALUE = 6969.6969 ether; + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + + lpm = new NonfungiblePositionManager(manager); + + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + } + + function test_collect(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + uint256 tokenId; + liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity + (tokenId, tickLower, tickUpper, liquidityDelta,) = + createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); + vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, int256(swapAmount), ZERO_BYTES); + + // collect fees + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES); + + assertEq(delta.amount0(), 0, "a"); + + // express key.fee as wad (i.e. 3000 = 0.003e18) + uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + assertApproxEqAbs(uint256(int256(-delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); + } + + // two users with the same range; one user cannot collect the other's fees + function test_collect_sameRange( + int24 tickLower, + int24 tickUpper, + uint128 liquidityDeltaAlice, + uint128 liquidityDeltaBob + ) public { + uint256 tokenIdAlice; + uint256 tokenIdBob; + liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity + liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); + + (tickLower, tickUpper, liquidityDeltaAlice) = + createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); + vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (,,liquidityDeltaBob) = + createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaBob); + + vm.prank(alice); + (tokenIdAlice,) = lpm.mint(LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), liquidityDeltaAlice, block.timestamp + 1, alice, ZERO_BYTES); + + vm.prank(bob); + (tokenIdBob,) = lpm.mint(LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), liquidityDeltaBob, block.timestamp + 1, alice, ZERO_BYTES); + + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, int256(swapAmount), ZERO_BYTES); + + // alice collects only her fees + vm.prank(alice); + BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES); + } + + function test_collect_donate() public {} + function test_collect_donate_sameRange() public {} + + function test_mintTransferCollect() public {} + function test_mintTransferIncrease() public {} + function test_mintTransferDecrease() public {} +} diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 958fb7af..fa820461 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -234,29 +234,6 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_collect(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { - uint256 tokenId; - liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity - - uint256 swapAmount = 0.01e18; - // swap to create fees - swap(key, false, int256(swapAmount), ZERO_BYTES); - - // collect fees - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES); - - assertEq(delta.amount0(), 0, "a"); - - // express key.fee as wad (i.e. 3000 = 0.003e18) - uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - assertApproxEqAbs(uint256(int256(-delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); - } - function test_increaseLiquidity() public {} function test_decreaseLiquidity( From 4be3c2a80eb6946c31fc2850c5ffc47bac76cae5 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 19 Mar 2024 12:37:56 +0000 Subject: [PATCH 20/98] misc fix --- contracts/NonfungiblePositionManager.sol | 2 +- contracts/libraries/PoolStateLibrary.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 07a2467b..edcd3705 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -88,7 +88,7 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi // NOTE: more expensive since LiquidityAmounts is used onchain function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { - (uint160 sqrtPriceX96,,) = poolManager.getSlot0(params.position.key.toId()); + (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, params.position.key.toId()); (tokenId, delta) = mint( params.position, LiquidityAmounts.getLiquidityForAmounts( diff --git a/contracts/libraries/PoolStateLibrary.sol b/contracts/libraries/PoolStateLibrary.sol index 63b36ac9..487c5530 100644 --- a/contracts/libraries/PoolStateLibrary.sol +++ b/contracts/libraries/PoolStateLibrary.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; -import {PoolId} from "v4-core/src/types/PoolId.sol"; -import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; library PoolStateLibrary { // forge inspect lib/v4-core/src/PoolManager.sol:PoolManager storage --pretty From 7fa4c5463152f848ebc6b386396af240b06ac811 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 19 Mar 2024 13:41:42 +0000 Subject: [PATCH 21/98] fee collection for independent same-range parties --- contracts/NonfungiblePositionManager.sol | 50 +++++++------- contracts/base/BaseLiquidityManagement.sol | 25 +++++++ .../INonfungiblePositionManager.sol | 2 +- test/position-managers/FeeCollection.t.sol | 65 +++++++++++++++---- 4 files changed, 104 insertions(+), 38 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index edcd3705..08fca3b2 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -8,8 +8,9 @@ import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityPosition, LiquidityPositionIdLibrary} from "./types/LiquidityPositionId.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; @@ -17,7 +18,11 @@ import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; import {PoolStateLibrary} from "./libraries/PoolStateLibrary.sol"; +// TODO: remove +import {console2} from "forge-std/console2.sol"; + contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { + using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; using LiquidityPositionIdLibrary for LiquidityPosition; using PoolStateLibrary for IPoolManager; @@ -180,46 +185,43 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi } // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData) + function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external returns (BalanceDelta delta) { Position memory position = positions[tokenId]; - delta = BaseLiquidityManagement.modifyLiquidity( - position.position.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: position.position.tickLower, - tickUpper: position.position.tickUpper, - liquidityDelta: 0 - }), - hookData, - recipient - ); + BaseLiquidityManagement.collect(position.position, hookData); (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( - position.position.key.toId(), - position.position.tickLower, - position.position.tickUpper + position.position.key.toId(), position.position.tickLower, position.position.tickUpper ); - + + console2.log(feeGrowthInside0X128, position.feeGrowthInside0LastX128); + console2.log(feeGrowthInside1X128, position.feeGrowthInside1LastX128); + // TODO: for now we'll assume user always collects the totality of their fees - uint128 tokensOwed0 = uint128( + uint128 token0Owed = uint128( FullMath.mulDiv( - feeGrowthInside0X128 - position.feeGrowthInside0LastX128, - position.liquidity, - FixedPoint128.Q128 + feeGrowthInside0X128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 ) ); - uint128 tokens1Owed = uint128( + uint128 token1Owed = uint128( FullMath.mulDiv( - feeGrowthInside1X128 - position.feeGrowthInside1LastX128, - position.liquidity, - FixedPoint128.Q128 + feeGrowthInside1X128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 ) ); + delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); + position.feeGrowthInside0LastX128 = feeGrowthInside0X128; position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + if (claims) { + poolManager.transfer(recipient, position.position.key.currency0.toId(), token0Owed); + poolManager.transfer(recipient, position.position.key.currency1.toId(), token1Owed); + } else { + // TODO: erc20s + } + // TODO: event } diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 7d34c45d..6e912392 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -58,6 +58,31 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem // } } + function collect(LiquidityPosition memory position, bytes calldata hookData) + internal + returns (BalanceDelta delta) + { + delta = abi.decode( + poolManager.lock( + address(this), + abi.encode( + CallbackData( + address(this), + position.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidityDelta: 0 + }), + true, + hookData + ) + ) + ), + (BalanceDelta) + ); + } + function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory result) { CallbackData memory data = abi.decode(rawData, (CallbackData)); diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 995c0862..f87aae8c 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -45,7 +45,7 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { function burn(uint256 tokenId, bytes calldata hookData) external returns (BalanceDelta delta); // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData) + function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 51e1a005..7e9c499f 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -50,12 +50,25 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); lpm = new NonfungiblePositionManager(manager); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + // Give tokens to Alice and Bob, with approvals + IERC20(Currency.unwrap(currency0)).transfer(alice, 10_000_000 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 10_000_000 ether); + IERC20(Currency.unwrap(currency0)).transfer(bob, 10_000_000 ether); + IERC20(Currency.unwrap(currency1)).transfer(bob, 10_000_000 ether); + vm.startPrank(alice); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + vm.startPrank(bob); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); } - function test_collect(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + function test_collect_6909(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { uint256 tokenId; liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity (tokenId, tickLower, tickUpper, liquidityDelta,) = @@ -69,17 +82,19 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // collect fees uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES); + BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, true); - assertEq(delta.amount0(), 0, "a"); + assertEq(delta.amount0(), 0); // express key.fee as wad (i.e. 3000 = 0.003e18) uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); assertApproxEqAbs(uint256(int256(-delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); + + assertEq(uint256(int256(-delta.amount1())), manager.balanceOf(address(this), currency1.toId())); } // two users with the same range; one user cannot collect the other's fees - function test_collect_sameRange( + function test_collect_sameRange_6909( int24 tickLower, int24 tickUpper, uint128 liquidityDeltaAlice, @@ -93,23 +108,47 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { (tickLower, tickUpper, liquidityDeltaAlice) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity - (,,liquidityDeltaBob) = - createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaBob); - + (,, liquidityDeltaBob) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaBob); + vm.prank(alice); - (tokenIdAlice,) = lpm.mint(LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), liquidityDeltaAlice, block.timestamp + 1, alice, ZERO_BYTES); - + (tokenIdAlice,) = lpm.mint( + LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), + liquidityDeltaAlice, + block.timestamp + 1, + alice, + ZERO_BYTES + ); + vm.prank(bob); - (tokenIdBob,) = lpm.mint(LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), liquidityDeltaBob, block.timestamp + 1, alice, ZERO_BYTES); - - + (tokenIdBob,) = lpm.mint( + LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), + liquidityDeltaBob, + block.timestamp + 1, + alice, + ZERO_BYTES + ); + // swap to create fees uint256 swapAmount = 0.01e18; swap(key, false, int256(swapAmount), ZERO_BYTES); // alice collects only her fees vm.prank(alice); - BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES); + BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, true); + assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); + assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); + assertTrue(delta.amount1() != 0); + + // bob collects only his fees + vm.prank(bob); + delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, true); + assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); + assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); + assertTrue(delta.amount1() != 0); + + // position manager holds no fees now + assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); + assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); } function test_collect_donate() public {} From aae96974975f395a9434389fbb852fb9943eef40 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 19 Mar 2024 13:51:36 +0000 Subject: [PATCH 22/98] LiquidityPosition -> LiquidityRange --- contracts/NonfungiblePositionManager.sol | 42 +++++++++---------- contracts/base/BaseLiquidityManagement.sol | 21 ++++------ .../IAdvancedLiquidityManagement.sol | 6 +-- .../interfaces/IBaseLiquidityManagement.sol | 4 +- .../INonfungiblePositionManager.sol | 6 +-- ...idityPositionId.sol => LiquidityRange.sol} | 10 ++--- test/position-managers/FeeCollection.t.sol | 16 +++---- .../NonfungiblePositionManager.t.sol | 40 ++++++++---------- test/shared/fuzz/LiquidityFuzzers.sol | 4 +- 9 files changed, 69 insertions(+), 80 deletions(-) rename contracts/types/{LiquidityPositionId.sol => LiquidityRange.sol} (59%) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 08fca3b2..f6ba04f2 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -9,7 +9,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {LiquidityPosition, LiquidityPositionIdLibrary} from "./types/LiquidityPositionId.sol"; +import {LiquidityRange, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; @@ -24,7 +24,7 @@ import {console2} from "forge-std/console2.sol"; contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; - using LiquidityPositionIdLibrary for LiquidityPosition; + using LiquidityRangeIdLibrary for LiquidityRange; using PoolStateLibrary for IPoolManager; /// @dev The ID of the next token that will be minted. Skips 0 @@ -38,7 +38,7 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi uint96 nonce; // the address that is approved for spending this token address operator; - LiquidityPosition position; + LiquidityRange range; // the liquidity of the position // NOTE: this value will be less than BaseLiquidityManagement.liquidityOf, if the user // owns multiple positions with the same range @@ -56,17 +56,17 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi // NOTE: more gas efficient as LiquidityAmounts is used offchain // TODO: deadline check function mint( - LiquidityPosition calldata position, + LiquidityRange calldata range, uint256 liquidity, uint256 deadline, address recipient, bytes calldata hookData ) public payable returns (uint256 tokenId, BalanceDelta delta) { delta = BaseLiquidityManagement.modifyLiquidity( - position.key, + range.key, IPoolManager.ModifyLiquidityParams({ - tickLower: position.tickLower, - tickUpper: position.tickUpper, + tickLower: range.tickLower, + tickUpper: range.tickUpper, liquidityDelta: int256(liquidity) }), hookData, @@ -80,7 +80,7 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi positions[tokenId] = Position({ nonce: 0, operator: address(0), - position: position, + range: range, liquidity: uint128(liquidity), feeGrowthInside0LastX128: 0, // TODO: feeGrowthInside1LastX128: 0, // TODO: @@ -93,13 +93,13 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi // NOTE: more expensive since LiquidityAmounts is used onchain function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, params.position.key.toId()); + (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, params.range.key.toId()); (tokenId, delta) = mint( - params.position, + params.range, LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(params.position.tickLower), - TickMath.getSqrtRatioAtTick(params.position.tickUpper), + TickMath.getSqrtRatioAtTick(params.range.tickLower), + TickMath.getSqrtRatioAtTick(params.range.tickUpper), params.amount0Desired, params.amount1Desired ), @@ -119,10 +119,10 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi require(params.liquidityDelta != 0, "Must decrease liquidity"); Position storage position = positions[params.tokenId]; delta = BaseLiquidityManagement.modifyLiquidity( - position.position.key, + position.range.key, IPoolManager.ModifyLiquidityParams({ - tickLower: position.position.tickLower, - tickUpper: position.position.tickUpper, + tickLower: position.range.tickLower, + tickUpper: position.range.tickUpper, liquidityDelta: -int256(uint256(params.liquidityDelta)) }), hookData, @@ -190,10 +190,10 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi returns (BalanceDelta delta) { Position memory position = positions[tokenId]; - BaseLiquidityManagement.collect(position.position, hookData); + BaseLiquidityManagement.collect(position.range, hookData); (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( - position.position.key.toId(), position.position.tickLower, position.position.tickUpper + position.range.key.toId(), position.range.tickLower, position.range.tickUpper ); console2.log(feeGrowthInside0X128, position.feeGrowthInside0LastX128); @@ -216,8 +216,8 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi position.feeGrowthInside1LastX128 = feeGrowthInside1X128; if (claims) { - poolManager.transfer(recipient, position.position.key.currency0.toId(), token0Owed); - poolManager.transfer(recipient, position.position.key.currency1.toId(), token1Owed); + poolManager.transfer(recipient, position.range.key.currency0.toId(), token0Owed); + poolManager.transfer(recipient, position.range.key.currency1.toId(), token1Owed); } else { // TODO: erc20s } @@ -228,8 +228,8 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { Position storage position = positions[firstTokenId]; position.operator = address(0x0); - liquidityOf[from][position.position.toId()] -= position.liquidity; - liquidityOf[to][position.position.toId()] += position.liquidity; + liquidityOf[from][position.range.toId()] -= position.liquidity; + liquidityOf[to][position.range.toId()] += position.liquidity; } modifier isAuthorizedForToken(uint256 tokenId) { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 6e912392..8f3d339a 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -5,7 +5,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {LiquidityPosition, LiquidityPositionId, LiquidityPositionIdLibrary} from "../types/LiquidityPositionId.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; @@ -14,7 +14,7 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagement { - using LiquidityPositionIdLibrary for LiquidityPosition; + using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; @@ -26,7 +26,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem bytes hookData; } - mapping(address owner => mapping(LiquidityPositionId positionId => uint256 liquidity)) public liquidityOf; + mapping(address owner => mapping(LiquidityRangeId positionId => uint256 liquidity)) public liquidityOf; constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} @@ -46,9 +46,9 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem ); params.liquidityDelta < 0 - ? liquidityOf[owner][LiquidityPosition(key, params.tickLower, params.tickUpper).toId()] -= + ? liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] -= uint256(-params.liquidityDelta) - : liquidityOf[owner][LiquidityPosition(key, params.tickLower, params.tickUpper).toId()] += + : liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] += uint256(params.liquidityDelta); // TODO: handle & test @@ -58,20 +58,17 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem // } } - function collect(LiquidityPosition memory position, bytes calldata hookData) - internal - returns (BalanceDelta delta) - { + function collect(LiquidityRange memory range, bytes calldata hookData) internal returns (BalanceDelta delta) { delta = abi.decode( poolManager.lock( address(this), abi.encode( CallbackData( address(this), - position.key, + range.key, IPoolManager.ModifyLiquidityParams({ - tickLower: position.tickLower, - tickUpper: position.tickUpper, + tickLower: range.tickLower, + tickUpper: range.tickUpper, liquidityDelta: 0 }), true, diff --git a/contracts/interfaces/IAdvancedLiquidityManagement.sol b/contracts/interfaces/IAdvancedLiquidityManagement.sol index 3c944641..5f5f9f8f 100644 --- a/contracts/interfaces/IAdvancedLiquidityManagement.sol +++ b/contracts/interfaces/IAdvancedLiquidityManagement.sol @@ -4,17 +4,17 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; -import {LiquidityPosition} from "../types/LiquidityPositionId.sol"; +import {LiquidityRange} from "../types/LiquidityRange.sol"; interface IAdvancedLiquidityManagement is IBaseLiquidityManagement { /// @notice Move an existing liquidity position into a new range function rebalanceLiquidity( - LiquidityPosition memory position, + LiquidityRange memory position, int24 tickLowerNew, int24 tickUpperNew, int256 liquidityDelta ) external; /// @notice Move an existing liquidity position into a new pool, keeping the same range - function migrateLiquidity(LiquidityPosition memory position, PoolKey memory newKey) external; + function migrateLiquidity(LiquidityRange memory position, PoolKey memory newKey) external; } diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol index 2b27f8e0..fe289195 100644 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -6,10 +6,10 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; -import {LiquidityPosition, LiquidityPositionId} from "../types/LiquidityPositionId.sol"; +import {LiquidityRange, LiquidityRangeId} from "../types/LiquidityRange.sol"; interface IBaseLiquidityManagement is ILockCallback { - function liquidityOf(address owner, LiquidityPositionId positionId) external view returns (uint256 liquidity); + function liquidityOf(address owner, LiquidityRangeId positionId) external view returns (uint256 liquidity); // NOTE: handles add/remove/collect function modifyLiquidity( diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index f87aae8c..23f17e6d 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {LiquidityPosition} from "../types/LiquidityPositionId.sol"; +import {LiquidityRange} from "../types/LiquidityRange.sol"; import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; interface INonfungiblePositionManager is IBaseLiquidityManagement { struct MintParams { - LiquidityPosition position; + LiquidityRange range; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; @@ -20,7 +20,7 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { // NOTE: more gas efficient as LiquidityAmounts is used offchain function mint( - LiquidityPosition calldata position, + LiquidityRange calldata position, uint256 liquidity, uint256 deadline, address recipient, diff --git a/contracts/types/LiquidityPositionId.sol b/contracts/types/LiquidityRange.sol similarity index 59% rename from contracts/types/LiquidityPositionId.sol rename to contracts/types/LiquidityRange.sol index 063db61b..88545687 100644 --- a/contracts/types/LiquidityPositionId.sol +++ b/contracts/types/LiquidityRange.sol @@ -4,18 +4,18 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; // TODO: move into core? some of the mappings / pool.state seem to hash position id's -struct LiquidityPosition { +struct LiquidityRange { PoolKey key; int24 tickLower; int24 tickUpper; } -type LiquidityPositionId is bytes32; +type LiquidityRangeId is bytes32; /// @notice Library for computing the ID of a pool -library LiquidityPositionIdLibrary { - function toId(LiquidityPosition memory position) internal pure returns (LiquidityPositionId) { +library LiquidityRangeIdLibrary { + function toId(LiquidityRange memory position) internal pure returns (LiquidityRangeId) { // TODO: gas, is it better to encodePacked? - return LiquidityPositionId.wrap(keccak256(abi.encode(position))); + return LiquidityRangeId.wrap(keccak256(abi.encode(position))); } } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 7e9c499f..83fe891f 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -21,18 +21,14 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import { - LiquidityPosition, - LiquidityPositionId, - LiquidityPositionIdLibrary -} from "../../contracts/types/LiquidityPositionId.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityPositionIdLibrary for LiquidityPosition; + using LiquidityRangeIdLibrary for LiquidityRange; NonfungiblePositionManager lpm; @@ -88,9 +84,9 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // express key.fee as wad (i.e. 3000 = 0.003e18) uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - assertApproxEqAbs(uint256(int256(-delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); + assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); - assertEq(uint256(int256(-delta.amount1())), manager.balanceOf(address(this), currency1.toId())); + assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); } // two users with the same range; one user cannot collect the other's fees @@ -112,7 +108,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { vm.prank(alice); (tokenIdAlice,) = lpm.mint( - LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), + LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), liquidityDeltaAlice, block.timestamp + 1, alice, @@ -121,7 +117,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { vm.prank(bob); (tokenIdBob,) = lpm.mint( - LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}), + LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), liquidityDeltaBob, block.timestamp + 1, alice, diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index fa820461..32e1e53a 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -21,18 +21,14 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import { - LiquidityPosition, - LiquidityPositionId, - LiquidityPositionIdLibrary -} from "../../contracts/types/LiquidityPositionId.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityPositionIdLibrary for LiquidityPosition; + using LiquidityRangeIdLibrary for LiquidityRange; NonfungiblePositionManager lpm; @@ -56,7 +52,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mint_withLiquidityDelta(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { (tickLower, tickUpper, liquidityDelta) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); @@ -77,12 +73,12 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi (amount0Desired, amount1Desired) = createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - position: position, + range: range, amount0Desired: amount0Desired, amount1Desired: amount1Desired, amount0Min: 0, @@ -107,12 +103,12 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi int24 tickUpper = int24(key.tickSpacing); uint256 amount0Desired = 100e18; uint256 amount1Desired = 100e18; - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - position: position, + range: range, amount0Desired: amount0Desired, amount1Desired: amount1Desired, amount0Min: amount0Desired, @@ -140,9 +136,9 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi (amount0Desired, amount1Desired) = createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - position: position, + range: range, amount0Desired: amount0Desired, amount1Desired: amount1Desired, amount0Min: 0, @@ -171,9 +167,9 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 amount0Min = amount0Desired - 1; uint256 amount1Min = amount1Desired - 1; - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - position: position, + range: range, amount0Desired: amount0Desired, amount1Desired: amount1Desired, amount0Min: amount0Min, @@ -210,7 +206,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 tokenId; (tokenId, tickLower, tickUpper, liquidityDelta,) = createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); @@ -248,7 +244,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi vm.assume(0 < decreaseLiquidityDelta); vm.assume(decreaseLiquidityDelta <= liquidityDelta); - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); @@ -273,12 +269,12 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi (amount0Desired, amount1Desired) = createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - LiquidityPosition memory position = LiquidityPosition({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - position: position, + range: range, amount0Desired: amount0Desired, amount1Desired: amount1Desired, amount0Min: 0, @@ -288,14 +284,14 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi hookData: ZERO_BYTES }); (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - uint256 liquidity = lpm.liquidityOf(address(this), position.toId()); + uint256 liquidity = lpm.liquidityOf(address(this), range.toId()); // transfer to Alice lpm.transferFrom(address(this), alice, tokenId); - assertEq(lpm.liquidityOf(address(this), position.toId()), 0); + assertEq(lpm.liquidityOf(address(this), range.toId()), 0); assertEq(lpm.ownerOf(tokenId), alice); - assertEq(lpm.liquidityOf(alice, position.toId()), liquidity); + assertEq(lpm.liquidityOf(alice, range.toId()), liquidity); // Alice can burn the token vm.prank(alice); diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 7710299d..9cadec9b 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -10,7 +10,7 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {LiquidityPosition} from "../../../contracts/types/LiquidityPositionId.sol"; +import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; contract LiquidityFuzzers is StdUtils { Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -69,7 +69,7 @@ contract LiquidityFuzzers is StdUtils { (_tickLower, _tickUpper, _liquidityDelta) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); (_tokenId, _delta) = lpm.mint( - LiquidityPosition({key: key, tickLower: _tickLower, tickUpper: _tickUpper}), + LiquidityRange({key: key, tickLower: _tickLower, tickUpper: _tickUpper}), _liquidityDelta, block.timestamp, recipient, From 5dec5345fb4b64ab8ae558c3c1ebeb4a3763278d Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 19 Mar 2024 18:22:32 +0000 Subject: [PATCH 23/98] erc20 fee collection --- contracts/NonfungiblePositionManager.sol | 3 +- contracts/base/BaseLiquidityManagement.sol | 60 ++++++++++---- test/position-managers/FeeCollection.t.sol | 92 ++++++++++++++++++++++ 3 files changed, 140 insertions(+), 15 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index f6ba04f2..91288062 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -219,7 +219,8 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi poolManager.transfer(recipient, position.range.key.currency0.toId(), token0Owed); poolManager.transfer(recipient, position.range.key.currency1.toId(), token1Owed); } else { - // TODO: erc20s + sendToken(recipient, position.range.key.currency0, token0Owed); + sendToken(recipient, position.range.key.currency1, token1Owed); } // TODO: event diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 8f3d339a..d5ee0479 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -13,11 +13,16 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +// TODO: remove +import {console2} from "forge-std/console2.sol"; + abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagement { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; + error LockFailure(); + struct CallbackData { address sender; PoolKey key; @@ -41,7 +46,9 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); delta = abi.decode( - poolManager.lock(address(this), abi.encode(CallbackData(msg.sender, key, params, false, hookData))), + poolManager.lock( + address(this), abi.encodeCall(this.handleModifyPosition, (msg.sender, key, params, hookData, false)) + ), (BalanceDelta) ); @@ -62,8 +69,9 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem delta = abi.decode( poolManager.lock( address(this), - abi.encode( - CallbackData( + abi.encodeCall( + this.handleModifyPosition, + ( address(this), range.key, IPoolManager.ModifyLiquidityParams({ @@ -71,8 +79,8 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem tickUpper: range.tickUpper, liquidityDelta: 0 }), - true, - hookData + hookData, + true ) ) ), @@ -80,21 +88,45 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem ); } - function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory result) { - CallbackData memory data = abi.decode(rawData, (CallbackData)); + function sendToken(address recipient, Currency currency, uint256 amount) internal { + poolManager.lock(address(this), abi.encodeCall(this.handleRedeemClaim, (recipient, currency, amount))); + } + + function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { + (bool success, bytes memory returnData) = address(this).call(data); + if (success) return returnData; + if (returnData.length == 0) revert LockFailure(); + // if the call failed, bubble up the reason + /// @solidity memory-safe-assembly + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } - BalanceDelta delta = poolManager.modifyLiquidity(data.key, data.params, data.hookData); + // TODO: selfOnly modifier + function handleModifyPosition( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData, + bool claims + ) external returns (BalanceDelta delta) { + delta = poolManager.modifyLiquidity(key, params, hookData); - if (data.params.liquidityDelta <= 0) { + if (params.liquidityDelta <= 0) { // removing liquidity/fees so take tokens - data.key.currency0.take(poolManager, data.sender, uint128(-delta.amount0()), data.claims); - data.key.currency1.take(poolManager, data.sender, uint128(-delta.amount1()), data.claims); + key.currency0.take(poolManager, sender, uint128(-delta.amount0()), claims); + key.currency1.take(poolManager, sender, uint128(-delta.amount1()), claims); } else { // adding liquidity so pay tokens - data.key.currency0.settle(poolManager, data.sender, uint128(delta.amount0()), data.claims); - data.key.currency1.settle(poolManager, data.sender, uint128(delta.amount1()), data.claims); + key.currency0.settle(poolManager, sender, uint128(delta.amount0()), claims); + key.currency1.settle(poolManager, sender, uint128(delta.amount1()), claims); } + } - result = abi.encode(delta); + // TODO: selfOnly modifier + function handleRedeemClaim(address recipient, Currency currency, uint256 amount) external { + poolManager.burn(address(this), currency.toId(), amount); + poolManager.take(currency, recipient, amount); } } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 83fe891f..f3a4a46e 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -89,6 +89,31 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); } + function test_collect_erc20(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + uint256 tokenId; + liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity + (tokenId, tickLower, tickUpper, liquidityDelta,) = + createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); + vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, int256(swapAmount), ZERO_BYTES); + + // collect fees + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, false); + + assertEq(delta.amount0(), 0); + + // express key.fee as wad (i.e. 3000 = 0.003e18) + uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); + + assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); + } + // two users with the same range; one user cannot collect the other's fees function test_collect_sameRange_6909( int24 tickLower, @@ -147,6 +172,73 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); } + function test_collect_sameRange_erc20( + int24 tickLower, + int24 tickUpper, + uint128 liquidityDeltaAlice, + uint128 liquidityDeltaBob + ) public { + uint256 tokenIdAlice; + uint256 tokenIdBob; + liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity + liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); + + (tickLower, tickUpper, liquidityDeltaAlice) = + createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); + vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (,, liquidityDeltaBob) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaBob); + + vm.prank(alice); + (tokenIdAlice,) = lpm.mint( + LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), + liquidityDeltaAlice, + block.timestamp + 1, + alice, + ZERO_BYTES + ); + + vm.prank(bob); + (tokenIdBob,) = lpm.mint( + LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), + liquidityDeltaBob, + block.timestamp + 1, + alice, + ZERO_BYTES + ); + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, int256(swapAmount), ZERO_BYTES); + + // alice collects only her fees + uint256 balance0AliceBefore = currency0.balanceOf(alice); + uint256 balance1AliceBefore = currency1.balanceOf(alice); + vm.prank(alice); + BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); + uint256 balance0AliceAfter = currency0.balanceOf(alice); + uint256 balance1AliceAfter = currency1.balanceOf(alice); + + assertEq(balance0AliceBefore, balance0AliceAfter); + assertEq(uint256(uint128(delta.amount1())), balance1AliceAfter - balance1AliceBefore); + assertTrue(delta.amount1() != 0); + + // bob collects only his fees + uint256 balance0BobBefore = currency0.balanceOf(bob); + uint256 balance1BobBefore = currency1.balanceOf(bob); + vm.prank(bob); + delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + uint256 balance0BobAfter = currency0.balanceOf(bob); + uint256 balance1BobAfter = currency1.balanceOf(bob); + + assertEq(balance0BobBefore, balance0BobAfter); + assertEq(uint256(uint128(delta.amount1())), balance1BobAfter - balance1BobBefore); + assertTrue(delta.amount1() != 0); + + // position manager holds no fees now + assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); + assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); + } + function test_collect_donate() public {} function test_collect_donate_sameRange() public {} From 1196c6a730cd23a28429456f26c82ed3f90ae1b5 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 25 Mar 2024 14:19:35 -0400 Subject: [PATCH 24/98] decrease liquidity with fee collection --- contracts/NonfungiblePositionManager.sol | 112 +++++++++--------- contracts/base/BaseLiquidityManagement.sol | 7 +- .../INonfungiblePositionManager.sol | 7 +- contracts/libraries/FeeMath.sol | 27 +++++ .../NonfungiblePositionManager.t.sol | 51 +++++++- 5 files changed, 142 insertions(+), 62 deletions(-) create mode 100644 contracts/libraries/FeeMath.sol diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 91288062..fa2ba382 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -14,8 +14,7 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; +import {FeeMath} from "./libraries/FeeMath.sol"; import {PoolStateLibrary} from "./libraries/PoolStateLibrary.sol"; // TODO: remove @@ -111,14 +110,22 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); } - function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData) + function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData, bool claims) public isAuthorizedForToken(params.tokenId) returns (BalanceDelta delta) { require(params.liquidityDelta != 0, "Must decrease liquidity"); Position storage position = positions[params.tokenId]; - delta = BaseLiquidityManagement.modifyLiquidity( + + (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, position.range.key.toId()); + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + sqrtPriceX96, + TickMath.getSqrtRatioAtTick(position.range.tickLower), + TickMath.getSqrtRatioAtTick(position.range.tickUpper), + params.liquidityDelta + ); + BaseLiquidityManagement.modifyLiquidity( position.range.key, IPoolManager.ModifyLiquidityParams({ tickLower: position.range.tickLower, @@ -131,33 +138,27 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi require(params.amount0Min <= uint256(uint128(-delta.amount0())), "INSUFFICIENT_AMOUNT0"); require(params.amount1Min <= uint256(uint128(-delta.amount1())), "INSUFFICIENT_AMOUNT1"); - // position.tokensOwed0 += - // uint128(amount0) + - // uint128( - // FullMath.mulDiv( - // feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, - // positionLiquidity, - // FixedPoint128.Q128 - // ) - // ); - // position.tokensOwed1 += - // uint128(amount1) + - // uint128( - // FullMath.mulDiv( - // feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, - // positionLiquidity, - // FixedPoint128.Q128 - // ) - // ); - - // position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; - // position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; - - // update the position + (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(position); + // TODO: for now we'll assume user always collects the totality of their fees + token0Owed += (position.tokensOwed0 + uint128(amount0)); + token1Owed += (position.tokensOwed1 + uint128(amount1)); + + // TODO: does this account for 0 token transfers + if (claims) { + poolManager.transfer(params.recipient, position.range.key.currency0.toId(), token0Owed); + poolManager.transfer(params.recipient, position.range.key.currency1.toId(), token1Owed); + } else { + sendToken(params.recipient, position.range.key.currency0, token0Owed); + sendToken(params.recipient, position.range.key.currency1, token1Owed); + } + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; position.liquidity -= params.liquidityDelta; + delta = toBalanceDelta(-int128(token0Owed), -int128(token1Owed)); } - function burn(uint256 tokenId, bytes calldata hookData) + function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external isAuthorizedForToken(tokenId) returns (BalanceDelta delta) @@ -171,9 +172,11 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi liquidityDelta: position.liquidity, amount0Min: 0, amount1Min: 0, + recipient: recipient, deadline: block.timestamp }), - hookData + hookData, + claims ); } @@ -189,41 +192,42 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi external returns (BalanceDelta delta) { - Position memory position = positions[tokenId]; + Position storage position = positions[tokenId]; BaseLiquidityManagement.collect(position.range, hookData); + (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(position); + delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); + + // TODO: for now we'll assume user always collects the totality of their fees + if (claims) { + poolManager.transfer(recipient, position.range.key.currency0.toId(), token0Owed + position.tokensOwed0); + poolManager.transfer(recipient, position.range.key.currency1.toId(), token1Owed + position.tokensOwed1); + } else { + sendToken(recipient, position.range.key.currency0, token0Owed + position.tokensOwed0); + sendToken(recipient, position.range.key.currency1, token1Owed + position.tokensOwed1); + } + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + + // TODO: event + } + + function _updateFeeGrowth(Position storage position) internal returns (uint128 token0Owed, uint128 token1Owed) { (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( position.range.key.toId(), position.range.tickLower, position.range.tickUpper ); - console2.log(feeGrowthInside0X128, position.feeGrowthInside0LastX128); - console2.log(feeGrowthInside1X128, position.feeGrowthInside1LastX128); - - // TODO: for now we'll assume user always collects the totality of their fees - uint128 token0Owed = uint128( - FullMath.mulDiv( - feeGrowthInside0X128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 - ) - ); - uint128 token1Owed = uint128( - FullMath.mulDiv( - feeGrowthInside1X128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 - ) + (token0Owed, token1Owed) = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity ); - delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); position.feeGrowthInside0LastX128 = feeGrowthInside0X128; position.feeGrowthInside1LastX128 = feeGrowthInside1X128; - - if (claims) { - poolManager.transfer(recipient, position.range.key.currency0.toId(), token0Owed); - poolManager.transfer(recipient, position.range.key.currency1.toId(), token1Owed); - } else { - sendToken(recipient, position.range.key.currency0, token0Owed); - sendToken(recipient, position.range.key.currency1, token1Owed); - } - - // TODO: event } function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index d5ee0479..fc8ca918 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -114,9 +114,10 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem delta = poolManager.modifyLiquidity(key, params, hookData); if (params.liquidityDelta <= 0) { - // removing liquidity/fees so take tokens - key.currency0.take(poolManager, sender, uint128(-delta.amount0()), claims); - key.currency1.take(poolManager, sender, uint128(-delta.amount1()), claims); + // removing liquidity/fees so mint tokens to the router + // the router will be responsible for sending the tokens to the desired recipient + key.currency0.take(poolManager, address(this), uint128(-delta.amount0()), true); + key.currency1.take(poolManager, address(this), uint128(-delta.amount1()), true); } else { // adding liquidity so pay tokens key.currency0.settle(poolManager, sender, uint128(delta.amount0()), claims); diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 23f17e6d..cb7c2c6b 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -36,13 +36,16 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { uint256 amount0Min; uint256 amount1Min; uint256 deadline; + address recipient; } - function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData) + function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); - function burn(uint256 tokenId, bytes calldata hookData) external returns (BalanceDelta delta); + function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) + external + returns (BalanceDelta delta); // TODO: in v3, we can partially collect fees, but what was the usecase here? function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) diff --git a/contracts/libraries/FeeMath.sol b/contracts/libraries/FeeMath.sol new file mode 100644 index 00000000..30e97d6c --- /dev/null +++ b/contracts/libraries/FeeMath.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; + +library FeeMath { + function getFeesOwed( + uint256 feeGrowthInside0X128, + uint256 feeGrowthInside1X128, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 liquidity + ) internal pure returns (uint128 token0Owed, uint128 token1Owed) { + token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); + token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); + } + + function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint128 liquidity) + internal + pure + returns (uint128 tokenOwed) + { + tokenOwed = + uint128(FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)); + } +} diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 32e1e53a..7a1b86f9 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -214,7 +214,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // burn liquidity uint256 balance0BeforeBurn = currency0.balanceOfSelf(); uint256 balance1BeforeBurn = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.burn(tokenId, ZERO_BYTES); + BalanceDelta delta = lpm.burn(tokenId, address(this), ZERO_BYTES, false); assertEq(lpm.liquidityOf(address(this), position.toId()), 0); // TODO: slightly off by 1 bip (0.0001%) @@ -254,14 +254,57 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi liquidityDelta: decreaseLiquidityDelta, amount0Min: 0, amount1Min: 0, + recipient: address(this), deadline: block.timestamp + 1 }); - BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES); + BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES, false); assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta - decreaseLiquidityDelta); + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0()))); assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1()))); } + function test_decreaseLiquidity_collectFees( + int24 tickLower, + int24 tickUpper, + uint128 liquidityDelta, + uint128 decreaseLiquidityDelta + ) public { + uint256 tokenId; + liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity + (tokenId, tickLower, tickUpper, liquidityDelta,) = + createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); + vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + vm.assume(0 < decreaseLiquidityDelta); + vm.assume(decreaseLiquidityDelta <= liquidityDelta); + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, int256(swapAmount), ZERO_BYTES); + + LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ + tokenId: tokenId, + liquidityDelta: decreaseLiquidityDelta, + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + 1 + }); + BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES, false); + assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta - decreaseLiquidityDelta, "GRR"); + + // express key.fee as wad (i.e. 3000 = 0.003e18) + uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); + assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); + } + function test_mintTransferBurn(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { @@ -295,7 +338,9 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // Alice can burn the token vm.prank(alice); - lpm.burn(tokenId, ZERO_BYTES); + lpm.burn(tokenId, address(this), ZERO_BYTES, false); + + // TODO: assert balances } function test_mintTransferCollect() public {} From 3d317e8775cf53fe4bb0159b4a076bfe391a67a7 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 25 Mar 2024 22:45:10 -0400 Subject: [PATCH 25/98] wip test decrease liquidity on same range --- test/position-managers/FeeCollection.t.sol | 82 ++++++++++++++++------ test/shared/fuzz/LiquidityFuzzers.sol | 28 ++++++-- 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index f3a4a46e..63ae8d30 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -178,33 +178,20 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { uint128 liquidityDeltaAlice, uint128 liquidityDeltaBob ) public { - uint256 tokenIdAlice; - uint256 tokenIdBob; liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); - - (tickLower, tickUpper, liquidityDeltaAlice) = - createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity - (,, liquidityDeltaBob) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaBob); - - vm.prank(alice); - (tokenIdAlice,) = lpm.mint( - LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), - liquidityDeltaAlice, - block.timestamp + 1, + uint256 tokenIdAlice; + uint256 tokenIdBob; + (tokenIdAlice, tokenIdBob, tickLower, tickUpper,,) = createFuzzySameRange( + lpm, alice, - ZERO_BYTES - ); - - vm.prank(bob); - (tokenIdBob,) = lpm.mint( + bob, LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), + liquidityDeltaAlice, liquidityDeltaBob, - block.timestamp + 1, - alice, ZERO_BYTES ); + vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity // swap to create fees uint256 swapAmount = 0.01e18; @@ -242,7 +229,56 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_collect_donate() public {} function test_collect_donate_sameRange() public {} - function test_mintTransferCollect() public {} - function test_mintTransferIncrease() public {} - function test_mintTransferDecrease() public {} + function test_decreaseLiquidity_sameRange( + int24 tickLower, + int24 tickUpper, + uint128 liquidityDeltaAlice, + uint128 liquidityDeltaBob + ) public { + liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity + liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); + uint256 tokenIdAlice; + uint256 tokenIdBob; + uint128 liquidityAlice; + uint128 liquidityBob; + (tokenIdAlice, tokenIdBob, tickLower, tickUpper, liquidityAlice, liquidityBob) = createFuzzySameRange( + lpm, + alice, + bob, + LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), + liquidityDeltaAlice, + liquidityDeltaBob, + ZERO_BYTES + ); + vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, true, int256(swapAmount), ZERO_BYTES); + + // alice removes all of her liquidity + uint256 balance0AliceBefore = manager.balanceOf(alice, currency0.toId()); + uint256 balance1AliceBefore = manager.balanceOf(alice, currency1.toId()); + console2.log(lpm.ownerOf(tokenIdAlice)); + console2.log(alice); + console2.log(address(this)); + vm.prank(alice); + BalanceDelta aliceDelta = lpm.decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: tokenIdAlice, + liquidityDelta: liquidityAlice, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: alice + }), + ZERO_BYTES, + true + ); + uint256 balance0AliceAfter = manager.balanceOf(alice, currency0.toId()); + uint256 balance1AliceAfter = manager.balanceOf(alice, currency1.toId()); + + assertEq(uint256(uint128(aliceDelta.amount0())), balance0AliceAfter - balance0AliceBefore); + assertEq(uint256(uint128(aliceDelta.amount1())), balance1AliceAfter - balance1AliceBefore); + } } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 9cadec9b..395c4249 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -45,12 +45,10 @@ contract LiquidityFuzzers is StdUtils { ); // round down ticks - tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; - tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; - _vm.assume(tickLower < tickUpper); + _tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; + _tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; + _vm.assume(_tickLower < _tickUpper); - _tickLower = tickLower; - _tickUpper = tickUpper; _liquidityDelta = liquidityDelta; } @@ -93,4 +91,24 @@ contract LiquidityFuzzers is StdUtils { _amount1 = bound(amount1, 0, maxAmount1); _vm.assume(_amount0 != 0 && _amount1 != 0); } + + function createFuzzySameRange( + INonfungiblePositionManager lpm, + address alice, + address bob, + LiquidityRange memory range, + uint128 liquidityA, + uint128 liquidityB, + bytes memory hookData + ) internal returns (uint256, uint256, int24, int24, uint128, uint128) { + (range.tickLower, range.tickUpper, liquidityA) = + createFuzzyLiquidityParams(range.key, range.tickLower, range.tickUpper, liquidityA); + // (,, liquidityB) = createFuzzyLiquidityParams(range.key, range.tickLower, range.tickUpper, liquidityB); + _vm.assume(liquidityB < Pool.tickSpacingToMaxLiquidityPerTick(range.key.tickSpacing)); + + (uint256 tokenIdA,) = lpm.mint(range, liquidityA, block.timestamp + 1, alice, hookData); + + (uint256 tokenIdB,) = lpm.mint(range, liquidityB, block.timestamp + 1, bob, hookData); + return (tokenIdA, tokenIdB, range.tickLower, range.tickUpper, liquidityA, liquidityB); + } } From 31a70cbaeb7d086061ad109e1703b83b09340468 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 26 Mar 2024 16:16:02 -0400 Subject: [PATCH 26/98] reworked fuzzers; more testing on fee claims for liquidity decreasing --- contracts/NonfungiblePositionManager.sol | 19 -- .../INonfungiblePositionManager.sol | 19 ++ test/position-managers/FeeCollection.t.sol | 194 ++++++++++++++---- .../NonfungiblePositionManager.t.sol | 10 +- test/shared/fuzz/LiquidityFuzzers.sol | 47 +++-- 5 files changed, 201 insertions(+), 88 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index fa2ba382..5977420b 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -31,25 +31,6 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} - // details about the uniswap position - struct Position { - // the nonce for permits - uint96 nonce; - // the address that is approved for spending this token - address operator; - LiquidityRange range; - // the liquidity of the position - // NOTE: this value will be less than BaseLiquidityManagement.liquidityOf, if the user - // owns multiple positions with the same range - uint128 liquidity; - // the fee growth of the aggregate position as of the last action on the individual position - uint256 feeGrowthInside0LastX128; - uint256 feeGrowthInside1LastX128; - // how many uncollected tokens are owed to the position, as of the last computation - uint128 tokensOwed0; - uint128 tokensOwed1; - } - mapping(uint256 tokenId => Position position) public positions; // NOTE: more gas efficient as LiquidityAmounts is used offchain diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index cb7c2c6b..f1b541ca 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -7,6 +7,25 @@ import {LiquidityRange} from "../types/LiquidityRange.sol"; import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; interface INonfungiblePositionManager is IBaseLiquidityManagement { + // details about the uniswap position + struct Position { + // the nonce for permits + uint96 nonce; + // the address that is approved for spending this token + address operator; + LiquidityRange range; + // the liquidity of the position + // NOTE: this value will be less than BaseLiquidityManagement.liquidityOf, if the user + // owns multiple positions with the same range + uint128 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } + struct MintParams { LiquidityRange range; uint256 amount0Desired; diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 63ae8d30..a710fae2 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -36,24 +36,30 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + // unused value for the fuzz helper functions uint128 constant DEAD_VALUE = 6969.6969 ether; + // expresses the fee as a wad (i.e. 3000 = 0.003e18) + uint256 FEE_WAD; + function setUp() public { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); lpm = new NonfungiblePositionManager(manager); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, 10_000_000 ether); - IERC20(Currency.unwrap(currency1)).transfer(alice, 10_000_000 ether); - IERC20(Currency.unwrap(currency0)).transfer(bob, 10_000_000 ether); - IERC20(Currency.unwrap(currency1)).transfer(bob, 10_000_000 ether); + IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); vm.startPrank(alice); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); @@ -82,9 +88,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertEq(delta.amount0(), 0); - // express key.fee as wad (i.e. 3000 = 0.003e18) - uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); + assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); } @@ -108,8 +112,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertEq(delta.amount0(), 0); // express key.fee as wad (i.e. 3000 = 0.003e18) - uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(feeWad), 1 wei); + assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); } @@ -126,10 +129,8 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); - (tickLower, tickUpper, liquidityDeltaAlice) = - createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); + (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity - (,, liquidityDeltaBob) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaBob); vm.prank(alice); (tokenIdAlice,) = lpm.mint( @@ -180,19 +181,26 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { ) public { liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); + uint256 tokenIdAlice; + vm.startPrank(alice); + (tokenIdAlice, tickLower, tickUpper, liquidityDeltaAlice,) = + createFuzzyLiquidity(lpm, alice, key, tickLower, tickUpper, liquidityDeltaAlice, ZERO_BYTES); + vm.stopPrank(); + uint256 tokenIdBob; - (tokenIdAlice, tokenIdBob, tickLower, tickUpper,,) = createFuzzySameRange( - lpm, - alice, - bob, - LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), - liquidityDeltaAlice, - liquidityDeltaBob, - ZERO_BYTES - ); + vm.startPrank(bob); + (tokenIdBob,,,,) = createFuzzyLiquidity(lpm, bob, key, tickLower, tickUpper, liquidityDeltaBob, ZERO_BYTES); + vm.stopPrank(); + vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity + // confirm the positions are same range + (,, LiquidityRange memory rangeAlice,,,,,) = lpm.positions(tokenIdAlice); + (,, LiquidityRange memory rangeBob,,,,,) = lpm.positions(tokenIdBob); + assertEq(rangeAlice.tickLower, rangeBob.tickLower); + assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); + // swap to create fees uint256 swapAmount = 0.01e18; swap(key, false, int256(swapAmount), ZERO_BYTES); @@ -237,36 +245,35 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { ) public { liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); + uint256 tokenIdAlice; + BalanceDelta lpDeltaAlice; + vm.startPrank(alice); + (tokenIdAlice, tickLower, tickUpper, liquidityDeltaAlice, lpDeltaAlice) = + createFuzzyLiquidity(lpm, alice, key, tickLower, tickUpper, liquidityDeltaAlice, ZERO_BYTES); + vm.stopPrank(); + uint256 tokenIdBob; - uint128 liquidityAlice; - uint128 liquidityBob; - (tokenIdAlice, tokenIdBob, tickLower, tickUpper, liquidityAlice, liquidityBob) = createFuzzySameRange( - lpm, - alice, - bob, - LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), - liquidityDeltaAlice, - liquidityDeltaBob, - ZERO_BYTES - ); + BalanceDelta lpDeltaBob; + vm.startPrank(bob); + (tokenIdBob,,,, lpDeltaBob) = + createFuzzyLiquidity(lpm, bob, key, tickLower, tickUpper, liquidityDeltaBob, ZERO_BYTES); + vm.stopPrank(); + vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity // swap to create fees - uint256 swapAmount = 0.01e18; + uint256 swapAmount = 0.001e18; swap(key, true, int256(swapAmount), ZERO_BYTES); // alice removes all of her liquidity - uint256 balance0AliceBefore = manager.balanceOf(alice, currency0.toId()); - uint256 balance1AliceBefore = manager.balanceOf(alice, currency1.toId()); - console2.log(lpm.ownerOf(tokenIdAlice)); - console2.log(alice); - console2.log(address(this)); + // uint256 balance0AliceBefore = manager.balanceOf(alice, currency0.toId()); + // uint256 balance1AliceBefore = manager.balanceOf(alice, currency1.toId()); vm.prank(alice); BalanceDelta aliceDelta = lpm.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: tokenIdAlice, - liquidityDelta: liquidityAlice, + liquidityDelta: liquidityDeltaAlice, amount0Min: 0, amount1Min: 0, deadline: block.timestamp + 1, @@ -275,10 +282,111 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { ZERO_BYTES, true ); - uint256 balance0AliceAfter = manager.balanceOf(alice, currency0.toId()); - uint256 balance1AliceAfter = manager.balanceOf(alice, currency1.toId()); + assertEq(uint256(uint128(-aliceDelta.amount0())), manager.balanceOf(alice, currency0.toId())); + assertEq(uint256(uint128(-aliceDelta.amount1())), manager.balanceOf(alice, currency1.toId())); - assertEq(uint256(uint128(aliceDelta.amount0())), balance0AliceAfter - balance0AliceBefore); - assertEq(uint256(uint128(aliceDelta.amount1())), balance1AliceAfter - balance1AliceBefore); + // bob removes half of his liquidity + vm.prank(bob); + BalanceDelta bobDelta = lpm.decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: tokenIdBob, + liquidityDelta: liquidityDeltaBob / 2, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: bob + }), + ZERO_BYTES, + true + ); + assertEq(uint256(uint128(-bobDelta.amount0())), manager.balanceOf(bob, currency0.toId())); + assertEq(uint256(uint128(-bobDelta.amount1())), manager.balanceOf(bob, currency1.toId())); + + // position manager holds no fees now + assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); + assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); + } + + /// @dev Alice and bob create liquidity on the same range + /// when alice decreases liquidity, she should only collect her fees + function test_decreaseLiquidity_sameRange_exact() public { + // alice and bob create liquidity on the same range [-120, 120] + LiquidityRange memory range = LiquidityRange({key: key, tickLower: -120, tickUpper: 120}); + + // alice provisions 3x the amount of liquidity as bob + uint256 liquidityAlice = 3000e18; + uint256 liquidityBob = 1000e18; + vm.prank(alice); + (uint256 tokenIdAlice, BalanceDelta lpDeltaAlice) = + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + vm.prank(bob); + (uint256 tokenIdBob, BalanceDelta lpDeltaBob) = + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, int256(swapAmount), ZERO_BYTES); + swap(key, false, int256(swapAmount), ZERO_BYTES); // move the price back + + // alice decreases liquidity + vm.prank(alice); + BalanceDelta aliceDelta = lpm.decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: tokenIdAlice, + liquidityDelta: uint128(liquidityAlice), + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: alice + }), + ZERO_BYTES, + true + ); + + uint256 tolerance = 0.000000001 ether; + + // alice claims original principal + her fees + assertApproxEqAbs( + manager.balanceOf(alice, currency0.toId()), + uint256(int256(lpDeltaAlice.amount0())) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + tolerance + ); + assertApproxEqAbs( + manager.balanceOf(alice, currency1.toId()), + uint256(int256(lpDeltaAlice.amount1())) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + tolerance + ); + + // bob decreases half of his liquidity + vm.prank(bob); + BalanceDelta bobDelta = lpm.decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: tokenIdBob, + liquidityDelta: uint128(liquidityBob / 2), + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: bob + }), + ZERO_BYTES, + true + ); + + // bob claims half of the original principal + his fees + assertApproxEqAbs( + manager.balanceOf(bob, currency0.toId()), + uint256(int256(lpDeltaBob.amount0()) / 2) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + tolerance + ); + assertApproxEqAbs( + manager.balanceOf(bob, currency1.toId()), + uint256(int256(lpDeltaBob.amount1()) / 2) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + tolerance + ); } } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 7a1b86f9..91568044 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -51,7 +51,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi } function test_mint_withLiquidityDelta(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { - (tickLower, tickUpper, liquidityDelta) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); + (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); @@ -69,7 +69,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi } function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); (amount0Desired, amount1Desired) = createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); @@ -132,7 +132,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); (amount0Desired, amount1Desired) = createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); @@ -155,7 +155,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); vm.assume(tickLower < 0); vm.assume(tickUpper > 0); @@ -308,7 +308,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mintTransferBurn(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - (tickLower, tickUpper,) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); (amount0Desired, amount1Desired) = createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 395c4249..1facdf59 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -14,21 +14,13 @@ import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; contract LiquidityFuzzers is StdUtils { Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - /// @dev Obtain fuzzed parameters for creating liquidity - /// @param key The pool key - /// @param tickLower The lower tick - /// @param tickUpper The upper tick - /// @param liquidityDelta The liquidity delta - function createFuzzyLiquidityParams(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidityDelta) - internal - view - returns (int24 _tickLower, int24 _tickUpper, uint128 _liquidityDelta) - { + function assumeLiquidityDelta(PoolKey memory key, uint128 liquidityDelta) internal pure { _vm.assume(0.0000001e18 < liquidityDelta); - _vm.assume(liquidityDelta < Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)); + } + function boundTicks(PoolKey memory key, int24 tickLower, int24 tickUpper) internal view returns (int24, int24) { tickLower = int24( bound( int256(tickLower), @@ -45,11 +37,24 @@ contract LiquidityFuzzers is StdUtils { ); // round down ticks - _tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; - _tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; - _vm.assume(_tickLower < _tickUpper); + tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; + tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; + _vm.assume(tickLower < tickUpper); + return (tickLower, tickUpper); + } - _liquidityDelta = liquidityDelta; + /// @dev Obtain fuzzed parameters for creating liquidity + /// @param key The pool key + /// @param tickLower The lower tick + /// @param tickUpper The upper tick + /// @param liquidityDelta The liquidity delta + function createFuzzyLiquidityParams(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidityDelta) + internal + view + returns (int24 _tickLower, int24 _tickUpper) + { + assumeLiquidityDelta(key, liquidityDelta); + (_tickLower, _tickUpper) = boundTicks(key, tickLower, tickUpper); } function createFuzzyLiquidity( @@ -64,8 +69,8 @@ contract LiquidityFuzzers is StdUtils { internal returns (uint256 _tokenId, int24 _tickLower, int24 _tickUpper, uint128 _liquidityDelta, BalanceDelta _delta) { - (_tickLower, _tickUpper, _liquidityDelta) = - createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); + (_tickLower, _tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); + _liquidityDelta = liquidityDelta; (_tokenId, _delta) = lpm.mint( LiquidityRange({key: key, tickLower: _tickLower, tickUpper: _tickUpper}), _liquidityDelta, @@ -101,10 +106,10 @@ contract LiquidityFuzzers is StdUtils { uint128 liquidityB, bytes memory hookData ) internal returns (uint256, uint256, int24, int24, uint128, uint128) { - (range.tickLower, range.tickUpper, liquidityA) = - createFuzzyLiquidityParams(range.key, range.tickLower, range.tickUpper, liquidityA); - // (,, liquidityB) = createFuzzyLiquidityParams(range.key, range.tickLower, range.tickUpper, liquidityB); - _vm.assume(liquidityB < Pool.tickSpacingToMaxLiquidityPerTick(range.key.tickSpacing)); + assumeLiquidityDelta(range.key, liquidityA); + assumeLiquidityDelta(range.key, liquidityB); + + (range.tickLower, range.tickUpper) = boundTicks(range.key, range.tickLower, range.tickUpper); (uint256 tokenIdA,) = lpm.mint(range, liquidityA, block.timestamp + 1, alice, hookData); From 666faf80b73ca0f892a12d0dd0a64f640a46a78d Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 27 Mar 2024 13:14:37 -0400 Subject: [PATCH 27/98] forge fmt --- contracts/base/LockAndBatchCall.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol index 4b89b033..76deb511 100644 --- a/contracts/base/LockAndBatchCall.sol +++ b/contracts/base/LockAndBatchCall.sol @@ -14,8 +14,7 @@ abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { /// @param executeData The function selectors and calldata for any of the function selectors in ICallsWithLock encoded as an array of bytes. function execute(bytes memory executeData, bytes memory settleData) external { - (bytes memory lockReturnData) = - poolManager.lock(abi.encode(executeData, abi.encode(msg.sender, settleData))); + (bytes memory lockReturnData) = poolManager.lock(abi.encode(executeData, abi.encode(msg.sender, settleData))); (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); _handleAfterExecute(executeReturnData, settleReturnData); } From 3c56d48a89402d3c229a60b347a4e042bf99a0fb Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 27 Mar 2024 17:17:02 -0400 Subject: [PATCH 28/98] test fixes for flipped deltas --- contracts/base/BaseLiquidityManagement.sol | 15 +++++-------- test/position-managers/FeeCollection.t.sol | 22 +++++++++---------- .../NonfungiblePositionManager.t.sol | 21 +++++++++--------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index fc8ca918..8cce6577 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -46,9 +46,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); delta = abi.decode( - poolManager.lock( - address(this), abi.encodeCall(this.handleModifyPosition, (msg.sender, key, params, hookData, false)) - ), + poolManager.lock(abi.encodeCall(this.handleModifyPosition, (msg.sender, key, params, hookData, false))), (BalanceDelta) ); @@ -68,7 +66,6 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem function collect(LiquidityRange memory range, bytes calldata hookData) internal returns (BalanceDelta delta) { delta = abi.decode( poolManager.lock( - address(this), abi.encodeCall( this.handleModifyPosition, ( @@ -89,7 +86,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem } function sendToken(address recipient, Currency currency, uint256 amount) internal { - poolManager.lock(address(this), abi.encodeCall(this.handleRedeemClaim, (recipient, currency, amount))); + poolManager.lock(abi.encodeCall(this.handleRedeemClaim, (recipient, currency, amount))); } function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { @@ -116,12 +113,12 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem if (params.liquidityDelta <= 0) { // removing liquidity/fees so mint tokens to the router // the router will be responsible for sending the tokens to the desired recipient - key.currency0.take(poolManager, address(this), uint128(-delta.amount0()), true); - key.currency1.take(poolManager, address(this), uint128(-delta.amount1()), true); + key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); + key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); } else { // adding liquidity so pay tokens - key.currency0.settle(poolManager, sender, uint128(delta.amount0()), claims); - key.currency1.settle(poolManager, sender, uint128(delta.amount1()), claims); + key.currency0.settle(poolManager, sender, uint128(-delta.amount0()), claims); + key.currency1.settle(poolManager, sender, uint128(-delta.amount1()), claims); } } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index a710fae2..0f6afbc7 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -79,7 +79,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // swap to create fees uint256 swapAmount = 0.01e18; - swap(key, false, int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // collect fees uint256 balance0Before = currency0.balanceOfSelf(); @@ -102,7 +102,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // swap to create fees uint256 swapAmount = 0.01e18; - swap(key, false, int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // collect fees uint256 balance0Before = currency0.balanceOfSelf(); @@ -152,7 +152,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // swap to create fees uint256 swapAmount = 0.01e18; - swap(key, false, int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // alice collects only her fees vm.prank(alice); @@ -203,7 +203,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // swap to create fees uint256 swapAmount = 0.01e18; - swap(key, false, int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // alice collects only her fees uint256 balance0AliceBefore = currency0.balanceOf(alice); @@ -264,7 +264,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // swap to create fees uint256 swapAmount = 0.001e18; - swap(key, true, int256(swapAmount), ZERO_BYTES); + swap(key, true, -int256(swapAmount), ZERO_BYTES); // alice removes all of her liquidity // uint256 balance0AliceBefore = manager.balanceOf(alice, currency0.toId()); @@ -326,8 +326,8 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // swap to create fees uint256 swapAmount = 0.001e18; - swap(key, true, int256(swapAmount), ZERO_BYTES); - swap(key, false, int256(swapAmount), ZERO_BYTES); // move the price back + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back // alice decreases liquidity vm.prank(alice); @@ -349,13 +349,13 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice claims original principal + her fees assertApproxEqAbs( manager.balanceOf(alice, currency0.toId()), - uint256(int256(lpDeltaAlice.amount0())) + uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), tolerance ); assertApproxEqAbs( manager.balanceOf(alice, currency1.toId()), - uint256(int256(lpDeltaAlice.amount1())) + uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), tolerance ); @@ -378,13 +378,13 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // bob claims half of the original principal + his fees assertApproxEqAbs( manager.balanceOf(bob, currency0.toId()), - uint256(int256(lpDeltaBob.amount0()) / 2) + uint256(int256(-lpDeltaBob.amount0()) / 2) + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), tolerance ); assertApproxEqAbs( manager.balanceOf(bob, currency1.toId()), - uint256(int256(lpDeltaBob.amount1()) / 2) + uint256(int256(-lpDeltaBob.amount1()) / 2) + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), tolerance ); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 91568044..d4d0ee6c 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -64,8 +64,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); - assertEq(balance0Before - balance0After, uint256(int256(delta.amount0())), "incorrect amount0"); - assertEq(balance1Before - balance1After, uint256(int256(delta.amount1())), "incorrect amount1"); + assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); } function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { @@ -93,8 +93,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - assertEq(balance0Before - balance0After, uint256(int256(delta.amount0()))); - assertEq(balance1Before - balance1After, uint256(int256(delta.amount1()))); + assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); } // minting with perfect token ratios will use all of the tokens @@ -123,10 +123,10 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - assertEq(uint256(int256(delta.amount0())), amount0Desired); - assertEq(uint256(int256(delta.amount1())), amount1Desired); - assertEq(balance0Before - balance0After, uint256(int256(delta.amount0()))); - assertEq(balance1Before - balance1After, uint256(int256(delta.amount1()))); + assertEq(uint256(int256(-delta.amount0())), amount0Desired); + assertEq(uint256(int256(-delta.amount1())), amount1Desired); + assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); } function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) @@ -156,8 +156,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi public { (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - vm.assume(tickLower < 0); - vm.assume(tickUpper > 0); + vm.assume(tickLower < 0 && 0 < tickUpper); (amount0Desired, amount1Desired) = createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); @@ -191,7 +190,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi ); // swap to move the price - swap(key, true, 1000e18, ZERO_BYTES); + swap(key, true, -1000e18, ZERO_BYTES); // will revert because amount0Min and amount1Min are very strict vm.expectRevert(); From f4275ccb20229a6fbe82bc9913913d9de5011cd9 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 3 Apr 2024 11:09:42 -0400 Subject: [PATCH 29/98] wip --- contracts/NonfungiblePositionManager.sol | 52 ++++++- contracts/base/BaseLiquidityManagement.sol | 71 ++++++++- .../INonfungiblePositionManager.sol | 8 ++ .../position-managers/IncreaseLiquidity.t.sol | 136 ++++++++++++++++++ 4 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 test/position-managers/IncreaseLiquidity.t.sol diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 5977420b..994172c3 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -9,6 +9,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; import {LiquidityRange, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; @@ -22,16 +23,35 @@ import {console2} from "forge-std/console2.sol"; contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; using LiquidityRangeIdLibrary for LiquidityRange; using PoolStateLibrary for IPoolManager; /// @dev The ID of the next token that will be minted. Skips 0 uint256 private _nextId = 1; + mapping(uint256 tokenId => Position position) public positions; constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} - mapping(uint256 tokenId => Position position) public positions; + // --- View Functions --- // + function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { + Position memory position = positions[tokenId]; + + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( + position.range.key.toId(), position.range.tickLower, position.range.tickUpper + ); + + (token0Owed, token1Owed) = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity + ); + token0Owed += position.tokensOwed0; + token1Owed += position.tokensOwed1; + } // NOTE: more gas efficient as LiquidityAmounts is used offchain // TODO: deadline check @@ -91,6 +111,36 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); } + function increaseLiquidity(IncreaseLiquidityParams memory params, bytes calldata hookData, bool claims) + public + isAuthorizedForToken(params.tokenId) + returns (BalanceDelta delta) + { + require(params.liquidityDelta != 0, "Must increase liquidity"); + Position storage position = positions[params.tokenId]; + + (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(position); + + BaseLiquidityManagement.increaseLiquidity( + position.range.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: position.range.tickLower, + tickUpper: position.range.tickUpper, + liquidityDelta: int256(uint256(params.liquidityDelta)) + }), + hookData, + claims, + ownerOf(params.tokenId), + token0Owed, + token1Owed + ); + // TODO: slippage checks & test + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + position.liquidity += params.liquidityDelta; + } + function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData, bool claims) public isAuthorizedForToken(params.tokenId) diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 8cce6577..7e6479df 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -3,15 +3,18 @@ pragma solidity ^0.8.24; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {PoolStateLibrary} from "../libraries/PoolStateLibrary.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {FeeMath} from "../libraries/FeeMath.sol"; // TODO: remove import {console2} from "forge-std/console2.sol"; @@ -20,6 +23,8 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; + using PoolIdLibrary for PoolKey; + using PoolStateLibrary for IPoolManager; error LockFailure(); @@ -35,7 +40,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} - // NOTE: handles add/remove/collect + // NOTE: handles mint/remove/collect function modifyLiquidity( PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params, @@ -63,6 +68,28 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem // } } + function increaseLiquidity( + PoolKey memory key, + IPoolManager.ModifyLiquidityParams memory params, + bytes calldata hookData, + bool claims, + address owner, + uint256 token0Owed, + uint256 token1Owed + ) internal returns (BalanceDelta delta) { + delta = abi.decode( + poolManager.lock( + abi.encodeCall( + this.handleIncreaseLiquidity, (msg.sender, key, params, hookData, claims, token0Owed, token1Owed) + ) + ), + (BalanceDelta) + ); + + liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] += + uint256(params.liquidityDelta); + } + function collect(LiquidityRange memory range, bytes calldata hookData) internal returns (BalanceDelta delta) { delta = abi.decode( poolManager.lock( @@ -122,6 +149,46 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem } } + // TODO: selfOnly modifier + function handleIncreaseLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData, + bool claims, + uint256 token0Owed, + uint256 token1Owed + ) external returns (BalanceDelta delta) { + BalanceDelta feeDelta = poolManager.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams({ + tickLower: params.tickLower, + tickUpper: params.tickUpper, + liquidityDelta: 0 + }), + hookData + ); + + { + BalanceDelta d = poolManager.modifyLiquidity(key, params, hookData); + console2.log("d0", int256(d.amount0())); + console2.log("d1", int256(d.amount1())); + } + + { + BalanceDelta excessFees = feeDelta - toBalanceDelta(int128(int256(token0Owed)), int128(int256(token1Owed))); + key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); + key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); + + int256 amount0Delta = poolManager.currencyDelta(address(this), key.currency0); + int256 amount1Delta = poolManager.currencyDelta(address(this), key.currency1); + if (amount0Delta < 0) key.currency0.settle(poolManager, sender, uint256(-amount0Delta), claims); + if (amount1Delta < 0) key.currency1.settle(poolManager, sender, uint256(-amount1Delta), claims); + if (amount0Delta > 0) key.currency0.take(poolManager, address(this), uint256(amount0Delta), true); + if (amount1Delta > 0) key.currency1.take(poolManager, address(this), uint256(amount1Delta), true); + } + } + // TODO: selfOnly modifier function handleRedeemClaim(address recipient, Currency currency, uint256 amount) external { poolManager.burn(address(this), currency.toId(), amount); diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index f1b541ca..cde005e9 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -49,6 +49,14 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { // NOTE: more expensive since LiquidityAmounts is used onchain function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); + struct IncreaseLiquidityParams { + uint256 tokenId; + uint128 liquidityDelta; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidityDelta; diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol new file mode 100644 index 00000000..9420c468 --- /dev/null +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {PoolStateLibrary} from "../../contracts/libraries/PoolStateLibrary.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + using PoolIdLibrary for PoolKey; + + NonfungiblePositionManager lpm; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + // unused value for the fuzz helper functions + uint128 constant DEAD_VALUE = 6969.6969 ether; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + LiquidityRange range; + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + lpm = new NonfungiblePositionManager(manager); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + + // Give tokens to Alice and Bob, with approvals + IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); + vm.startPrank(alice); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + vm.startPrank(bob); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + + // define a reusable range + range = LiquidityRange({key: key, tickLower: -300, tickUpper: 300}); + } + + function test_increaseLiquidity_withExactFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice uses her exact fees to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + console2.log("token0Owed", token0Owed); + console2.log("token1Owed", token1Owed); + + (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtRatioAtTick(range.tickLower), + TickMath.getSqrtRatioAtTick(range.tickUpper), + token0Owed, + token1Owed + ); + + vm.prank(alice); + lpm.increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: tokenIdAlice, + liquidityDelta: uint128(liquidityDelta), + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1 + }), + ZERO_BYTES, + false + ); + } + + function test_increaseLiquidity_withExcessFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Excess fees are returned to her as 6909 + } + function test_increaseLiquidity_withInsufficientFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Additional funds are used by alice to increase liquidity + } +} From 245cc3eb4f50a98d54f73aab77af58d5e0e2348a Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 5 Apr 2024 14:58:20 -0400 Subject: [PATCH 30/98] test coverage for increase liquidity cases --- contracts/NonfungiblePositionManager.sol | 6 +- contracts/base/BaseLiquidityManagement.sol | 22 ++- .../position-managers/IncreaseLiquidity.t.sol | 156 +++++++++++++++++- 3 files changed, 171 insertions(+), 13 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 994172c3..9ad8df13 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -121,7 +121,7 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(position); - BaseLiquidityManagement.increaseLiquidity( + delta = BaseLiquidityManagement.increaseLiquidity( position.range.key, IPoolManager.ModifyLiquidityParams({ tickLower: position.range.tickLower, @@ -136,8 +136,8 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi ); // TODO: slippage checks & test - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; + delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; + delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; position.liquidity += params.liquidityDelta; } diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 7e6479df..6b243e20 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -80,7 +80,15 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem delta = abi.decode( poolManager.lock( abi.encodeCall( - this.handleIncreaseLiquidity, (msg.sender, key, params, hookData, claims, token0Owed, token1Owed) + this.handleIncreaseLiquidity, + ( + msg.sender, + key, + params, + hookData, + claims, + toBalanceDelta(int128(int256(token0Owed)), int128(int256(token1Owed))) + ) ) ), (BalanceDelta) @@ -156,8 +164,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem IPoolManager.ModifyLiquidityParams calldata params, bytes calldata hookData, bool claims, - uint256 token0Owed, - uint256 token1Owed + BalanceDelta tokensOwed ) external returns (BalanceDelta delta) { BalanceDelta feeDelta = poolManager.modifyLiquidity( key, @@ -169,14 +176,10 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem hookData ); - { - BalanceDelta d = poolManager.modifyLiquidity(key, params, hookData); - console2.log("d0", int256(d.amount0())); - console2.log("d1", int256(d.amount1())); - } + poolManager.modifyLiquidity(key, params, hookData); { - BalanceDelta excessFees = feeDelta - toBalanceDelta(int128(int256(token0Owed)), int128(int256(token1Owed))); + BalanceDelta excessFees = feeDelta - tokensOwed; key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); @@ -186,6 +189,7 @@ abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagem if (amount1Delta < 0) key.currency1.settle(poolManager, sender, uint256(-amount1Delta), claims); if (amount0Delta > 0) key.currency0.take(poolManager, address(this), uint256(amount0Delta), true); if (amount1Delta > 0) key.currency1.take(poolManager, address(this), uint256(amount1Delta), true); + delta = toBalanceDelta(int128(amount0Delta), int128(amount1Delta)); } } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 9420c468..666619db 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -123,14 +123,168 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { ZERO_BYTES, false ); + + // TODO: assertions, currently increasing liquidity does not perfectly use the fees } function test_increaseLiquidity_withExcessFees() public { // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Excess fees are returned to her as 6909 + // Alice uses her fees to increase liquidity. Excess fees are accounted to alice + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice will half of her fees to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + { + (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtRatioAtTick(range.tickLower), + TickMath.getSqrtRatioAtTick(range.tickUpper), + token0Owed / 2, + token1Owed / 2 + ); + + vm.prank(alice); + lpm.increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: tokenIdAlice, + liquidityDelta: uint128(liquidityDelta), + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1 + }), + ZERO_BYTES, + false + ); + } + + { + // bob collects his fees + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.prank(bob); + lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + uint256 balance0AfterBob = currency0.balanceOf(bob); + uint256 balance1AfterBob = currency1.balanceOf(bob); + assertApproxEqAbs( + balance0AfterBob - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + assertApproxEqAbs( + balance1AfterBob - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + } + + { + // alice collects her fees, which should be about half of the fees + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.prank(alice); + lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); + uint256 balance0AfterAlice = currency0.balanceOf(alice); + uint256 balance1AfterAlice = currency1.balanceOf(alice); + assertApproxEqAbs( + balance0AfterAlice - balance0BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + 9 wei + ); + assertApproxEqAbs( + balance1AfterAlice - balance1BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + 1 wei + ); + } } + function test_increaseLiquidity_withInsufficientFees() public { // Alice and Bob provide liquidity on the range // Alice uses her fees to increase liquidity. Additional funds are used by alice to increase liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice will use all of her fees + additional capital to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + { + (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtRatioAtTick(range.tickLower), + TickMath.getSqrtRatioAtTick(range.tickUpper), + token0Owed * 2, + token1Owed * 2 + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.prank(alice); + lpm.increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: tokenIdAlice, + liquidityDelta: uint128(liquidityDelta), + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1 + }), + ZERO_BYTES, + false + ); + uint256 balance0AfterAlice = currency0.balanceOf(alice); + uint256 balance1AfterAlice = currency1.balanceOf(alice); + + assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, token0Owed, 37 wei); + assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, token1Owed, 1 wei); + } + + { + // bob collects his fees + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.prank(bob); + lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + uint256 balance0AfterBob = currency0.balanceOf(bob); + uint256 balance1AfterBob = currency1.balanceOf(bob); + assertApproxEqAbs( + balance0AfterBob - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + assertApproxEqAbs( + balance1AfterBob - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + } } } From f971b3d41df5b9c44715cea8301ce9797ccd46a1 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 5 Apr 2024 17:39:19 -0400 Subject: [PATCH 31/98] preliminary gas benchmarks --- .forge-snapshots/decreaseLiquidity_erc20.snap | 1 + .../decreaseLiquidity_erc6909.snap | 1 + .forge-snapshots/increaseLiquidity_erc20.snap | 1 + .../increaseLiquidity_erc6909.snap | 1 + .forge-snapshots/mint.snap | 1 + .forge-snapshots/mintWithLiquidity.snap | 1 + test/position-managers/Gas.t.sol | 164 ++++++++++++++++++ 7 files changed, 170 insertions(+) create mode 100644 .forge-snapshots/decreaseLiquidity_erc20.snap create mode 100644 .forge-snapshots/decreaseLiquidity_erc6909.snap create mode 100644 .forge-snapshots/increaseLiquidity_erc20.snap create mode 100644 .forge-snapshots/increaseLiquidity_erc6909.snap create mode 100644 .forge-snapshots/mint.snap create mode 100644 .forge-snapshots/mintWithLiquidity.snap create mode 100644 test/position-managers/Gas.t.sol diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap new file mode 100644 index 00000000..73c96768 --- /dev/null +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -0,0 +1 @@ +222794 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap new file mode 100644 index 00000000..4d9543e1 --- /dev/null +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -0,0 +1 @@ +167494 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap new file mode 100644 index 00000000..af1b03da --- /dev/null +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -0,0 +1 @@ +128154 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap new file mode 100644 index 00000000..58654a31 --- /dev/null +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -0,0 +1 @@ +136428 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap new file mode 100644 index 00000000..a9b719e8 --- /dev/null +++ b/.forge-snapshots/mint.snap @@ -0,0 +1 @@ +475877 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap new file mode 100644 index 00000000..7ca9159e --- /dev/null +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -0,0 +1 @@ +478504 \ No newline at end of file diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol new file mode 100644 index 00000000..5b98ac97 --- /dev/null +++ b/test/position-managers/Gas.t.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {PoolStateLibrary} from "../../contracts/libraries/PoolStateLibrary.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +contract GasTest is Test, Deployers, GasSnapshot { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + using PoolIdLibrary for PoolKey; + + NonfungiblePositionManager lpm; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + // unused value for the fuzz helper functions + uint128 constant DEAD_VALUE = 6969.6969 ether; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + LiquidityRange range; + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + lpm = new NonfungiblePositionManager(manager); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + + // mint some ERC6909 tokens + claimsRouter.deposit(currency0, address(this), 100_000_000 ether); + claimsRouter.deposit(currency1, address(this), 100_000_000 ether); + manager.setOperator(address(lpm), true); + + // define a reusable range + range = LiquidityRange({key: key, tickLower: -300, tickUpper: 300}); + } + + function test_gas_mint() public { + uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity + uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + range: range, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1, + recipient: address(this), + hookData: ZERO_BYTES + }); + snapStart("mint"); + lpm.mint(params); + snapEnd(); + } + + function test_gas_mintWithLiquidity() public { + snapStart("mintWithLiquidity"); + lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + snapEnd(); + } + + function test_gas_increaseLiquidity_erc20() public { + (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + + INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager + .IncreaseLiquidityParams({ + tokenId: tokenId, + liquidityDelta: 1000 ether, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1 + }); + snapStart("increaseLiquidity_erc20"); + lpm.increaseLiquidity(params, ZERO_BYTES, false); + snapEnd(); + } + + function test_gas_increaseLiquidity_erc6909() public { + (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + + INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager + .IncreaseLiquidityParams({ + tokenId: tokenId, + liquidityDelta: 1000 ether, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1 + }); + snapStart("increaseLiquidity_erc6909"); + lpm.increaseLiquidity(params, ZERO_BYTES, true); + snapEnd(); + } + + function test_gas_decreaseLiquidity_erc20() public { + (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + + INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ + tokenId: tokenId, + liquidityDelta: 10_000 ether, + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + 1 + }); + snapStart("decreaseLiquidity_erc20"); + lpm.decreaseLiquidity(params, ZERO_BYTES, false); + snapEnd(); + } + + function test_gas_decreaseLiquidity_erc6909() public { + (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + + INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ + tokenId: tokenId, + liquidityDelta: 10_000 ether, + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + 1 + }); + snapStart("decreaseLiquidity_erc6909"); + lpm.decreaseLiquidity(params, ZERO_BYTES, true); + snapEnd(); + } + + function test_gas_burn() public {} + function test_gas_burnEmpty() public {} + function test_gas_collect() public {} +} From 0165be580dcc69976596029ead6302ed9e641065 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:43:46 -0400 Subject: [PATCH 32/98] Position manager refactor (#2) * chore: update v4-core:latest (#105) * update core * rename lockAcquired to unlockCallback * update core; temporary path hack in remappings * update v4-core; remove remapping * wip: fix compatibility * update core; fix renaming of swap fee to lp fee * update core; fix events * update core; address liquidity salt and modify liquidity return values * fix incorrect delta accounting when modifying liquidity * fix todo, use CurrencySettleTake * remove deadcode * update core; use StateLibrary; update sqrtRatio to sqrtPrice * fix beforeSwap return signatures * forge fmt; remove commented out code * update core (wow gas savings) * update core * update core * update core; hook flags LSB * update core * update core * chore: update v4 core (#115) * Update v4-core * CurrencySettleTake -> CurrencySettler * Snapshots * compiling but very broken * replace PoolStateLibrary * update currency settle take * compiling * wip * use v4-core's forge-std * test liquidity increase * additional fixes for collection and liquidity decrease * test migration * replace old implementation with new --------- Signed-off-by: saucepoint Co-authored-by: 0x57 --- .../FullRangeAddInitialLiquidity.snap | 2 +- .forge-snapshots/FullRangeAddLiquidity.snap | 2 +- .forge-snapshots/FullRangeFirstSwap.snap | 2 +- .forge-snapshots/FullRangeInitialize.snap | 2 +- .../FullRangeRemoveLiquidity.snap | 2 +- .../FullRangeRemoveLiquidityAndRebalance.snap | 2 +- .forge-snapshots/FullRangeSecondSwap.snap | 2 +- .forge-snapshots/FullRangeSwap.snap | 2 +- .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mint.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- .gitmodules | 3 - contracts/BaseHook.sol | 11 +- contracts/NonfungiblePositionManager.sol | 247 +++------- contracts/SimpleBatchCall.sol | 21 +- contracts/base/BaseLiquidityHandler.sol | 237 ++++++++++ contracts/base/BaseLiquidityManagement.sol | 198 ++------ contracts/base/CallsWithLock.sol | 4 +- contracts/base/LockAndBatchCall.sol | 6 +- contracts/base/SafeCallback.sol | 10 +- contracts/hooks/examples/FullRange.sol | 66 +-- contracts/hooks/examples/GeomeanOracle.sol | 13 +- contracts/hooks/examples/LimitOrder.sol | 93 ++-- contracts/hooks/examples/TWAMM.sol | 43 +- contracts/hooks/examples/VolatilityOracle.sol | 12 +- .../IAdvancedLiquidityManagement.sol | 20 - .../interfaces/IBaseLiquidityManagement.sol | 21 - .../INonfungiblePositionManager.sol | 57 +-- contracts/interfaces/IQuoter.sol | 2 +- contracts/lens/Quoter.sol | 22 +- contracts/libraries/CurrencyDeltas.sol | 40 ++ contracts/libraries/CurrencySenderLibrary.sol | 31 ++ contracts/libraries/CurrencySettleTake.sol | 33 +- contracts/libraries/FeeMath.sol | 9 +- contracts/libraries/LiquiditySaltLibrary.sol | 21 + contracts/libraries/PoolGetters.sol | 9 +- contracts/libraries/PoolStateLibrary.sol | 336 -------------- contracts/libraries/PoolTicksCounter.sol | 11 +- lib/forge-std | 1 - lib/v4-core | 2 +- remappings.txt | 2 +- test/FullRange.t.sol | 72 +-- test/GeomeanOracle.t.sol | 30 +- test/LimitOrder.t.sol | 31 +- test/Quoter.t.sol | 47 +- test/SimpleBatchCallTest.t.sol | 20 +- test/TWAMM.t.sol | 12 +- test/position-managers/FeeCollection.t.sol | 190 +++----- test/position-managers/Gas.t.sol | 82 +--- .../position-managers/IncreaseLiquidity.t.sol | 67 +-- .../NonfungiblePositionManager.t.sol | 435 ++++++++---------- test/shared/fuzz/LiquidityFuzzers.sol | 107 +---- test/utils/HookEnabledSwapRouter.sol | 26 +- 57 files changed, 1097 insertions(+), 1633 deletions(-) create mode 100644 contracts/base/BaseLiquidityHandler.sol delete mode 100644 contracts/interfaces/IAdvancedLiquidityManagement.sol delete mode 100644 contracts/interfaces/IBaseLiquidityManagement.sol create mode 100644 contracts/libraries/CurrencyDeltas.sol create mode 100644 contracts/libraries/CurrencySenderLibrary.sol create mode 100644 contracts/libraries/LiquiditySaltLibrary.sol delete mode 100644 contracts/libraries/PoolStateLibrary.sol delete mode 160000 lib/forge-std diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index 443e2528..b9d81858 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -384795 \ No newline at end of file +311137 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index d54245e8..c3edfa69 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -179162 \ No newline at end of file +122946 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index 8f92ae5c..b9e04365 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -128156 \ No newline at end of file +80287 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 3098aea8..7a0170eb 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1017534 \ No newline at end of file +1015181 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 35b55d27..4444368b 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -169368 \ No newline at end of file +110544 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index da17b718..1bc2d893 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -345987 \ No newline at end of file +240022 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index 9295bd7a..c1cac22b 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -89085 \ No newline at end of file +45997 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index 111771b5..97d86500 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -126958 \ No newline at end of file +79418 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index d1007040..03924f26 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -122853 \ No newline at end of file +122359 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 73c96768..e34af74b 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -222794 \ No newline at end of file +114257 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 4d9543e1..9bf14262 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -167494 \ No newline at end of file +112378 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index af1b03da..79a741b2 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -128154 \ No newline at end of file +74001 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 58654a31..c8a011cf 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -136428 \ No newline at end of file +77793 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index a9b719e8..5d250ba5 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -475877 \ No newline at end of file +422785 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 7ca9159e..95aa41f9 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -478504 \ No newline at end of file +475768 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d2dc450b..8e108254 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 72bff2c4..7a31a8d9 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -8,6 +8,7 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {SafeCallback} from "./base/SafeCallback.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; +import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); @@ -40,7 +41,7 @@ abstract contract BaseHook is IHooks, SafeCallback { Hooks.validateHookPermissions(_this, getHookPermissions()); } - function _lockAcquired(bytes calldata data) internal virtual override returns (bytes memory) { + function _unlockCallback(bytes calldata data) internal virtual override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); @@ -86,7 +87,7 @@ abstract contract BaseHook is IHooks, SafeCallback { IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external virtual returns (bytes4) { + ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } @@ -96,14 +97,14 @@ abstract contract BaseHook is IHooks, SafeCallback { IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external virtual returns (bytes4) { + ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { revert HookNotImplemented(); } @@ -111,7 +112,7 @@ abstract contract BaseHook is IHooks, SafeCallback { function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, int128) { revert HookNotImplemented(); } diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 9ad8df13..500e95d8 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -10,48 +10,35 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; -import {LiquidityRange, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FeeMath} from "./libraries/FeeMath.sol"; -import {PoolStateLibrary} from "./libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; // TODO: remove import {console2} from "forge-std/console2.sol"; -contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { +contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721 { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; using LiquidityRangeIdLibrary for LiquidityRange; - using PoolStateLibrary for IPoolManager; - /// @dev The ID of the next token that will be minted. Skips 0 + using StateLibrary for IPoolManager; + /// @dev The ID of the next token that will be minted. Skips 0 uint256 private _nextId = 1; - mapping(uint256 tokenId => Position position) public positions; - constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} - - // --- View Functions --- // - function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { - Position memory position = positions[tokenId]; + struct TokenPosition { + address owner; + LiquidityRange range; + } - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( - position.range.key.toId(), position.range.tickLower, position.range.tickUpper - ); + mapping(uint256 tokenId => TokenPosition position) public tokenPositions; - (token0Owed, token1Owed) = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); - token0Owed += position.tokensOwed0; - token1Owed += position.tokensOwed1; - } + constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} // NOTE: more gas efficient as LiquidityAmounts is used offchain // TODO: deadline check @@ -62,131 +49,47 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi address recipient, bytes calldata hookData ) public payable returns (uint256 tokenId, BalanceDelta delta) { - delta = BaseLiquidityManagement.modifyLiquidity( - range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: int256(liquidity) - }), - hookData, - recipient - ); + delta = _increaseLiquidity(range, liquidity, hookData, false, msg.sender); // mint receipt token - // GAS: uncheck this mf _mint(recipient, (tokenId = _nextId++)); - - positions[tokenId] = Position({ - nonce: 0, - operator: address(0), - range: range, - liquidity: uint128(liquidity), - feeGrowthInside0LastX128: 0, // TODO: - feeGrowthInside1LastX128: 0, // TODO: - tokensOwed0: 0, - tokensOwed1: 0 - }); - - // TODO: event + tokenPositions[tokenId] = TokenPosition({owner: msg.sender, range: range}); } // NOTE: more expensive since LiquidityAmounts is used onchain - function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, params.range.key.toId()); - (tokenId, delta) = mint( - params.range, - LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtRatioAtTick(params.range.tickLower), - TickMath.getSqrtRatioAtTick(params.range.tickUpper), - params.amount0Desired, - params.amount1Desired - ), - params.deadline, - params.recipient, - params.hookData - ); - require(params.amount0Min <= uint256(uint128(delta.amount0())), "INSUFFICIENT_AMOUNT0"); - require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); - } - - function increaseLiquidity(IncreaseLiquidityParams memory params, bytes calldata hookData, bool claims) - public - isAuthorizedForToken(params.tokenId) + // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { + // (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(params.range.key.toId()); + // (tokenId, delta) = mint( + // params.range, + // LiquidityAmounts.getLiquidityForAmounts( + // sqrtPriceX96, + // TickMath.getSqrtPriceAtTick(params.range.tickLower), + // TickMath.getSqrtPriceAtTick(params.range.tickUpper), + // params.amount0Desired, + // params.amount1Desired + // ), + // params.deadline, + // params.recipient, + // params.hookData + // ); + // require(params.amount0Min <= uint256(uint128(delta.amount0())), "INSUFFICIENT_AMOUNT0"); + // require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); + // } + + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) + external + isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - require(params.liquidityDelta != 0, "Must increase liquidity"); - Position storage position = positions[params.tokenId]; - - (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(position); - - delta = BaseLiquidityManagement.increaseLiquidity( - position.range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: position.range.tickLower, - tickUpper: position.range.tickUpper, - liquidityDelta: int256(uint256(params.liquidityDelta)) - }), - hookData, - claims, - ownerOf(params.tokenId), - token0Owed, - token1Owed - ); - // TODO: slippage checks & test - - delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; - delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; - position.liquidity += params.liquidityDelta; + delta = _increaseLiquidity(tokenPositions[tokenId].range, liquidity, hookData, claims, msg.sender); } - function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData, bool claims) + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) public - isAuthorizedForToken(params.tokenId) + isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - require(params.liquidityDelta != 0, "Must decrease liquidity"); - Position storage position = positions[params.tokenId]; - - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, position.range.key.toId()); - (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( - sqrtPriceX96, - TickMath.getSqrtRatioAtTick(position.range.tickLower), - TickMath.getSqrtRatioAtTick(position.range.tickUpper), - params.liquidityDelta - ); - BaseLiquidityManagement.modifyLiquidity( - position.range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: position.range.tickLower, - tickUpper: position.range.tickUpper, - liquidityDelta: -int256(uint256(params.liquidityDelta)) - }), - hookData, - ownerOf(params.tokenId) - ); - require(params.amount0Min <= uint256(uint128(-delta.amount0())), "INSUFFICIENT_AMOUNT0"); - require(params.amount1Min <= uint256(uint128(-delta.amount1())), "INSUFFICIENT_AMOUNT1"); - - (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(position); - // TODO: for now we'll assume user always collects the totality of their fees - token0Owed += (position.tokensOwed0 + uint128(amount0)); - token1Owed += (position.tokensOwed1 + uint128(amount1)); - - // TODO: does this account for 0 token transfers - if (claims) { - poolManager.transfer(params.recipient, position.range.key.currency0.toId(), token0Owed); - poolManager.transfer(params.recipient, position.range.key.currency1.toId(), token1Owed); - } else { - sendToken(params.recipient, position.range.key.currency0, token0Owed); - sendToken(params.recipient, position.range.key.currency1, token1Owed); - } - - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - position.liquidity -= params.liquidityDelta; - delta = toBalanceDelta(-int128(token0Owed), -int128(token1Owed)); + delta = _decreaseLiquidity(tokenPositions[tokenId].range, liquidity, hookData, claims, msg.sender); } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -195,24 +98,15 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi returns (BalanceDelta delta) { // remove liquidity - Position storage position = positions[tokenId]; + TokenPosition storage tokenPosition = tokenPositions[tokenId]; + LiquidityRangeId rangeId = tokenPosition.range.toId(); + Position storage position = positions[msg.sender][rangeId]; if (0 < position.liquidity) { - decreaseLiquidity( - DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: position.liquidity, - amount0Min: 0, - amount1Min: 0, - recipient: recipient, - deadline: block.timestamp - }), - hookData, - claims - ); + decreaseLiquidity(tokenId, position.liquidity, hookData, claims); } - require(position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "NOT_EMPTY"); - delete positions[tokenId]; + delete positions[msg.sender][rangeId]; + delete tokenPositions[tokenId]; // burn the token _burn(tokenId); @@ -223,49 +117,26 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi external returns (BalanceDelta delta) { - Position storage position = positions[tokenId]; - BaseLiquidityManagement.collect(position.range, hookData); - - (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(position); - delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); - - // TODO: for now we'll assume user always collects the totality of their fees - if (claims) { - poolManager.transfer(recipient, position.range.key.currency0.toId(), token0Owed + position.tokensOwed0); - poolManager.transfer(recipient, position.range.key.currency1.toId(), token1Owed + position.tokensOwed1); - } else { - sendToken(recipient, position.range.key.currency0, token0Owed + position.tokensOwed0); - sendToken(recipient, position.range.key.currency1, token1Owed + position.tokensOwed1); - } - - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - - // TODO: event + delta = _collect(tokenPositions[tokenId].range, hookData, claims, msg.sender); } - function _updateFeeGrowth(Position storage position) internal returns (uint128 token0Owed, uint128 token1Owed) { - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( - position.range.key.toId(), position.range.tickLower, position.range.tickUpper - ); - - (token0Owed, token1Owed) = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); - - position.feeGrowthInside0LastX128 = feeGrowthInside0X128; - position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { + TokenPosition memory tokenPosition = tokenPositions[tokenId]; + return feesOwed(tokenPosition.owner, tokenPosition.range); } function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { - Position storage position = positions[firstTokenId]; + TokenPosition storage tokenPosition = tokenPositions[firstTokenId]; + LiquidityRangeId rangeId = tokenPosition.range.toId(); + Position storage position = positions[from][rangeId]; position.operator = address(0x0); - liquidityOf[from][position.range.toId()] -= position.liquidity; - liquidityOf[to][position.range.toId()] += position.liquidity; + + // transfer position data to destination + positions[to][rangeId] = position; + delete positions[from][rangeId]; + + // update token position + tokenPositions[firstTokenId] = TokenPosition({owner: to, range: tokenPosition.range}); } modifier isAuthorizedForToken(uint256 tokenId) { diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol index 8657478b..e203becc 100644 --- a/contracts/SimpleBatchCall.sol +++ b/contracts/SimpleBatchCall.sol @@ -6,17 +6,21 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; /// @title SimpleBatchCall /// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. contract SimpleBatchCall is LockAndBatchCall { using CurrencyLibrary for Currency; + using TransientStateLibrary for IPoolManager; + using CurrencySettler for Currency; constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} struct SettleConfig { - bool withdrawTokens; // If true, takes the underlying ERC20s. - bool settleUsingTransfer; // If true, sends the underlying ERC20s. + bool takeClaims; + bool settleUsingBurn; // If true, sends the underlying ERC20s. } /// @notice We naively settle all currencies that are touched by the batch call. This data is passed in intially to `execute`. @@ -30,19 +34,10 @@ contract SimpleBatchCall is LockAndBatchCall { int256 delta = poolManager.currencyDelta(address(this), currenciesTouched[i]); if (delta < 0) { - if (config.settleUsingTransfer) { - ERC20(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), uint256(-delta)); - poolManager.settle(currency); - } else { - poolManager.transferFrom(address(poolManager), address(this), currency.toId(), uint256(-delta)); - } + currency.settle(poolManager, sender, uint256(-delta), config.settleUsingBurn); } if (delta > 0) { - if (config.withdrawTokens) { - poolManager.mint(address(this), currency.toId(), uint256(delta)); - } else { - poolManager.take(currency, address(this), uint256(delta)); - } + currency.take(poolManager, address(this), uint256(delta), config.takeClaims); } } } diff --git a/contracts/base/BaseLiquidityHandler.sol b/contracts/base/BaseLiquidityHandler.sol new file mode 100644 index 00000000..0b66c450 --- /dev/null +++ b/contracts/base/BaseLiquidityHandler.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {SafeCallback} from "./SafeCallback.sol"; +import {ImmutableState} from "./ImmutableState.sol"; +import {FeeMath} from "../libraries/FeeMath.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {CurrencySenderLibrary} from "../libraries/CurrencySenderLibrary.sol"; +import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; +import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; + +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; + +// TODO: remove +import {console2} from "forge-std/console2.sol"; + +abstract contract BaseLiquidityHandler is SafeCallback { + using LiquidityRangeIdLibrary for LiquidityRange; + using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; + using CurrencySenderLibrary for Currency; + using CurrencyDeltas for IPoolManager; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; + using LiquiditySaltLibrary for IHooks; + using PoolIdLibrary for PoolKey; + using SafeCast for uint256; + + // details about the liquidity position + struct Position { + // the nonce for permits + uint96 nonce; + // the address that is approved for spending this token + address operator; + uint256 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } + + mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; + + error LockFailure(); + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + (bool success, bytes memory returnData) = address(this).call(data); + if (success) return returnData; + if (returnData.length == 0) revert LockFailure(); + // if the call failed, bubble up the reason + /// @solidity memory-safe-assembly + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + + // TODO: selfOnly modifier + function handleIncreaseLiquidity( + address sender, + LiquidityRange calldata range, + uint256 liquidityToAdd, + bytes calldata hookData, + bool claims + ) external returns (BalanceDelta delta) { + Position storage position = positions[sender][range.toId()]; + + { + BalanceDelta feeDelta; + (delta, feeDelta) = poolManager.modifyLiquidity( + range.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: int256(liquidityToAdd), + salt: range.key.hooks.getLiquiditySalt(sender) + }), + hookData + ); + // take fees not accrued by user's position + (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(range, position); + BalanceDelta excessFees = feeDelta - toBalanceDelta(token0Owed.toInt128(), token1Owed.toInt128()); + range.key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); + range.key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); + } + + { + // get remaining deltas: the user pays additional to increase liquidity OR the user collects fees + delta = poolManager.currencyDeltas(address(this), range.key.currency0, range.key.currency1); + if (delta.amount0() < 0) { + range.key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), claims); + } + if (delta.amount1() < 0) { + range.key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), claims); + } + if (delta.amount0() > 0) { + range.key.currency0.take(poolManager, address(this), uint256(int256(delta.amount0())), true); + } + if (delta.amount1() > 0) { + range.key.currency1.take(poolManager, address(this), uint256(int256(delta.amount1())), true); + } + } + + { + positions[sender][range.toId()].liquidity += liquidityToAdd; + + // collected fees are credited to the position OR zero'd out + delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; + delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; + } + return delta; + } + + function handleDecreaseLiquidity( + address owner, + LiquidityRange calldata range, + uint256 liquidityToRemove, + bytes calldata hookData, + bool useClaims + ) external returns (BalanceDelta) { + (BalanceDelta delta, BalanceDelta feesAccrued) = poolManager.modifyLiquidity( + range.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: -int256(liquidityToRemove), + salt: range.key.hooks.getLiquiditySalt(owner) + }), + hookData + ); + + // take all tokens first + // do NOT take tokens directly to the owner because this contract might be holding fees + // that need to be paid out (position.tokensOwed) + if (delta.amount0() > 0) { + range.key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); + } + if (delta.amount1() > 0) { + range.key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); + } + + uint128 token0Owed; + uint128 token1Owed; + { + Position storage position = positions[owner][range.toId()]; + (token0Owed, token1Owed) = _updateFeeGrowth(range, position); + + BalanceDelta principalDelta = delta - feesAccrued; + token0Owed += position.tokensOwed0 + uint128(principalDelta.amount0()); + token1Owed += position.tokensOwed1 + uint128(principalDelta.amount1()); + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + position.liquidity -= liquidityToRemove; + } + { + delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); + + // sending tokens to the owner + if (token0Owed > 0) range.key.currency0.send(poolManager, owner, token0Owed, useClaims); + if (token1Owed > 0) range.key.currency1.send(poolManager, owner, token1Owed, useClaims); + } + + return delta; + } + + function handleCollect(address owner, LiquidityRange calldata range, bytes calldata hookData, bool takeClaims) + external + returns (BalanceDelta) + { + PoolKey memory key = range.key; + Position storage position = positions[owner][range.toId()]; + + (, BalanceDelta feesAccrued) = poolManager.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: 0, + salt: key.hooks.getLiquiditySalt(owner) + }), + hookData + ); + + // take all fees first then distribute + if (feesAccrued.amount0() > 0) { + key.currency0.take(poolManager, address(this), uint128(feesAccrued.amount0()), true); + } + if (feesAccrued.amount1() > 0) { + key.currency1.take(poolManager, address(this), uint128(feesAccrued.amount1()), true); + } + + (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); + token0Owed += position.tokensOwed0; + token1Owed += position.tokensOwed1; + + if (token0Owed > 0) key.currency0.send(poolManager, owner, token0Owed, takeClaims); + if (token1Owed > 0) key.currency1.send(poolManager, owner, token1Owed, takeClaims); + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + + return toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); + } + + function _updateFeeGrowth(LiquidityRange memory range, Position storage position) + internal + returns (uint128 token0Owed, uint128 token1Owed) + { + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + poolManager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); + + (token0Owed, token1Owed) = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity + ); + + position.feeGrowthInside0LastX128 = feeGrowthInside0X128; + position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + } +} diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 6b243e20..13269f69 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -6,196 +6,88 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; -import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {PoolStateLibrary} from "../libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; import {FeeMath} from "../libraries/FeeMath.sol"; +import {BaseLiquidityHandler} from "./BaseLiquidityHandler.sol"; // TODO: remove import {console2} from "forge-std/console2.sol"; -abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagement { +abstract contract BaseLiquidityManagement is BaseLiquidityHandler { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; - using PoolStateLibrary for IPoolManager; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; - error LockFailure(); + constructor(IPoolManager _poolManager) BaseLiquidityHandler(_poolManager) {} - struct CallbackData { - address sender; - PoolKey key; - IPoolManager.ModifyLiquidityParams params; - bool claims; - bytes hookData; - } - - mapping(address owner => mapping(LiquidityRangeId positionId => uint256 liquidity)) public liquidityOf; - - constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} - - // NOTE: handles mint/remove/collect - function modifyLiquidity( - PoolKey memory key, - IPoolManager.ModifyLiquidityParams memory params, + function _increaseLiquidity( + LiquidityRange memory range, + uint256 liquidityToAdd, bytes calldata hookData, + bool claims, address owner - ) public payable override returns (BalanceDelta delta) { - // if removing liquidity, check that the owner is the sender? - if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); - + ) internal returns (BalanceDelta delta) { delta = abi.decode( - poolManager.lock(abi.encodeCall(this.handleModifyPosition, (msg.sender, key, params, hookData, false))), + poolManager.unlock( + abi.encodeCall(this.handleIncreaseLiquidity, (msg.sender, range, liquidityToAdd, hookData, claims)) + ), (BalanceDelta) ); - - params.liquidityDelta < 0 - ? liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] -= - uint256(-params.liquidityDelta) - : liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] += - uint256(params.liquidityDelta); - - // TODO: handle & test - // uint256 ethBalance = address(this).balance; - // if (ethBalance > 0) { - // CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); - // } } - function increaseLiquidity( - PoolKey memory key, - IPoolManager.ModifyLiquidityParams memory params, + function _decreaseLiquidity( + LiquidityRange memory range, + uint256 liquidityToRemove, bytes calldata hookData, bool claims, - address owner, - uint256 token0Owed, - uint256 token1Owed + address owner ) internal returns (BalanceDelta delta) { delta = abi.decode( - poolManager.lock( - abi.encodeCall( - this.handleIncreaseLiquidity, - ( - msg.sender, - key, - params, - hookData, - claims, - toBalanceDelta(int128(int256(token0Owed)), int128(int256(token1Owed))) - ) - ) + poolManager.unlock( + abi.encodeCall(this.handleDecreaseLiquidity, (owner, range, liquidityToRemove, hookData, claims)) ), (BalanceDelta) ); - - liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] += - uint256(params.liquidityDelta); } - function collect(LiquidityRange memory range, bytes calldata hookData) internal returns (BalanceDelta delta) { + function _collect(LiquidityRange memory range, bytes calldata hookData, bool claims, address owner) + internal + returns (BalanceDelta delta) + { delta = abi.decode( - poolManager.lock( - abi.encodeCall( - this.handleModifyPosition, - ( - address(this), - range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: 0 - }), - hookData, - true - ) - ) - ), - (BalanceDelta) + poolManager.unlock(abi.encodeCall(this.handleCollect, (owner, range, hookData, claims))), (BalanceDelta) ); } - function sendToken(address recipient, Currency currency, uint256 amount) internal { - poolManager.lock(abi.encodeCall(this.handleRedeemClaim, (recipient, currency, amount))); - } - - function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { - (bool success, bytes memory returnData) = address(this).call(data); - if (success) return returnData; - if (returnData.length == 0) revert LockFailure(); - // if the call failed, bubble up the reason - /// @solidity memory-safe-assembly - assembly { - revert(add(returnData, 32), mload(returnData)) - } - } - - // TODO: selfOnly modifier - function handleModifyPosition( - address sender, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata hookData, - bool claims - ) external returns (BalanceDelta delta) { - delta = poolManager.modifyLiquidity(key, params, hookData); - - if (params.liquidityDelta <= 0) { - // removing liquidity/fees so mint tokens to the router - // the router will be responsible for sending the tokens to the desired recipient - key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); - key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); - } else { - // adding liquidity so pay tokens - key.currency0.settle(poolManager, sender, uint128(-delta.amount0()), claims); - key.currency1.settle(poolManager, sender, uint128(-delta.amount1()), claims); - } - } - - // TODO: selfOnly modifier - function handleIncreaseLiquidity( - address sender, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata hookData, - bool claims, - BalanceDelta tokensOwed - ) external returns (BalanceDelta delta) { - BalanceDelta feeDelta = poolManager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: params.tickLower, - tickUpper: params.tickUpper, - liquidityDelta: 0 - }), - hookData + // --- View Functions --- // + function feesOwed(address owner, LiquidityRange memory range) + public + view + returns (uint256 token0Owed, uint256 token1Owed) + { + Position memory position = positions[owner][range.toId()]; + + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + poolManager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); + + (token0Owed, token1Owed) = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity ); - - poolManager.modifyLiquidity(key, params, hookData); - - { - BalanceDelta excessFees = feeDelta - tokensOwed; - key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); - key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); - - int256 amount0Delta = poolManager.currencyDelta(address(this), key.currency0); - int256 amount1Delta = poolManager.currencyDelta(address(this), key.currency1); - if (amount0Delta < 0) key.currency0.settle(poolManager, sender, uint256(-amount0Delta), claims); - if (amount1Delta < 0) key.currency1.settle(poolManager, sender, uint256(-amount1Delta), claims); - if (amount0Delta > 0) key.currency0.take(poolManager, address(this), uint256(amount0Delta), true); - if (amount1Delta > 0) key.currency1.take(poolManager, address(this), uint256(amount1Delta), true); - delta = toBalanceDelta(int128(amount0Delta), int128(amount1Delta)); - } - } - - // TODO: selfOnly modifier - function handleRedeemClaim(address recipient, Currency currency, uint256 amount) external { - poolManager.burn(address(this), currency.toId(), amount); - poolManager.take(currency, recipient, amount); + token0Owed += position.tokensOwed0; + token1Owed += position.tokensOwed1; } } diff --git a/contracts/base/CallsWithLock.sol b/contracts/base/CallsWithLock.sol index c871c797..113d1ebd 100644 --- a/contracts/base/CallsWithLock.sol +++ b/contracts/base/CallsWithLock.sol @@ -5,6 +5,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {ICallsWithLock} from "../interfaces/ICallsWithLock.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; /// @title CallsWithLock /// @notice Handles all the calls to the pool manager contract. Assumes the integrating contract has already acquired a lock. @@ -29,7 +30,8 @@ abstract contract CallsWithLock is ICallsWithLock, ImmutableState { IPoolManager.ModifyLiquidityParams calldata params, bytes calldata hookData ) external onlyBySelf returns (bytes memory) { - return abi.encode(poolManager.modifyLiquidity(key, params, hookData)); + (BalanceDelta delta, BalanceDelta feeDelta) = poolManager.modifyLiquidity(key, params, hookData); + return abi.encode(delta, feeDelta); } function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol index 76deb511..fe450730 100644 --- a/contracts/base/LockAndBatchCall.sol +++ b/contracts/base/LockAndBatchCall.sol @@ -14,14 +14,14 @@ abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { /// @param executeData The function selectors and calldata for any of the function selectors in ICallsWithLock encoded as an array of bytes. function execute(bytes memory executeData, bytes memory settleData) external { - (bytes memory lockReturnData) = poolManager.lock(abi.encode(executeData, abi.encode(msg.sender, settleData))); + (bytes memory lockReturnData) = poolManager.unlock(abi.encode(executeData, abi.encode(msg.sender, settleData))); (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); _handleAfterExecute(executeReturnData, settleReturnData); } /// @param data This data is passed from the top-level execute function to the internal _executeWithLockCalls and _settle function. It is decoded as two separate dynamic bytes parameters. - /// @dev _lockAcquired is responsible for executing the internal calls under the lock and settling open deltas left on the pool - function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { + /// @dev _unlockCallback is responsible for executing the internal calls under the lock and settling open deltas left on the pool + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { (bytes memory executeData, bytes memory settleDataWithSender) = abi.decode(data, (bytes, bytes)); (address sender, bytes memory settleData) = abi.decode(settleDataWithSender, (address, bytes)); return abi.encode(_executeWithLockCalls(executeData), _settle(sender, settleData)); diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol index a2656287..3eb693dd 100644 --- a/contracts/base/SafeCallback.sol +++ b/contracts/base/SafeCallback.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./ImmutableState.sol"; -abstract contract SafeCallback is ImmutableState, ILockCallback { +abstract contract SafeCallback is ImmutableState, IUnlockCallback { error NotManager(); modifier onlyByManager() { @@ -14,9 +14,9 @@ abstract contract SafeCallback is ImmutableState, ILockCallback { } /// @dev There is no way to force the onlyByManager modifier but for this callback to be safe, it MUST check that the msg.sender is the pool manager. - function lockAcquired(bytes calldata data) external onlyByManager returns (bytes memory) { - return _lockAcquired(data); + function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { + return _unlockCallback(data); } - function _lockAcquired(bytes calldata data) internal virtual returns (bytes memory); + function _unlockCallback(bytes calldata data) internal virtual returns (bytes memory); } diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol index 6f7b1178..8d750a76 100644 --- a/contracts/hooks/examples/FullRange.sol +++ b/contracts/hooks/examples/FullRange.sol @@ -8,10 +8,11 @@ import {BaseHook} from "../../BaseHook.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; @@ -20,14 +21,18 @@ import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import "../../libraries/LiquidityAmounts.sol"; contract FullRange is BaseHook { using CurrencyLibrary for Currency; + using CurrencySettler for Currency; using PoolIdLibrary for PoolKey; using SafeCast for uint256; using SafeCast for uint128; + using StateLibrary for IPoolManager; /// @notice Thrown when trying to interact with a non-initialized pool error PoolNotInitialized(); @@ -98,7 +103,11 @@ contract FullRange is BaseHook { beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -127,8 +136,8 @@ contract FullRange is BaseHook { liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(MIN_TICK), - TickMath.getSqrtRatioAtTick(MAX_TICK), + TickMath.getSqrtPriceAtTick(MIN_TICK), + TickMath.getSqrtPriceAtTick(MAX_TICK), params.amount0Desired, params.amount1Desired ); @@ -141,7 +150,8 @@ contract FullRange is BaseHook { IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256() + liquidityDelta: liquidity.toInt256(), + salt: 0 }) ); @@ -185,7 +195,8 @@ contract FullRange is BaseHook { IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: -(params.liquidity.toInt256()) + liquidityDelta: -(params.liquidity.toInt256()), + salt: 0 }) ); @@ -233,7 +244,7 @@ contract FullRange is BaseHook { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { PoolId poolId = key.toId(); @@ -242,32 +253,19 @@ contract FullRange is BaseHook { pool.hasAccruedFees = true; } - return IHooks.beforeSwap.selector; + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function modifyLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) internal returns (BalanceDelta delta) { - delta = abi.decode(poolManager.lock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); + delta = abi.decode(poolManager.unlock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); } function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - _settleDelta(sender, key.currency0, uint128(-delta.amount0())); - _settleDelta(sender, key.currency1, uint128(-delta.amount1())); - } - - function _settleDelta(address sender, Currency currency, uint128 amount) internal { - if (currency.isNative()) { - poolManager.settle{value: amount}(currency); - } else { - if (sender == address(this)) { - currency.transfer(address(poolManager), amount); - } else { - IERC20Minimal(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), amount); - } - poolManager.settle(currency); - } + key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), false); + key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), false); } function _takeDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { @@ -293,11 +291,11 @@ contract FullRange is BaseHook { ); params.liquidityDelta = -(liquidityToRemove.toInt256()); - delta = poolManager.modifyLiquidity(key, params, ZERO_BYTES); + (delta,) = poolManager.modifyLiquidity(key, params, ZERO_BYTES); pool.hasAccruedFees = false; } - function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory) { + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { CallbackData memory data = abi.decode(rawData, (CallbackData)); BalanceDelta delta; @@ -305,7 +303,7 @@ contract FullRange is BaseHook { delta = _removeLiquidity(data.key, data.params); _takeDeltas(data.sender, data.key, delta); } else { - delta = poolManager.modifyLiquidity(data.key, data.params, ZERO_BYTES); + (delta,) = poolManager.modifyLiquidity(data.key, data.params, ZERO_BYTES); _settleDeltas(data.sender, data.key, delta); } return abi.encode(delta); @@ -313,12 +311,13 @@ contract FullRange is BaseHook { function _rebalance(PoolKey memory key) public { PoolId poolId = key.toId(); - BalanceDelta balanceDelta = poolManager.modifyLiquidity( + (BalanceDelta balanceDelta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()) + liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()), + salt: 0 }), ZERO_BYTES ); @@ -343,18 +342,19 @@ contract FullRange is BaseHook { uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( newSqrtPriceX96, - TickMath.getSqrtRatioAtTick(MIN_TICK), - TickMath.getSqrtRatioAtTick(MAX_TICK), + TickMath.getSqrtPriceAtTick(MIN_TICK), + TickMath.getSqrtPriceAtTick(MAX_TICK), uint256(uint128(balanceDelta.amount0())), uint256(uint128(balanceDelta.amount1())) ); - BalanceDelta balanceDeltaAfter = poolManager.modifyLiquidity( + (BalanceDelta balanceDeltaAfter,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256() + liquidityDelta: liquidity.toInt256(), + salt: 0 }), ZERO_BYTES ); diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol index 9dfb2210..137d4207 100644 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ b/contracts/hooks/examples/GeomeanOracle.sol @@ -8,6 +8,8 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Oracle} from "../../libraries/Oracle.sol"; import {BaseHook} from "../../BaseHook.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; /// @notice A hook for a pool that allows a Uniswap pool to act as an oracle. Pools that use this hook must have full range /// tick spacing and liquidity is always permanently locked in these pools. This is the suggested configuration @@ -15,6 +17,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; contract GeomeanOracle is BaseHook { using Oracle for Oracle.Observation[65535]; using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; /// @notice Oracle pools do not have fees because they exist to serve as an oracle for a pair of tokens error OnlyOneOraclePoolAllowed(); @@ -71,7 +74,11 @@ contract GeomeanOracle is BaseHook { beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -141,10 +148,10 @@ contract GeomeanOracle is BaseHook { external override onlyByManager - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { _updatePool(key); - return GeomeanOracle.beforeSwap.selector; + return (GeomeanOracle.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } /// @notice Observe the given pool for the timestamps diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol index 530922a6..3d26f740 100644 --- a/contracts/hooks/examples/LimitOrder.sol +++ b/contracts/hooks/examples/LimitOrder.sol @@ -10,8 +10,10 @@ import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Mini import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {BaseHook} from "../../BaseHook.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; type Epoch is uint232; @@ -31,6 +33,8 @@ contract LimitOrder is BaseHook { using EpochLibrary for Epoch; using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; + using CurrencySettler for Currency; + using StateLibrary for IPoolManager; error ZeroLiquidity(); error InRange(); @@ -84,7 +88,11 @@ contract LimitOrder is BaseHook { beforeSwap: false, afterSwap: true, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -134,9 +142,9 @@ contract LimitOrder is BaseHook { IPoolManager.SwapParams calldata params, BalanceDelta, bytes calldata - ) external override onlyByManager returns (bytes4) { + ) external override onlyByManager returns (bytes4, int128) { (int24 tickLower, int24 lower, int24 upper) = _getCrossedTicks(key.toId(), key.tickSpacing); - if (lower > upper) return LimitOrder.afterSwap.selector; + if (lower > upper) return (LimitOrder.afterSwap.selector, 0); // note that a zeroForOne swap means that the pool is actually gaining token0, so limit // order fills are the opposite of swap fills, hence the inversion below @@ -146,7 +154,7 @@ contract LimitOrder is BaseHook { } setTickLowerLast(key.toId(), tickLower); - return LimitOrder.afterSwap.selector; + return (LimitOrder.afterSwap.selector, 0); } function _fillEpoch(PoolKey calldata key, int24 lower, bool zeroForOne) internal { @@ -157,7 +165,7 @@ contract LimitOrder is BaseHook { epochInfo.filled = true; (uint256 amount0, uint256 amount1) = - _lockAcquiredFill(key, lower, -int256(uint256(epochInfo.liquidityTotal))); + _unlockCallbackFill(key, lower, -int256(uint256(epochInfo.liquidityTotal))); unchecked { epochInfo.token0Total += amount0; @@ -187,17 +195,18 @@ contract LimitOrder is BaseHook { } } - function _lockAcquiredFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) + function _unlockCallbackFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) private onlyByManager returns (uint128 amount0, uint128 amount1) { - BalanceDelta delta = poolManager.modifyLiquidity( + (BalanceDelta delta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta + liquidityDelta: liquidityDelta, + salt: 0 }), ZERO_BYTES ); @@ -216,8 +225,10 @@ contract LimitOrder is BaseHook { { if (liquidity == 0) revert ZeroLiquidity(); - poolManager.lock( - abi.encodeCall(this.lockAcquiredPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender)) + poolManager.unlock( + abi.encodeCall( + this.unlockCallbackPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender) + ) ); EpochInfo storage epochInfo; @@ -245,19 +256,20 @@ contract LimitOrder is BaseHook { emit Place(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); } - function lockAcquiredPlace( + function unlockCallbackPlace( PoolKey calldata key, int24 tickLower, bool zeroForOne, int256 liquidityDelta, address owner ) external selfOnly { - BalanceDelta delta = poolManager.modifyLiquidity( + (BalanceDelta delta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta + liquidityDelta: liquidityDelta, + salt: 0 }), ZERO_BYTES ); @@ -265,26 +277,15 @@ contract LimitOrder is BaseHook { if (delta.amount0() < 0) { if (delta.amount1() != 0) revert InRange(); if (!zeroForOne) revert CrossedRange(); - // TODO use safeTransferFrom - IERC20Minimal(Currency.unwrap(key.currency0)).transferFrom( - owner, address(poolManager), uint256(uint128(-delta.amount0())) - ); - poolManager.settle(key.currency0); + key.currency0.settle(poolManager, owner, uint256(uint128(-delta.amount0())), false); } else { if (delta.amount0() != 0) revert InRange(); if (zeroForOne) revert CrossedRange(); - // TODO use safeTransferFrom - IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom( - owner, address(poolManager), uint256(uint128(-delta.amount1())) - ); - poolManager.settle(key.currency1); + key.currency1.settle(poolManager, owner, uint256(uint128(-delta.amount1())), false); } } - function kill(PoolKey calldata key, int24 tickLower, bool zeroForOne, address to) - external - returns (uint256 amount0, uint256 amount1) - { + function kill(PoolKey calldata key, int24 tickLower, bool zeroForOne, address to) external { Epoch epoch = getEpoch(key, tickLower, zeroForOne); EpochInfo storage epochInfo = epochInfos[epoch]; @@ -296,14 +297,14 @@ contract LimitOrder is BaseHook { uint256 amount0Fee; uint256 amount1Fee; - (amount0, amount1, amount0Fee, amount1Fee) = abi.decode( - poolManager.lock( + (amount0Fee, amount1Fee) = abi.decode( + poolManager.unlock( abi.encodeCall( - this.lockAcquiredKill, + this.unlockCallbackKill, (key, tickLower, -int256(uint256(liquidity)), to, liquidity == epochInfo.liquidityTotal) ) ), - (uint256, uint256, uint256, uint256) + (uint256, uint256) ); epochInfo.liquidityTotal -= liquidity; unchecked { @@ -314,13 +315,13 @@ contract LimitOrder is BaseHook { emit Kill(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); } - function lockAcquiredKill( + function unlockCallbackKill( PoolKey calldata key, int24 tickLower, int256 liquidityDelta, address to, bool removingAllLiquidity - ) external selfOnly returns (uint256 amount0, uint256 amount1, uint128 amount0Fee, uint128 amount1Fee) { + ) external selfOnly returns (uint128 amount0Fee, uint128 amount1Fee) { int24 tickUpper = tickLower + key.tickSpacing; // because `modifyPosition` includes not just principal value but also fees, we cannot allocate @@ -328,9 +329,14 @@ contract LimitOrder is BaseHook { // could be unfairly diluted by a user sychronously placing then killing a limit order to skim off fees. // to prevent this, we allocate all fee revenue to remaining limit order placers, unless this is the last order. if (!removingAllLiquidity) { - BalanceDelta deltaFee = poolManager.modifyLiquidity( + (, BalanceDelta deltaFee) = poolManager.modifyLiquidity( key, - IPoolManager.ModifyLiquidityParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: 0}), + IPoolManager.ModifyLiquidityParams({ + tickLower: tickLower, + tickUpper: tickUpper, + liquidityDelta: 0, + salt: 0 + }), ZERO_BYTES ); @@ -342,21 +348,22 @@ contract LimitOrder is BaseHook { } } - BalanceDelta delta = poolManager.modifyLiquidity( + (BalanceDelta delta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickUpper, - liquidityDelta: liquidityDelta + liquidityDelta: liquidityDelta, + salt: 0 }), ZERO_BYTES ); if (delta.amount0() > 0) { - poolManager.take(key.currency0, to, amount0 = uint128(delta.amount0())); + key.currency0.take(poolManager, to, uint256(uint128(delta.amount0())), false); } if (delta.amount1() > 0) { - poolManager.take(key.currency1, to, amount1 = uint128(delta.amount1())); + key.currency1.take(poolManager, to, uint256(uint128(delta.amount1())), false); } } @@ -378,14 +385,16 @@ contract LimitOrder is BaseHook { epochInfo.token1Total -= amount1; epochInfo.liquidityTotal = liquidityTotal - liquidity; - poolManager.lock( - abi.encodeCall(this.lockAcquiredWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to)) + poolManager.unlock( + abi.encodeCall( + this.unlockCallbackWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to) + ) ); emit Withdraw(msg.sender, epoch, liquidity); } - function lockAcquiredWithdraw( + function unlockCallbackWithdraw( Currency currency0, Currency currency1, uint256 token0Amount, diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index 8bc3aadb..c619e900 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -19,10 +19,14 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolGetters} from "../../libraries/PoolGetters.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; contract TWAMM is BaseHook, ITWAMM { using TransferHelper for IERC20Minimal; using CurrencyLibrary for Currency; + using CurrencySettler for Currency; using OrderPool for OrderPool.State; using PoolIdLibrary for PoolKey; using TickMath for int24; @@ -30,6 +34,7 @@ contract TWAMM is BaseHook, ITWAMM { using SafeCast for uint256; using PoolGetters for IPoolManager; using TickBitmap for mapping(int16 => uint256); + using StateLibrary for IPoolManager; bytes internal constant ZERO_BYTES = bytes(""); @@ -71,7 +76,11 @@ contract TWAMM is BaseHook, ITWAMM { beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -101,10 +110,10 @@ contract TWAMM is BaseHook, ITWAMM { external override onlyByManager - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { executeTWAMMOrders(key); - return BaseHook.beforeSwap.selector; + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function lastVirtualOrderTimestamp(PoolId key) external view returns (uint256) { @@ -142,7 +151,9 @@ contract TWAMM is BaseHook, ITWAMM { ); if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - poolManager.lock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96))); + poolManager.unlock( + abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96)) + ); } } @@ -298,7 +309,7 @@ contract TWAMM is BaseHook, ITWAMM { IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); } - function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory) { + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); @@ -306,19 +317,17 @@ contract TWAMM is BaseHook, ITWAMM { if (swapParams.zeroForOne) { if (delta.amount0() < 0) { - key.currency0.transfer(address(poolManager), uint256(uint128(-delta.amount0()))); - poolManager.settle(key.currency0); + key.currency0.settle(poolManager, address(this), uint256(uint128(-delta.amount0())), false); } if (delta.amount1() > 0) { - poolManager.take(key.currency1, address(this), uint256(uint128(delta.amount1()))); + key.currency1.take(poolManager, address(this), uint256(uint128(delta.amount1())), false); } } else { if (delta.amount1() < 0) { - key.currency1.transfer(address(poolManager), uint256(uint128(-delta.amount1()))); - poolManager.settle(key.currency1); + key.currency1.settle(poolManager, address(this), uint256(uint128(-delta.amount1())), false); } if (delta.amount0() > 0) { - poolManager.take(key.currency0, address(this), uint256(uint128(delta.amount0()))); + key.currency0.take(poolManager, address(this), uint256(uint128(delta.amount0())), false); } } return bytes(""); @@ -512,8 +521,8 @@ contract TWAMM is BaseHook, ITWAMM { _isCrossingInitializedTick(params.pool, poolManager, poolKey, finalSqrtPriceX96); if (crossingInitializedTick) { - int128 liquidityNetAtTick = poolManager.getPoolTickInfo(poolKey.toId(), tick).liquidityNet; - uint160 initializedSqrtPrice = TickMath.getSqrtRatioAtTick(tick); + (, int128 liquidityNetAtTick) = poolManager.getTickLiquidity(poolKey.toId(), tick); + uint160 initializedSqrtPrice = TickMath.getSqrtPriceAtTick(tick); uint256 swapDelta0 = SqrtPriceMath.getAmount0Delta( params.pool.sqrtPriceX96, initializedSqrtPrice, params.pool.liquidity, true @@ -570,7 +579,7 @@ contract TWAMM is BaseHook, ITWAMM { PoolKey memory poolKey, TickCrossingParams memory params ) private returns (PoolParamsOnExecute memory, uint256) { - uint160 initializedSqrtPrice = params.initializedTick.getSqrtRatioAtTick(); + uint160 initializedSqrtPrice = params.initializedTick.getSqrtPriceAtTick(); uint256 secondsUntilCrossingX96 = TwammMath.calculateTimeBetweenTicks( params.pool.liquidity, @@ -596,7 +605,7 @@ contract TWAMM is BaseHook, ITWAMM { unchecked { // update pool - int128 liquidityNet = poolManager.getPoolTickInfo(poolKey.toId(), params.initializedTick).liquidityNet; + (, int128 liquidityNet) = poolManager.getTickLiquidity(poolKey.toId(), params.initializedTick); if (initializedSqrtPrice < params.pool.sqrtPriceX96) liquidityNet = -liquidityNet; params.pool.liquidity = liquidityNet < 0 ? params.pool.liquidity - uint128(-liquidityNet) @@ -614,8 +623,8 @@ contract TWAMM is BaseHook, ITWAMM { uint160 nextSqrtPriceX96 ) internal view returns (bool crossingInitializedTick, int24 nextTickInit) { // use current price as a starting point for nextTickInit - nextTickInit = pool.sqrtPriceX96.getTickAtSqrtRatio(); - int24 targetTick = nextSqrtPriceX96.getTickAtSqrtRatio(); + nextTickInit = pool.sqrtPriceX96.getTickAtSqrtPrice(); + int24 targetTick = nextSqrtPriceX96.getTickAtSqrtPrice(); bool searchingLeft = nextSqrtPriceX96 < pool.sqrtPriceX96; bool nextTickInitFurtherThanTarget = false; // initialize as false diff --git a/contracts/hooks/examples/VolatilityOracle.sol b/contracts/hooks/examples/VolatilityOracle.sol index 76a3e8ce..ede61bf5 100644 --- a/contracts/hooks/examples/VolatilityOracle.sol +++ b/contracts/hooks/examples/VolatilityOracle.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.19; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {SwapFeeLibrary} from "@uniswap/v4-core/src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; import {BaseHook} from "../../BaseHook.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; contract VolatilityOracle is BaseHook { - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; error MustUseDynamicFee(); @@ -34,7 +34,11 @@ contract VolatilityOracle is BaseHook { beforeSwap: false, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -52,7 +56,7 @@ contract VolatilityOracle is BaseHook { uint24 startingFee = 3000; uint32 lapsed = _blockTimestamp() - deployTimestamp; uint24 fee = startingFee + (uint24(lapsed) * 100) / 60; // 100 bps a minute - poolManager.updateDynamicSwapFee(key, fee); // initial fee 0.30% + poolManager.updateDynamicLPFee(key, fee); // initial fee 0.30% } function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) diff --git a/contracts/interfaces/IAdvancedLiquidityManagement.sol b/contracts/interfaces/IAdvancedLiquidityManagement.sol deleted file mode 100644 index 5f5f9f8f..00000000 --- a/contracts/interfaces/IAdvancedLiquidityManagement.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; -import {LiquidityRange} from "../types/LiquidityRange.sol"; - -interface IAdvancedLiquidityManagement is IBaseLiquidityManagement { - /// @notice Move an existing liquidity position into a new range - function rebalanceLiquidity( - LiquidityRange memory position, - int24 tickLowerNew, - int24 tickUpperNew, - int256 liquidityDelta - ) external; - - /// @notice Move an existing liquidity position into a new pool, keeping the same range - function migrateLiquidity(LiquidityRange memory position, PoolKey memory newKey) external; -} diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol deleted file mode 100644 index fe289195..00000000 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; -import {LiquidityRange, LiquidityRangeId} from "../types/LiquidityRange.sol"; - -interface IBaseLiquidityManagement is ILockCallback { - function liquidityOf(address owner, LiquidityRangeId positionId) external view returns (uint256 liquidity); - - // NOTE: handles add/remove/collect - function modifyLiquidity( - PoolKey memory key, - IPoolManager.ModifyLiquidityParams memory params, - bytes calldata hookData, - address owner - ) external payable returns (BalanceDelta delta); -} diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index cde005e9..be182907 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -4,39 +4,8 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; -import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; - -interface INonfungiblePositionManager is IBaseLiquidityManagement { - // details about the uniswap position - struct Position { - // the nonce for permits - uint96 nonce; - // the address that is approved for spending this token - address operator; - LiquidityRange range; - // the liquidity of the position - // NOTE: this value will be less than BaseLiquidityManagement.liquidityOf, if the user - // owns multiple positions with the same range - uint128 liquidity; - // the fee growth of the aggregate position as of the last action on the individual position - uint256 feeGrowthInside0LastX128; - uint256 feeGrowthInside1LastX128; - // how many uncollected tokens are owed to the position, as of the last computation - uint128 tokensOwed0; - uint128 tokensOwed1; - } - - struct MintParams { - LiquidityRange range; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - address recipient; - bytes hookData; - } +interface INonfungiblePositionManager { // NOTE: more gas efficient as LiquidityAmounts is used offchain function mint( LiquidityRange calldata position, @@ -47,26 +16,12 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { ) external payable returns (uint256 tokenId, BalanceDelta delta); // NOTE: more expensive since LiquidityAmounts is used onchain - function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); - - struct IncreaseLiquidityParams { - uint256 tokenId; - uint128 liquidityDelta; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - struct DecreaseLiquidityParams { - uint256 tokenId; - uint128 liquidityDelta; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - address recipient; - } + // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) + external + returns (BalanceDelta delta); - function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData, bool claims) + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); diff --git a/contracts/interfaces/IQuoter.sol b/contracts/interfaces/IQuoter.sol index 90a390fc..8774e548 100644 --- a/contracts/interfaces/IQuoter.sol +++ b/contracts/interfaces/IQuoter.sol @@ -11,7 +11,7 @@ import {PathKey} from "../libraries/PathKey.sol"; /// @dev These functions are not marked view because they rely on calling non-view functions and reverting /// to compute the result. They are also not gas efficient and should not be called on-chain. interface IQuoter { - error InvalidLockAcquiredSender(); + error InvalidUnlockCallbackSender(); error InvalidLockCaller(); error InvalidQuoteBatchParams(); error InsufficientAmountOut(); diff --git a/contracts/lens/Quoter.sol b/contracts/lens/Quoter.sol index c039a7b7..9e9bfda2 100644 --- a/contracts/lens/Quoter.sol +++ b/contracts/lens/Quoter.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; @@ -13,11 +13,13 @@ import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {IQuoter} from "../interfaces/IQuoter.sol"; import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -contract Quoter is IQuoter, ILockCallback { +contract Quoter is IQuoter, IUnlockCallback { using Hooks for IHooks; using PoolIdLibrary for PoolKey; using PathKeyLib for PathKey; + using StateLibrary for IPoolManager; /// @dev cache used to check a safety condition in exact output swaps. uint128 private amountOutCached; @@ -62,7 +64,7 @@ contract Quoter is IQuoter, ILockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.lock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} catch (bytes memory reason) { return _handleRevertSingle(reason); } @@ -77,7 +79,7 @@ contract Quoter is IQuoter, ILockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.lock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } @@ -89,7 +91,7 @@ contract Quoter is IQuoter, ILockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.lock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} catch (bytes memory reason) { if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; return _handleRevertSingle(reason); @@ -106,16 +108,16 @@ contract Quoter is IQuoter, ILockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.lock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } } - /// @inheritdoc ILockCallback - function lockAcquired(bytes calldata data) external returns (bytes memory) { + /// @inheritdoc IUnlockCallback + function unlockCallback(bytes calldata data) external returns (bytes memory) { if (msg.sender != address(manager)) { - revert InvalidLockAcquiredSender(); + revert InvalidUnlockCallbackSender(); } (bool success, bytes memory returnData) = address(this).call(data); @@ -331,7 +333,7 @@ contract Quoter is IQuoter, ILockCallback { /// @dev return either the sqrtPriceLimit from user input, or the max/min value possible depending on trade direction function _sqrtPriceLimitOrDefault(uint160 sqrtPriceLimitX96, bool zeroForOne) private pure returns (uint160) { return sqrtPriceLimitX96 == 0 - ? zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 + ? zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 : sqrtPriceLimitX96; } } diff --git a/contracts/libraries/CurrencyDeltas.sol b/contracts/libraries/CurrencyDeltas.sol new file mode 100644 index 00000000..339e71f6 --- /dev/null +++ b/contracts/libraries/CurrencyDeltas.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {console2} from "forge-std/console2.sol"; + +library CurrencyDeltas { + using SafeCast for uint256; + + /// @notice Get the current delta for a caller in the two given currencies + /// @param caller_ The address of the caller + /// @param currency0 The currency for which to lookup the delta + /// @param currency1 The other currency for which to lookup the delta + function currencyDeltas(IPoolManager manager, address caller_, Currency currency0, Currency currency1) + internal + view + returns (BalanceDelta) + { + bytes32 key0; + bytes32 key1; + assembly { + mstore(0, caller_) + mstore(32, currency0) + key0 := keccak256(0, 64) + + mstore(0, caller_) + mstore(32, currency1) + key1 := keccak256(0, 64) + } + bytes32[] memory slots = new bytes32[](2); + slots[0] = key0; + slots[1] = key1; + bytes32[] memory result = manager.exttload(slots); + return toBalanceDelta(int128(int256(uint256(result[0]))), int128(int256(uint256(result[1])))); + } +} diff --git a/contracts/libraries/CurrencySenderLibrary.sol b/contracts/libraries/CurrencySenderLibrary.sol new file mode 100644 index 00000000..65a44e07 --- /dev/null +++ b/contracts/libraries/CurrencySenderLibrary.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; +import {CurrencySettleTake} from "./CurrencySettleTake.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; + +/// @notice Library used to send Currencies from address to address +library CurrencySenderLibrary { + using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; + + /// @notice Send a custodied Currency to a recipient + /// @dev If sending ERC20 or native, the PoolManager must be unlocked + /// @param currency The Currency to send + /// @param manager The PoolManager + /// @param recipient The recipient address + /// @param amount The amount to send + /// @param useClaims If true, transfer ERC-6909 tokens + function send(Currency currency, IPoolManager manager, address recipient, uint256 amount, bool useClaims) + internal + { + if (useClaims) { + manager.transfer(recipient, currency.toId(), amount); + } else { + manager.burn(address(this), currency.toId(), amount); + currency.take(manager, recipient, amount, false); + } + } +} diff --git a/contracts/libraries/CurrencySettleTake.sol b/contracts/libraries/CurrencySettleTake.sol index 858963bf..9ea8f1c2 100644 --- a/contracts/libraries/CurrencySettleTake.sol +++ b/contracts/libraries/CurrencySettleTake.sol @@ -5,20 +5,41 @@ import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; +/// @notice Library used to interact with PoolManager.sol to settle any open deltas. +/// To settle a positive delta (a credit to the user), a user may take or mint. +/// To settle a negative delta (a debt on the user), a user make transfer or burn to pay off a debt. +/// @dev Note that sync() is called before any erc-20 transfer in `settle`. 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 { - if (currency.isNative()) { - manager.settle{value: uint128(amount)}(currency); - } else if (burn) { + // 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 { - IERC20Minimal(Currency.unwrap(currency)).transferFrom(payer, address(manager), uint128(amount)); + manager.sync(currency); + if (payer != address(this)) { + IERC20Minimal(Currency.unwrap(currency)).transferFrom(payer, address(manager), amount); + } else { + IERC20Minimal(Currency.unwrap(currency)).transfer(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/contracts/libraries/FeeMath.sol b/contracts/libraries/FeeMath.sol index 30e97d6c..cf202dc2 100644 --- a/contracts/libraries/FeeMath.sol +++ b/contracts/libraries/FeeMath.sol @@ -3,25 +3,28 @@ pragma solidity ^0.8.24; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; library FeeMath { + using SafeCast for uint256; + function getFeesOwed( uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, - uint128 liquidity + uint256 liquidity ) internal pure returns (uint128 token0Owed, uint128 token1Owed) { token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); } - function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint128 liquidity) + function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint256 liquidity) internal pure returns (uint128 tokenOwed) { tokenOwed = - uint128(FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)); + (FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)).toUint128(); } } diff --git a/contracts/libraries/LiquiditySaltLibrary.sol b/contracts/libraries/LiquiditySaltLibrary.sol new file mode 100644 index 00000000..c0a4fda8 --- /dev/null +++ b/contracts/libraries/LiquiditySaltLibrary.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +/// @notice Library used to interact with PoolManager.sol to settle any open deltas. +/// To settle a positive delta (a credit to the user), a user may take or mint. +/// To settle a negative delta (a debt on the user), a user make transfer or burn to pay off a debt. +/// @dev Note that sync() is called before any erc-20 transfer in `settle`. +library LiquiditySaltLibrary { + /// @notice Calculates the salt parameters for IPoolManager.ModifyLiquidityParams + /// If the hook uses after*LiquidityReturnDelta, the salt is the address of the sender + /// otherwise, use 0 for warm-storage gas savings + function getLiquiditySalt(IHooks hooks, address sender) internal pure returns (bytes32 salt) { + salt = Hooks.hasPermission(hooks, Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG) + || Hooks.hasPermission(hooks, Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG) + ? bytes32(uint256(uint160(sender))) + : bytes32(0); + } +} diff --git a/contracts/libraries/PoolGetters.sol b/contracts/libraries/PoolGetters.sol index e3cb318b..df31f3c1 100644 --- a/contracts/libraries/PoolGetters.sol +++ b/contracts/libraries/PoolGetters.sol @@ -5,6 +5,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; /// @title Helper functions to access pool information /// TODO: Expose other getters on core with extsload. Only use when extsload is available and storage layout is frozen. @@ -13,6 +14,8 @@ library PoolGetters { uint256 constant TICKS_OFFSET = 4; uint256 constant TICK_BITMAP_OFFSET = 5; + using StateLibrary for IPoolManager; + function getNetLiquidityAtTick(IPoolManager poolManager, PoolId poolId, int24 tick) internal view @@ -63,7 +66,8 @@ library PoolGetters { // all the 1s at or to the right of the current bitPos uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); // uint256 masked = self[wordPos] & mask; - uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; + uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); + uint256 masked = tickBitmap & mask; // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word initialized = masked != 0; @@ -76,7 +80,8 @@ library PoolGetters { (int16 wordPos, uint8 bitPos) = position(compressed + 1); // all the 1s at or to the left of the bitPos uint256 mask = ~((1 << bitPos) - 1); - uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; + uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); + uint256 masked = tickBitmap & mask; // if there are no initialized ticks to the left of the current tick, return leftmost in the word initialized = masked != 0; diff --git a/contracts/libraries/PoolStateLibrary.sol b/contracts/libraries/PoolStateLibrary.sol deleted file mode 100644 index 487c5530..00000000 --- a/contracts/libraries/PoolStateLibrary.sol +++ /dev/null @@ -1,336 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; - -import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; - -library PoolStateLibrary { - // forge inspect lib/v4-core/src/PoolManager.sol:PoolManager storage --pretty - // | Name | Type | Slot | Offset | Bytes | Contract | - // |-----------------------|---------------------------------------------------------------------|------|--------|-------|---------------------------------------------| - // | pools | mapping(PoolId => struct Pool.State) | 8 | 0 | 32 | lib/v4-core/src/PoolManager.sol:PoolManager | - uint256 public constant POOLS_SLOT = 8; - - // index of feeGrowthGlobal0X128 in Pool.State - uint256 public constant FEE_GROWTH_GLOBAL0_OFFSET = 1; - // index of feeGrowthGlobal1X128 in Pool.State - uint256 public constant FEE_GROWTH_GLOBAL1_OFFSET = 2; - - // index of liquidity in Pool.State - uint256 public constant LIQUIDITY_OFFSET = 3; - - // index of TicksInfo mapping in Pool.State - uint256 public constant TICK_INFO_OFFSET = 4; - - // index of tickBitmap mapping in Pool.State - uint256 public constant TICK_BITMAP_OFFSET = 5; - - // index of Position.Info mapping in Pool.State - uint256 public constant POSITION_INFO_OFFSET = 6; - - /** - * @notice Get Slot0 of the pool: sqrtPriceX96, tick, protocolFee, swapFee - * @dev Corresponds to pools[poolId].slot0 - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @return sqrtPriceX96 The square root of the price of the pool, in Q96 precision. - * @return tick The current tick of the pool. - * @return protocolFee The protocol fee of the pool. - * @return swapFee The swap fee of the pool. - */ - function getSlot0(IPoolManager manager, PoolId poolId) - internal - view - returns (uint160 sqrtPriceX96, int24 tick, uint16 protocolFee, uint24 swapFee) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - bytes32 data = manager.extsload(stateSlot); - - // 32 bits |24bits|16bits |24 bits|160 bits - // 0x00000000 000bb8 0000 ffff75 0000000000000000fe3aa841ba359daa0ea9eff7 - // ---------- | fee |protocolfee | tick | sqrtPriceX96 - assembly { - // bottom 160 bits of data - sqrtPriceX96 := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - // next 24 bits of data - tick := and(shr(160, data), 0xFFFFFF) - // next 16 bits of data - protocolFee := and(shr(184, data), 0xFFFF) - // last 24 bits of data - swapFee := and(shr(200, data), 0xFFFFFF) - } - } - - /** - * @notice Retrieves the tick information of a pool at a specific tick. - * @dev Corresponds to pools[poolId].ticks[tick] - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve information for. - * @return liquidityGross The total position liquidity that references this tick - * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) - * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - */ - function getTickInfo(IPoolManager manager, PoolId poolId, int24 tick) - internal - view - returns ( - uint128 liquidityGross, - int128 liquidityNet, - uint256 feeGrowthOutside0X128, - uint256 feeGrowthOutside1X128 - ) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int24 => TickInfo) ticks` - bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); - - // slot key of the tick key: `pools[poolId].ticks[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); - - // read all 3 words of the TickInfo struct - bytes memory data = manager.extsload(slot, 3); - assembly { - liquidityGross := shr(128, mload(add(data, 32))) - liquidityNet := and(mload(add(data, 32)), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - feeGrowthOutside0X128 := mload(add(data, 64)) - feeGrowthOutside1X128 := mload(add(data, 96)) - } - } - - /** - * @notice Retrieves the liquidity information of a pool at a specific tick. - * @dev Corresponds to pools[poolId].ticks[tick].liquidityGross and pools[poolId].ticks[tick].liquidityNet. A more gas efficient version of getTickInfo - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve liquidity for. - * @return liquidityGross The total position liquidity that references this tick - * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) - */ - function getTickLiquidity(IPoolManager manager, PoolId poolId, int24 tick) - internal - view - returns (uint128 liquidityGross, int128 liquidityNet) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int24 => TickInfo) ticks` - bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); - - // slot key of the tick key: `pools[poolId].ticks[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); - - bytes32 value = manager.extsload(slot); - assembly { - liquidityNet := shr(128, value) - liquidityGross := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - } - } - - /** - * @notice Retrieves the fee growth outside a tick range of a pool - * @dev Corresponds to pools[poolId].ticks[tick].feeGrowthOutside0X128 and pools[poolId].ticks[tick].feeGrowthOutside1X128. A more gas efficient version of getTickInfo - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve fee growth for. - * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - */ - function getTickFeeGrowthOutside(IPoolManager manager, PoolId poolId, int24 tick) - internal - view - returns (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int24 => TickInfo) ticks` - bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); - - // slot key of the tick key: `pools[poolId].ticks[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); - - // TODO: offset to feeGrowth, to avoid 3-word read - bytes memory data = manager.extsload(slot, 3); - assembly { - feeGrowthOutside0X128 := mload(add(data, 64)) - feeGrowthOutside1X128 := mload(add(data, 96)) - } - } - - /** - * @notice Retrieves the global fee growth of a pool. - * @dev Corresponds to pools[poolId].feeGrowthGlobal0X128 and pools[poolId].feeGrowthGlobal1X128 - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @return feeGrowthGlobal0 The global fee growth for token0. - * @return feeGrowthGlobal1 The global fee growth for token1. - */ - function getFeeGrowthGlobal(IPoolManager manager, PoolId poolId) - internal - view - returns (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State, `uint256 feeGrowthGlobal0X128` - bytes32 slot_feeGrowthGlobal0X128 = bytes32(uint256(stateSlot) + FEE_GROWTH_GLOBAL0_OFFSET); - - // reads 3rd word of Pool.State, `uint256 feeGrowthGlobal1X128` - // bytes32 slot_feeGrowthGlobal1X128 = bytes32(uint256(stateSlot) + uint256(FEE_GROWTH_GLOBAL1_OFFSET)); - - // feeGrowthGlobal0 = uint256(manager.extsload(slot_feeGrowthGlobal0X128)); - // feeGrowthGlobal1 = uint256(manager.extsload(slot_feeGrowthGlobal1X128)); - - // read the 2 words of feeGrowthGlobal - bytes memory data = manager.extsload(slot_feeGrowthGlobal0X128, 2); - assembly { - feeGrowthGlobal0 := mload(add(data, 32)) - feeGrowthGlobal1 := mload(add(data, 64)) - } - } - - /** - * @notice Retrieves total the liquidity of a pool. - * @dev Corresponds to pools[poolId].liquidity - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @return liquidity The liquidity of the pool. - */ - function getLiquidity(IPoolManager manager, PoolId poolId) internal view returns (uint128 liquidity) { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `uint128 liquidity` - bytes32 slot = bytes32(uint256(stateSlot) + LIQUIDITY_OFFSET); - - liquidity = uint128(uint256(manager.extsload(slot))); - } - - /** - * @notice Retrieves the tick bitmap of a pool at a specific tick. - * @dev Corresponds to pools[poolId].tickBitmap[tick] - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve the bitmap for. - * @return tickBitmap The bitmap of the tick. - */ - function getTickBitmap(IPoolManager manager, PoolId poolId, int16 tick) - internal - view - returns (uint256 tickBitmap) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int16 => uint256) tickBitmap;` - bytes32 tickBitmapMapping = bytes32(uint256(stateSlot) + TICK_BITMAP_OFFSET); - - // slot id of the mapping key: `pools[poolId].tickBitmap[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), tickBitmapMapping)); - - tickBitmap = uint256(manager.extsload(slot)); - } - - /** - * @notice Retrieves the position information of a pool at a specific position ID. - * @dev Corresponds to pools[poolId].positions[positionId] - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param positionId The ID of the position. - * @return liquidity The liquidity of the position. - * @return feeGrowthInside0LastX128 The fee growth inside the position for token0. - * @return feeGrowthInside1LastX128 The fee growth inside the position for token1. - */ - function getPositionInfo(IPoolManager manager, PoolId poolId, bytes32 positionId) - internal - view - returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(bytes32 => Position.Info) positions;` - bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITION_INFO_OFFSET); - - // first value slot of the mapping key: `pools[poolId].positions[positionId] (liquidity) - bytes32 slot = keccak256(abi.encodePacked(positionId, positionMapping)); - - // read all 3 words of the Position.Info struct - bytes memory data = manager.extsload(slot, 3); - - assembly { - liquidity := mload(add(data, 32)) - feeGrowthInside0LastX128 := mload(add(data, 64)) - feeGrowthInside1LastX128 := mload(add(data, 96)) - } - } - - /** - * @notice Retrieves the liquidity of a position. - * @dev Corresponds to pools[poolId].positions[positionId].liquidity. A more gas efficient version of getPositionInfo - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param positionId The ID of the position. - * @return liquidity The liquidity of the position. - */ - function getPositionLiquidity(IPoolManager manager, PoolId poolId, bytes32 positionId) - internal - view - returns (uint128 liquidity) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(bytes32 => Position.Info) positions;` - bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITION_INFO_OFFSET); - - // first value slot of the mapping key: `pools[poolId].positions[positionId] (liquidity) - bytes32 slot = keccak256(abi.encodePacked(positionId, positionMapping)); - - liquidity = uint128(uint256(manager.extsload(slot))); - } - - /** - * @notice Live calculate the fee growth inside a tick range of a pool - * @dev pools[poolId].feeGrowthInside0LastX128 in Position.Info is cached and can become stale. This function will live calculate the feeGrowthInside - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tickLower The lower tick of the range. - * @param tickUpper The upper tick of the range. - * @return feeGrowthInside0X128 The fee growth inside the tick range for token0. - * @return feeGrowthInside1X128 The fee growth inside the tick range for token1. - */ - function getFeeGrowthInside(IPoolManager manager, PoolId poolId, int24 tickLower, int24 tickUpper) - internal - view - returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) - { - (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = getFeeGrowthGlobal(manager, poolId); - - (uint256 lowerFeeGrowthOutside0X128, uint256 lowerFeeGrowthOutside1X128) = - getTickFeeGrowthOutside(manager, poolId, tickLower); - (uint256 upperFeeGrowthOutside0X128, uint256 upperFeeGrowthOutside1X128) = - getTickFeeGrowthOutside(manager, poolId, tickUpper); - (, int24 tickCurrent,,) = getSlot0(manager, poolId); - unchecked { - if (tickCurrent < tickLower) { - feeGrowthInside0X128 = lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; - feeGrowthInside1X128 = lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; - } else if (tickCurrent >= tickUpper) { - feeGrowthInside0X128 = upperFeeGrowthOutside0X128 - lowerFeeGrowthOutside0X128; - feeGrowthInside1X128 = upperFeeGrowthOutside1X128 - lowerFeeGrowthOutside1X128; - } else { - feeGrowthInside0X128 = feeGrowthGlobal0X128 - lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; - feeGrowthInside1X128 = feeGrowthGlobal1X128 - lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; - } - } - } -} diff --git a/contracts/libraries/PoolTicksCounter.sol b/contracts/libraries/PoolTicksCounter.sol index 077ef4a6..60fdbbe5 100644 --- a/contracts/libraries/PoolTicksCounter.sol +++ b/contracts/libraries/PoolTicksCounter.sol @@ -5,9 +5,11 @@ import {PoolGetters} from "./PoolGetters.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; library PoolTicksCounter { using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; struct TickCache { int16 wordPosLower; @@ -41,15 +43,13 @@ library PoolTicksCounter { // If the initializable tick after the swap is initialized, our original tickAfter is a // multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized // and we shouldn't count it. - uint256 bmAfter = self.getPoolBitmapInfo(key.toId(), wordPosAfter); - //uint256 bmAfter = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPosAfter); + uint256 bmAfter = self.getTickBitmap(key.toId(), wordPosAfter); cache.tickAfterInitialized = ((bmAfter & (1 << bitPosAfter)) > 0) && ((tickAfter % key.tickSpacing) == 0) && (tickBefore > tickAfter); // In the case where tickBefore is initialized, we only want to count it if we are swapping upwards. // Use the same logic as above to decide whether we should count tickBefore or not. - uint256 bmBefore = self.getPoolBitmapInfo(key.toId(), wordPos); - //uint256 bmBefore = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPos); + uint256 bmBefore = self.getTickBitmap(key.toId(), wordPos); cache.tickBeforeInitialized = ((bmBefore & (1 << bitPos)) > 0) && ((tickBefore % key.tickSpacing) == 0) && (tickBefore < tickAfter); @@ -76,8 +76,7 @@ library PoolTicksCounter { mask = mask & (type(uint256).max >> (255 - cache.bitPosHigher)); } - //uint256 bmLower = PoolGetters.getTickBitmapAtWord(self, key.toId(), cache.wordPosLower); - uint256 bmLower = self.getPoolBitmapInfo(key.toId(), cache.wordPosLower); + uint256 bmLower = self.getTickBitmap(key.toId(), cache.wordPosLower); uint256 masked = bmLower & mask; initializedTicksLoaded += countOneBits(masked); cache.wordPosLower++; diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index 2b58ecbc..00000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 diff --git a/lib/v4-core b/lib/v4-core index f5674e46..6e6ce35b 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit f5674e46720c0fc4606b287cccc583d56245e724 +Subproject commit 6e6ce35b69b15cb61bd8cb8488c7d064fab52886 diff --git a/remappings.txt b/remappings.txt index e05c5bd6..94b76d6a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,4 @@ @uniswap/v4-core/=lib/v4-core/ solmate/=lib/solmate/src/ -forge-std/=lib/forge-std/src/ @openzeppelin/=lib/openzeppelin-contracts/ +forge-std/=lib/v4-core/lib/forge-std/src/ \ No newline at end of file diff --git a/test/FullRange.t.sol b/test/FullRange.t.sol index f0867ba4..5edec106 100644 --- a/test/FullRange.t.sol +++ b/test/FullRange.t.sol @@ -20,14 +20,16 @@ import {UniswapV4ERC20} from "../contracts/libraries/UniswapV4ERC20.sol"; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; contract TestFullRange is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; using SafeCast for uint256; using CurrencyLibrary for Currency; + using StateLibrary for IPoolManager; event Initialize( - PoolId indexed poolId, + PoolId poolId, Currency indexed currency0, Currency indexed currency1, uint24 fee, @@ -39,7 +41,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); event Swap( PoolId indexed id, - address indexed sender, + address sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, @@ -104,7 +106,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { token1.approve(address(router), type(uint256).max); token2.approve(address(router), type(uint256).max); - initPool(keyWithLiq.currency0, keyWithLiq.currency1, fullRange, 3000, SQRT_RATIO_1_1, ZERO_BYTES); + initPool(keyWithLiq.currency0, keyWithLiq.currency1, fullRange, 3000, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( keyWithLiq.currency0, @@ -127,7 +129,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { emit Initialize(id, testKey.currency0, testKey.currency1, testKey.fee, testKey.tickSpacing, testKey.hooks); snapStart("FullRangeInitialize"); - manager.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); snapEnd(); (, address liquidityToken) = fullRange.poolInfo(id); @@ -139,11 +141,11 @@ contract TestFullRange is Test, Deployers, GasSnapshot { PoolKey memory wrongKey = PoolKey(key.currency0, key.currency1, 0, TICK_SPACING + 1, fullRange); vm.expectRevert(FullRange.TickSpacingNotDefault.selector); - manager.initialize(wrongKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(wrongKey, SQRT_PRICE_1_1, ZERO_BYTES); } function testFullRange_addLiquidity_InitialAddSucceeds() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -169,7 +171,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_InitialAddFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); if (amount <= LOCKED_LIQUIDITY) { vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); fullRange.addLiquidity( @@ -244,7 +246,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_SwapThenAddSucceeds() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -269,9 +271,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("FullRangeSwap"); router.swap(key, params, settings, ZERO_BYTES); @@ -298,7 +300,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_FailsIfTooMuchSlippage() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -307,9 +309,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1000 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1000 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, settings, ZERO_BYTES); @@ -323,7 +325,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { function testFullRange_swap_TwoSwaps() public { PoolKey memory testKey = key; - manager.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -332,9 +334,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("FullRangeFirstSwap"); router.swap(testKey, params, settings, ZERO_BYTES); @@ -352,8 +354,8 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_swap_TwoPools() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - manager.initialize(key2, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(key2, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -367,10 +369,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10000000, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10000000, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, testSettings, ZERO_BYTES); router.swap(key2, params, testSettings, ZERO_BYTES); @@ -408,7 +410,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_InitialRemoveFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -456,7 +458,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_FailsIfNoLiquidity() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); @@ -468,7 +470,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_SucceedsWithPartial() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOfSelf(); uint256 prevBalance1 = key.currency1.balanceOfSelf(); @@ -503,7 +505,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_DiffRatios() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -550,10 +552,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { (, address liquidityToken) = fullRange.poolInfo(idWithLiq); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(keyWithLiq, params, testSettings, ZERO_BYTES); @@ -571,7 +573,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_RemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); if (amount <= LOCKED_LIQUIDITY) { @@ -626,7 +628,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { vm.prank(address(2)); token1.approve(address(fullRange), type(uint256).max); - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); // Test contract adds liquidity @@ -677,10 +679,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: SQRT_RATIO_1_4}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: SQRT_PRICE_1_4}); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, testSettings, ZERO_BYTES); @@ -704,7 +706,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_SwapRemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); if (amount <= LOCKED_LIQUIDITY) { @@ -731,11 +733,11 @@ contract TestFullRange is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: (FullMath.mulDiv(amount, 1, 4)).toInt256(), - sqrtPriceLimitX96: SQRT_RATIO_1_4 + sqrtPriceLimitX96: SQRT_PRICE_1_4 }); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, testSettings, ZERO_BYTES); @@ -753,12 +755,12 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_BeforeModifyPositionFailsWithWrongMsgSender() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); vm.expectRevert(FullRange.SenderMustBeHook.selector); modifyLiquidityRouter.modifyLiquidity( key, - IPoolManager.ModifyLiquidityParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100}), + IPoolManager.ModifyLiquidityParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100, salt: 0}), ZERO_BYTES ); } diff --git a/test/GeomeanOracle.t.sol b/test/GeomeanOracle.t.sol index 05255e93..e6ff1695 100644 --- a/test/GeomeanOracle.t.sol +++ b/test/GeomeanOracle.t.sol @@ -65,14 +65,14 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeInitializeAllowsPoolCreation() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); } function testBeforeInitializeRevertsIfFee() public { vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); manager.initialize( PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 1, MAX_TICK_SPACING, geomeanOracle), - SQRT_RATIO_1_1, + SQRT_PRICE_1_1, ZERO_BYTES ); } @@ -81,13 +81,13 @@ contract TestGeomeanOracle is Test, Deployers { vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); manager.initialize( PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 0, 60, geomeanOracle), - SQRT_RATIO_1_1, + SQRT_PRICE_1_1, ZERO_BYTES ); } function testAfterInitializeState() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); assertEq(observationState.index, 0); assertEq(observationState.cardinality, 1); @@ -95,7 +95,7 @@ contract TestGeomeanOracle is Test, Deployers { } function testAfterInitializeObservation() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); assertTrue(observation.initialized); assertEq(observation.blockTimestamp, 1); @@ -104,7 +104,7 @@ contract TestGeomeanOracle is Test, Deployers { } function testAfterInitializeObserve0() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); uint32[] memory secondsAgo = new uint32[](1); secondsAgo[0] = 0; (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = @@ -116,11 +116,11 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeModifyPositionNoObservations() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -138,12 +138,12 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeModifyPositionObservation() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -161,7 +161,7 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeModifyPositionObservationAndCardinality() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds geomeanOracle.increaseCardinalityNext(key, 2); GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); @@ -172,7 +172,7 @@ contract TestGeomeanOracle is Test, Deployers { modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -199,12 +199,12 @@ contract TestGeomeanOracle is Test, Deployers { } function testPermanentLiquidity() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -213,7 +213,7 @@ contract TestGeomeanOracle is Test, Deployers { modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), -1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), -1000, 0 ), ZERO_BYTES ); diff --git a/test/LimitOrder.t.sol b/test/LimitOrder.t.sol index 9b9e3116..29b1093f 100644 --- a/test/LimitOrder.t.sol +++ b/test/LimitOrder.t.sol @@ -15,11 +15,13 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; contract TestLimitOrder is Test, Deployers { using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; - uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + uint160 constant SQRT_PRICE_10_1 = 250541448375047931186413801569; HookEnabledSwapRouter router; TestERC20 token0; @@ -48,7 +50,7 @@ contract TestLimitOrder is Test, Deployers { } // key = PoolKey(currency0, currency1, 3000, 60, limitOrder); - (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_PRICE_1_1, ZERO_BYTES); token0.approve(address(limitOrder), type(uint256).max); token1.approve(address(limitOrder), type(uint256).max); @@ -63,7 +65,7 @@ contract TestLimitOrder is Test, Deployers { function testGetTickLowerLastWithDifferentPrice() public { PoolKey memory differentKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 61, limitOrder); - manager.initialize(differentKey, SQRT_RATIO_10_1, ZERO_BYTES); + manager.initialize(differentKey, SQRT_PRICE_10_1, ZERO_BYTES); assertEq(limitOrder.getTickLowerLast(differentKey.toId()), 22997); } @@ -82,7 +84,8 @@ contract TestLimitOrder is Test, Deployers { uint128 liquidity = 1000000; limitOrder.place(key, tickLower, zeroForOne, liquidity); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity); + + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); } function testZeroForOneLeftBoundaryOfCurrentRange() public { @@ -91,7 +94,7 @@ contract TestLimitOrder is Test, Deployers { uint128 liquidity = 1000000; limitOrder.place(key, tickLower, zeroForOne, liquidity); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); } function testZeroForOneCrossedRangeRevert() public { @@ -103,8 +106,8 @@ contract TestLimitOrder is Test, Deployers { // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei router.swap( key, - IPoolManager.SwapParams(false, -1 ether, SQRT_RATIO_1_1 + 1), - HookEnabledSwapRouter.TestSettings(true, true), + IPoolManager.SwapParams(false, -1 ether, SQRT_PRICE_1_1 + 1), + HookEnabledSwapRouter.TestSettings(false, false), ZERO_BYTES ); vm.expectRevert(LimitOrder.InRange.selector); @@ -117,7 +120,7 @@ contract TestLimitOrder is Test, Deployers { uint128 liquidity = 1000000; limitOrder.place(key, tickLower, zeroForOne, liquidity); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); } function testNotZeroForOneCrossedRangeRevert() public { @@ -129,8 +132,8 @@ contract TestLimitOrder is Test, Deployers { // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei router.swap( key, - IPoolManager.SwapParams(true, -1 ether, SQRT_RATIO_1_1 - 1), - HookEnabledSwapRouter.TestSettings(true, true), + IPoolManager.SwapParams(true, -1 ether, SQRT_PRICE_1_1 - 1), + HookEnabledSwapRouter.TestSettings(false, false), ZERO_BYTES ); vm.expectRevert(LimitOrder.InRange.selector); @@ -151,7 +154,7 @@ contract TestLimitOrder is Test, Deployers { limitOrder.place(key, tickLower, zeroForOne, liquidity); vm.stopPrank(); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity * 2); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity * 2); ( bool filled, @@ -191,8 +194,8 @@ contract TestLimitOrder is Test, Deployers { router.swap( key, - IPoolManager.SwapParams(false, -1e18, TickMath.getSqrtRatioAtTick(60)), - HookEnabledSwapRouter.TestSettings(true, true), + IPoolManager.SwapParams(false, -1e18, TickMath.getSqrtPriceAtTick(60)), + HookEnabledSwapRouter.TestSettings(false, false), ZERO_BYTES ); @@ -205,7 +208,7 @@ contract TestLimitOrder is Test, Deployers { assertTrue(filled); assertEq(token0Total, 0); assertEq(token1Total, 2996 + 17); // 3013, 2 wei of dust - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), 0); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, 0); vm.expectEmit(true, true, true, true, address(token1)); emit Transfer(address(manager), new GetSender().sender(), 2996 + 17); diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index f3d2ceb1..f434fd19 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -19,18 +19,20 @@ import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; contract QuoterTest is Test, Deployers { using SafeCast for *; using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; // Min tick for full range with tick spacing of 60 int24 internal constant MIN_TICK = -887220; // Max tick for full range with tick spacing of 60 int24 internal constant MAX_TICK = -MIN_TICK; - uint160 internal constant SQRT_RATIO_100_102 = 78447570448055484695608110440; - uint160 internal constant SQRT_RATIO_102_100 = 80016521857016594389520272648; + uint160 internal constant SQRT_PRICE_100_102 = 78447570448055484695608110440; + uint160 internal constant SQRT_PRICE_102_100 = 80016521857016594389520272648; uint256 internal constant CONTROLLER_GAS_LIMIT = 500000; @@ -119,11 +121,11 @@ contract QuoterTest is Test, Deployers { assertEq(initializedTicksLoaded, 2); } - // nested self-call into lockAcquired reverts - function testQuoter_callLockAcquired_reverts() public { + // nested self-call into unlockCallback reverts + function testQuoter_callUnlockCallback_reverts() public { vm.expectRevert(IQuoter.LockFailure.selector); vm.prank(address(manager)); - quoter.lockAcquired(abi.encodeWithSelector(quoter.lockAcquired.selector, address(this), "0x")); + quoter.unlockCallback(abi.encodeWithSelector(quoter.unlockCallback.selector, address(this), "0x")); } function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { @@ -325,13 +327,13 @@ contract QuoterTest is Test, Deployers { zeroForOne: true, recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_100_102, + sqrtPriceLimitX96: SQRT_PRICE_100_102, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[0], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_100_102); + assertEq(sqrtPriceX96After, SQRT_PRICE_100_102); assertEq(initializedTicksLoaded, 0); } @@ -343,13 +345,13 @@ contract QuoterTest is Test, Deployers { zeroForOne: false, recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_102_100, + sqrtPriceLimitX96: SQRT_PRICE_102_100, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[1], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_102_100); + assertEq(sqrtPriceX96After, SQRT_PRICE_102_100); assertEq(initializedTicksLoaded, 0); } @@ -542,7 +544,7 @@ contract QuoterTest is Test, Deployers { } function setupPool(PoolKey memory poolKey) internal { - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity( @@ -550,14 +552,15 @@ contract QuoterTest is Test, Deployers { IPoolManager.ModifyLiquidityParams( MIN_TICK, MAX_TICK, - calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + 0 ), ZERO_BYTES ); } function setupPoolMultiplePositions(PoolKey memory poolKey) internal { - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity( @@ -565,21 +568,22 @@ contract QuoterTest is Test, Deployers { IPoolManager.ModifyLiquidityParams( MIN_TICK, MAX_TICK, - calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - -60, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -60, 60, 100, 100).toInt256() + -60, 60, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -60, 60, 100, 100).toInt256(), 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - -120, 120, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 120, 100, 100).toInt256() + -120, 120, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -120, 120, 100, 100).toInt256(), 0 ), ZERO_BYTES ); @@ -589,7 +593,7 @@ contract QuoterTest is Test, Deployers { PoolId poolId = poolKey.toId(); (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); if (sqrtPriceX96 == 0) { - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); } MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); @@ -599,21 +603,22 @@ contract QuoterTest is Test, Deployers { IPoolManager.ModifyLiquidityParams( MIN_TICK, MAX_TICK, - calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - 0, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, 0, 60, 100, 100).toInt256() + 0, 60, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, 0, 60, 100, 100).toInt256(), 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - -120, 0, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 0, 100, 100).toInt256() + -120, 0, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -120, 0, 100, 100).toInt256(), 0 ), ZERO_BYTES ); @@ -626,8 +631,8 @@ contract QuoterTest is Test, Deployers { uint256 amount0, uint256 amount1 ) internal pure returns (uint128 liquidity) { - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); + uint160 sqrtRatioAX96 = TickMath.getSqrtPriceAtTick(tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtPriceAtTick(tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1); } diff --git a/test/SimpleBatchCallTest.t.sol b/test/SimpleBatchCallTest.t.sol index 367dcb1a..04a0e922 100644 --- a/test/SimpleBatchCallTest.t.sol +++ b/test/SimpleBatchCallTest.t.sol @@ -14,11 +14,13 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Test} from "forge-std/Test.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; /// @title SimpleBatchCall /// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. contract SimpleBatchCallTest is Test, Deployers { using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; SimpleBatchCall batchCall; @@ -35,30 +37,28 @@ contract SimpleBatchCallTest is Test, Deployers { function test_initialize() public { bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_RATIO_1_1, ZERO_BYTES); - bytes memory settleData = - abi.encode(SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true})); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + bytes memory settleData = abi.encode(SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false})); batchCall.execute(abi.encode(calls), ZERO_BYTES); (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId()); - assertEq(sqrtPriceX96, SQRT_RATIO_1_1); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); } function test_initialize_modifyPosition() public { bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_RATIO_1_1, ZERO_BYTES); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); calls[1] = abi.encodeWithSelector( ICallsWithLock.modifyPositionWithLock.selector, key, - IPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18}), + IPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18, salt: 0}), ZERO_BYTES ); Currency[] memory currenciesTouched = new Currency[](2); currenciesTouched[0] = currency0; currenciesTouched[1] = currency1; - bytes memory settleData = abi.encode( - currenciesTouched, SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true}) - ); + bytes memory settleData = + abi.encode(currenciesTouched, SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false})); uint256 balance0 = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); uint256 balance1 = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); batchCall.execute(abi.encode(calls), settleData); @@ -69,6 +69,6 @@ contract SimpleBatchCallTest is Test, Deployers { assertGt(balance0After, balance0); assertGt(balance1After, balance1); - assertEq(sqrtPriceX96, SQRT_RATIO_1_1); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); } } diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol index 96941963..0f2f82e0 100644 --- a/test/TWAMM.t.sol +++ b/test/TWAMM.t.sol @@ -69,21 +69,21 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { } } - (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_PRICE_1_1, ZERO_BYTES); token0.approve(address(modifyLiquidityRouter), 100 ether); token1.approve(address(modifyLiquidityRouter), 100 ether); token0.mint(address(this), 100 ether); token1.mint(address(this), 100 ether); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether, 0), ZERO_BYTES ); } @@ -93,7 +93,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(twamm.lastVirtualOrderTimestamp(initId), 0); vm.warp(10000); - manager.initialize(initKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(initKey, SQRT_PRICE_1_1, ZERO_BYTES); assertEq(twamm.lastVirtualOrderTimestamp(initId), 10000); } @@ -363,7 +363,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { token0.approve(address(twamm), 100e18); token1.approve(address(twamm), 100e18); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES ); vm.warp(10000); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 0f6afbc7..a0b78ac0 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -19,7 +19,6 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; @@ -48,7 +47,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); lpm = new NonfungiblePositionManager(manager); @@ -70,20 +69,17 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { vm.stopPrank(); } - function test_collect_6909(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees uint256 swapAmount = 0.01e18; swap(key, false, -int256(swapAmount), ZERO_BYTES); // collect fees - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, true); assertEq(delta.amount0(), 0); @@ -93,12 +89,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); } - function test_collect_erc20(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees uint256 swapAmount = 0.01e18; @@ -118,37 +113,24 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { } // two users with the same range; one user cannot collect the other's fees - function test_collect_sameRange_6909( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDeltaAlice, - uint128 liquidityDeltaBob - ) public { + function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) + public + { uint256 tokenIdAlice; uint256 tokenIdBob; - liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity - liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); - (tokenIdAlice,) = lpm.mint( - LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), - liquidityDeltaAlice, - block.timestamp + 1, - alice, - ZERO_BYTES - ); + (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); vm.prank(bob); - (tokenIdBob,) = lpm.mint( - LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), - liquidityDeltaBob, - block.timestamp + 1, - alice, - ZERO_BYTES - ); + (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); // swap to create fees uint256 swapAmount = 0.01e18; @@ -173,31 +155,28 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); } - function test_collect_sameRange_erc20( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDeltaAlice, - uint128 liquidityDeltaBob - ) public { - liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity - liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); - + function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) + public + { uint256 tokenIdAlice; - vm.startPrank(alice); - (tokenIdAlice, tickLower, tickUpper, liquidityDeltaAlice,) = - createFuzzyLiquidity(lpm, alice, key, tickLower, tickUpper, liquidityDeltaAlice, ZERO_BYTES); - vm.stopPrank(); - uint256 tokenIdBob; - vm.startPrank(bob); - (tokenIdBob,,,,) = createFuzzyLiquidity(lpm, bob, key, tickLower, tickUpper, liquidityDeltaBob, ZERO_BYTES); - vm.stopPrank(); + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + vm.prank(alice); + (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); - vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity + vm.prank(bob); + (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); // confirm the positions are same range - (,, LiquidityRange memory rangeAlice,,,,,) = lpm.positions(tokenIdAlice); - (,, LiquidityRange memory rangeBob,,,,,) = lpm.positions(tokenIdBob); + (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); + (, LiquidityRange memory rangeBob) = lpm.tokenPositions(tokenIdBob); assertEq(rangeAlice.tickLower, rangeBob.tickLower); assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); @@ -238,69 +217,40 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_collect_donate_sameRange() public {} function test_decreaseLiquidity_sameRange( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDeltaAlice, - uint128 liquidityDeltaBob + IPoolManager.ModifyLiquidityParams memory params, + uint256 liquidityDeltaBob ) public { - liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity - liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); - uint256 tokenIdAlice; - BalanceDelta lpDeltaAlice; - vm.startPrank(alice); - (tokenIdAlice, tickLower, tickUpper, liquidityDeltaAlice, lpDeltaAlice) = - createFuzzyLiquidity(lpm, alice, key, tickLower, tickUpper, liquidityDeltaAlice, ZERO_BYTES); - vm.stopPrank(); - uint256 tokenIdBob; - BalanceDelta lpDeltaBob; - vm.startPrank(bob); - (tokenIdBob,,,, lpDeltaBob) = - createFuzzyLiquidity(lpm, bob, key, tickLower, tickUpper, liquidityDeltaBob, ZERO_BYTES); - vm.stopPrank(); + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + vm.prank(alice); + (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + + vm.prank(bob); + (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); // swap to create fees uint256 swapAmount = 0.001e18; swap(key, true, -int256(swapAmount), ZERO_BYTES); // alice removes all of her liquidity - // uint256 balance0AliceBefore = manager.balanceOf(alice, currency0.toId()); - // uint256 balance1AliceBefore = manager.balanceOf(alice, currency1.toId()); vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: liquidityDeltaAlice, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: alice - }), - ZERO_BYTES, - true - ); - assertEq(uint256(uint128(-aliceDelta.amount0())), manager.balanceOf(alice, currency0.toId())); - assertEq(uint256(uint128(-aliceDelta.amount1())), manager.balanceOf(alice, currency1.toId())); + BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, uint256(params.liquidityDelta), ZERO_BYTES, true); + assertEq(uint256(uint128(aliceDelta.amount0())), manager.balanceOf(alice, currency0.toId())); + assertEq(uint256(uint128(aliceDelta.amount1())), manager.balanceOf(alice, currency1.toId())); // bob removes half of his liquidity vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdBob, - liquidityDelta: liquidityDeltaBob / 2, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: bob - }), - ZERO_BYTES, - true - ); - assertEq(uint256(uint128(-bobDelta.amount0())), manager.balanceOf(bob, currency0.toId())); - assertEq(uint256(uint128(-bobDelta.amount1())), manager.balanceOf(bob, currency1.toId())); + BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityDeltaBob / 2, ZERO_BYTES, true); + assertEq(uint256(uint128(bobDelta.amount0())), manager.balanceOf(bob, currency0.toId())); + assertEq(uint256(uint128(bobDelta.amount1())), manager.balanceOf(bob, currency1.toId())); // position manager holds no fees now assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); @@ -331,18 +281,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice decreases liquidity vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityAlice), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: alice - }), - ZERO_BYTES, - true - ); + BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); uint256 tolerance = 0.000000001 ether; @@ -362,18 +301,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // bob decreases half of his liquidity vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdBob, - liquidityDelta: uint128(liquidityBob / 2), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: bob - }), - ZERO_BYTES, - true - ); + BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); // bob claims half of the original principal + his fees assertApproxEqAbs( diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 5b98ac97..939d88be 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -15,16 +15,14 @@ import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {PoolStateLibrary} from "../../contracts/libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; -import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; contract GasTest is Test, Deployers, GasSnapshot { using FixedPointMathLib for uint256; @@ -52,7 +50,7 @@ contract GasTest is Test, Deployers, GasSnapshot { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); lpm = new NonfungiblePositionManager(manager); @@ -68,23 +66,23 @@ contract GasTest is Test, Deployers, GasSnapshot { range = LiquidityRange({key: key, tickLower: -300, tickUpper: 300}); } - function test_gas_mint() public { - uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - snapStart("mint"); - lpm.mint(params); - snapEnd(); - } + // function test_gas_mint() public { + // uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity + // uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: 0, + // amount1Min: 0, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // snapStart("mint"); + // lpm.mint(params); + // snapEnd(); + // } function test_gas_mintWithLiquidity() public { snapStart("mintWithLiquidity"); @@ -95,66 +93,32 @@ contract GasTest is Test, Deployers, GasSnapshot { function test_gas_increaseLiquidity_erc20() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager - .IncreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 1000 ether, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }); snapStart("increaseLiquidity_erc20"); - lpm.increaseLiquidity(params, ZERO_BYTES, false); + lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, false); snapEnd(); } function test_gas_increaseLiquidity_erc6909() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager - .IncreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 1000 ether, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }); snapStart("increaseLiquidity_erc6909"); - lpm.increaseLiquidity(params, ZERO_BYTES, true); + lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, true); snapEnd(); } function test_gas_decreaseLiquidity_erc20() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 10_000 ether, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); snapStart("decreaseLiquidity_erc20"); - lpm.decreaseLiquidity(params, ZERO_BYTES, false); + lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, false); snapEnd(); } function test_gas_decreaseLiquidity_erc6909() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 10_000 ether, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); snapStart("decreaseLiquidity_erc6909"); - lpm.decreaseLiquidity(params, ZERO_BYTES, true); + lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, true); snapEnd(); } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 666619db..c3863b9f 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -15,18 +15,17 @@ import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {PoolStateLibrary} from "../../contracts/libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; -import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; @@ -52,7 +51,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); lpm = new NonfungiblePositionManager(manager); @@ -99,30 +98,18 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice uses her exact fees to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - console2.log("token0Owed", token0Owed); - console2.log("token1Owed", token1Owed); - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(range.tickLower), - TickMath.getSqrtRatioAtTick(range.tickUpper), + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), token0Owed, token1Owed ); vm.prank(alice); - lpm.increaseLiquidity( - INonfungiblePositionManager.IncreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityDelta), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }), - ZERO_BYTES, - false - ); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); // TODO: assertions, currently increasing liquidity does not perfectly use the fees } @@ -147,30 +134,20 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { swap(key, true, -int256(swapAmount), ZERO_BYTES); swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - // alice will half of her fees to increase liquidity + // alice will use half of her fees to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); { - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(range.tickLower), - TickMath.getSqrtRatioAtTick(range.tickUpper), + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), token0Owed / 2, token1Owed / 2 ); vm.prank(alice); - lpm.increaseLiquidity( - INonfungiblePositionManager.IncreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityDelta), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }), - ZERO_BYTES, - false - ); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); } { @@ -237,11 +214,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice will use all of her fees + additional capital to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); { - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(range.tickLower), - TickMath.getSqrtRatioAtTick(range.tickUpper), + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), token0Owed * 2, token1Owed * 2 ); @@ -249,17 +226,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); vm.prank(alice); - lpm.increaseLiquidity( - INonfungiblePositionManager.IncreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityDelta), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }), - ZERO_BYTES, - false - ); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index d4d0ee6c..47d537d4 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -19,7 +19,6 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; @@ -42,7 +41,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); lpm = new NonfungiblePositionManager(manager); @@ -50,171 +49,176 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); } - function test_mint_withLiquidityDelta(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); (uint256 tokenId, BalanceDelta delta) = - lpm.mint(position, liquidityDelta, block.timestamp + 1, address(this), ZERO_BYTES); + lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta)); assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0())), "incorrect amount0"); assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); } - function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency1.balanceOfSelf(); - - assertEq(tokenId, 1); - assertEq(lpm.ownerOf(1), address(this)); - assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - } - - // minting with perfect token ratios will use all of the tokens - function test_mint_perfect() public { - int24 tickLower = -int24(key.tickSpacing); - int24 tickUpper = int24(key.tickSpacing); - uint256 amount0Desired = 100e18; - uint256 amount1Desired = 100e18; - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: amount0Desired, - amount1Min: amount1Desired, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency1.balanceOfSelf(); - - assertEq(tokenId, 1); - assertEq(lpm.ownerOf(1), address(this)); - assertEq(uint256(int256(-delta.amount0())), amount0Desired); - assertEq(uint256(int256(-delta.amount1())), amount1Desired); - assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - } - - function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - public - { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: alice, - hookData: ZERO_BYTES - }); - (uint256 tokenId,) = lpm.mint(params); - assertEq(tokenId, 1); - assertEq(lpm.ownerOf(tokenId), alice); - } - - function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - public - { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - vm.assume(tickLower < 0 && 0 < tickUpper); - - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - vm.assume(0.00001e18 < amount0Desired); - vm.assume(0.00001e18 < amount1Desired); - - uint256 amount0Min = amount0Desired - 1; - uint256 amount1Min = amount1Desired - 1; - - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: amount0Min, - amount1Min: amount1Min, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - - // seed some liquidity so we can move the price - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: TickMath.minUsableTick(key.tickSpacing), - tickUpper: TickMath.maxUsableTick(key.tickSpacing), - liquidityDelta: 100_000e18 - }), - ZERO_BYTES - ); - - // swap to move the price - swap(key, true, -1000e18, ZERO_BYTES); - - // will revert because amount0Min and amount1Min are very strict - vm.expectRevert(); - lpm.mint(params); - } - - function test_burn(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + // function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: 0, + // amount1Min: 0, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // uint256 balance0After = currency0.balanceOfSelf(); + // uint256 balance1After = currency1.balanceOfSelf(); + + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(1), address(this)); + // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // } + + // // minting with perfect token ratios will use all of the tokens + // function test_mint_perfect() public { + // int24 tickLower = -int24(key.tickSpacing); + // int24 tickUpper = int24(key.tickSpacing); + // uint256 amount0Desired = 100e18; + // uint256 amount1Desired = 100e18; + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: amount0Desired, + // amount1Min: amount1Desired, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // uint256 balance0After = currency0.balanceOfSelf(); + // uint256 balance1After = currency1.balanceOfSelf(); + + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(1), address(this)); + // assertEq(uint256(int256(-delta.amount0())), amount0Desired); + // assertEq(uint256(int256(-delta.amount1())), amount1Desired); + // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // } + + // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // public + // { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: 0, + // amount1Min: 0, + // deadline: block.timestamp + 1, + // recipient: alice, + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId,) = lpm.mint(params); + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(tokenId), alice); + // } + + // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // public + // { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // vm.assume(tickLower < 0 && 0 < tickUpper); + + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + // vm.assume(0.00001e18 < amount0Desired); + // vm.assume(0.00001e18 < amount1Desired); + + // uint256 amount0Min = amount0Desired - 1; + // uint256 amount1Min = amount1Desired - 1; + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: amount0Min, + // amount1Min: amount1Min, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + + // // seed some liquidity so we can move the price + // modifyLiquidityRouter.modifyLiquidity( + // key, + // IPoolManager.ModifyLiquidityParams({ + // tickLower: TickMath.minUsableTick(key.tickSpacing), + // tickUpper: TickMath.maxUsableTick(key.tickSpacing), + // liquidityDelta: 100_000e18, + // salt: 0 + // }), + // ZERO_BYTES + // ); + + // // swap to move the price + // swap(key, true, -1000e18, ZERO_BYTES); + + // // will revert because amount0Min and amount1Min are very strict + // vm.expectRevert(); + // lpm.mint(params); + // } + + function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { uint256 balance0Start = currency0.balanceOfSelf(); uint256 balance1Start = currency1.balanceOfSelf(); // create liquidity we can burn uint256 tokenId; - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta)); // burn liquidity uint256 balance0BeforeBurn = currency0.balanceOfSelf(); uint256 balance1BeforeBurn = currency1.balanceOfSelf(); BalanceDelta delta = lpm.burn(tokenId, address(this), ZERO_BYTES, false); - assertEq(lpm.liquidityOf(address(this), position.toId()), 0); + (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, 0); // TODO: slightly off by 1 bip (0.0001%) assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(-delta.amount0())), 0.0001e18); @@ -229,119 +233,60 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_increaseLiquidity() public {} - - function test_decreaseLiquidity( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDelta, - uint128 decreaseLiquidityDelta - ) public { - uint256 tokenId; - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(0 < decreaseLiquidityDelta); - vm.assume(decreaseLiquidityDelta <= liquidityDelta); - - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: decreaseLiquidityDelta, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); - BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES, false); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta - decreaseLiquidityDelta); - - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0()))); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1()))); - } - - function test_decreaseLiquidity_collectFees( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDelta, - uint128 decreaseLiquidityDelta - ) public { + function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + public + { uint256 tokenId; - liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(0 < decreaseLiquidityDelta); - vm.assume(decreaseLiquidityDelta <= liquidityDelta); - - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, int256(swapAmount), ZERO_BYTES); + vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: decreaseLiquidityDelta, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); - BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES, false); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta - decreaseLiquidityDelta, "GRR"); - - // express key.fee as wad (i.e. 3000 = 0.003e18) - uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); - } - - function test_mintTransferBurn(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - public - { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - uint256 liquidity = lpm.liquidityOf(address(this), range.toId()); - - // transfer to Alice - lpm.transferFrom(address(this), alice, tokenId); - - assertEq(lpm.liquidityOf(address(this), range.toId()), 0); - assertEq(lpm.ownerOf(tokenId), alice); - assertEq(lpm.liquidityOf(alice, range.toId()), liquidity); - - // Alice can burn the token - vm.prank(alice); - lpm.burn(tokenId, address(this), ZERO_BYTES, false); - - // TODO: assert balances + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); + assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); } + // function test_decreaseLiquidity_collectFees( + // IPoolManager.ModifyLiquidityParams memory params, + // uint256 decreaseLiquidityDelta + // ) public { + // uint256 tokenId; + // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + // vm.assume(0 < decreaseLiquidityDelta); + // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // // swap to create fees + // uint256 swapAmount = 0.01e18; + // swap(key, false, int256(swapAmount), ZERO_BYTES); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // // express key.fee as wad (i.e. 3000 = 0.003e18) + // uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); + // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); + // } + + function test_mintTransferBurn() public {} function test_mintTransferCollect() public {} function test_mintTransferIncrease() public {} function test_mintTransferDecrease() public {} diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 1facdf59..6f1e7f0a 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -2,118 +2,35 @@ pragma solidity ^0.8.24; import {Vm} from "forge-std/Vm.sol"; -import {StdUtils} from "forge-std/StdUtils.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; + import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; -contract LiquidityFuzzers is StdUtils { - Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - - function assumeLiquidityDelta(PoolKey memory key, uint128 liquidityDelta) internal pure { - _vm.assume(0.0000001e18 < liquidityDelta); - _vm.assume(liquidityDelta < Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)); - } - - function boundTicks(PoolKey memory key, int24 tickLower, int24 tickUpper) internal view returns (int24, int24) { - tickLower = int24( - bound( - int256(tickLower), - int256(TickMath.minUsableTick(key.tickSpacing)), - int256(TickMath.maxUsableTick(key.tickSpacing)) - ) - ); - tickUpper = int24( - bound( - int256(tickUpper), - int256(TickMath.minUsableTick(key.tickSpacing)), - int256(TickMath.maxUsableTick(key.tickSpacing)) - ) - ); - - // round down ticks - tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; - tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; - _vm.assume(tickLower < tickUpper); - return (tickLower, tickUpper); - } - - /// @dev Obtain fuzzed parameters for creating liquidity - /// @param key The pool key - /// @param tickLower The lower tick - /// @param tickUpper The upper tick - /// @param liquidityDelta The liquidity delta - function createFuzzyLiquidityParams(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidityDelta) - internal - view - returns (int24 _tickLower, int24 _tickUpper) - { - assumeLiquidityDelta(key, liquidityDelta); - (_tickLower, _tickUpper) = boundTicks(key, tickLower, tickUpper); - } - +contract LiquidityFuzzers is Fuzzers { function createFuzzyLiquidity( INonfungiblePositionManager lpm, address recipient, PoolKey memory key, - int24 tickLower, - int24 tickUpper, - uint128 liquidityDelta, + IPoolManager.ModifyLiquidityParams memory params, + uint160 sqrtPriceX96, bytes memory hookData - ) - internal - returns (uint256 _tokenId, int24 _tickLower, int24 _tickUpper, uint128 _liquidityDelta, BalanceDelta _delta) - { - (_tickLower, _tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); - _liquidityDelta = liquidityDelta; - (_tokenId, _delta) = lpm.mint( - LiquidityRange({key: key, tickLower: _tickLower, tickUpper: _tickUpper}), - _liquidityDelta, + ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { + params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); + + (uint256 tokenId, BalanceDelta delta) = lpm.mint( + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}), + uint256(params.liquidityDelta), block.timestamp, recipient, hookData ); - } - - function createFuzzyAmountDesired( - PoolKey memory key, - int24 tickLower, - int24 tickUpper, - uint256 amount0, - uint256 amount1 - ) internal view returns (uint256 _amount0, uint256 _amount1) { - // fuzzing amount desired is a nice to have instead of using liquidityDelta, however we often violate TickOverflow - // (too many tokens in a tight range) -- need to figure out how to bound it better - bool tight = (tickUpper - tickLower) < 300 * key.tickSpacing; - uint256 maxAmount0 = tight ? 100e18 : 1_000e18; - uint256 maxAmount1 = tight ? 100e18 : 1_000e18; - _amount0 = bound(amount0, 0, maxAmount0); - _amount1 = bound(amount1, 0, maxAmount1); - _vm.assume(_amount0 != 0 && _amount1 != 0); - } - - function createFuzzySameRange( - INonfungiblePositionManager lpm, - address alice, - address bob, - LiquidityRange memory range, - uint128 liquidityA, - uint128 liquidityB, - bytes memory hookData - ) internal returns (uint256, uint256, int24, int24, uint128, uint128) { - assumeLiquidityDelta(range.key, liquidityA); - assumeLiquidityDelta(range.key, liquidityB); - - (range.tickLower, range.tickUpper) = boundTicks(range.key, range.tickLower, range.tickUpper); - - (uint256 tokenIdA,) = lpm.mint(range, liquidityA, block.timestamp + 1, alice, hookData); - - (uint256 tokenIdB,) = lpm.mint(range, liquidityB, block.timestamp + 1, bob, hookData); - return (tokenIdA, tokenIdB, range.tickLower, range.tickUpper, liquidityA, liquidityB); + return (tokenId, params, delta); } } diff --git a/test/utils/HookEnabledSwapRouter.sol b/test/utils/HookEnabledSwapRouter.sol index 4311439c..4021f453 100644 --- a/test/utils/HookEnabledSwapRouter.sol +++ b/test/utils/HookEnabledSwapRouter.sol @@ -8,9 +8,11 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolTestBase} from "@uniswap/v4-core/src/test/PoolTestBase.sol"; import {Test} from "forge-std/Test.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; contract HookEnabledSwapRouter is PoolTestBase { using CurrencyLibrary for Currency; + using CurrencySettler for Currency; error NoSwapOccurred(); @@ -25,8 +27,8 @@ contract HookEnabledSwapRouter is PoolTestBase { } struct TestSettings { - bool withdrawTokens; - bool settleUsingTransfer; + bool takeClaims; + bool settleUsingBurn; } function swap( @@ -36,14 +38,14 @@ contract HookEnabledSwapRouter is PoolTestBase { bytes memory hookData ) external payable returns (BalanceDelta delta) { delta = abi.decode( - manager.lock(abi.encode(CallbackData(msg.sender, testSettings, key, params, hookData))), (BalanceDelta) + manager.unlock(abi.encode(CallbackData(msg.sender, testSettings, key, params, hookData))), (BalanceDelta) ); uint256 ethBalance = address(this).balance; if (ethBalance > 0) CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); } - function lockAcquired(bytes calldata rawData) external returns (bytes memory) { + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { require(msg.sender == address(manager)); CallbackData memory data = abi.decode(rawData, (CallbackData)); @@ -54,14 +56,22 @@ contract HookEnabledSwapRouter is PoolTestBase { if (BalanceDelta.unwrap(delta) == 0) revert NoSwapOccurred(); if (data.params.zeroForOne) { - _settle(data.key.currency0, data.sender, delta.amount0(), data.testSettings.settleUsingTransfer); + data.key.currency0.settle( + manager, data.sender, uint256(int256(-delta.amount0())), data.testSettings.settleUsingBurn + ); if (delta.amount1() > 0) { - _take(data.key.currency1, data.sender, delta.amount1(), data.testSettings.withdrawTokens); + data.key.currency1.take( + manager, data.sender, uint256(int256(delta.amount1())), data.testSettings.takeClaims + ); } } else { - _settle(data.key.currency1, data.sender, delta.amount1(), data.testSettings.settleUsingTransfer); + data.key.currency1.settle( + manager, data.sender, uint256(int256(-delta.amount1())), data.testSettings.settleUsingBurn + ); if (delta.amount0() > 0) { - _take(data.key.currency0, data.sender, delta.amount0(), data.testSettings.withdrawTokens); + data.key.currency0.take( + manager, data.sender, uint256(int256(delta.amount0())), data.testSettings.takeClaims + ); } } From 52b304e4ca82a1c5b74f198912e7f17f7d9fc936 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 12 Jun 2024 11:00:44 -0400 Subject: [PATCH 33/98] cleanup: TODOs and imports --- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .forge-snapshots/increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 4 ---- contracts/base/BaseLiquidityHandler.sol | 6 ------ contracts/base/BaseLiquidityManagement.sol | 3 --- contracts/interfaces/INonfungiblePositionManager.sol | 2 +- contracts/libraries/CurrencyDeltas.sol | 6 ++---- contracts/libraries/CurrencySenderLibrary.sol | 5 +---- contracts/types/LiquidityRange.sol | 3 +-- test/shared/fuzz/LiquidityFuzzers.sol | 4 ---- 13 files changed, 10 insertions(+), 33 deletions(-) diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index e34af74b..be10dbf2 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -114257 \ No newline at end of file +114113 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 9bf14262..510f90cd 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -112378 \ No newline at end of file +112380 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 79a741b2..ea276824 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -74001 \ No newline at end of file +74115 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index c8a011cf..78a659ce 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -77793 \ No newline at end of file +77907 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 95aa41f9..1df963dc 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -475768 \ No newline at end of file +475882 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 500e95d8..b8a84a78 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -15,12 +15,8 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FeeMath} from "./libraries/FeeMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -// TODO: remove -import {console2} from "forge-std/console2.sol"; - contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721 { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; diff --git a/contracts/base/BaseLiquidityHandler.sol b/contracts/base/BaseLiquidityHandler.sol index 0b66c450..7790bffc 100644 --- a/contracts/base/BaseLiquidityHandler.sol +++ b/contracts/base/BaseLiquidityHandler.sol @@ -8,7 +8,6 @@ import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {FeeMath} from "../libraries/FeeMath.sol"; @@ -21,9 +20,6 @@ import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; -// TODO: remove -import {console2} from "forge-std/console2.sol"; - abstract contract BaseLiquidityHandler is SafeCallback { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; @@ -31,7 +27,6 @@ abstract contract BaseLiquidityHandler is SafeCallback { using CurrencySenderLibrary for Currency; using CurrencyDeltas for IPoolManager; using StateLibrary for IPoolManager; - using TransientStateLibrary for IPoolManager; using LiquiditySaltLibrary for IHooks; using PoolIdLibrary for PoolKey; using SafeCast for uint256; @@ -68,7 +63,6 @@ abstract contract BaseLiquidityHandler is SafeCallback { } } - // TODO: selfOnly modifier function handleIncreaseLiquidity( address sender, LiquidityRange calldata range, diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 13269f69..862b4734 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -17,9 +17,6 @@ import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; import {FeeMath} from "../libraries/FeeMath.sol"; import {BaseLiquidityHandler} from "./BaseLiquidityHandler.sol"; -// TODO: remove -import {console2} from "forge-std/console2.sol"; - abstract contract BaseLiquidityManagement is BaseLiquidityHandler { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index be182907..5fe1590e 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; @@ -17,6 +16,7 @@ interface INonfungiblePositionManager { // NOTE: more expensive since LiquidityAmounts is used onchain // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); diff --git a/contracts/libraries/CurrencyDeltas.sol b/contracts/libraries/CurrencyDeltas.sol index 339e71f6..55389e4f 100644 --- a/contracts/libraries/CurrencyDeltas.sol +++ b/contracts/libraries/CurrencyDeltas.sol @@ -6,10 +6,8 @@ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {console2} from "forge-std/console2.sol"; - library CurrencyDeltas { - using SafeCast for uint256; + using SafeCast for int256; /// @notice Get the current delta for a caller in the two given currencies /// @param caller_ The address of the caller @@ -35,6 +33,6 @@ library CurrencyDeltas { slots[0] = key0; slots[1] = key1; bytes32[] memory result = manager.exttload(slots); - return toBalanceDelta(int128(int256(uint256(result[0]))), int128(int256(uint256(result[1])))); + return toBalanceDelta(int256(uint256(result[0])).toInt128(), int256(uint256(result[1])).toInt128()); } } diff --git a/contracts/libraries/CurrencySenderLibrary.sol b/contracts/libraries/CurrencySenderLibrary.sol index 65a44e07..eb991892 100644 --- a/contracts/libraries/CurrencySenderLibrary.sol +++ b/contracts/libraries/CurrencySenderLibrary.sol @@ -2,14 +2,11 @@ pragma solidity ^0.8.24; import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; -import {CurrencySettleTake} from "./CurrencySettleTake.sol"; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; -import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; /// @notice Library used to send Currencies from address to address library CurrencySenderLibrary { using CurrencyLibrary for Currency; - using CurrencySettleTake for Currency; /// @notice Send a custodied Currency to a recipient /// @dev If sending ERC20 or native, the PoolManager must be unlocked @@ -25,7 +22,7 @@ library CurrencySenderLibrary { manager.transfer(recipient, currency.toId(), amount); } else { manager.burn(address(this), currency.toId(), amount); - currency.take(manager, recipient, amount, false); + manager.take(currency, recipient, amount); } } } diff --git a/contracts/types/LiquidityRange.sol b/contracts/types/LiquidityRange.sol index 88545687..4d00fb4b 100644 --- a/contracts/types/LiquidityRange.sol +++ b/contracts/types/LiquidityRange.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -// TODO: move into core? some of the mappings / pool.state seem to hash position id's struct LiquidityRange { PoolKey key; int24 tickLower; @@ -12,7 +11,7 @@ struct LiquidityRange { type LiquidityRangeId is bytes32; -/// @notice Library for computing the ID of a pool +/// @notice Library for computing the ID of a liquidity range library LiquidityRangeIdLibrary { function toId(LiquidityRange memory position) internal pure returns (LiquidityRangeId) { // TODO: gas, is it better to encodePacked? diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 6f1e7f0a..03e50f9b 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -1,13 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Vm} from "forge-std/Vm.sol"; - import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; From af6766167370a2645ee89d9df6d2ba9005e3775b Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:37:06 -0400 Subject: [PATCH 34/98] Position manager Consolidate (#3) * wip: consolidation * further consolidation * consolidate to single file * yay no more stack too deep * some code comments --- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 10 +- contracts/base/BaseLiquidityHandler.sol | 231 ------------------ contracts/base/BaseLiquidityManagement.sol | 222 +++++++++++++++-- 8 files changed, 207 insertions(+), 266 deletions(-) delete mode 100644 contracts/base/BaseLiquidityHandler.sol diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index be10dbf2..1e089f81 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -114113 \ No newline at end of file +114275 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 510f90cd..4a28d829 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -112380 \ No newline at end of file +112542 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index ea276824..f8f00e7d 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -74115 \ No newline at end of file +74130 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 78a659ce..d6934799 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -77907 \ No newline at end of file +77922 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 1df963dc..c81b8ef6 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -475882 \ No newline at end of file +475868 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index b8a84a78..f2acdbc1 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -16,6 +16,7 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721 { using CurrencyLibrary for Currency; @@ -23,6 +24,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit using PoolIdLibrary for PoolKey; using LiquidityRangeIdLibrary for LiquidityRange; using StateLibrary for IPoolManager; + using SafeCast for uint256; /// @dev The ID of the next token that will be minted. Skips 0 uint256 private _nextId = 1; @@ -45,7 +47,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit address recipient, bytes calldata hookData ) public payable returns (uint256 tokenId, BalanceDelta delta) { - delta = _increaseLiquidity(range, liquidity, hookData, false, msg.sender); + delta = modifyLiquidity(range, liquidity.toInt256(), hookData, false); // mint receipt token _mint(recipient, (tokenId = _nextId++)); @@ -77,7 +79,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - delta = _increaseLiquidity(tokenPositions[tokenId].range, liquidity, hookData, claims, msg.sender); + delta = modifyLiquidity(tokenPositions[tokenId].range, liquidity.toInt256(), hookData, claims); } function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) @@ -85,7 +87,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - delta = _decreaseLiquidity(tokenPositions[tokenId].range, liquidity, hookData, claims, msg.sender); + delta = modifyLiquidity(tokenPositions[tokenId].range, -(liquidity.toInt256()), hookData, claims); } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -113,7 +115,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit external returns (BalanceDelta delta) { - delta = _collect(tokenPositions[tokenId].range, hookData, claims, msg.sender); + delta = modifyLiquidity(tokenPositions[tokenId].range, 0, hookData, claims); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { diff --git a/contracts/base/BaseLiquidityHandler.sol b/contracts/base/BaseLiquidityHandler.sol deleted file mode 100644 index 7790bffc..00000000 --- a/contracts/base/BaseLiquidityHandler.sol +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {SafeCallback} from "./SafeCallback.sol"; -import {ImmutableState} from "./ImmutableState.sol"; -import {FeeMath} from "../libraries/FeeMath.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; - -import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; -import {CurrencySenderLibrary} from "../libraries/CurrencySenderLibrary.sol"; -import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; -import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; - -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; - -abstract contract BaseLiquidityHandler is SafeCallback { - using LiquidityRangeIdLibrary for LiquidityRange; - using CurrencyLibrary for Currency; - using CurrencySettleTake for Currency; - using CurrencySenderLibrary for Currency; - using CurrencyDeltas for IPoolManager; - using StateLibrary for IPoolManager; - using LiquiditySaltLibrary for IHooks; - using PoolIdLibrary for PoolKey; - using SafeCast for uint256; - - // details about the liquidity position - struct Position { - // the nonce for permits - uint96 nonce; - // the address that is approved for spending this token - address operator; - uint256 liquidity; - // the fee growth of the aggregate position as of the last action on the individual position - uint256 feeGrowthInside0LastX128; - uint256 feeGrowthInside1LastX128; - // how many uncollected tokens are owed to the position, as of the last computation - uint128 tokensOwed0; - uint128 tokensOwed1; - } - - mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; - - error LockFailure(); - - constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} - - function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { - (bool success, bytes memory returnData) = address(this).call(data); - if (success) return returnData; - if (returnData.length == 0) revert LockFailure(); - // if the call failed, bubble up the reason - /// @solidity memory-safe-assembly - assembly { - revert(add(returnData, 32), mload(returnData)) - } - } - - function handleIncreaseLiquidity( - address sender, - LiquidityRange calldata range, - uint256 liquidityToAdd, - bytes calldata hookData, - bool claims - ) external returns (BalanceDelta delta) { - Position storage position = positions[sender][range.toId()]; - - { - BalanceDelta feeDelta; - (delta, feeDelta) = poolManager.modifyLiquidity( - range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: int256(liquidityToAdd), - salt: range.key.hooks.getLiquiditySalt(sender) - }), - hookData - ); - // take fees not accrued by user's position - (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(range, position); - BalanceDelta excessFees = feeDelta - toBalanceDelta(token0Owed.toInt128(), token1Owed.toInt128()); - range.key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); - range.key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); - } - - { - // get remaining deltas: the user pays additional to increase liquidity OR the user collects fees - delta = poolManager.currencyDeltas(address(this), range.key.currency0, range.key.currency1); - if (delta.amount0() < 0) { - range.key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), claims); - } - if (delta.amount1() < 0) { - range.key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), claims); - } - if (delta.amount0() > 0) { - range.key.currency0.take(poolManager, address(this), uint256(int256(delta.amount0())), true); - } - if (delta.amount1() > 0) { - range.key.currency1.take(poolManager, address(this), uint256(int256(delta.amount1())), true); - } - } - - { - positions[sender][range.toId()].liquidity += liquidityToAdd; - - // collected fees are credited to the position OR zero'd out - delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; - delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; - } - return delta; - } - - function handleDecreaseLiquidity( - address owner, - LiquidityRange calldata range, - uint256 liquidityToRemove, - bytes calldata hookData, - bool useClaims - ) external returns (BalanceDelta) { - (BalanceDelta delta, BalanceDelta feesAccrued) = poolManager.modifyLiquidity( - range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: -int256(liquidityToRemove), - salt: range.key.hooks.getLiquiditySalt(owner) - }), - hookData - ); - - // take all tokens first - // do NOT take tokens directly to the owner because this contract might be holding fees - // that need to be paid out (position.tokensOwed) - if (delta.amount0() > 0) { - range.key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); - } - if (delta.amount1() > 0) { - range.key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); - } - - uint128 token0Owed; - uint128 token1Owed; - { - Position storage position = positions[owner][range.toId()]; - (token0Owed, token1Owed) = _updateFeeGrowth(range, position); - - BalanceDelta principalDelta = delta - feesAccrued; - token0Owed += position.tokensOwed0 + uint128(principalDelta.amount0()); - token1Owed += position.tokensOwed1 + uint128(principalDelta.amount1()); - - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - position.liquidity -= liquidityToRemove; - } - { - delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); - - // sending tokens to the owner - if (token0Owed > 0) range.key.currency0.send(poolManager, owner, token0Owed, useClaims); - if (token1Owed > 0) range.key.currency1.send(poolManager, owner, token1Owed, useClaims); - } - - return delta; - } - - function handleCollect(address owner, LiquidityRange calldata range, bytes calldata hookData, bool takeClaims) - external - returns (BalanceDelta) - { - PoolKey memory key = range.key; - Position storage position = positions[owner][range.toId()]; - - (, BalanceDelta feesAccrued) = poolManager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: 0, - salt: key.hooks.getLiquiditySalt(owner) - }), - hookData - ); - - // take all fees first then distribute - if (feesAccrued.amount0() > 0) { - key.currency0.take(poolManager, address(this), uint128(feesAccrued.amount0()), true); - } - if (feesAccrued.amount1() > 0) { - key.currency1.take(poolManager, address(this), uint128(feesAccrued.amount1()), true); - } - - (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); - token0Owed += position.tokensOwed0; - token1Owed += position.tokensOwed1; - - if (token0Owed > 0) key.currency0.send(poolManager, owner, token0Owed, takeClaims); - if (token1Owed > 0) key.currency1.send(poolManager, owner, token1Owed, takeClaims); - - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - - return toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); - } - - function _updateFeeGrowth(LiquidityRange memory range, Position storage position) - internal - returns (uint128 token0Owed, uint128 token1Owed) - { - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = - poolManager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); - - (token0Owed, token1Owed) = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); - - position.feeGrowthInside0LastX128 = feeGrowthInside0X128; - position.feeGrowthInside1LastX128 = feeGrowthInside1X128; - } -} diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 862b4734..1d9b71c6 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; @@ -12,58 +13,227 @@ import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {CurrencySenderLibrary} from "../libraries/CurrencySenderLibrary.sol"; +import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; + import {FeeMath} from "../libraries/FeeMath.sol"; -import {BaseLiquidityHandler} from "./BaseLiquidityHandler.sol"; +import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; -abstract contract BaseLiquidityManagement is BaseLiquidityHandler { +contract BaseLiquidityManagement is SafeCallback { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; + using CurrencySenderLibrary for Currency; + using CurrencyDeltas for IPoolManager; using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager; + using SafeCast for uint256; + using LiquiditySaltLibrary for IHooks; - constructor(IPoolManager _poolManager) BaseLiquidityHandler(_poolManager) {} + // details about the liquidity position + struct Position { + // the nonce for permits + uint96 nonce; + // the address that is approved for spending this token + address operator; + uint256 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } - function _increaseLiquidity( - LiquidityRange memory range, - uint256 liquidityToAdd, - bytes calldata hookData, - bool claims, - address owner - ) internal returns (BalanceDelta delta) { + mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; + + error LockFailure(); + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + function modifyLiquidity(LiquidityRange memory range, int256 liquidityDelta, bytes calldata hookData, bool claims) + internal + returns (BalanceDelta delta) + { delta = abi.decode( poolManager.unlock( - abi.encodeCall(this.handleIncreaseLiquidity, (msg.sender, range, liquidityToAdd, hookData, claims)) + abi.encodeCall(this.handleModifyLiquidity, (msg.sender, range, liquidityDelta, hookData, claims)) ), (BalanceDelta) ); } - function _decreaseLiquidity( - LiquidityRange memory range, - uint256 liquidityToRemove, + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + (bool success, bytes memory returnData) = address(this).call(data); + if (success) return returnData; + if (returnData.length == 0) revert LockFailure(); + // if the call failed, bubble up the reason + /// @solidity memory-safe-assembly + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + + function handleModifyLiquidity( + address sender, + LiquidityRange calldata range, + int256 liquidityDelta, bytes calldata hookData, - bool claims, - address owner - ) internal returns (BalanceDelta delta) { - delta = abi.decode( - poolManager.unlock( - abi.encodeCall(this.handleDecreaseLiquidity, (owner, range, liquidityToRemove, hookData, claims)) - ), - (BalanceDelta) + bool claims + ) external returns (BalanceDelta delta) { + (BalanceDelta _delta, BalanceDelta _feesAccrued) = poolManager.modifyLiquidity( + range.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: liquidityDelta, + salt: range.key.hooks.getLiquiditySalt(sender) + }), + hookData ); + + if (liquidityDelta > 0) { + delta = _settleIncreaseLiquidity(_delta, _feesAccrued, sender, range, uint256(liquidityDelta), claims); + } else if (liquidityDelta < 0) { + delta = _settleDecreaseLiquidity(_delta, _feesAccrued, sender, range, uint256(-liquidityDelta), claims); + } else { + delta = _settleCollect(_feesAccrued, sender, range, claims); + } + } + + function _settleIncreaseLiquidity( + BalanceDelta delta, + BalanceDelta feesAccrued, + address sender, + LiquidityRange calldata range, + uint256 liquidityToAdd, + bool claims + ) internal returns (BalanceDelta) { + Position storage position = positions[sender][range.toId()]; + + // take fees not accrued by user's position + (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(range, position); + BalanceDelta excessFees = feesAccrued - toBalanceDelta(token0Owed.toInt128(), token1Owed.toInt128()); + range.key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); + range.key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); + + // get remaining deltas: the user pays additional to increase liquidity OR the user collects their fees + delta = poolManager.currencyDeltas(address(this), range.key.currency0, range.key.currency1); + + // TODO: use position.tokensOwed0 to pay the delta? + if (delta.amount0() < 0) { + range.key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), claims); + } + if (delta.amount1() < 0) { + range.key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), claims); + } + if (delta.amount0() > 0) { + range.key.currency0.take(poolManager, address(this), uint256(int256(delta.amount0())), true); + } + if (delta.amount1() > 0) { + range.key.currency1.take(poolManager, address(this), uint256(int256(delta.amount1())), true); + } + + positions[sender][range.toId()].liquidity += liquidityToAdd; + + // collected fees are credited to the position OR zero'd out + delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; + delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; + + return delta; + } + + function _settleDecreaseLiquidity( + BalanceDelta delta, + BalanceDelta feesAccrued, + address owner, + LiquidityRange calldata range, + uint256 liquidityToRemove, + bool claims + ) internal returns (BalanceDelta) { + // take all tokens first + // do NOT take tokens directly to the owner because this contract might be holding fees + // that need to be paid out (position.tokensOwed) + if (delta.amount0() > 0) { + range.key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); + } + if (delta.amount1() > 0) { + range.key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); + } + + // when decreasing liquidity, the user collects: 1) principal liquidity, 2) new fees, 3) old fees (position.tokensOwed) + + Position storage position = positions[owner][range.toId()]; + (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); + BalanceDelta principalDelta = delta - feesAccrued; + + // new fees += old fees + principal liquidity + token0Owed += position.tokensOwed0 + uint128(principalDelta.amount0()); + token1Owed += position.tokensOwed1 + uint128(principalDelta.amount1()); + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + position.liquidity -= liquidityToRemove; + + delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); + + // sending tokens to the owner + if (token0Owed > 0) range.key.currency0.send(poolManager, owner, token0Owed, claims); + if (token1Owed > 0) range.key.currency1.send(poolManager, owner, token1Owed, claims); + + return delta; } - function _collect(LiquidityRange memory range, bytes calldata hookData, bool claims, address owner) + function _settleCollect(BalanceDelta feesAccrued, address owner, LiquidityRange calldata range, bool takeClaims) internal - returns (BalanceDelta delta) + returns (BalanceDelta) { - delta = abi.decode( - poolManager.unlock(abi.encodeCall(this.handleCollect, (owner, range, hookData, claims))), (BalanceDelta) + PoolKey memory key = range.key; + Position storage position = positions[owner][range.toId()]; + + // take all fees first then distribute + if (feesAccrued.amount0() > 0) { + key.currency0.take(poolManager, address(this), uint128(feesAccrued.amount0()), true); + } + if (feesAccrued.amount1() > 0) { + key.currency1.take(poolManager, address(this), uint128(feesAccrued.amount1()), true); + } + + // collecting fees: new fees and old fees + (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); + token0Owed += position.tokensOwed0; + token1Owed += position.tokensOwed1; + + if (token0Owed > 0) key.currency0.send(poolManager, owner, token0Owed, takeClaims); + if (token1Owed > 0) key.currency1.send(poolManager, owner, token1Owed, takeClaims); + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + + return toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); + } + + function _updateFeeGrowth(LiquidityRange memory range, Position storage position) + internal + returns (uint128 token0Owed, uint128 token1Owed) + { + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + poolManager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); + + (token0Owed, token1Owed) = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity ); + + position.feeGrowthInside0LastX128 = feeGrowthInside0X128; + position.feeGrowthInside1LastX128 = feeGrowthInside1X128; } // --- View Functions --- // From 48f38c43b88aeb54b60689dd92778bee7648860b Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 12 Jun 2024 17:46:50 -0400 Subject: [PATCH 35/98] use currency settler syntax --- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .forge-snapshots/increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/libraries/CurrencySenderLibrary.sol | 6 ++++-- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 1e089f81..210b2a35 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -114275 \ No newline at end of file +114609 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 4a28d829..077c79f6 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -112542 \ No newline at end of file +112540 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index f8f00e7d..37ac7301 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -74130 \ No newline at end of file +74128 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index d6934799..d047c3b9 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -77922 \ No newline at end of file +77920 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index c81b8ef6..aabe76e0 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -475868 \ No newline at end of file +475866 \ No newline at end of file diff --git a/contracts/libraries/CurrencySenderLibrary.sol b/contracts/libraries/CurrencySenderLibrary.sol index eb991892..ce343325 100644 --- a/contracts/libraries/CurrencySenderLibrary.sol +++ b/contracts/libraries/CurrencySenderLibrary.sol @@ -2,11 +2,13 @@ pragma solidity ^0.8.24; import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; +import {CurrencySettleTake} from "./CurrencySettleTake.sol"; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; /// @notice Library used to send Currencies from address to address library CurrencySenderLibrary { using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; /// @notice Send a custodied Currency to a recipient /// @dev If sending ERC20 or native, the PoolManager must be unlocked @@ -21,8 +23,8 @@ library CurrencySenderLibrary { if (useClaims) { manager.transfer(recipient, currency.toId(), amount); } else { - manager.burn(address(this), currency.toId(), amount); - manager.take(currency, recipient, amount); + currency.settle(manager, address(this), amount, true); + currency.take(manager, recipient, amount, false); } } } From c8ce67bf337b315acf57687b4f8db204c8662a9b Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 13 Jun 2024 17:05:28 -0400 Subject: [PATCH 36/98] use v4-core's gas snapshot --- .gitmodules | 3 --- lib/forge-gas-snapshot | 1 - 2 files changed, 4 deletions(-) delete mode 160000 lib/forge-gas-snapshot diff --git a/.gitmodules b/.gitmodules index 8e108254..b5a4d742 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/forge-gas-snapshot"] - path = lib/forge-gas-snapshot - url = https://github.com/marktoda/forge-gas-snapshot [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core diff --git a/lib/forge-gas-snapshot b/lib/forge-gas-snapshot deleted file mode 160000 index 2f884282..00000000 --- a/lib/forge-gas-snapshot +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2f884282b4cd067298e798974f5b534288b13bc2 From da91136aff452a8378f6916db131bdf44c010c65 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 13 Jun 2024 17:39:22 -0400 Subject: [PATCH 37/98] use snapLastCall and isolate for posm benchmarks --- .../FullRangeAddInitialLiquidity.snap | 2 +- .forge-snapshots/FullRangeAddLiquidity.snap | 2 +- .forge-snapshots/FullRangeFirstSwap.snap | 2 +- .forge-snapshots/FullRangeInitialize.snap | 2 +- .forge-snapshots/FullRangeRemoveLiquidity.snap | 2 +- .../FullRangeRemoveLiquidityAndRebalance.snap | 2 +- .forge-snapshots/FullRangeSecondSwap.snap | 2 +- .forge-snapshots/FullRangeSwap.snap | 2 +- .forge-snapshots/OracleGrow10Slots.snap | 2 +- .../OracleGrow10SlotsCardinalityGreater.snap | 2 +- .forge-snapshots/OracleGrow1Slot.snap | 2 +- .../OracleGrow1SlotCardinalityGreater.snap | 2 +- .forge-snapshots/OracleInitialize.snap | 2 +- .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .forge-snapshots/increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- test/position-managers/Gas.t.sol | 17 ++++++----------- 20 files changed, 25 insertions(+), 30 deletions(-) diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index b9d81858..bcaa687e 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -311137 \ No newline at end of file +354433 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index c3edfa69..22ea7d07 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -122946 \ No newline at end of file +161742 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index b9e04365..c0d45a14 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -80287 \ No newline at end of file +146467 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 7a0170eb..22412ada 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1015181 \ No newline at end of file +1037821 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 4444368b..b90db119 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -110544 \ No newline at end of file +146372 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index 1bc2d893..88c6540c 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -240022 \ No newline at end of file +281650 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index c1cac22b..a07f7da8 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -45997 \ No newline at end of file +116177 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index 97d86500..3845587a 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -79418 \ No newline at end of file +145886 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10Slots.snap b/.forge-snapshots/OracleGrow10Slots.snap index 3dada479..96c9f369 100644 --- a/.forge-snapshots/OracleGrow10Slots.snap +++ b/.forge-snapshots/OracleGrow10Slots.snap @@ -1 +1 @@ -232960 \ No newline at end of file +254164 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap index f623cfa5..9fc5bce2 100644 --- a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap @@ -1 +1 @@ -223649 \ No newline at end of file +249653 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1Slot.snap b/.forge-snapshots/OracleGrow1Slot.snap index 137baa16..ced15d76 100644 --- a/.forge-snapshots/OracleGrow1Slot.snap +++ b/.forge-snapshots/OracleGrow1Slot.snap @@ -1 +1 @@ -32845 \ No newline at end of file +54049 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap index e6dc42ce..8ad5646e 100644 --- a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap @@ -1 +1 @@ -23545 \ No newline at end of file +49549 \ No newline at end of file diff --git a/.forge-snapshots/OracleInitialize.snap b/.forge-snapshots/OracleInitialize.snap index e4e9e6b2..a9ee0288 100644 --- a/.forge-snapshots/OracleInitialize.snap +++ b/.forge-snapshots/OracleInitialize.snap @@ -1 +1 @@ -51310 \ No newline at end of file +72794 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index 03924f26..fe88810b 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -122359 \ No newline at end of file +156851 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 210b2a35..9d667ef7 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -114609 \ No newline at end of file +187091 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 077c79f6..e9492b3e 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -112540 \ No newline at end of file +166084 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 37ac7301..5280964c 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -74128 \ No newline at end of file +187781 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index d047c3b9..460aeb49 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -77920 \ No newline at end of file +163384 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index aabe76e0..b7ef4c1e 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -475866 \ No newline at end of file +485624 \ No newline at end of file diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 551465c3..495d6f22 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -80,45 +80,40 @@ contract GasTest is Test, Deployers, GasSnapshot { // }); // snapStart("mint"); // lpm.mint(params); - // snapEnd(); + // snapLastCall(); // } function test_gas_mintWithLiquidity() public { - snapStart("mintWithLiquidity"); lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - snapEnd(); + snapLastCall("mintWithLiquidity"); } function test_gas_increaseLiquidity_erc20() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - snapStart("increaseLiquidity_erc20"); lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, false); - snapEnd(); + snapLastCall("increaseLiquidity_erc20"); } function test_gas_increaseLiquidity_erc6909() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - snapStart("increaseLiquidity_erc6909"); lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, true); - snapEnd(); + snapLastCall("increaseLiquidity_erc6909"); } function test_gas_decreaseLiquidity_erc20() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - snapStart("decreaseLiquidity_erc20"); lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, false); - snapEnd(); + snapLastCall("decreaseLiquidity_erc20"); } function test_gas_decreaseLiquidity_erc6909() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - snapStart("decreaseLiquidity_erc6909"); lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, true); - snapEnd(); + snapLastCall("decreaseLiquidity_erc6909"); } function test_gas_burn() public {} From 18600bd61814189335c3e15b7216c60b7a6eb05b Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 14 Jun 2024 12:56:54 -0400 Subject: [PATCH 38/98] Update contracts/libraries/CurrencySettleTake.sol Co-authored-by: 0x57 --- contracts/libraries/CurrencySettleTake.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libraries/CurrencySettleTake.sol b/contracts/libraries/CurrencySettleTake.sol index 9ea8f1c2..30f1d868 100644 --- a/contracts/libraries/CurrencySettleTake.sol +++ b/contracts/libraries/CurrencySettleTake.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.24; -import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; From f52adcf0073358d1695c91667188d08f1f670de0 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 14 Jun 2024 13:08:47 -0400 Subject: [PATCH 39/98] use v4-core's solmate its more recent --- .gitmodules | 3 --- lib/solmate | 1 - remappings.txt | 2 -- 3 files changed, 6 deletions(-) delete mode 160000 lib/solmate diff --git a/.gitmodules b/.gitmodules index b5a4d742..88aaa704 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core -[submodule "lib/solmate"] - path = lib/solmate - url = https://github.com/transmissions11/solmate diff --git a/lib/solmate b/lib/solmate deleted file mode 160000 index bfc9c258..00000000 --- a/lib/solmate +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bfc9c25865a274a7827fea5abf6e4fb64fc64e6c diff --git a/remappings.txt b/remappings.txt index 94b76d6a..0e0ef791 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,2 @@ @uniswap/v4-core/=lib/v4-core/ -solmate/=lib/solmate/src/ @openzeppelin/=lib/openzeppelin-contracts/ -forge-std/=lib/v4-core/lib/forge-std/src/ \ No newline at end of file From 07cc628e0f7f3a6771aad957a41c3dbb18b15a57 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 17 Jun 2024 09:44:36 -0400 Subject: [PATCH 40/98] use v4-core's openzeppelin-contracts --- .gitmodules | 3 --- lib/openzeppelin-contracts | 1 - remappings.txt | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) delete mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 88aaa704..b6d49e52 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index 5ae63068..00000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5ae630684a0f57de400ef69499addab4c32ac8fb diff --git a/remappings.txt b/remappings.txt index 0e0ef791..11b1a65e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,2 @@ @uniswap/v4-core/=lib/v4-core/ -@openzeppelin/=lib/openzeppelin-contracts/ +@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/ From 240c8e1eef4962ea4f95baeea6c0a88e80c6c980 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 17 Jun 2024 09:45:38 -0400 Subject: [PATCH 41/98] add ERC721Permit --- .forge-snapshots/FullRangeInitialize.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 20 +++-- contracts/base/ERC721Permit.sol | 76 +++++++++++++++++++ contracts/base/SelfPermit.sol | 2 +- contracts/interfaces/IERC721Permit.sol | 25 ++++++ contracts/libraries/ChainId.sol | 13 ++++ 11 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 contracts/base/ERC721Permit.sol create mode 100644 contracts/interfaces/IERC721Permit.sol create mode 100644 contracts/libraries/ChainId.sol diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 22412ada..9661da18 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1037821 \ No newline at end of file +1039616 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 9d667ef7..558500f4 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -187091 \ No newline at end of file +187220 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index e9492b3e..8d2b2de0 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -166084 \ No newline at end of file +166214 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 5280964c..fd8256e5 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -187781 \ No newline at end of file +187943 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 460aeb49..075aab60 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -163384 \ No newline at end of file +163546 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index b7ef4c1e..3ed18a4f 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -485624 \ No newline at end of file +485501 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index f2acdbc1..0e1b32d8 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {ERC721Permit} from "./base/ERC721Permit.sol"; import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; @@ -18,7 +18,7 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721 { +contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; @@ -36,7 +36,10 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit mapping(uint256 tokenId => TokenPosition position) public tokenPositions; - constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} + constructor(IPoolManager _poolManager) + BaseLiquidityManagement(_poolManager) + ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V3-POS", "1") + {} // NOTE: more gas efficient as LiquidityAmounts is used offchain // TODO: deadline check @@ -123,8 +126,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit return feesOwed(tokenPosition.owner, tokenPosition.range); } - function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { - TokenPosition storage tokenPosition = tokenPositions[firstTokenId]; + function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override { + TokenPosition storage tokenPosition = tokenPositions[tokenId]; LiquidityRangeId rangeId = tokenPosition.range.toId(); Position storage position = positions[from][rangeId]; position.operator = address(0x0); @@ -134,7 +137,12 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit delete positions[from][rangeId]; // update token position - tokenPositions[firstTokenId] = TokenPosition({owner: to, range: tokenPosition.range}); + tokenPositions[tokenId] = TokenPosition({owner: to, range: tokenPosition.range}); + } + + function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256) { + TokenPosition memory tokenPosition = tokenPositions[tokenId]; + return uint256(positions[tokenPosition.owner][tokenPosition.range.toId()].nonce++); } modifier isAuthorizedForToken(uint256 tokenId) { diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol new file mode 100644 index 00000000..8eb86521 --- /dev/null +++ b/contracts/base/ERC721Permit.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {ChainId} from "../libraries/ChainId.sol"; +import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; +import {IERC1271} from "../interfaces/external/IERC1271.sol"; + +/// @title ERC721 with permit +/// @notice Nonfungible tokens that support an approve via signature, i.e. permit +abstract contract ERC721Permit is ERC721, IERC721Permit { + /// @dev Gets the current nonce for a token ID and then increments it, returning the original value + function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256); + + /// @dev The hash of the name used in the permit signature verification + bytes32 private immutable nameHash; + + /// @dev The hash of the version string used in the permit signature verification + bytes32 private immutable versionHash; + + /// @notice Computes the nameHash and versionHash + constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) { + nameHash = keccak256(bytes(name_)); + versionHash = keccak256(bytes(version_)); + } + + /// @inheritdoc IERC721Permit + function DOMAIN_SEPARATOR() public view override returns (bytes32) { + return keccak256( + abi.encode( + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, + nameHash, + versionHash, + ChainId.get(), + address(this) + ) + ); + } + + /// @inheritdoc IERC721Permit + /// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); + bytes32 public constant override PERMIT_TYPEHASH = + 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad; + + /// @inheritdoc IERC721Permit + function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable + override + { + require(block.timestamp <= deadline, "Permit expired"); + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline)) + ) + ); + address owner = ownerOf(tokenId); + require(spender != owner, "ERC721Permit: approval to current owner"); + + if (Address.isContract(owner)) { + require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, "Unauthorized"); + } else { + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0), "Invalid signature"); + require(recoveredAddress == owner, "Unauthorized"); + } + + approve(spender, tokenId); + } +} diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol index 40449636..60ae6762 100644 --- a/contracts/base/SelfPermit.sol +++ b/contracts/base/SelfPermit.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; import {IERC20PermitAllowed} from "../interfaces/external/IERC20PermitAllowed.sol"; import {ISelfPermit} from "../interfaces/ISelfPermit.sol"; diff --git a/contracts/interfaces/IERC721Permit.sol b/contracts/interfaces/IERC721Permit.sol new file mode 100644 index 00000000..daa27030 --- /dev/null +++ b/contracts/interfaces/IERC721Permit.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; + +/// @title ERC721 with permit +/// @notice Extension to ERC721 that includes a permit function for signature based approvals +interface IERC721Permit { + /// @notice The permit typehash used in the permit signature + /// @return The typehash for the permit + function PERMIT_TYPEHASH() external pure returns (bytes32); + + /// @notice The domain separator used in the permit signature + /// @return The domain seperator used in encoding of permit signature + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice Approve of a specific token ID for spending by spender via signature + /// @param spender The account that is being approved + /// @param tokenId The ID of the token that is being approved for spending + /// @param deadline The deadline timestamp by which the call must be mined for the approve to work + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable; +} diff --git a/contracts/libraries/ChainId.sol b/contracts/libraries/ChainId.sol new file mode 100644 index 00000000..7e67989c --- /dev/null +++ b/contracts/libraries/ChainId.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.0; + +/// @title Function for getting the current chain ID +library ChainId { + /// @dev Gets the current chain ID + /// @return chainId The current chain ID + function get() internal view returns (uint256 chainId) { + assembly { + chainId := chainid() + } + } +} From 1cb19483d6fa9acf62c6a5772a31f5ccac9ccae2 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 17 Jun 2024 15:10:37 -0400 Subject: [PATCH 42/98] feedback: memory hookData --- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .forge-snapshots/increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/base/BaseLiquidityManagement.sol | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 558500f4..6f12f218 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -187220 \ No newline at end of file +187367 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 8d2b2de0..55c8acdd 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -166214 \ No newline at end of file +166360 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index fd8256e5..cc20fd54 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -187943 \ No newline at end of file +188126 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 075aab60..304af8aa 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -163546 \ No newline at end of file +163729 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 3ed18a4f..50c6b412 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -485501 \ No newline at end of file +485679 \ No newline at end of file diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 1d9b71c6..d486fdc5 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -51,11 +51,11 @@ contract BaseLiquidityManagement is SafeCallback { mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; - error LockFailure(); + error UnlockFailure(); constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} - function modifyLiquidity(LiquidityRange memory range, int256 liquidityDelta, bytes calldata hookData, bool claims) + function modifyLiquidity(LiquidityRange memory range, int256 liquidityDelta, bytes memory hookData, bool claims) internal returns (BalanceDelta delta) { @@ -70,7 +70,7 @@ contract BaseLiquidityManagement is SafeCallback { function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; - if (returnData.length == 0) revert LockFailure(); + if (returnData.length == 0) revert UnlockFailure(); // if the call failed, bubble up the reason /// @solidity memory-safe-assembly assembly { From 227683b68ebe20a02d734c04eef15be68d53f38c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 19 Jun 2024 12:19:34 -0400 Subject: [PATCH 43/98] initial refactor. stack too deep --- contracts/NonfungiblePositionManager.sol | 12 +- contracts/base/BaseLiquidityManagement.sol | 231 +++++++++++++-------- 2 files changed, 152 insertions(+), 91 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 0e1b32d8..fe4cf04b 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -50,7 +50,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit address recipient, bytes calldata hookData ) public payable returns (uint256 tokenId, BalanceDelta delta) { - delta = modifyLiquidity(range, liquidity.toInt256(), hookData, false); + // delta = modifyLiquidity(range, liquidity.toInt256(), hookData, false); + delta = _increaseLiquidityWithLock(msg.sender, range, liquidity, hookData, false); // mint receipt token _mint(recipient, (tokenId = _nextId++)); @@ -82,7 +83,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - delta = modifyLiquidity(tokenPositions[tokenId].range, liquidity.toInt256(), hookData, claims); + TokenPosition memory tokenPos = tokenPositions[tokenId]; + delta = _increaseLiquidityWithLock(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); } function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) @@ -90,7 +92,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - delta = modifyLiquidity(tokenPositions[tokenId].range, -(liquidity.toInt256()), hookData, claims); + TokenPosition memory tokenPos = tokenPositions[tokenId]; + delta = _decreaseLiquidityWithLock(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -118,7 +121,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit external returns (BalanceDelta delta) { - delta = modifyLiquidity(tokenPositions[tokenId].range, 0, hookData, claims); + TokenPosition memory tokenPos = tokenPositions[tokenId]; + delta = _collectWithLock(tokenPos.owner, tokenPos.range, hookData, claims); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index d486fdc5..34207f3f 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -49,127 +49,145 @@ contract BaseLiquidityManagement is SafeCallback { uint128 tokensOwed1; } + enum LiquidityOperation { + INCREASE, + DECREASE, + COLLECT + } + mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; error UnlockFailure(); constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} - function modifyLiquidity(LiquidityRange memory range, int256 liquidityDelta, bytes memory hookData, bool claims) - internal - returns (BalanceDelta delta) - { - delta = abi.decode( - poolManager.unlock( - abi.encodeCall(this.handleModifyLiquidity, (msg.sender, range, liquidityDelta, hookData, claims)) - ), - (BalanceDelta) - ); + function zeroOut(BalanceDelta delta, Currency currency0, Currency currency1, address owner, bool claims) public { + if (delta.amount0() < 0) currency0.settle(poolManager, owner, uint256(int256(-delta.amount0())), claims); + else if (delta.amount0() > 0) currency0.send(poolManager, owner, uint128(delta.amount0()), claims); + + if (delta.amount1() < 0) currency1.settle(poolManager, owner, uint256(int256(-delta.amount1())), claims); + else if (delta.amount1() > 0) currency1.send(poolManager, owner, uint128(delta.amount1()), claims); } function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { - (bool success, bytes memory returnData) = address(this).call(data); - if (success) return returnData; - if (returnData.length == 0) revert UnlockFailure(); - // if the call failed, bubble up the reason - /// @solidity memory-safe-assembly - assembly { - revert(add(returnData, 32), mload(returnData)) + ( + LiquidityOperation op, + address owner, + LiquidityRange memory range, + uint256 liquidityChange, + bytes memory hookData, + bool claims + ) = abi.decode(data, (LiquidityOperation, address, LiquidityRange, uint256, bytes, bool)); + + if (op == LiquidityOperation.INCREASE) { + return abi.encode(_increaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); + } else if (op == LiquidityOperation.DECREASE) { + return abi.encode(_decreaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); + } else if (op == LiquidityOperation.COLLECT) { + return abi.encode(_collectAndZeroOut(owner, range, 0, hookData, claims)); + } else { + revert UnlockFailure(); } } - function handleModifyLiquidity( - address sender, - LiquidityRange calldata range, - int256 liquidityDelta, - bytes calldata hookData, - bool claims - ) external returns (BalanceDelta delta) { - (BalanceDelta _delta, BalanceDelta _feesAccrued) = poolManager.modifyLiquidity( + function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) + internal + returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) + { + (liquidityDelta, totalFeesAccrued) = poolManager.modifyLiquidity( range.key, IPoolManager.ModifyLiquidityParams({ tickLower: range.tickLower, tickUpper: range.tickUpper, - liquidityDelta: liquidityDelta, - salt: range.key.hooks.getLiquiditySalt(sender) + liquidityDelta: liquidityChange, + salt: range.key.hooks.getLiquiditySalt(owner) }), hookData ); - - if (liquidityDelta > 0) { - delta = _settleIncreaseLiquidity(_delta, _feesAccrued, sender, range, uint256(liquidityDelta), claims); - } else if (liquidityDelta < 0) { - delta = _settleDecreaseLiquidity(_delta, _feesAccrued, sender, range, uint256(-liquidityDelta), claims); - } else { - delta = _settleCollect(_feesAccrued, sender, range, claims); - } } - function _settleIncreaseLiquidity( - BalanceDelta delta, - BalanceDelta feesAccrued, - address sender, - LiquidityRange calldata range, + function _increaseLiquidity( + address owner, + LiquidityRange memory range, uint256 liquidityToAdd, + bytes memory hookData, bool claims ) internal returns (BalanceDelta) { - Position storage position = positions[sender][range.toId()]; + // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. + (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = + _modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData); - // take fees not accrued by user's position + Position storage position = positions[owner][range.toId()]; + + // Account for fees that were potentially collected to other users on the same range. (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(range, position); - BalanceDelta excessFees = feesAccrued - toBalanceDelta(token0Owed.toInt128(), token1Owed.toInt128()); - range.key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); - range.key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); + BalanceDelta callerFeesAccrued = toBalanceDelta(token0Owed.toInt128(), token1Owed.toInt128()); + BalanceDelta feesToCollect = totalFeesAccrued - callerFeesAccrued; + range.key.currency0.take(poolManager, address(this), uint128(feesToCollect.amount0()), true); + range.key.currency1.take(poolManager, address(this), uint128(feesToCollect.amount1()), true); - // get remaining deltas: the user pays additional to increase liquidity OR the user collects their fees - delta = poolManager.currencyDeltas(address(this), range.key.currency0, range.key.currency1); + { + // the delta applied from the above actions is liquidityDelta - feesToCollect, note that the actual total delta for the caller may be different because actions can be chained + BalanceDelta callerDelta = liquidityDelta - feesToCollect; - // TODO: use position.tokensOwed0 to pay the delta? - if (delta.amount0() < 0) { - range.key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), claims); - } - if (delta.amount1() < 0) { - range.key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), claims); - } - if (delta.amount0() > 0) { - range.key.currency0.take(poolManager, address(this), uint256(int256(delta.amount0())), true); - } - if (delta.amount1() > 0) { - range.key.currency1.take(poolManager, address(this), uint256(int256(delta.amount1())), true); - } + // Update the tokensOwed0 and tokensOwed1 values for the caller. + // if callerDelta <= 0, then tokensOwed0 and tokensOwed1 should be zero'd out as all fees were re-invested into a new position. + // if callerDelta > 0, then even after re-investing old fees, the caller still has some fees to collect that were not added into the position so they are accounted. - positions[sender][range.toId()].liquidity += liquidityToAdd; + position.tokensOwed0 = callerDelta.amount0() > 0 ? position.tokensOwed0 += uint128(callerDelta.amount0()) : 0; + position.tokensOwed1 = callerDelta.amount1() > 0 ? position.tokensOwed1 += uint128(callerDelta.amount1()) : 0; + } + } - // collected fees are credited to the position OR zero'd out - delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; - delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; + function _increaseLiquidityAndZeroOut( + address owner, + LiquidityRange memory range, + uint256 liquidityToAdd, + bytes memory hookData, + bool claims + ) internal returns (BalanceDelta delta) { + delta = _increaseLiquidity(owner, range, liquidityToAdd, hookData, claims); + zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); + } - return delta; + function _increaseLiquidityWithLock( + address owner, + LiquidityRange memory range, + uint256 liquidityToAdd, + bytes memory hookData, + bool claims + ) internal returns (BalanceDelta) { + return abi.decode( + poolManager.unlock(abi.encode(LiquidityOperation.INCREASE, owner, range, liquidityToAdd, hookData, claims)), + (BalanceDelta) + ); } - function _settleDecreaseLiquidity( - BalanceDelta delta, - BalanceDelta feesAccrued, + function _decreaseLiquidity( address owner, - LiquidityRange calldata range, + LiquidityRange memory range, uint256 liquidityToRemove, + bytes memory hookData, bool claims - ) internal returns (BalanceDelta) { + ) internal returns (BalanceDelta delta) { + (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = + _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); + // take all tokens first // do NOT take tokens directly to the owner because this contract might be holding fees // that need to be paid out (position.tokensOwed) - if (delta.amount0() > 0) { - range.key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); + if (liquidityDelta.amount0() > 0) { + range.key.currency0.take(poolManager, address(this), uint128(liquidityDelta.amount0()), true); } - if (delta.amount1() > 0) { - range.key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); + if (liquidityDelta.amount1() > 0) { + range.key.currency1.take(poolManager, address(this), uint128(liquidityDelta.amount1()), true); } // when decreasing liquidity, the user collects: 1) principal liquidity, 2) new fees, 3) old fees (position.tokensOwed) Position storage position = positions[owner][range.toId()]; (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); - BalanceDelta principalDelta = delta - feesAccrued; + BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued; // new fees += old fees + principal liquidity token0Owed += position.tokensOwed0 + uint128(principalDelta.amount0()); @@ -181,26 +199,50 @@ contract BaseLiquidityManagement is SafeCallback { delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); - // sending tokens to the owner - if (token0Owed > 0) range.key.currency0.send(poolManager, owner, token0Owed, claims); - if (token1Owed > 0) range.key.currency1.send(poolManager, owner, token1Owed, claims); - return delta; } - function _settleCollect(BalanceDelta feesAccrued, address owner, LiquidityRange calldata range, bool takeClaims) + function _decreaseLiquidityAndZeroOut( + address owner, + LiquidityRange memory range, + uint256 liquidityToRemove, + bytes memory hookData, + bool claims + ) internal returns (BalanceDelta delta) { + delta = _decreaseLiquidity(owner, range, liquidityToRemove, hookData, claims); + zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); + } + + function _decreaseLiquidityWithLock( + address owner, + LiquidityRange memory range, + uint256 liquidityToRemove, + bytes memory hookData, + bool claims + ) internal returns (BalanceDelta) { + return abi.decode( + poolManager.unlock( + abi.encode(LiquidityOperation.DECREASE, owner, range, liquidityToRemove, hookData, claims) + ), + (BalanceDelta) + ); + } + + function _collect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) internal returns (BalanceDelta) { + (, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, 0, hookData); + PoolKey memory key = range.key; Position storage position = positions[owner][range.toId()]; // take all fees first then distribute - if (feesAccrued.amount0() > 0) { - key.currency0.take(poolManager, address(this), uint128(feesAccrued.amount0()), true); + if (totalFeesAccrued.amount0() > 0) { + key.currency0.take(poolManager, address(this), uint128(totalFeesAccrued.amount0()), true); } - if (feesAccrued.amount1() > 0) { - key.currency1.take(poolManager, address(this), uint128(feesAccrued.amount1()), true); + if (totalFeesAccrued.amount1() > 0) { + key.currency1.take(poolManager, address(this), uint128(totalFeesAccrued.amount1()), true); } // collecting fees: new fees and old fees @@ -208,15 +250,30 @@ contract BaseLiquidityManagement is SafeCallback { token0Owed += position.tokensOwed0; token1Owed += position.tokensOwed1; - if (token0Owed > 0) key.currency0.send(poolManager, owner, token0Owed, takeClaims); - if (token1Owed > 0) key.currency1.send(poolManager, owner, token1Owed, takeClaims); - position.tokensOwed0 = 0; position.tokensOwed1 = 0; return toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); } + function _collectAndZeroOut(address owner, LiquidityRange memory range, uint256, bytes memory hookData, bool claims) + internal + returns (BalanceDelta delta) + { + delta = _collect(owner, range, hookData, claims); + zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); + } + + function _collectWithLock(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + return abi.decode( + poolManager.unlock(abi.encode(LiquidityOperation.COLLECT, owner, range, 0, hookData, claims)), + (BalanceDelta) + ); + } + function _updateFeeGrowth(LiquidityRange memory range, Position storage position) internal returns (uint128 token0Owed, uint128 token1Owed) From a19636f725bb87ea156cf9960f9189221eecbe6d Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 19 Jun 2024 14:25:49 -0400 Subject: [PATCH 44/98] passing tests --- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/base/BaseLiquidityManagement.sol | 55 ++++++++++++------- 6 files changed, 40 insertions(+), 25 deletions(-) diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 6f12f218..2e47c819 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -187367 \ No newline at end of file +187542 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 55c8acdd..640ee360 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -166360 \ No newline at end of file +166537 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index cc20fd54..d5f6b76e 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -188126 \ No newline at end of file +183234 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 304af8aa..251abea4 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -163729 \ No newline at end of file +158816 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 50c6b412..0a322b48 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -485679 \ No newline at end of file +478523 \ No newline at end of file diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 34207f3f..b6663b2a 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -120,23 +120,38 @@ contract BaseLiquidityManagement is SafeCallback { Position storage position = positions[owner][range.toId()]; // Account for fees that were potentially collected to other users on the same range. - (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(range, position); - BalanceDelta callerFeesAccrued = toBalanceDelta(token0Owed.toInt128(), token1Owed.toInt128()); + BalanceDelta callerFeesAccrued = _updateFeeGrowth(range, position); BalanceDelta feesToCollect = totalFeesAccrued - callerFeesAccrued; range.key.currency0.take(poolManager, address(this), uint128(feesToCollect.amount0()), true); range.key.currency1.take(poolManager, address(this), uint128(feesToCollect.amount1()), true); - { // the delta applied from the above actions is liquidityDelta - feesToCollect, note that the actual total delta for the caller may be different because actions can be chained BalanceDelta callerDelta = liquidityDelta - feesToCollect; + // update liquidity after feeGrowth is updated + position.liquidity += liquidityToAdd; + // Update the tokensOwed0 and tokensOwed1 values for the caller. - // if callerDelta <= 0, then tokensOwed0 and tokensOwed1 should be zero'd out as all fees were re-invested into a new position. - // if callerDelta > 0, then even after re-investing old fees, the caller still has some fees to collect that were not added into the position so they are accounted. + // if callerDelta < 0, existing fees were re-invested AND net new tokens are required for the liquidity increase + // if callerDelta == 0, existing fees were reinvested (autocompounded) + // if callerDelta > 0, some but not all existing fees were used to increase liquidity. Any remainder is added to the position's owed tokens + if (callerDelta.amount0() > 0) { + position.tokensOwed0 += uint128(callerDelta.amount0()); + range.key.currency0.take(poolManager, address(this), uint128(callerDelta.amount0()), true); + callerDelta = toBalanceDelta(0, callerDelta.amount1()); + } else { + position.tokensOwed0 = 0; + } - position.tokensOwed0 = callerDelta.amount0() > 0 ? position.tokensOwed0 += uint128(callerDelta.amount0()) : 0; - position.tokensOwed1 = callerDelta.amount1() > 0 ? position.tokensOwed1 += uint128(callerDelta.amount1()) : 0; + if (callerDelta.amount1() > 0) { + position.tokensOwed1 += uint128(callerDelta.amount1()); + range.key.currency1.take(poolManager, address(this), uint128(callerDelta.amount1()), true); + callerDelta = toBalanceDelta(callerDelta.amount0(), 0); + } else { + position.tokensOwed1 = 0; } + + return callerDelta; } function _increaseLiquidityAndZeroOut( @@ -186,20 +201,19 @@ contract BaseLiquidityManagement is SafeCallback { // when decreasing liquidity, the user collects: 1) principal liquidity, 2) new fees, 3) old fees (position.tokensOwed) Position storage position = positions[owner][range.toId()]; - (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); + BalanceDelta callerFeesAccrued = _updateFeeGrowth(range, position); BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued; - // new fees += old fees + principal liquidity - token0Owed += position.tokensOwed0 + uint128(principalDelta.amount0()); - token1Owed += position.tokensOwed1 + uint128(principalDelta.amount1()); + // new fees = new fees + old fees + principal liquidity + callerFeesAccrued = callerFeesAccrued + + toBalanceDelta(uint256(position.tokensOwed0).toInt128(), uint256(position.tokensOwed1).toInt128()) + + principalDelta; position.tokensOwed0 = 0; position.tokensOwed1 = 0; position.liquidity -= liquidityToRemove; - delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); - - return delta; + return callerFeesAccrued; } function _decreaseLiquidityAndZeroOut( @@ -246,14 +260,14 @@ contract BaseLiquidityManagement is SafeCallback { } // collecting fees: new fees and old fees - (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); - token0Owed += position.tokensOwed0; - token1Owed += position.tokensOwed1; + BalanceDelta callerFeesAccrued = _updateFeeGrowth(range, position); + callerFeesAccrued = callerFeesAccrued + + toBalanceDelta(uint256(position.tokensOwed0).toInt128(), uint256(position.tokensOwed1).toInt128()); position.tokensOwed0 = 0; position.tokensOwed1 = 0; - return toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); + return callerFeesAccrued; } function _collectAndZeroOut(address owner, LiquidityRange memory range, uint256, bytes memory hookData, bool claims) @@ -276,18 +290,19 @@ contract BaseLiquidityManagement is SafeCallback { function _updateFeeGrowth(LiquidityRange memory range, Position storage position) internal - returns (uint128 token0Owed, uint128 token1Owed) + returns (BalanceDelta feesOwed) { (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); - (token0Owed, token1Owed) = FeeMath.getFeesOwed( + (uint128 token0Owed, uint128 token1Owed) = FeeMath.getFeesOwed( feeGrowthInside0X128, feeGrowthInside1X128, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128, position.liquidity ); + feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); position.feeGrowthInside0LastX128 = feeGrowthInside0X128; position.feeGrowthInside1LastX128 = feeGrowthInside1X128; From fc04651bfe09a5f2969f85f10a368ddf3a4ed4c5 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 19 Jun 2024 15:24:48 -0400 Subject: [PATCH 45/98] gutted LockAndBatchCall --- contracts/SimpleBatchCall.sol | 53 ------------------ contracts/base/CallsWithLock.sol | 52 ----------------- contracts/base/LockAndBatchCall.sol | 41 -------------- contracts/interfaces/ICallsWithLock.sol | 25 --------- test/SimpleBatchCallTest.t.sol | 74 ------------------------- 5 files changed, 245 deletions(-) delete mode 100644 contracts/SimpleBatchCall.sol delete mode 100644 contracts/base/CallsWithLock.sol delete mode 100644 contracts/base/LockAndBatchCall.sol delete mode 100644 contracts/interfaces/ICallsWithLock.sol delete mode 100644 test/SimpleBatchCallTest.t.sol diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol deleted file mode 100644 index bf1c63f2..00000000 --- a/contracts/SimpleBatchCall.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {LockAndBatchCall} from "./base/LockAndBatchCall.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {ImmutableState} from "./base/ImmutableState.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; - -/// @title SimpleBatchCall -/// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. -contract SimpleBatchCall is LockAndBatchCall { - using CurrencyLibrary for Currency; - using TransientStateLibrary for IPoolManager; - using CurrencySettler for Currency; - - constructor(IPoolManager _manager) ImmutableState(_manager) {} - - struct SettleConfig { - bool takeClaims; - bool settleUsingBurn; // If true, sends the underlying ERC20s. - } - - /// @notice We naively settle all currencies that are touched by the batch call. This data is passed in intially to `execute`. - function _settle(address sender, bytes memory data) internal override returns (bytes memory settleData) { - if (data.length != 0) { - (Currency[] memory currenciesTouched, SettleConfig memory config) = - abi.decode(data, (Currency[], SettleConfig)); - - for (uint256 i = 0; i < currenciesTouched.length; i++) { - Currency currency = currenciesTouched[i]; - int256 delta = manager.currencyDelta(address(this), currenciesTouched[i]); - - if (delta < 0) { - currency.settle(manager, sender, uint256(-delta), config.settleUsingBurn); - } - if (delta > 0) { - currency.take(manager, address(this), uint256(delta), config.takeClaims); - } - } - } - } - - function _handleAfterExecute(bytes memory, /*callReturnData*/ bytes memory /*settleReturnData*/ ) - internal - pure - override - { - return; - } -} diff --git a/contracts/base/CallsWithLock.sol b/contracts/base/CallsWithLock.sol deleted file mode 100644 index 9ddc1eec..00000000 --- a/contracts/base/CallsWithLock.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {ImmutableState} from "./ImmutableState.sol"; -import {ICallsWithLock} from "../interfaces/ICallsWithLock.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - -/// @title CallsWithLock -/// @notice Handles all the calls to the pool manager contract. Assumes the integrating contract has already acquired a lock. -abstract contract CallsWithLock is ICallsWithLock, ImmutableState { - error NotSelf(); - - modifier onlyBySelf() { - if (msg.sender != address(this)) revert NotSelf(); - _; - } - - function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) - external - onlyBySelf - returns (bytes memory) - { - return abi.encode(manager.initialize(key, sqrtPriceX96, hookData)); - } - - function modifyPositionWithLock( - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata hookData - ) external onlyBySelf returns (bytes memory) { - (BalanceDelta delta, BalanceDelta feeDelta) = manager.modifyLiquidity(key, params, hookData); - return abi.encode(delta, feeDelta); - } - - function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) - external - onlyBySelf - returns (bytes memory) - { - return abi.encode(manager.swap(key, params, hookData)); - } - - function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) - external - onlyBySelf - returns (bytes memory) - { - return abi.encode(manager.donate(key, amount0, amount1, hookData)); - } -} diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol deleted file mode 100644 index e0f517d2..00000000 --- a/contracts/base/LockAndBatchCall.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.19; - -import {SafeCallback} from "./SafeCallback.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {CallsWithLock} from "./CallsWithLock.sol"; - -abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { - error CallFail(bytes reason); - - function _settle(address sender, bytes memory data) internal virtual returns (bytes memory settleData); - function _handleAfterExecute(bytes memory callReturnData, bytes memory settleReturnData) internal virtual; - - /// @param executeData The function selectors and calldata for any of the function selectors in ICallsWithLock encoded as an array of bytes. - function execute(bytes memory executeData, bytes memory settleData) external { - (bytes memory lockReturnData) = manager.unlock(abi.encode(executeData, abi.encode(msg.sender, settleData))); - (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); - _handleAfterExecute(executeReturnData, settleReturnData); - } - - /// @param data This data is passed from the top-level execute function to the internal _executeWithLockCalls and _settle function. It is decoded as two separate dynamic bytes parameters. - /// @dev _unlockCallback is responsible for executing the internal calls under the lock and settling open deltas left on the pool - function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { - (bytes memory executeData, bytes memory settleDataWithSender) = abi.decode(data, (bytes, bytes)); - (address sender, bytes memory settleData) = abi.decode(settleDataWithSender, (address, bytes)); - return abi.encode(_executeWithLockCalls(executeData), _settle(sender, settleData)); - } - - function _executeWithLockCalls(bytes memory data) internal returns (bytes memory) { - bytes[] memory calls = abi.decode(data, (bytes[])); - bytes[] memory callsReturnData = new bytes[](calls.length); - - for (uint256 i = 0; i < calls.length; i++) { - (bool success, bytes memory returnData) = address(this).call(calls[i]); - if (!success) revert(string(returnData)); - callsReturnData[i] = returnData; - } - return abi.encode(callsReturnData); - } -} diff --git a/contracts/interfaces/ICallsWithLock.sol b/contracts/interfaces/ICallsWithLock.sol deleted file mode 100644 index 26017356..00000000 --- a/contracts/interfaces/ICallsWithLock.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; - -interface ICallsWithLock { - function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) - external - returns (bytes memory); - - function modifyPositionWithLock( - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata hookData - ) external returns (bytes memory); - - function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) - external - returns (bytes memory); - - function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) - external - returns (bytes memory); -} diff --git a/test/SimpleBatchCallTest.t.sol b/test/SimpleBatchCallTest.t.sol deleted file mode 100644 index 04a0e922..00000000 --- a/test/SimpleBatchCallTest.t.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {SimpleBatchCall} from "../contracts/SimpleBatchCall.sol"; -import {ICallsWithLock} from "../contracts/interfaces/ICallsWithLock.sol"; - -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {Test} from "forge-std/Test.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -/// @title SimpleBatchCall -/// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. -contract SimpleBatchCallTest is Test, Deployers { - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - SimpleBatchCall batchCall; - - function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); - key = - PoolKey({currency0: currency0, currency1: currency1, fee: 3000, tickSpacing: 60, hooks: IHooks(address(0))}); - - batchCall = new SimpleBatchCall(manager); - ERC20(Currency.unwrap(currency0)).approve(address(batchCall), 2 ** 255); - ERC20(Currency.unwrap(currency1)).approve(address(batchCall), 2 ** 255); - } - - function test_initialize() public { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); - bytes memory settleData = abi.encode(SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false})); - batchCall.execute(abi.encode(calls), ZERO_BYTES); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId()); - assertEq(sqrtPriceX96, SQRT_PRICE_1_1); - } - - function test_initialize_modifyPosition() public { - bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); - calls[1] = abi.encodeWithSelector( - ICallsWithLock.modifyPositionWithLock.selector, - key, - IPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18, salt: 0}), - ZERO_BYTES - ); - Currency[] memory currenciesTouched = new Currency[](2); - currenciesTouched[0] = currency0; - currenciesTouched[1] = currency1; - bytes memory settleData = - abi.encode(currenciesTouched, SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false})); - uint256 balance0 = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); - uint256 balance1 = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); - batchCall.execute(abi.encode(calls), settleData); - uint256 balance0After = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); - uint256 balance1After = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId()); - - assertGt(balance0After, balance0); - assertGt(balance1After, balance1); - assertEq(sqrtPriceX96, SQRT_PRICE_1_1); - } -} From e1d55f8a858614c9911a27b199ddfb7a95981405 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 19 Jun 2024 15:26:14 -0400 Subject: [PATCH 46/98] cleanup diff --- contracts/BaseHook.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 8962fa3c..01fc4954 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -6,8 +6,6 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {SafeCallback} from "./base/SafeCallback.sol"; -import {ImmutableState} from "./base/ImmutableState.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {SafeCallback} from "./base/SafeCallback.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; From b73a2404c8c20222b795dc4eb1ce0482d775af8f Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 19 Jun 2024 15:58:56 -0400 Subject: [PATCH 47/98] renaming vanilla functions --- contracts/NonfungiblePositionManager.sol | 8 ++++---- contracts/base/BaseLiquidityManagement.sol | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index bfbfeb30..fec44dc3 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -51,7 +51,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit bytes calldata hookData ) public payable returns (uint256 tokenId, BalanceDelta delta) { // delta = modifyLiquidity(range, liquidity.toInt256(), hookData, false); - delta = _increaseLiquidityWithLock(msg.sender, range, liquidity, hookData, false); + delta = _lockAndIncreaseLiquidity(msg.sender, range, liquidity, hookData, false); // mint receipt token _mint(recipient, (tokenId = _nextId++)); @@ -84,7 +84,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - delta = _increaseLiquidityWithLock(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + delta = _lockAndIncreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); } function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) @@ -93,7 +93,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - delta = _decreaseLiquidityWithLock(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + delta = _lockAndDecreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -122,7 +122,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - delta = _collectWithLock(tokenPos.owner, tokenPos.range, hookData, claims); + delta = _lockAndCollect(tokenPos.owner, tokenPos.range, hookData, claims); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 9baa3917..ac80fb86 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -165,7 +165,7 @@ contract BaseLiquidityManagement is SafeCallback { zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); } - function _increaseLiquidityWithLock( + function _lockAndIncreaseLiquidity( address owner, LiquidityRange memory range, uint256 liquidityToAdd, @@ -227,7 +227,7 @@ contract BaseLiquidityManagement is SafeCallback { zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); } - function _decreaseLiquidityWithLock( + function _lockAndDecreaseLiquidity( address owner, LiquidityRange memory range, uint256 liquidityToRemove, @@ -278,7 +278,7 @@ contract BaseLiquidityManagement is SafeCallback { zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); } - function _collectWithLock(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) + function _lockAndCollect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) internal returns (BalanceDelta) { From 2227265484d8eddcec9c1539d5b5542328d4a530 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 20 Jun 2024 09:43:34 -0400 Subject: [PATCH 48/98] sanitize --- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 8 +-- contracts/base/BaseLiquidityManagement.sol | 53 +++++-------------- .../interfaces/IBaseLiquidityManagement.sol | 48 +++++++++++++++++ .../INonfungiblePositionManager.sol | 30 +++++++++++ .../NonfungiblePositionManager.t.sol | 4 +- 10 files changed, 100 insertions(+), 53 deletions(-) create mode 100644 contracts/interfaces/IBaseLiquidityManagement.sol diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 3abe533b..db4b2042 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -187560 \ No newline at end of file +187556 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index d864649e..407e1fdc 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -166555 \ No newline at end of file +166551 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index f183fb08..b0e4e46d 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -183256 \ No newline at end of file +183251 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index aaa914c7..00ea1e2a 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -158838 \ No newline at end of file +158833 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 42792686..140676d9 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -478545 \ No newline at end of file +478540 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index fec44dc3..8a74b2e4 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -29,11 +29,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit /// @dev The ID of the next token that will be minted. Skips 0 uint256 private _nextId = 1; - struct TokenPosition { - address owner; - LiquidityRange range; - } - + // maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range) mapping(uint256 tokenId => TokenPosition position) public tokenPositions; constructor(IPoolManager _manager) @@ -106,7 +102,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit LiquidityRangeId rangeId = tokenPosition.range.toId(); Position storage position = positions[msg.sender][rangeId]; if (0 < position.liquidity) { - decreaseLiquidity(tokenId, position.liquidity, hookData, claims); + delta = decreaseLiquidity(tokenId, position.liquidity, hookData, claims); } require(position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "NOT_EMPTY"); delete positions[msg.sender][rangeId]; diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index ac80fb86..45542c93 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -21,8 +21,9 @@ import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; import {FeeMath} from "../libraries/FeeMath.sol"; import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; +import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; -contract BaseLiquidityManagement is SafeCallback { +contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; @@ -34,31 +35,8 @@ contract BaseLiquidityManagement is SafeCallback { using SafeCast for uint256; using LiquiditySaltLibrary for IHooks; - // details about the liquidity position - struct Position { - // the nonce for permits - uint96 nonce; - // the address that is approved for spending this token - address operator; - uint256 liquidity; - // the fee growth of the aggregate position as of the last action on the individual position - uint256 feeGrowthInside0LastX128; - uint256 feeGrowthInside1LastX128; - // how many uncollected tokens are owed to the position, as of the last computation - uint128 tokensOwed0; - uint128 tokensOwed1; - } - - enum LiquidityOperation { - INCREASE, - DECREASE, - COLLECT - } - mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; - error UnlockFailure(); - constructor(IPoolManager _manager) ImmutableState(_manager) {} function zeroOut(BalanceDelta delta, Currency currency0, Currency currency1, address owner, bool claims) public { @@ -86,7 +64,7 @@ contract BaseLiquidityManagement is SafeCallback { } else if (op == LiquidityOperation.COLLECT) { return abi.encode(_collectAndZeroOut(owner, range, 0, hookData, claims)); } else { - revert UnlockFailure(); + return new bytes(0); } } @@ -110,8 +88,7 @@ contract BaseLiquidityManagement is SafeCallback { address owner, LiquidityRange memory range, uint256 liquidityToAdd, - bytes memory hookData, - bool claims + bytes memory hookData ) internal returns (BalanceDelta) { // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = @@ -161,7 +138,7 @@ contract BaseLiquidityManagement is SafeCallback { bytes memory hookData, bool claims ) internal returns (BalanceDelta delta) { - delta = _increaseLiquidity(owner, range, liquidityToAdd, hookData, claims); + delta = _increaseLiquidity(owner, range, liquidityToAdd, hookData); zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); } @@ -182,8 +159,7 @@ contract BaseLiquidityManagement is SafeCallback { address owner, LiquidityRange memory range, uint256 liquidityToRemove, - bytes memory hookData, - bool claims + bytes memory hookData ) internal returns (BalanceDelta delta) { (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); @@ -223,7 +199,7 @@ contract BaseLiquidityManagement is SafeCallback { bytes memory hookData, bool claims ) internal returns (BalanceDelta delta) { - delta = _decreaseLiquidity(owner, range, liquidityToRemove, hookData, claims); + delta = _decreaseLiquidity(owner, range, liquidityToRemove, hookData); zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); } @@ -235,14 +211,12 @@ contract BaseLiquidityManagement is SafeCallback { bool claims ) internal returns (BalanceDelta) { return abi.decode( - manager.unlock( - abi.encode(LiquidityOperation.DECREASE, owner, range, liquidityToRemove, hookData, claims) - ), + manager.unlock(abi.encode(LiquidityOperation.DECREASE, owner, range, liquidityToRemove, hookData, claims)), (BalanceDelta) ); } - function _collect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) + function _collect(address owner, LiquidityRange memory range, bytes memory hookData) internal returns (BalanceDelta) { @@ -274,7 +248,7 @@ contract BaseLiquidityManagement is SafeCallback { internal returns (BalanceDelta delta) { - delta = _collect(owner, range, hookData, claims); + delta = _collect(owner, range, hookData); zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); } @@ -283,14 +257,13 @@ contract BaseLiquidityManagement is SafeCallback { returns (BalanceDelta) { return abi.decode( - manager.unlock(abi.encode(LiquidityOperation.COLLECT, owner, range, 0, hookData, claims)), - (BalanceDelta) + manager.unlock(abi.encode(LiquidityOperation.COLLECT, owner, range, 0, hookData, claims)), (BalanceDelta) ); } function _updateFeeGrowth(LiquidityRange memory range, Position storage position) internal - returns (BalanceDelta feesOwed) + returns (BalanceDelta _feesOwed) { (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = manager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); @@ -302,7 +275,7 @@ contract BaseLiquidityManagement is SafeCallback { position.feeGrowthInside1LastX128, position.liquidity ); - feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); + _feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); position.feeGrowthInside0LastX128 = feeGrowthInside0X128; position.feeGrowthInside1LastX128 = feeGrowthInside1X128; diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol new file mode 100644 index 00000000..550f58c7 --- /dev/null +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {LiquidityRange, LiquidityRangeId} from "../types/LiquidityRange.sol"; + +interface IBaseLiquidityManagement { + // details about the liquidity position + struct Position { + // the nonce for permits + uint96 nonce; + // the address that is approved for spending this token + address operator; + uint256 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } + + enum LiquidityOperation { + INCREASE, + DECREASE, + COLLECT + } + + /// @notice Zero-out outstanding deltas for the PoolManager + /// @dev To be called for batched operations where delta-zeroing happens once at the end of a sequence of operations + /// @param delta The amounts to zero out. Negatives are paid by the sender, positives are collected by the sender + /// @param currency0 The currency of the token0 + /// @param currency1 The currency of the token1 + /// @param user The user zero'ing the deltas. I.e. negative delta (debit) is paid by the user, positive delta (credit) is collected to the user + /// @param claims Whether deltas are zeroed with ERC-6909 claim tokens + function zeroOut(BalanceDelta delta, Currency currency0, Currency currency1, address user, bool claims) external; + + /// @notice Fees owed for a given liquidity position. Includes materialized fees and uncollected fees. + /// @param owner The owner of the liquidity position + /// @param range The range of the liquidity position + /// @return token0Owed The amount of token0 owed to the owner + /// @return token1Owed The amount of token1 owed to the owner + function feesOwed(address owner, LiquidityRange memory range) + external + view + returns (uint256 token0Owed, uint256 token1Owed); +} diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 5fe1590e..6b09efe5 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -5,6 +5,11 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; interface INonfungiblePositionManager { + struct TokenPosition { + address owner; + LiquidityRange range; + } + // NOTE: more gas efficient as LiquidityAmounts is used offchain function mint( LiquidityRange calldata position, @@ -17,19 +22,44 @@ interface INonfungiblePositionManager { // NOTE: more expensive since LiquidityAmounts is used onchain // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); + /// @notice Increase liquidity for an existing position + /// @param tokenId The ID of the position + /// @param liquidity The amount of liquidity to add + /// @param hookData Arbitrary data passed to the hook + /// @param claims Whether the liquidity increase uses ERC-6909 claim tokens + /// @return delta Corresponding balance changes as a result of increasing liquidity function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); + /// @notice Decrease liquidity for an existing position + /// @param tokenId The ID of the position + /// @param liquidity The amount of liquidity to remove + /// @param hookData Arbitrary data passed to the hook + /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens + /// @return delta Corresponding balance changes as a result of decreasing liquidity function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); + /// @notice Burn a position and delete the tokenId + /// @dev It removes liquidity and collects fees if the position is not empty + /// @param tokenId The ID of the position + /// @param recipient The address to send the collected tokens to + /// @param hookData Arbitrary data passed to the hook + /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens + /// @return delta Corresponding balance changes as a result of burning the position function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); // TODO: in v3, we can partially collect fees, but what was the usecase here? + /// @notice Collect fees for a position + /// @param tokenId The ID of the position + /// @param recipient The address to send the collected tokens to + /// @param hookData Arbitrary data passed to the hook + /// @param claims Whether the collected fees are sent as ERC-6909 claim tokens + /// @return delta Corresponding balance changes as a result of collecting fees function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 47d537d4..4f9a74dc 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -221,8 +221,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(liquidity, 0); // TODO: slightly off by 1 bip (0.0001%) - assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(-delta.amount0())), 0.0001e18); - assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(-delta.amount1())), 0.0001e18); + assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(delta.amount0())), 0.0001e18); + assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(delta.amount1())), 0.0001e18); // OZ 721 will revert if the token does not exist vm.expectRevert(); From 0cff6ef693958d4f6b6fc6791bf34513904a3691 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:04:47 -0400 Subject: [PATCH 49/98] change add liq accounting (#126) * change add liq accounting * remove rand comments * fix exact fees * use closeAllDeltas * comments cleanup * additional liquidity tests (#129) * additional increase liquidity tests * edge case of using cached fees for autocompound * wip * fix autocompound bug, use custodied and unclaimed fees in the autocompound * fix tests and use BalanceDeltas (#130) * fix some assertions * use BalanceDeltas for arithmetic * cleanest code in the game??? * additional cleaning * typo lol * autocompound gas benchmarks * autocompound excess credit gas benchmark * save 600 gas, cleaner code when moving caller delta to tokensOwed --------- Co-authored-by: saucepoint <98790946+saucepoint@users.noreply.github.com> --- .../autocompound_exactUnclaimedFees.snap | 1 + ...exactUnclaimedFees_exactCustodiedFees.snap | 1 + .../autocompound_excessFeesCredit.snap | 1 + .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 4 +- contracts/base/BaseLiquidityManagement.sol | 181 +++++++++++----- .../interfaces/IBaseLiquidityManagement.sol | 9 - .../BalanceDeltaExtensionLibrary.sol | 53 +++++ contracts/libraries/CurrencySenderLibrary.sol | 4 +- contracts/libraries/FeeMath.sol | 8 +- contracts/libraries/Position.sol | 30 +++ contracts/types/LiquidityRange.sol | 2 +- test/position-managers/FeeCollection.t.sol | 8 +- test/position-managers/Gas.t.sol | 129 ++++++++++- .../position-managers/IncreaseLiquidity.t.sol | 200 +++++++++++++++++- .../NonfungiblePositionManager.t.sol | 16 +- test/shared/fuzz/LiquidityFuzzers.sol | 2 +- 21 files changed, 567 insertions(+), 92 deletions(-) create mode 100644 .forge-snapshots/autocompound_exactUnclaimedFees.snap create mode 100644 .forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap create mode 100644 .forge-snapshots/autocompound_excessFeesCredit.snap create mode 100644 contracts/libraries/BalanceDeltaExtensionLibrary.sol create mode 100644 contracts/libraries/Position.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap new file mode 100644 index 00000000..40ad7ac8 --- /dev/null +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -0,0 +1 @@ +258477 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap new file mode 100644 index 00000000..e2e7eb05 --- /dev/null +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -0,0 +1 @@ +190850 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap new file mode 100644 index 00000000..bcf9757d --- /dev/null +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -0,0 +1 @@ +279016 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index db4b2042..ae013013 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -187556 \ No newline at end of file +190026 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 407e1fdc..4d5e683a 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -166551 \ No newline at end of file +168894 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index b0e4e46d..4ea517e8 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -183251 \ No newline at end of file +171241 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 00ea1e2a..c2e421fa 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -158833 \ No newline at end of file +146823 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 140676d9..d2591995 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -478540 \ No newline at end of file +466530 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 8a74b2e4..a461d1db 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -34,7 +34,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit constructor(IPoolManager _manager) BaseLiquidityManagement(_manager) - ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V3-POS", "1") + ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} // NOTE: more gas efficient as LiquidityAmounts is used offchain @@ -56,7 +56,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // NOTE: more expensive since LiquidityAmounts is used onchain // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { - // (uint160 sqrtPriceX96,,,) = manager.getSlot0(params.range.key.toId()); + // (uint160 sqrtPriceX96,,,) = manager.getSlot0(params.range.poolKey.toId()); // (tokenId, delta) = mint( // params.range, // LiquidityAmounts.getLiquidityForAmounts( diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 45542c93..bc9ab1da 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -22,6 +22,10 @@ import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; import {FeeMath} from "../libraries/FeeMath.sol"; import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; +import {PositionLibrary} from "../libraries/Position.sol"; +import {BalanceDeltaExtensionLibrary} from "../libraries/BalanceDeltaExtensionLibrary.sol"; + +import "forge-std/console2.sol"; contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { using LiquidityRangeIdLibrary for LiquidityRange; @@ -34,17 +38,30 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { using TransientStateLibrary for IPoolManager; using SafeCast for uint256; using LiquiditySaltLibrary for IHooks; + using PositionLibrary for IBaseLiquidityManagement.Position; + using BalanceDeltaExtensionLibrary for BalanceDelta; mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; constructor(IPoolManager _manager) ImmutableState(_manager) {} - function zeroOut(BalanceDelta delta, Currency currency0, Currency currency1, address owner, bool claims) public { - if (delta.amount0() < 0) currency0.settle(manager, owner, uint256(int256(-delta.amount0())), claims); - else if (delta.amount0() > 0) currency0.send(manager, owner, uint128(delta.amount0()), claims); + function _closeCallerDeltas( + BalanceDelta callerDeltas, + Currency currency0, + Currency currency1, + address owner, + bool claims + ) internal { + int128 callerDelta0 = callerDeltas.amount0(); + int128 callerDelta1 = callerDeltas.amount1(); + // On liquidity increase, the deltas should never be > 0. + // We always 0 out a caller positive delta because it is instead accounted for in position.tokensOwed. - if (delta.amount1() < 0) currency1.settle(manager, owner, uint256(int256(-delta.amount1())), claims); - else if (delta.amount1() > 0) currency1.send(manager, owner, uint128(delta.amount1()), claims); + if (callerDelta0 < 0) currency0.settle(manager, owner, uint256(int256(-callerDelta0)), claims); + else if (callerDelta0 > 0) currency0.send(manager, owner, uint128(callerDelta0), claims); + + if (callerDelta1 < 0) currency1.settle(manager, owner, uint256(int256(-callerDelta1)), claims); + else if (callerDelta1 > 0) currency1.send(manager, owner, uint128(callerDelta1), claims); } function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { @@ -73,62 +90,75 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) { (liquidityDelta, totalFeesAccrued) = manager.modifyLiquidity( - range.key, + range.poolKey, IPoolManager.ModifyLiquidityParams({ tickLower: range.tickLower, tickUpper: range.tickUpper, liquidityDelta: liquidityChange, - salt: range.key.hooks.getLiquiditySalt(owner) + salt: range.poolKey.hooks.getLiquiditySalt(owner) }), hookData ); } + /// @dev The delta returned from this call must be settled by the caller. + /// Zeroing out the full balance of open deltas accounted to this address is unsafe until the callerDeltas are handled. function _increaseLiquidity( address owner, LiquidityRange memory range, uint256 liquidityToAdd, bytes memory hookData - ) internal returns (BalanceDelta) { + ) internal returns (BalanceDelta callerDelta, BalanceDelta thisDelta) { // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData); Position storage position = positions[owner][range.toId()]; - // Account for fees that were potentially collected to other users on the same range. - BalanceDelta callerFeesAccrued = _updateFeeGrowth(range, position); - BalanceDelta feesToCollect = totalFeesAccrued - callerFeesAccrued; - range.key.currency0.take(manager, address(this), uint128(feesToCollect.amount0()), true); - range.key.currency1.take(manager, address(this), uint128(feesToCollect.amount1()), true); + // Calculate the portion of the liquidityDelta that is attributable to the caller. + // We must account for fees that might be owed to other users on the same range. + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); + + BalanceDelta callerFeesAccrued = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity + ); - // the delta applied from the above actions is liquidityDelta - feesToCollect, note that the actual total delta for the caller may be different because actions can be chained - BalanceDelta callerDelta = liquidityDelta - feesToCollect; + if (totalFeesAccrued == callerFeesAccrued) { + // when totalFeesAccrued == callerFeesAccrued, the caller is not sharing the range + // therefore, the caller is responsible for the entire liquidityDelta + callerDelta = liquidityDelta; + } else { + // the delta for increasing liquidity assuming that totalFeesAccrued was not applied + BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued; + + // outstanding deltas the caller is responsible for, after their fees are credited to the principal delta + callerDelta = principalDelta + callerFeesAccrued; - // update liquidity after feeGrowth is updated - position.liquidity += liquidityToAdd; + // outstanding deltas this contract is responsible for, intuitively the contract is responsible for taking fees external to the caller's accrued fees + thisDelta = totalFeesAccrued - callerFeesAccrued; + } - // Update the tokensOwed0 and tokensOwed1 values for the caller. - // if callerDelta < 0, existing fees were re-invested AND net new tokens are required for the liquidity increase - // if callerDelta == 0, existing fees were reinvested (autocompounded) - // if callerDelta > 0, some but not all existing fees were used to increase liquidity. Any remainder is added to the position's owed tokens + // Update position storage, flushing the callerDelta value to tokensOwed first if necessary. + // If callerDelta > 0, then even after investing callerFeesAccrued, the caller still has some amount to collect that were not added into the position so they are accounted to tokensOwed and removed from the final callerDelta returned. + BalanceDelta tokensOwed; if (callerDelta.amount0() > 0) { - position.tokensOwed0 += uint128(callerDelta.amount0()); - range.key.currency0.take(manager, address(this), uint128(callerDelta.amount0()), true); - callerDelta = toBalanceDelta(0, callerDelta.amount1()); - } else { - position.tokensOwed0 = 0; + (tokensOwed, callerDelta, thisDelta) = + _moveCallerDeltaToTokensOwed(true, tokensOwed, callerDelta, thisDelta); } if (callerDelta.amount1() > 0) { - position.tokensOwed1 += uint128(callerDelta.amount1()); - range.key.currency1.take(manager, address(this), uint128(callerDelta.amount1()), true); - callerDelta = toBalanceDelta(callerDelta.amount0(), 0); - } else { - position.tokensOwed1 = 0; + (tokensOwed, callerDelta, thisDelta) = + _moveCallerDeltaToTokensOwed(false, tokensOwed, callerDelta, thisDelta); } - return callerDelta; + position.addTokensOwed(tokensOwed); + position.addLiquidity(liquidityToAdd); + position.updateFeeGrowthInside(feeGrowthInside0X128, feeGrowthInside1X128); } function _increaseLiquidityAndZeroOut( @@ -137,9 +167,60 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { uint256 liquidityToAdd, bytes memory hookData, bool claims - ) internal returns (BalanceDelta delta) { - delta = _increaseLiquidity(owner, range, liquidityToAdd, hookData); - zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); + ) internal returns (BalanceDelta callerDelta) { + BalanceDelta thisDelta; + // TODO move callerDelta and thisDelta to transient storage? + (callerDelta, thisDelta) = _increaseLiquidity(owner, range, liquidityToAdd, hookData); + _closeCallerDeltas(callerDelta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); + _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); + } + + // When chaining many actions, this should be called at the very end to close out any open deltas owed to or by this contract for other users on the same range. + // This is safe because any amounts the caller should not pay or take have already been accounted for in closeCallerDeltas. + function _closeThisDeltas(BalanceDelta delta, Currency currency0, Currency currency1) internal { + int128 delta0 = delta.amount0(); + int128 delta1 = delta.amount1(); + + // Mint a receipt for the tokens owed to this address. + if (delta0 > 0) currency0.take(manager, address(this), uint128(delta0), true); + if (delta1 > 0) currency1.take(manager, address(this), uint128(delta1), true); + // Burn the receipt for tokens owed to this address. + if (delta0 < 0) currency0.settle(manager, address(this), uint256(int256(-delta0)), true); + if (delta1 < 0) currency1.settle(manager, address(this), uint256(int256(-delta1)), true); + } + + //TODO @sara deprecate when moving to _closeThisDeltas for decreaes and collect + function _closeAllDeltas(Currency currency0, Currency currency1) internal { + (BalanceDelta delta) = manager.currencyDeltas(address(this), currency0, currency1); + int128 delta0 = delta.amount0(); + int128 delta1 = delta.amount1(); + + // Mint a receipt for the tokens owed to this address. + if (delta0 > 0) currency0.take(manager, address(this), uint128(delta0), true); + if (delta1 > 0) currency1.take(manager, address(this), uint128(delta1), true); + // Burn the receipt for tokens owed to this address. + if (delta0 < 0) currency0.settle(manager, address(this), uint256(int256(-delta0)), true); + if (delta1 < 0) currency1.settle(manager, address(this), uint256(int256(-delta1)), true); + } + + function _moveCallerDeltaToTokensOwed( + bool useAmount0, + BalanceDelta tokensOwed, + BalanceDelta callerDelta, + BalanceDelta thisDelta + ) private returns (BalanceDelta, BalanceDelta, BalanceDelta) { + // credit the excess tokens to the position's tokensOwed + tokensOwed = + useAmount0 ? tokensOwed.setAmount0(callerDelta.amount0()) : tokensOwed.setAmount1(callerDelta.amount1()); + + // this contract is responsible for custodying the excess tokens + thisDelta = + useAmount0 ? thisDelta.addAmount0(callerDelta.amount0()) : thisDelta.addAmount1(callerDelta.amount1()); + + // the caller is not expected to collect the excess tokens + callerDelta = useAmount0 ? callerDelta.setAmount0(0) : callerDelta.setAmount1(0); + + return (tokensOwed, callerDelta, thisDelta); } function _lockAndIncreaseLiquidity( @@ -168,10 +249,10 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { // do NOT take tokens directly to the owner because this contract might be holding fees // that need to be paid out (position.tokensOwed) if (liquidityDelta.amount0() > 0) { - range.key.currency0.take(manager, address(this), uint128(liquidityDelta.amount0()), true); + range.poolKey.currency0.take(manager, address(this), uint128(liquidityDelta.amount0()), true); } if (liquidityDelta.amount1() > 0) { - range.key.currency1.take(manager, address(this), uint128(liquidityDelta.amount1()), true); + range.poolKey.currency1.take(manager, address(this), uint128(liquidityDelta.amount1()), true); } // when decreasing liquidity, the user collects: 1) principal liquidity, 2) new fees, 3) old fees (position.tokensOwed) @@ -200,7 +281,8 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { bool claims ) internal returns (BalanceDelta delta) { delta = _decreaseLiquidity(owner, range, liquidityToRemove, hookData); - zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); + _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); + _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); } function _lockAndDecreaseLiquidity( @@ -222,7 +304,7 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { { (, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, 0, hookData); - PoolKey memory key = range.key; + PoolKey memory key = range.poolKey; Position storage position = positions[owner][range.toId()]; // take all fees first then distribute @@ -249,7 +331,8 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { returns (BalanceDelta delta) { delta = _collect(owner, range, hookData); - zeroOut(delta, range.key.currency0, range.key.currency1, owner, claims); + _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); + _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); } function _lockAndCollect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) @@ -261,21 +344,22 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { ); } + // TODO: I deprecated this bc I liked to see the accounting in line in the top level function... and I like to do all the position updates at once. + // can keep but should at at least use the position library in here. function _updateFeeGrowth(LiquidityRange memory range, Position storage position) internal returns (BalanceDelta _feesOwed) { (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = - manager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); + manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); - (uint128 token0Owed, uint128 token1Owed) = FeeMath.getFeesOwed( + _feesOwed = FeeMath.getFeesOwed( feeGrowthInside0X128, feeGrowthInside1X128, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128, position.liquidity ); - _feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); position.feeGrowthInside0LastX128 = feeGrowthInside0X128; position.feeGrowthInside1LastX128 = feeGrowthInside1X128; @@ -290,15 +374,10 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { Position memory position = positions[owner][range.toId()]; (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = - manager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); + manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); - (token0Owed, token1Owed) = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); + (token0Owed) = FeeMath.getFeeOwed(feeGrowthInside0X128, position.feeGrowthInside0LastX128, position.liquidity); + (token1Owed) = FeeMath.getFeeOwed(feeGrowthInside1X128, position.feeGrowthInside1LastX128, position.liquidity); token0Owed += position.tokensOwed0; token1Owed += position.tokensOwed1; } diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol index 550f58c7..893d991e 100644 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -27,15 +27,6 @@ interface IBaseLiquidityManagement { COLLECT } - /// @notice Zero-out outstanding deltas for the PoolManager - /// @dev To be called for batched operations where delta-zeroing happens once at the end of a sequence of operations - /// @param delta The amounts to zero out. Negatives are paid by the sender, positives are collected by the sender - /// @param currency0 The currency of the token0 - /// @param currency1 The currency of the token1 - /// @param user The user zero'ing the deltas. I.e. negative delta (debit) is paid by the user, positive delta (credit) is collected to the user - /// @param claims Whether deltas are zeroed with ERC-6909 claim tokens - function zeroOut(BalanceDelta delta, Currency currency0, Currency currency1, address user, bool claims) external; - /// @notice Fees owed for a given liquidity position. Includes materialized fees and uncollected fees. /// @param owner The owner of the liquidity position /// @param range The range of the liquidity position diff --git a/contracts/libraries/BalanceDeltaExtensionLibrary.sol b/contracts/libraries/BalanceDeltaExtensionLibrary.sol new file mode 100644 index 00000000..e8b3a7f0 --- /dev/null +++ b/contracts/libraries/BalanceDeltaExtensionLibrary.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +library BalanceDeltaExtensionLibrary { + function setAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) { + assembly { + // set the upper 128 bits of a to amount0 + a := or(shl(128, amount0), and(sub(shl(128, 1), 1), a)) + } + return a; + } + + function setAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) { + assembly { + // set the lower 128 bits of a to amount1 + a := or(and(shl(128, sub(shl(128, 1), 1)), a), amount1) + } + return a; + } + + function addAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) { + assembly { + let a0 := sar(128, a) + let res0 := add(a0, amount0) + a := or(shl(128, res0), and(sub(shl(128, 1), 1), a)) + } + return a; + } + + function addAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) { + assembly { + let a1 := signextend(15, a) + let res1 := add(a1, amount1) + a := or(and(shl(128, sub(shl(128, 1), 1)), a), res1) + } + return a; + } + + function addAndAssign(BalanceDelta a, BalanceDelta b) internal pure returns (BalanceDelta) { + assembly { + let a0 := sar(128, a) + let a1 := signextend(15, a) + let b0 := sar(128, b) + let b1 := signextend(15, b) + let res0 := add(a0, b0) + let res1 := add(a1, b1) + a := or(shl(128, res0), and(sub(shl(128, 1), 1), res1)) + } + return a; + } +} diff --git a/contracts/libraries/CurrencySenderLibrary.sol b/contracts/libraries/CurrencySenderLibrary.sol index ce343325..656a9439 100644 --- a/contracts/libraries/CurrencySenderLibrary.sol +++ b/contracts/libraries/CurrencySenderLibrary.sol @@ -23,8 +23,8 @@ library CurrencySenderLibrary { if (useClaims) { manager.transfer(recipient, currency.toId(), amount); } else { - currency.settle(manager, address(this), amount, true); - currency.take(manager, recipient, amount, false); + // currency.settle(manager, address(this), amount, true); // sends in tokens into PM from this address + currency.take(manager, recipient, amount, false); // takes out tokens from PM to recipient } } } diff --git a/contracts/libraries/FeeMath.sol b/contracts/libraries/FeeMath.sol index cf202dc2..9a459252 100644 --- a/contracts/libraries/FeeMath.sol +++ b/contracts/libraries/FeeMath.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; library FeeMath { using SafeCast for uint256; @@ -14,9 +15,10 @@ library FeeMath { uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint256 liquidity - ) internal pure returns (uint128 token0Owed, uint128 token1Owed) { - token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); - token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); + ) internal pure returns (BalanceDelta feesOwed) { + uint128 token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); + uint128 token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); + feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); } function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint256 liquidity) diff --git a/contracts/libraries/Position.sol b/contracts/libraries/Position.sol new file mode 100644 index 00000000..79cd02c0 --- /dev/null +++ b/contracts/libraries/Position.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.20; + +import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; + +// Updates Position storage +library PositionLibrary { + // TODO ensure this is one sstore. + function addTokensOwed(IBaseLiquidityManagement.Position storage position, BalanceDelta tokensOwed) internal { + position.tokensOwed0 += uint128(tokensOwed.amount0()); + position.tokensOwed1 += uint128(tokensOwed.amount1()); + } + + function addLiquidity(IBaseLiquidityManagement.Position storage position, uint256 liquidity) internal { + unchecked { + position.liquidity += liquidity; + } + } + + // TODO ensure this is one sstore. + function updateFeeGrowthInside( + IBaseLiquidityManagement.Position storage position, + uint256 feeGrowthInside0X128, + uint256 feeGrowthInside1X128 + ) internal { + position.feeGrowthInside0LastX128 = feeGrowthInside0X128; + position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + } +} diff --git a/contracts/types/LiquidityRange.sol b/contracts/types/LiquidityRange.sol index 4d00fb4b..4f664027 100644 --- a/contracts/types/LiquidityRange.sol +++ b/contracts/types/LiquidityRange.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; struct LiquidityRange { - PoolKey key; + PoolKey poolKey; int24 tickLower; int24 tickUpper; } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index a0b78ac0..643f6303 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -125,7 +125,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); LiquidityRange memory range = - LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); @@ -167,7 +167,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); LiquidityRange memory range = - LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); @@ -229,7 +229,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); LiquidityRange memory range = - LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); @@ -261,7 +261,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { /// when alice decreases liquidity, she should only collect her fees function test_decreaseLiquidity_sameRange_exact() public { // alice and bob create liquidity on the same range [-120, 120] - LiquidityRange memory range = LiquidityRange({key: key, tickLower: -120, tickUpper: 120}); + LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120}); // alice provisions 3x the amount of liquidity as bob uint256 liquidityAlice = 3000e18; diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 495d6f22..fe2005e2 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -56,13 +56,27 @@ contract GasTest is Test, Deployers, GasSnapshot { IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + // Give tokens to Alice and Bob, with approvals + IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); + vm.startPrank(alice); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + vm.startPrank(bob); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + // mint some ERC6909 tokens claimsRouter.deposit(currency0, address(this), 100_000_000 ether); claimsRouter.deposit(currency1, address(this), 100_000_000 ether); manager.setOperator(address(lpm), true); // define a reusable range - range = LiquidityRange({key: key, tickLower: -300, tickUpper: 300}); + range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } // function test_gas_mint() public { @@ -102,6 +116,119 @@ contract GasTest is Test, Deployers, GasSnapshot { snapLastCall("increaseLiquidity_erc6909"); } + function test_gas_autocompound_exactUnclaimedFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // donate to create fees + donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); + + // alice uses her exact fees to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + token0Owed, + token1Owed + ); + + vm.prank(alice); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + snapLastCall("autocompound_exactUnclaimedFees"); + } + + function test_gas_autocompound_exactUnclaimedFees_exactCustodiedFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // donate to create fees + donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + + // bob collects fees so some of alice's fees are now cached + vm.prank(bob); + lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + + // donate to create more fees + donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + + (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + + // alice will use ALL of her fees to increase liquidity + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + newToken0Owed, + newToken1Owed + ); + + vm.prank(alice); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); + } + } + + // autocompounding but the excess fees are credited to tokensOwed + function test_gas_autocompound_excessFeesCredit() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Excess fees are accounted to alice + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // donate to create fees + donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + + // alice will use half of her fees to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + token0Owed / 2, + token1Owed / 2 + ); + + vm.prank(alice); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + snapLastCall("autocompound_excessFeesCredit"); + } + function test_gas_decreaseLiquidity_erc20() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index c3863b9f..1fa62382 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -73,7 +73,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { vm.stopPrank(); // define a reusable range - range = LiquidityRange({key: key, tickLower: -300, tickUpper: 300}); + range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } function test_increaseLiquidity_withExactFees() public { @@ -99,7 +99,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice uses her exact fees to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), @@ -108,10 +108,67 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { token1Owed ); + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.prank(alice); lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - // TODO: assertions, currently increasing liquidity does not perfectly use the fees + // alice did not spend any tokens + assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); + assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + + // alice spent all of the fees, approximately + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertApproxEqAbs(token0Owed, 0, 20 wei); + assertApproxEqAbs(token1Owed, 0, 20 wei); + } + + // uses donate to simulate fee revenue + function test_increaseLiquidity_withExactFees_donate() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // donate to create fees + donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); + + // alice uses her exact fees to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + token0Owed, + token1Owed + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + vm.prank(alice); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + + // alice did not spend any tokens + assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); + assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + + // alice spent all of the fees + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertEq(token0Owed, 0); + assertEq(token1Owed, 0); } function test_increaseLiquidity_withExcessFees() public { @@ -137,7 +194,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice will use half of her fees to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), @@ -214,7 +271,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice will use all of her fees + additional capital to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), @@ -254,4 +311,137 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { ); } } + + function test_increaseLiquidity_withExactFees_withExactCachedFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + // bob collects fees so some of alice's fees are now cached + vm.prank(bob); + lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + + // swap to create more fees + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + // alice's fees should be doubled + assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); + assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); + + uint256 balance0AliceBefore = currency0.balanceOf(alice); + uint256 balance1AliceBefore = currency1.balanceOf(alice); + + // alice will use ALL of her fees to increase liquidity + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + newToken0Owed, + newToken1Owed + ); + + vm.prank(alice); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + } + + // alice did not spend any tokens + assertEq(balance0AliceBefore, currency0.balanceOf(alice)); + assertEq(balance1AliceBefore, currency1.balanceOf(alice)); + + // some dust was credited to alice's tokensOwed + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertApproxEqAbs(token0Owed, 0, 80 wei); + assertApproxEqAbs(token1Owed, 0, 80 wei); + } + + // uses donate to simulate fee revenue + function test_increaseLiquidity_withExactFees_withExactCachedFees_donate() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + + // bob provides liquidity + vm.prank(bob); + (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + + // donate to create fees + donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + // bob collects fees so some of alice's fees are now cached + vm.prank(bob); + lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + + // donate to create more fees + donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + + (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + // alice's fees should be doubled + assertApproxEqAbs(newToken0Owed, token0Owed * 2, 1 wei); + assertApproxEqAbs(newToken1Owed, token1Owed * 2, 1 wei); + + uint256 balance0AliceBefore = currency0.balanceOf(alice); + uint256 balance1AliceBefore = currency1.balanceOf(alice); + + // alice will use ALL of her fees to increase liquidity + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + newToken0Owed, + newToken1Owed + ); + + vm.prank(alice); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + } + + // alice did not spend any tokens + assertEq(balance0AliceBefore, currency0.balanceOf(alice), "alice spent token0"); + assertEq(balance1AliceBefore, currency1.balanceOf(alice), "alice spent token1"); + + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertEq(token0Owed, 0); + assertEq(token1Owed, 0); + + // bob still collects 5 + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdBob); + assertApproxEqAbs(token0Owed, 5e18, 1 wei); + assertApproxEqAbs(token1Owed, 5e18, 1 wei); + + vm.prank(bob); + BalanceDelta result = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + assertApproxEqAbs(result.amount0(), 5e18, 1 wei); + assertApproxEqAbs(result.amount1(), 5e18, 1 wei); + } } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 4f9a74dc..c1cad0c1 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -52,7 +52,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); LiquidityRange memory range = - LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); @@ -74,7 +74,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // (amount0Desired, amount1Desired) = // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); // uint256 balance0Before = currency0.balanceOfSelf(); // uint256 balance1Before = currency1.balanceOfSelf(); @@ -104,7 +104,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // int24 tickUpper = int24(key.tickSpacing); // uint256 amount0Desired = 100e18; // uint256 amount1Desired = 100e18; - // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); // uint256 balance0Before = currency0.balanceOfSelf(); // uint256 balance1Before = currency1.balanceOfSelf(); @@ -137,7 +137,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // (amount0Desired, amount1Desired) = // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ // range: range, // amount0Desired: amount0Desired, @@ -167,7 +167,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // uint256 amount0Min = amount0Desired - 1; // uint256 amount1Min = amount1Desired - 1; - // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ // range: range, // amount0Desired: amount0Desired, @@ -207,7 +207,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 tokenId; (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); LiquidityRange memory range = - LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); @@ -243,7 +243,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); LiquidityRange memory range = - LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); @@ -267,7 +267,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - // LiquidityRange memory range = LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); // // swap to create fees // uint256 swapAmount = 0.01e18; diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 03e50f9b..cc401555 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -21,7 +21,7 @@ contract LiquidityFuzzers is Fuzzers { params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); (uint256 tokenId, BalanceDelta delta) = lpm.mint( - LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}), + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}), uint256(params.liquidityDelta), block.timestamp, recipient, From e21e847d2d80c35d888fc615cc859c82809e49c6 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 26 Jun 2024 16:58:27 -0400 Subject: [PATCH 50/98] create compatibility with arbitrary execute handler --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 20 ++++++++ contracts/base/BaseLiquidityManagement.sol | 47 +++++++++++-------- .../interfaces/IBaseLiquidityManagement.sol | 3 +- 11 files changed, 57 insertions(+), 29 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 40ad7ac8..bd0788e7 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -258477 \ No newline at end of file +260688 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index e2e7eb05..cc076b33 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -190850 \ No newline at end of file +193061 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index bcf9757d..bebd1523 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -279016 \ No newline at end of file +281227 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index ae013013..239b6055 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -190026 \ No newline at end of file +191813 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 4d5e683a..a63e6ad4 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -168894 \ No newline at end of file +170680 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 4ea517e8..00bc5def 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -171241 \ No newline at end of file +173452 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index c2e421fa..dae7dba4 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -146823 \ No newline at end of file +149034 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index d2591995..64520c53 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -466530 \ No newline at end of file +468881 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index a461d1db..6e870ace 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -37,6 +37,26 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} + function unlockAndExecute(bytes[] calldata data) external { + // TODO: bubble up the return + manager.unlock(abi.encode(LiquidityOperation.EXECUTE, abi.encode(data))); + } + + /// @param data bytes[] - array of abi.encodeWithSelector(, ) + function _execute(bytes[] memory data) internal override returns (bytes memory) { + bool success; + for (uint256 i; i < data.length; i++) { + // TODO: bubble up the return + (success,) = address(this).call(data[i]); + if (!success) revert("EXECUTE_FAILED"); + } + + // zeroOut(); + + // TODO: return something + return new bytes(0); + } + // NOTE: more gas efficient as LiquidityAmounts is used offchain // TODO: deadline check function mint( diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index bc9ab1da..7925a5f9 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -27,7 +27,7 @@ import {BalanceDeltaExtensionLibrary} from "../libraries/BalanceDeltaExtensionLi import "forge-std/console2.sol"; -contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { +abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; @@ -64,24 +64,26 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { else if (callerDelta1 > 0) currency1.send(manager, owner, uint128(callerDelta1), claims); } + function _execute(bytes[] memory data) internal virtual returns (bytes memory); + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { - ( - LiquidityOperation op, - address owner, - LiquidityRange memory range, - uint256 liquidityChange, - bytes memory hookData, - bool claims - ) = abi.decode(data, (LiquidityOperation, address, LiquidityRange, uint256, bytes, bool)); - - if (op == LiquidityOperation.INCREASE) { - return abi.encode(_increaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); - } else if (op == LiquidityOperation.DECREASE) { - return abi.encode(_decreaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); - } else if (op == LiquidityOperation.COLLECT) { - return abi.encode(_collectAndZeroOut(owner, range, 0, hookData, claims)); + (LiquidityOperation op, bytes memory args) = abi.decode(data, (LiquidityOperation, bytes)); + if (op == LiquidityOperation.EXECUTE) { + (bytes[] memory payload) = abi.decode(args, (bytes[])); + return _execute(payload); } else { - return new bytes(0); + (address owner, LiquidityRange memory range, uint256 liquidityChange, bytes memory hookData, bool claims) = + abi.decode(args, (address, LiquidityRange, uint256, bytes, bool)); + + if (op == LiquidityOperation.INCREASE) { + return abi.encode(_increaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); + } else if (op == LiquidityOperation.DECREASE) { + return abi.encode(_decreaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); + } else if (op == LiquidityOperation.COLLECT) { + return abi.encode(_collectAndZeroOut(owner, range, 0, hookData, claims)); + } else { + return new bytes(0); + } } } @@ -231,7 +233,9 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { bool claims ) internal returns (BalanceDelta) { return abi.decode( - manager.unlock(abi.encode(LiquidityOperation.INCREASE, owner, range, liquidityToAdd, hookData, claims)), + manager.unlock( + abi.encode(LiquidityOperation.INCREASE, abi.encode(owner, range, liquidityToAdd, hookData, claims)) + ), (BalanceDelta) ); } @@ -293,7 +297,9 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { bool claims ) internal returns (BalanceDelta) { return abi.decode( - manager.unlock(abi.encode(LiquidityOperation.DECREASE, owner, range, liquidityToRemove, hookData, claims)), + manager.unlock( + abi.encode(LiquidityOperation.DECREASE, abi.encode(owner, range, liquidityToRemove, hookData, claims)) + ), (BalanceDelta) ); } @@ -340,7 +346,8 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { returns (BalanceDelta) { return abi.decode( - manager.unlock(abi.encode(LiquidityOperation.COLLECT, owner, range, 0, hookData, claims)), (BalanceDelta) + manager.unlock(abi.encode(LiquidityOperation.COLLECT, abi.encode(owner, range, 0, hookData, claims))), + (BalanceDelta) ); } diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol index 893d991e..1dd19eb9 100644 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -24,7 +24,8 @@ interface IBaseLiquidityManagement { enum LiquidityOperation { INCREASE, DECREASE, - COLLECT + COLLECT, + EXECUTE } /// @notice Fees owed for a given liquidity position. Includes materialized fees and uncollected fees. From 1e52f828b6affff87dcac7ea7f18afea444f2682 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 26 Jun 2024 17:24:24 -0400 Subject: [PATCH 51/98] being supporting batched ops on vanilla functions --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 36 ++++++++++++++++--- contracts/base/BaseLiquidityManagement.sol | 6 ++-- 10 files changed, 42 insertions(+), 16 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index bd0788e7..24284db7 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -260688 \ No newline at end of file +261455 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index cc076b33..1dda941b 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -193061 \ No newline at end of file +193828 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index bebd1523..4b651ccc 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -281227 \ No newline at end of file +281994 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 239b6055..842ac7b0 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -191813 \ No newline at end of file +191784 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index a63e6ad4..6174cbfb 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -170680 \ No newline at end of file +170651 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 00bc5def..b36d56da 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -173452 \ No newline at end of file +174219 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index dae7dba4..d43a864a 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -149034 \ No newline at end of file +149801 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 64520c53..9ffd1f96 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -468881 \ No newline at end of file +469640 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 6e870ace..7b15a30a 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -16,6 +16,7 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit { @@ -24,6 +25,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit using PoolIdLibrary for PoolKey; using LiquidityRangeIdLibrary for LiquidityRange; using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; using SafeCast for uint256; /// @dev The ID of the next token that will be minted. Skips 0 @@ -66,8 +68,17 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit address recipient, bytes calldata hookData ) public payable returns (uint256 tokenId, BalanceDelta delta) { - // delta = modifyLiquidity(range, liquidity.toInt256(), hookData, false); - delta = _lockAndIncreaseLiquidity(msg.sender, range, liquidity, hookData, false); + // TODO: optimization, read/write manager.isUnlocked to avoid repeated external calls for batched execution + if (manager.isUnlocked()) { + BalanceDelta thisDelta; + (delta, thisDelta) = _increaseLiquidity(recipient, range, liquidity, hookData); + + // TODO: should be triggered by zeroOut in _execute... + _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, recipient, false); + _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); + } else { + delta = _unlockAndIncreaseLiquidity(msg.sender, range, liquidity, hookData, false); + } // mint receipt token _mint(recipient, (tokenId = _nextId++)); @@ -100,7 +111,19 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - delta = _lockAndIncreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + + if (manager.isUnlocked()) { + BalanceDelta thisDelta; + (delta, thisDelta) = _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + + // TODO: should be triggered by zeroOut in _execute... + _closeCallerDeltas( + delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims + ); + _closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1); + } else { + delta = _unlockAndIncreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + } } function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) @@ -109,7 +132,9 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - delta = _lockAndDecreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + + // TODO: @sauce update once _decreaseLiquidity returns callerDelta/thisDelta + delta = _unlockAndDecreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -138,7 +163,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - delta = _lockAndCollect(tokenPos.owner, tokenPos.range, hookData, claims); + // TODO: @sauce update once _collect returns callerDelta/thisDel + delta = _unlockAndCollect(tokenPos.owner, tokenPos.range, hookData, claims); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 7925a5f9..00c7ff92 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -225,7 +225,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb return (tokensOwed, callerDelta, thisDelta); } - function _lockAndIncreaseLiquidity( + function _unlockAndIncreaseLiquidity( address owner, LiquidityRange memory range, uint256 liquidityToAdd, @@ -289,7 +289,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); } - function _lockAndDecreaseLiquidity( + function _unlockAndDecreaseLiquidity( address owner, LiquidityRange memory range, uint256 liquidityToRemove, @@ -341,7 +341,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); } - function _lockAndCollect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) + function _unlockAndCollect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) internal returns (BalanceDelta) { From 97080cb351ffa3898713d22ab8a463c295ca4032 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 26 Jun 2024 18:29:23 -0400 Subject: [PATCH 52/98] some initial tests to drive TDD --- .../FullRangeAddInitialLiquidity.snap | 2 +- .forge-snapshots/FullRangeAddLiquidity.snap | 2 +- .forge-snapshots/FullRangeFirstSwap.snap | 2 +- .forge-snapshots/FullRangeInitialize.snap | 2 +- .../FullRangeRemoveLiquidity.snap | 2 +- .../FullRangeRemoveLiquidityAndRebalance.snap | 2 +- .forge-snapshots/FullRangeSecondSwap.snap | 2 +- .forge-snapshots/FullRangeSwap.snap | 2 +- .forge-snapshots/OracleGrow10Slots.snap | 2 +- .../OracleGrow10SlotsCardinalityGreater.snap | 2 +- .forge-snapshots/OracleGrow1Slot.snap | 2 +- .../OracleGrow1SlotCardinalityGreater.snap | 2 +- .forge-snapshots/OracleInitialize.snap | 2 +- .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 2 +- test/position-managers/Execute.t.sol | 169 ++++++++++++++++++ 24 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 test/position-managers/Execute.t.sol diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index e915877b..404cf12a 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -354477 \ No newline at end of file +311181 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index 03960543..a4a14676 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -161786 \ No newline at end of file +122990 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index e9aad527..da120795 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -146400 \ No newline at end of file +80220 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 9661da18..31ee7269 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1039616 \ No newline at end of file +1016976 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 7e064748..feea4936 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -146394 \ No newline at end of file +110566 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index ae347ed1..e0df7eb7 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -281672 \ No newline at end of file +240044 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index bb224d94..e68df8d3 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -116110 \ No newline at end of file +45930 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index 9355d4d2..b50d0ea2 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -145819 \ No newline at end of file +79351 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10Slots.snap b/.forge-snapshots/OracleGrow10Slots.snap index 96c9f369..3dada479 100644 --- a/.forge-snapshots/OracleGrow10Slots.snap +++ b/.forge-snapshots/OracleGrow10Slots.snap @@ -1 +1 @@ -254164 \ No newline at end of file +232960 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap index 9fc5bce2..f623cfa5 100644 --- a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap @@ -1 +1 @@ -249653 \ No newline at end of file +223649 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1Slot.snap b/.forge-snapshots/OracleGrow1Slot.snap index ced15d76..137baa16 100644 --- a/.forge-snapshots/OracleGrow1Slot.snap +++ b/.forge-snapshots/OracleGrow1Slot.snap @@ -1 +1 @@ -54049 \ No newline at end of file +32845 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap index 8ad5646e..e6dc42ce 100644 --- a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap @@ -1 +1 @@ -49549 \ No newline at end of file +23545 \ No newline at end of file diff --git a/.forge-snapshots/OracleInitialize.snap b/.forge-snapshots/OracleInitialize.snap index a9ee0288..e4e9e6b2 100644 --- a/.forge-snapshots/OracleInitialize.snap +++ b/.forge-snapshots/OracleInitialize.snap @@ -1 +1 @@ -72794 \ No newline at end of file +51310 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index c3858465..eb3b0f6b 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -156828 \ No newline at end of file +122336 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 24284db7..680f22f0 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -261455 \ No newline at end of file +177143 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 1dda941b..c002665e 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -193828 \ No newline at end of file +94292 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 4b651ccc..9af7ed01 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -281994 \ No newline at end of file +197658 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 842ac7b0..2ff81f64 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -191784 \ No newline at end of file +121605 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 6174cbfb..e448e8a8 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -170651 \ No newline at end of file +119377 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index b36d56da..39f9941e 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -174219 \ No newline at end of file +61707 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index d43a864a..aeaee2ca 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -149801 \ No newline at end of file +65477 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 9ffd1f96..40a008ad 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -469640 \ No newline at end of file +445756 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 7b15a30a..f8f55342 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -192,7 +192,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit } modifier isAuthorizedForToken(uint256 tokenId) { - require(_isApprovedOrOwner(msg.sender, tokenId), "Not approved"); + require(msg.sender == address(this) || _isApprovedOrOwner(msg.sender, tokenId), "Not approved"); _; } } diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol new file mode 100644 index 00000000..78ab07c0 --- /dev/null +++ b/test/position-managers/Execute.t.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + using PoolIdLibrary for PoolKey; + using SafeCast for uint256; + + NonfungiblePositionManager lpm; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + LiquidityRange range; + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + lpm = new NonfungiblePositionManager(manager); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + + // Give tokens to Alice and Bob, with approvals + IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); + vm.startPrank(alice); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + vm.startPrank(bob); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + + // define a reusable range + range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); + } + + function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { + initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); + liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + (uint256 tokenId,) = lpm.mint(range, initialLiquidity, 0, address(this), ZERO_BYTES); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector( + INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false + ); + + lpm.unlockAndExecute(data); + + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, initialLiquidity + liquidityToAdd); + } + + function test_execute_increaseLiquidity_twice( + uint256 initialiLiquidity, + uint256 liquidityToAdd, + uint256 liquidityToAdd2 + ) public { + initialiLiquidity = bound(initialiLiquidity, 1e18, 1000e18); + liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); + (uint256 tokenId,) = lpm.mint(range, initialiLiquidity, 0, address(this), ZERO_BYTES); + + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector( + INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false + ); + data[1] = abi.encodeWithSelector( + INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd2, ZERO_BYTES, false + ); + + lpm.unlockAndExecute(data); + + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, initialiLiquidity + liquidityToAdd + liquidityToAdd2); + } + + // this case doesnt make sense in real world usage, so it doesnt have a cool name. but its a good test case + function test_execute_mintAndIncrease(uint256 intialLiquidity, uint256 liquidityToAdd) public { + intialLiquidity = bound(intialLiquidity, 1e18, 1000e18); + liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + + uint256 tokenId = 1; // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector( + INonfungiblePositionManager.mint.selector, + range, + intialLiquidity, + block.timestamp + 1, + address(this), + ZERO_BYTES + ); + data[1] = abi.encodeWithSelector( + INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false + ); + + lpm.unlockAndExecute(data); + + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, intialLiquidity + liquidityToAdd); + } + + // rebalance: burn and mint + function test_execute_rebalance() public {} + // coalesce: burn and increase + function test_execute_coalesce() public {} + // split: decrease and mint + function test_execute_split() public {} + // shift: decrease and increase + function test_execute_shift() public {} + // shard: collect and mint + function test_execute_shard() public {} + // feed: collect and increase + function test_execute_feed() public {} + + // transplant: burn and mint on different keys + function test_execute_transplant() public {} + // cross-coalesce: burn and increase on different keys + function test_execute_crossCoalesce() public {} + // cross-split: decrease and mint on different keys + function test_execute_crossSplit() public {} + // cross-shift: decrease and increase on different keys + function test_execute_crossShift() public {} + // cross-shard: collect and mint on different keys + function test_execute_crossShard() public {} + // cross-feed: collect and increase on different keys + function test_execute_crossFeed() public {} +} From 80b51859b718e96f0ecd14d789bac6475d0556d4 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 26 Jun 2024 18:31:56 -0400 Subject: [PATCH 53/98] gas with isolate --- .forge-snapshots/FullRangeAddInitialLiquidity.snap | 2 +- .forge-snapshots/FullRangeAddLiquidity.snap | 2 +- .forge-snapshots/FullRangeFirstSwap.snap | 2 +- .forge-snapshots/FullRangeInitialize.snap | 2 +- .forge-snapshots/FullRangeRemoveLiquidity.snap | 2 +- .forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap | 2 +- .forge-snapshots/FullRangeSecondSwap.snap | 2 +- .forge-snapshots/FullRangeSwap.snap | 2 +- .forge-snapshots/OracleGrow10Slots.snap | 2 +- .forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap | 2 +- .forge-snapshots/OracleGrow1Slot.snap | 2 +- .forge-snapshots/OracleGrow1SlotCardinalityGreater.snap | 2 +- .forge-snapshots/OracleInitialize.snap | 2 +- .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- .forge-snapshots/autocompound_exactUnclaimedFees.snap | 2 +- .../autocompound_exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .forge-snapshots/autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .forge-snapshots/increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index 404cf12a..e915877b 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -311181 \ No newline at end of file +354477 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index a4a14676..03960543 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -122990 \ No newline at end of file +161786 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index da120795..e9aad527 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -80220 \ No newline at end of file +146400 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 31ee7269..9661da18 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1016976 \ No newline at end of file +1039616 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index feea4936..7e064748 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -110566 \ No newline at end of file +146394 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index e0df7eb7..ae347ed1 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -240044 \ No newline at end of file +281672 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index e68df8d3..bb224d94 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -45930 \ No newline at end of file +116110 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index b50d0ea2..9355d4d2 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -79351 \ No newline at end of file +145819 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10Slots.snap b/.forge-snapshots/OracleGrow10Slots.snap index 3dada479..96c9f369 100644 --- a/.forge-snapshots/OracleGrow10Slots.snap +++ b/.forge-snapshots/OracleGrow10Slots.snap @@ -1 +1 @@ -232960 \ No newline at end of file +254164 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap index f623cfa5..9fc5bce2 100644 --- a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap @@ -1 +1 @@ -223649 \ No newline at end of file +249653 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1Slot.snap b/.forge-snapshots/OracleGrow1Slot.snap index 137baa16..ced15d76 100644 --- a/.forge-snapshots/OracleGrow1Slot.snap +++ b/.forge-snapshots/OracleGrow1Slot.snap @@ -1 +1 @@ -32845 \ No newline at end of file +54049 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap index e6dc42ce..8ad5646e 100644 --- a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap @@ -1 +1 @@ -23545 \ No newline at end of file +49549 \ No newline at end of file diff --git a/.forge-snapshots/OracleInitialize.snap b/.forge-snapshots/OracleInitialize.snap index e4e9e6b2..a9ee0288 100644 --- a/.forge-snapshots/OracleInitialize.snap +++ b/.forge-snapshots/OracleInitialize.snap @@ -1 +1 @@ -51310 \ No newline at end of file +72794 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index eb3b0f6b..c3858465 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -122336 \ No newline at end of file +156828 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 680f22f0..1c8ed1da 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -177143 \ No newline at end of file +261480 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index c002665e..dbd217f0 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -94292 \ No newline at end of file +193853 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 9af7ed01..37ad1371 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -197658 \ No newline at end of file +282019 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 2ff81f64..278db523 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -121605 \ No newline at end of file +191804 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index e448e8a8..2f265e77 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -119377 \ No newline at end of file +170671 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 39f9941e..4ff353bf 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -61707 \ No newline at end of file +174244 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index aeaee2ca..e5854b96 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -65477 \ No newline at end of file +149826 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 40a008ad..9ffd1f96 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -445756 \ No newline at end of file +469640 \ No newline at end of file From 93f012ea6c0516cbbf62da3bb7b1cb9c0a838d4c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 26 Jun 2024 23:44:09 -0400 Subject: [PATCH 54/98] mint to recipient --- contracts/NonfungiblePositionManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index f8f55342..e2a98abb 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -77,12 +77,12 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, recipient, false); _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); } else { - delta = _unlockAndIncreaseLiquidity(msg.sender, range, liquidity, hookData, false); + delta = _unlockAndIncreaseLiquidity(recipient, range, liquidity, hookData, false); } // mint receipt token _mint(recipient, (tokenId = _nextId++)); - tokenPositions[tokenId] = TokenPosition({owner: msg.sender, range: range}); + tokenPositions[tokenId] = TokenPosition({owner: recipient, range: range}); } // NOTE: more expensive since LiquidityAmounts is used onchain From cc031aae87f0f894768a34e21eb79faaae2d4ece Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:42:18 -0400 Subject: [PATCH 55/98] refactor for external call and code reuse (#134) --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 51 +++++++--- contracts/base/BaseLiquidityManagement.sol | 98 ------------------- .../interfaces/IBaseLiquidityManagement.sol | 7 +- 11 files changed, 45 insertions(+), 127 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 1c8ed1da..e8b620bd 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -261480 \ No newline at end of file +262398 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index dbd217f0..e46fce64 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -193853 \ No newline at end of file +194771 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 37ad1371..853c5353 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -282019 \ No newline at end of file +282937 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 278db523..0808a085 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -191804 \ No newline at end of file +193172 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 2f265e77..df7eb4c5 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -170671 \ No newline at end of file +172032 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 4ff353bf..1f74a831 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -174244 \ No newline at end of file +175155 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index e5854b96..0f37872d 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -149826 \ No newline at end of file +150744 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 9ffd1f96..ac89220c 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -469640 \ No newline at end of file +600083 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index e2a98abb..c34028d3 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -39,24 +39,23 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} - function unlockAndExecute(bytes[] calldata data) external { - // TODO: bubble up the return - manager.unlock(abi.encode(LiquidityOperation.EXECUTE, abi.encode(data))); + function unlockAndExecute(bytes[] memory data) public returns (BalanceDelta delta) { + delta = abi.decode(manager.unlock(abi.encode(data)), (BalanceDelta)); } - /// @param data bytes[] - array of abi.encodeWithSelector(, ) - function _execute(bytes[] memory data) internal override returns (bytes memory) { + function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { + bytes[] memory data = abi.decode(payload, (bytes[])); + bool success; + bytes memory returnData; for (uint256 i; i < data.length; i++) { // TODO: bubble up the return - (success,) = address(this).call(data[i]); + (success, returnData) = address(this).call(data[i]); if (!success) revert("EXECUTE_FAILED"); } - // zeroOut(); - // TODO: return something - return new bytes(0); + return returnData; } // NOTE: more gas efficient as LiquidityAmounts is used offchain @@ -77,7 +76,9 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, recipient, false); _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); } else { - delta = _unlockAndIncreaseLiquidity(recipient, range, liquidity, hookData, false); + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(this.mint.selector, range, liquidity, deadline, recipient, hookData); + delta = unlockAndExecute(data); } // mint receipt token @@ -122,7 +123,9 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit ); _closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1); } else { - delta = _unlockAndIncreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(this.increaseLiquidity.selector, tokenId, liquidity, hookData, claims); + delta = unlockAndExecute(data); } } @@ -133,8 +136,17 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit { TokenPosition memory tokenPos = tokenPositions[tokenId]; - // TODO: @sauce update once _decreaseLiquidity returns callerDelta/thisDelta - delta = _unlockAndDecreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + if (manager.isUnlocked()) { + delta = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + _closeCallerDeltas( + delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims + ); + _closeAllDeltas(tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1); + } else { + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(this.decreaseLiquidity.selector, tokenId, liquidity, hookData, claims); + delta = unlockAndExecute(data); + } } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -163,8 +175,17 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - // TODO: @sauce update once _collect returns callerDelta/thisDel - delta = _unlockAndCollect(tokenPos.owner, tokenPos.range, hookData, claims); + if (manager.isUnlocked()) { + delta = _collect(tokenPos.owner, tokenPos.range, hookData); + _closeCallerDeltas( + delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims + ); + _closeAllDeltas(tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1); + } else { + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(this.collect.selector, tokenId, recipient, hookData, claims); + delta = unlockAndExecute(data); + } } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 00c7ff92..a6af4ed3 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -64,29 +64,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb else if (callerDelta1 > 0) currency1.send(manager, owner, uint128(callerDelta1), claims); } - function _execute(bytes[] memory data) internal virtual returns (bytes memory); - - function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { - (LiquidityOperation op, bytes memory args) = abi.decode(data, (LiquidityOperation, bytes)); - if (op == LiquidityOperation.EXECUTE) { - (bytes[] memory payload) = abi.decode(args, (bytes[])); - return _execute(payload); - } else { - (address owner, LiquidityRange memory range, uint256 liquidityChange, bytes memory hookData, bool claims) = - abi.decode(args, (address, LiquidityRange, uint256, bytes, bool)); - - if (op == LiquidityOperation.INCREASE) { - return abi.encode(_increaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); - } else if (op == LiquidityOperation.DECREASE) { - return abi.encode(_decreaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); - } else if (op == LiquidityOperation.COLLECT) { - return abi.encode(_collectAndZeroOut(owner, range, 0, hookData, claims)); - } else { - return new bytes(0); - } - } - } - function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) internal returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) @@ -163,20 +140,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb position.updateFeeGrowthInside(feeGrowthInside0X128, feeGrowthInside1X128); } - function _increaseLiquidityAndZeroOut( - address owner, - LiquidityRange memory range, - uint256 liquidityToAdd, - bytes memory hookData, - bool claims - ) internal returns (BalanceDelta callerDelta) { - BalanceDelta thisDelta; - // TODO move callerDelta and thisDelta to transient storage? - (callerDelta, thisDelta) = _increaseLiquidity(owner, range, liquidityToAdd, hookData); - _closeCallerDeltas(callerDelta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); - _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); - } - // When chaining many actions, this should be called at the very end to close out any open deltas owed to or by this contract for other users on the same range. // This is safe because any amounts the caller should not pay or take have already been accounted for in closeCallerDeltas. function _closeThisDeltas(BalanceDelta delta, Currency currency0, Currency currency1) internal { @@ -225,21 +188,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb return (tokensOwed, callerDelta, thisDelta); } - function _unlockAndIncreaseLiquidity( - address owner, - LiquidityRange memory range, - uint256 liquidityToAdd, - bytes memory hookData, - bool claims - ) internal returns (BalanceDelta) { - return abi.decode( - manager.unlock( - abi.encode(LiquidityOperation.INCREASE, abi.encode(owner, range, liquidityToAdd, hookData, claims)) - ), - (BalanceDelta) - ); - } - function _decreaseLiquidity( address owner, LiquidityRange memory range, @@ -277,33 +225,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb return callerFeesAccrued; } - function _decreaseLiquidityAndZeroOut( - address owner, - LiquidityRange memory range, - uint256 liquidityToRemove, - bytes memory hookData, - bool claims - ) internal returns (BalanceDelta delta) { - delta = _decreaseLiquidity(owner, range, liquidityToRemove, hookData); - _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); - _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); - } - - function _unlockAndDecreaseLiquidity( - address owner, - LiquidityRange memory range, - uint256 liquidityToRemove, - bytes memory hookData, - bool claims - ) internal returns (BalanceDelta) { - return abi.decode( - manager.unlock( - abi.encode(LiquidityOperation.DECREASE, abi.encode(owner, range, liquidityToRemove, hookData, claims)) - ), - (BalanceDelta) - ); - } - function _collect(address owner, LiquidityRange memory range, bytes memory hookData) internal returns (BalanceDelta) @@ -332,25 +253,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb return callerFeesAccrued; } - function _collectAndZeroOut(address owner, LiquidityRange memory range, uint256, bytes memory hookData, bool claims) - internal - returns (BalanceDelta delta) - { - delta = _collect(owner, range, hookData); - _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); - _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); - } - - function _unlockAndCollect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) - internal - returns (BalanceDelta) - { - return abi.decode( - manager.unlock(abi.encode(LiquidityOperation.COLLECT, abi.encode(owner, range, 0, hookData, claims))), - (BalanceDelta) - ); - } - // TODO: I deprecated this bc I liked to see the accounting in line in the top level function... and I like to do all the position updates at once. // can keep but should at at least use the position library in here. function _updateFeeGrowth(LiquidityRange memory range, Position storage position) diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol index 1dd19eb9..b5c07dd8 100644 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -21,12 +21,7 @@ interface IBaseLiquidityManagement { uint128 tokensOwed1; } - enum LiquidityOperation { - INCREASE, - DECREASE, - COLLECT, - EXECUTE - } + error LockFailure(); /// @notice Fees owed for a given liquidity position. Includes materialized fees and uncollected fees. /// @param owner The owner of the liquidity position From 1995c9e30ac582b63d75e55dcdeaa06de1d4b01a Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 28 Jun 2024 10:26:44 -0400 Subject: [PATCH 56/98] updated interface with unlockAndExecute --- contracts/interfaces/INonfungiblePositionManager.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 6b09efe5..17c2fc45 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -63,4 +63,15 @@ interface INonfungiblePositionManager { function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); + + /// @notice Execute a batch of external calls by unlocking the PoolManager + /// @param data an array of abi.encodeWithSelector(, ) for each call + /// @return delta The final delta changes of the caller + function unlockAndExecute(bytes[] memory data) external returns (BalanceDelta delta); + + /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees + /// @param tokenId The ID of the position + /// @return token0Owed The amount of token0 owed + /// @return token1Owed The amount of token1 owed + function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed); } From 0d6ab0b8501628a79d8bc6f616cf871f50bde81e Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:46:40 -0400 Subject: [PATCH 57/98] update decrease (#133) * update decrease * update collect * update decrease/collect * remove delta function * update burn --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 14 +- contracts/base/BaseLiquidityManagement.sol | 169 +++++++----------- .../INonfungiblePositionManager.sol | 5 +- contracts/libraries/CurrencySenderLibrary.sol | 30 ---- .../libraries/LiquidityDeltaAccounting.sol | 28 +++ contracts/libraries/Position.sol | 14 ++ test/position-managers/FeeCollection.t.sol | 78 ++------ .../NonfungiblePositionManager.t.sol | 3 +- 16 files changed, 151 insertions(+), 206 deletions(-) delete mode 100644 contracts/libraries/CurrencySenderLibrary.sol create mode 100644 contracts/libraries/LiquidityDeltaAccounting.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 40ad7ac8..8e881fb8 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -258477 \ No newline at end of file +258575 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index e2e7eb05..f44837b7 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -190850 \ No newline at end of file +190948 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index bcf9757d..81d04dab 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -279016 \ No newline at end of file +279114 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index ae013013..461e5928 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -190026 \ No newline at end of file +177014 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 4d5e683a..1a5a1ce2 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -168894 \ No newline at end of file +177026 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 4ea517e8..786ac121 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -171241 \ No newline at end of file +171339 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index c2e421fa..24ec8e92 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -146823 \ No newline at end of file +146921 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index d2591995..ee03852e 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -466530 \ No newline at end of file +466628 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index a461d1db..ab1670cd 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -86,10 +86,10 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) public isAuthorizedForToken(tokenId) - returns (BalanceDelta delta) + returns (BalanceDelta delta, BalanceDelta thisDelta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - delta = _lockAndDecreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); + (delta, thisDelta) = _lockAndDecreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims); } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -97,13 +97,17 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { + // TODO: Burn currently decreases and collects. However its done under different locks. + // Replace once we have the execute multicall. // remove liquidity TokenPosition storage tokenPosition = tokenPositions[tokenId]; LiquidityRangeId rangeId = tokenPosition.range.toId(); Position storage position = positions[msg.sender][rangeId]; - if (0 < position.liquidity) { - delta = decreaseLiquidity(tokenId, position.liquidity, hookData, claims); + if (position.liquidity > 0) { + (delta,) = decreaseLiquidity(tokenId, position.liquidity, hookData, claims); } + + collect(tokenId, recipient, hookData, claims); require(position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "NOT_EMPTY"); delete positions[msg.sender][rangeId]; delete tokenPositions[tokenId]; @@ -114,7 +118,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // TODO: in v3, we can partially collect fees, but what was the usecase here? function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) - external + public returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index bc9ab1da..df54345a 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -16,7 +16,6 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; -import {CurrencySenderLibrary} from "../libraries/CurrencySenderLibrary.sol"; import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; import {FeeMath} from "../libraries/FeeMath.sol"; @@ -24,6 +23,7 @@ import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {PositionLibrary} from "../libraries/Position.sol"; import {BalanceDeltaExtensionLibrary} from "../libraries/BalanceDeltaExtensionLibrary.sol"; +import {LiquidityDeltaAccounting} from "../libraries/LiquidityDeltaAccounting.sol"; import "forge-std/console2.sol"; @@ -31,7 +31,6 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; - using CurrencySenderLibrary for Currency; using CurrencyDeltas for IPoolManager; using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -40,6 +39,7 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { using LiquiditySaltLibrary for IHooks; using PositionLibrary for IBaseLiquidityManagement.Position; using BalanceDeltaExtensionLibrary for BalanceDelta; + using LiquidityDeltaAccounting for BalanceDelta; mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; @@ -58,10 +58,10 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { // We always 0 out a caller positive delta because it is instead accounted for in position.tokensOwed. if (callerDelta0 < 0) currency0.settle(manager, owner, uint256(int256(-callerDelta0)), claims); - else if (callerDelta0 > 0) currency0.send(manager, owner, uint128(callerDelta0), claims); + else if (callerDelta0 > 0) currency0.take(manager, owner, uint128(callerDelta0), claims); if (callerDelta1 < 0) currency1.settle(manager, owner, uint256(int256(-callerDelta1)), claims); - else if (callerDelta1 > 0) currency1.send(manager, owner, uint128(callerDelta1), claims); + else if (callerDelta1 > 0) currency1.take(manager, owner, uint128(callerDelta1), claims); } function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { @@ -77,7 +77,9 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { if (op == LiquidityOperation.INCREASE) { return abi.encode(_increaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); } else if (op == LiquidityOperation.DECREASE) { - return abi.encode(_decreaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims)); + (BalanceDelta callerDelta, BalanceDelta thisDelta) = + _decreaseLiquidityAndZeroOut(owner, range, liquidityChange, hookData, claims); + return abi.encode(callerDelta, thisDelta); } else if (op == LiquidityOperation.COLLECT) { return abi.encode(_collectAndZeroOut(owner, range, 0, hookData, claims)); } else { @@ -115,33 +117,13 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { Position storage position = positions[owner][range.toId()]; + // Calculates the fee growth since the last time the positions feeGrowthInside was updated. + // Also updates the feeGrowthInsideLast variables in storage. + (BalanceDelta callerFeesAccrued) = _updateFeeGrowth(range, position); + // Calculate the portion of the liquidityDelta that is attributable to the caller. // We must account for fees that might be owed to other users on the same range. - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = - manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); - - BalanceDelta callerFeesAccrued = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); - - if (totalFeesAccrued == callerFeesAccrued) { - // when totalFeesAccrued == callerFeesAccrued, the caller is not sharing the range - // therefore, the caller is responsible for the entire liquidityDelta - callerDelta = liquidityDelta; - } else { - // the delta for increasing liquidity assuming that totalFeesAccrued was not applied - BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued; - - // outstanding deltas the caller is responsible for, after their fees are credited to the principal delta - callerDelta = principalDelta + callerFeesAccrued; - - // outstanding deltas this contract is responsible for, intuitively the contract is responsible for taking fees external to the caller's accrued fees - thisDelta = totalFeesAccrued - callerFeesAccrued; - } + (callerDelta, thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); // Update position storage, flushing the callerDelta value to tokensOwed first if necessary. // If callerDelta > 0, then even after investing callerFeesAccrued, the caller still has some amount to collect that were not added into the position so they are accounted to tokensOwed and removed from the final callerDelta returned. @@ -158,7 +140,6 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { position.addTokensOwed(tokensOwed); position.addLiquidity(liquidityToAdd); - position.updateFeeGrowthInside(feeGrowthInside0X128, feeGrowthInside1X128); } function _increaseLiquidityAndZeroOut( @@ -189,20 +170,6 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { if (delta1 < 0) currency1.settle(manager, address(this), uint256(int256(-delta1)), true); } - //TODO @sara deprecate when moving to _closeThisDeltas for decreaes and collect - function _closeAllDeltas(Currency currency0, Currency currency1) internal { - (BalanceDelta delta) = manager.currencyDeltas(address(this), currency0, currency1); - int128 delta0 = delta.amount0(); - int128 delta1 = delta.amount1(); - - // Mint a receipt for the tokens owed to this address. - if (delta0 > 0) currency0.take(manager, address(this), uint128(delta0), true); - if (delta1 > 0) currency1.take(manager, address(this), uint128(delta1), true); - // Burn the receipt for tokens owed to this address. - if (delta0 < 0) currency0.settle(manager, address(this), uint256(int256(-delta0)), true); - if (delta1 < 0) currency1.settle(manager, address(this), uint256(int256(-delta1)), true); - } - function _moveCallerDeltaToTokensOwed( bool useAmount0, BalanceDelta tokensOwed, @@ -236,41 +203,40 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { ); } + /// Any outstanding amounts owed to the caller from the underlying modify call must be collected explicitly with `collect`. function _decreaseLiquidity( address owner, LiquidityRange memory range, uint256 liquidityToRemove, bytes memory hookData - ) internal returns (BalanceDelta delta) { + ) internal returns (BalanceDelta callerDelta, BalanceDelta thisDelta) { (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); - // take all tokens first - // do NOT take tokens directly to the owner because this contract might be holding fees - // that need to be paid out (position.tokensOwed) - if (liquidityDelta.amount0() > 0) { - range.poolKey.currency0.take(manager, address(this), uint128(liquidityDelta.amount0()), true); - } - if (liquidityDelta.amount1() > 0) { - range.poolKey.currency1.take(manager, address(this), uint128(liquidityDelta.amount1()), true); - } + Position storage position = positions[owner][range.toId()]; - // when decreasing liquidity, the user collects: 1) principal liquidity, 2) new fees, 3) old fees (position.tokensOwed) + // Calculates the fee growth since the last time the positions feeGrowthInside was updated + // Also updates the position's the feeGrowthInsideLast variables in storage. + (BalanceDelta callerFeesAccrued) = _updateFeeGrowth(range, position); - Position storage position = positions[owner][range.toId()]; - BalanceDelta callerFeesAccrued = _updateFeeGrowth(range, position); - BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued; + // Account for fees accrued to other users on the same range. + (callerDelta, thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); - // new fees = new fees + old fees + principal liquidity - callerFeesAccrued = callerFeesAccrued - + toBalanceDelta(uint256(position.tokensOwed0).toInt128(), uint256(position.tokensOwed1).toInt128()) - + principalDelta; + BalanceDelta tokensOwed; - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - position.liquidity -= liquidityToRemove; + // Flush the callerDelta, incrementing the tokensOwed to the user and the amount claimable to this contract. + if (callerDelta.amount0() > 0) { + (tokensOwed, callerDelta, thisDelta) = + _moveCallerDeltaToTokensOwed(true, tokensOwed, callerDelta, thisDelta); + } - return callerFeesAccrued; + if (callerDelta.amount1() > 0) { + (tokensOwed, callerDelta, thisDelta) = + _moveCallerDeltaToTokensOwed(false, tokensOwed, callerDelta, thisDelta); + } + + position.addTokensOwed(tokensOwed); + position.subtractLiquidity(liquidityToRemove); } function _decreaseLiquidityAndZeroOut( @@ -279,10 +245,10 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { uint256 liquidityToRemove, bytes memory hookData, bool claims - ) internal returns (BalanceDelta delta) { - delta = _decreaseLiquidity(owner, range, liquidityToRemove, hookData); - _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); - _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); + ) internal returns (BalanceDelta callerDelta, BalanceDelta thisDelta) { + (callerDelta, thisDelta) = _decreaseLiquidity(owner, range, liquidityToRemove, hookData); + _closeCallerDeltas(callerDelta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); + _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); } function _lockAndDecreaseLiquidity( @@ -291,48 +257,52 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { uint256 liquidityToRemove, bytes memory hookData, bool claims - ) internal returns (BalanceDelta) { + ) internal returns (BalanceDelta, BalanceDelta) { return abi.decode( manager.unlock(abi.encode(LiquidityOperation.DECREASE, owner, range, liquidityToRemove, hookData, claims)), - (BalanceDelta) + (BalanceDelta, BalanceDelta) ); } function _collect(address owner, LiquidityRange memory range, bytes memory hookData) internal - returns (BalanceDelta) + returns (BalanceDelta callerDelta, BalanceDelta thisDelta) { - (, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, 0, hookData); - - PoolKey memory key = range.poolKey; Position storage position = positions[owner][range.toId()]; - // take all fees first then distribute - if (totalFeesAccrued.amount0() > 0) { - key.currency0.take(manager, address(this), uint128(totalFeesAccrued.amount0()), true); - } - if (totalFeesAccrued.amount1() > 0) { - key.currency1.take(manager, address(this), uint128(totalFeesAccrued.amount1()), true); - } + // Only call modify if there is still liquidty in this position. + if (position.liquidity != 0) { + // Do not add or decrease liquidity, just trigger fee updates. + (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, 0, hookData); + + // Also updates the position's the feeGrowthInsideLast variables in storage. + (BalanceDelta callerFeesAccrued) = _updateFeeGrowth(range, position); - // collecting fees: new fees and old fees - BalanceDelta callerFeesAccrued = _updateFeeGrowth(range, position); - callerFeesAccrued = callerFeesAccrued - + toBalanceDelta(uint256(position.tokensOwed0).toInt128(), uint256(position.tokensOwed1).toInt128()); + // Account for fees accrued to other users on the same range. + // TODO: Opt when liquidityDelta == 0 + (callerDelta, thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); + } - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; + // Allow the caller to collect the tokens owed. + // Tokens owed that the caller collects is paid for by this contract. + // ie. Transfer the tokensOwed amounts to the caller from the position manager through the pool manager. + // TODO case where this contract does not have enough credits to pay the caller? + BalanceDelta tokensOwed = + toBalanceDelta(uint256(position.tokensOwed0).toInt128(), uint256(position.tokensOwed1).toInt128()); + callerDelta = callerDelta + tokensOwed; + thisDelta = thisDelta - tokensOwed; - return callerFeesAccrued; + position.clearTokensOwed(); } function _collectAndZeroOut(address owner, LiquidityRange memory range, uint256, bytes memory hookData, bool claims) internal - returns (BalanceDelta delta) + returns (BalanceDelta callerDelta) { - delta = _collect(owner, range, hookData); - _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); - _closeAllDeltas(range.poolKey.currency0, range.poolKey.currency1); + BalanceDelta thisDelta; + (callerDelta, thisDelta) = _collect(owner, range, hookData); + _closeCallerDeltas(callerDelta, range.poolKey.currency0, range.poolKey.currency1, owner, claims); + _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); } function _lockAndCollect(address owner, LiquidityRange memory range, bytes memory hookData, bool claims) @@ -344,16 +314,14 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { ); } - // TODO: I deprecated this bc I liked to see the accounting in line in the top level function... and I like to do all the position updates at once. - // can keep but should at at least use the position library in here. function _updateFeeGrowth(LiquidityRange memory range, Position storage position) internal - returns (BalanceDelta _feesOwed) + returns (BalanceDelta callerFeesAccrued) { (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); - _feesOwed = FeeMath.getFeesOwed( + callerFeesAccrued = FeeMath.getFeesOwed( feeGrowthInside0X128, feeGrowthInside1X128, position.feeGrowthInside0LastX128, @@ -361,8 +329,7 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { position.liquidity ); - position.feeGrowthInside0LastX128 = feeGrowthInside0X128; - position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + position.updateFeeGrowthInside(feeGrowthInside0X128, feeGrowthInside1X128); } // --- View Functions --- // diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 6b09efe5..af011df5 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -37,10 +37,11 @@ interface INonfungiblePositionManager { /// @param liquidity The amount of liquidity to remove /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of decreasing liquidity + /// @return delta Corresponding balance changes as a result of decreasing liquidity applied to user + /// @return thisDelta Corresponding balance changes as a result of decreasing liquidity applied to lpm function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external - returns (BalanceDelta delta); + returns (BalanceDelta delta, BalanceDelta thisDelta); /// @notice Burn a position and delete the tokenId /// @dev It removes liquidity and collects fees if the position is not empty diff --git a/contracts/libraries/CurrencySenderLibrary.sol b/contracts/libraries/CurrencySenderLibrary.sol deleted file mode 100644 index 656a9439..00000000 --- a/contracts/libraries/CurrencySenderLibrary.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.24; - -import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; -import {CurrencySettleTake} from "./CurrencySettleTake.sol"; -import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; - -/// @notice Library used to send Currencies from address to address -library CurrencySenderLibrary { - using CurrencyLibrary for Currency; - using CurrencySettleTake for Currency; - - /// @notice Send a custodied Currency to a recipient - /// @dev If sending ERC20 or native, the PoolManager must be unlocked - /// @param currency The Currency to send - /// @param manager The PoolManager - /// @param recipient The recipient address - /// @param amount The amount to send - /// @param useClaims If true, transfer ERC-6909 tokens - function send(Currency currency, IPoolManager manager, address recipient, uint256 amount, bool useClaims) - internal - { - if (useClaims) { - manager.transfer(recipient, currency.toId(), amount); - } else { - // currency.settle(manager, address(this), amount, true); // sends in tokens into PM from this address - currency.take(manager, recipient, amount, false); // takes out tokens from PM to recipient - } - } -} diff --git a/contracts/libraries/LiquidityDeltaAccounting.sol b/contracts/libraries/LiquidityDeltaAccounting.sol new file mode 100644 index 00000000..b6c99b10 --- /dev/null +++ b/contracts/libraries/LiquidityDeltaAccounting.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +import "forge-std/console2.sol"; + +library LiquidityDeltaAccounting { + function split(BalanceDelta liquidityDelta, BalanceDelta callerFeesAccrued, BalanceDelta totalFeesAccrued) + internal + returns (BalanceDelta callerDelta, BalanceDelta thisDelta) + { + if (totalFeesAccrued == callerFeesAccrued) { + // when totalFeesAccrued == callerFeesAccrued, the caller is not sharing the range + // therefore, the caller is responsible for the entire liquidityDelta + callerDelta = liquidityDelta; + } else { + // the delta for increasing liquidity assuming that totalFeesAccrued was not applied + BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued; + + // outstanding deltas the caller is responsible for, after their fees are credited to the principal delta + callerDelta = principalDelta + callerFeesAccrued; + + // outstanding deltas this contract is responsible for, intuitively the contract is responsible for taking fees external to the caller's accrued fees + thisDelta = totalFeesAccrued - callerFeesAccrued; + } + } +} diff --git a/contracts/libraries/Position.sol b/contracts/libraries/Position.sol index 79cd02c0..11ef1771 100644 --- a/contracts/libraries/Position.sol +++ b/contracts/libraries/Position.sol @@ -6,18 +6,32 @@ import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; // Updates Position storage library PositionLibrary { + error InsufficientLiquidity(); + // TODO ensure this is one sstore. function addTokensOwed(IBaseLiquidityManagement.Position storage position, BalanceDelta tokensOwed) internal { position.tokensOwed0 += uint128(tokensOwed.amount0()); position.tokensOwed1 += uint128(tokensOwed.amount1()); } + function clearTokensOwed(IBaseLiquidityManagement.Position storage position) internal { + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + } + function addLiquidity(IBaseLiquidityManagement.Position storage position, uint256 liquidity) internal { unchecked { position.liquidity += liquidity; } } + function subtractLiquidity(IBaseLiquidityManagement.Position storage position, uint256 liquidity) internal { + if (position.liquidity < liquidity) revert InsufficientLiquidity(); + unchecked { + position.liquidity -= liquidity; + } + } + // TODO ensure this is one sstore. function updateFeeGrowthInside( IBaseLiquidityManagement.Position storage position, diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 643f6303..e89ff68a 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -216,49 +216,10 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_collect_donate() public {} function test_collect_donate_sameRange() public {} - function test_decreaseLiquidity_sameRange( - IPoolManager.ModifyLiquidityParams memory params, - uint256 liquidityDeltaBob - ) public { - uint256 tokenIdAlice; - uint256 tokenIdBob; - params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); - vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); - - LiquidityRange memory range = - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - vm.prank(alice); - (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); - - vm.prank(bob); - (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - - // alice removes all of her liquidity - vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, uint256(params.liquidityDelta), ZERO_BYTES, true); - assertEq(uint256(uint128(aliceDelta.amount0())), manager.balanceOf(alice, currency0.toId())); - assertEq(uint256(uint128(aliceDelta.amount1())), manager.balanceOf(alice, currency1.toId())); - - // bob removes half of his liquidity - vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityDeltaBob / 2, ZERO_BYTES, true); - assertEq(uint256(uint128(bobDelta.amount0())), manager.balanceOf(bob, currency0.toId())); - assertEq(uint256(uint128(bobDelta.amount1())), manager.balanceOf(bob, currency1.toId())); - - // position manager holds no fees now - assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); - assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); - } - /// @dev Alice and bob create liquidity on the same range /// when alice decreases liquidity, she should only collect her fees + /// TODO Add back fuzz test on liquidityDeltaBob + /// TODO Assert state changes for lpm balance, position state, and return values function test_decreaseLiquidity_sameRange_exact() public { // alice and bob create liquidity on the same range [-120, 120] LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120}); @@ -281,39 +242,38 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice decreases liquidity vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); + BalanceDelta aliceDelta; + BalanceDelta thisDelta; + (aliceDelta, thisDelta) = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); uint256 tolerance = 0.000000001 ether; - // alice claims original principal + her fees + uint256 lpmBalance0 = manager.balanceOf(address(lpm), currency0.toId()); + uint256 lpmBalance1 = manager.balanceOf(address(lpm), currency1.toId()); + + // lpm collects alice's principal + all fees accrued on the range assertApproxEqAbs( - manager.balanceOf(alice, currency0.toId()), - uint256(int256(-lpDeltaAlice.amount0())) - + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), - tolerance + lpmBalance0, uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD), tolerance ); assertApproxEqAbs( - manager.balanceOf(alice, currency1.toId()), - uint256(int256(-lpDeltaAlice.amount1())) - + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), - tolerance + lpmBalance1, uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD), tolerance ); // bob decreases half of his liquidity vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); + BalanceDelta bobDelta; + (bobDelta, thisDelta) = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); - // bob claims half of the original principal + his fees + // lpm collects half of bobs principal + // the fee amount has already been collected with alice's calls assertApproxEqAbs( - manager.balanceOf(bob, currency0.toId()), - uint256(int256(-lpDeltaBob.amount0()) / 2) - + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + manager.balanceOf(address(lpm), currency0.toId()) - lpmBalance0, + uint256(int256(-lpDeltaBob.amount0()) / 2), tolerance ); assertApproxEqAbs( - manager.balanceOf(bob, currency1.toId()), - uint256(int256(-lpDeltaBob.amount1()) / 2) - + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + manager.balanceOf(address(lpm), currency1.toId()) - lpmBalance1, + uint256(int256(-lpDeltaBob.amount1()) / 2), tolerance ); } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index c1cad0c1..3d59572b 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -247,7 +247,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + (BalanceDelta delta, BalanceDelta thisDelta) = + lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); From 406506ffd9a52118340ea5584ae4cb2d8a0274f5 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 28 Jun 2024 11:13:20 -0400 Subject: [PATCH 58/98] fix bubbling different return types because of recursive calls --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 24 ++++---- .../INonfungiblePositionManager.sol | 2 +- contracts/libraries/TransientDemo.sol | 60 +++++++++++++++++++ 11 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 contracts/libraries/TransientDemo.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 2d491c86..2e536b9f 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -262492 \ No newline at end of file +262501 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 3f663c44..553b43e9 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -194865 \ No newline at end of file +194874 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 7da1ea95..ac41e55d 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -283031 \ No newline at end of file +283040 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 9d65fe6b..d780cfa9 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -180845 \ No newline at end of file +180891 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index f729ab62..d968e558 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -180857 \ No newline at end of file +180903 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 0c7dfe6a..6691256e 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -175249 \ No newline at end of file +175258 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index af512087..babfbeeb 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -150838 \ No newline at end of file +150847 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 46d7cd3c..33617d43 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -600177 \ No newline at end of file +472846 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index d78726a6..4f4056f1 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -39,8 +39,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} - function unlockAndExecute(bytes[] memory data) public returns (BalanceDelta delta) { - delta = abi.decode(manager.unlock(abi.encode(data)), (BalanceDelta)); + function unlockAndExecute(bytes[] memory data) public returns (bytes memory) { + return manager.unlock(abi.encode(data)); } function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { @@ -75,15 +75,16 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // TODO: should be triggered by zeroOut in _execute... _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, recipient, false); _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); + + // mint receipt token + _mint(recipient, (tokenId = _nextId++)); + tokenPositions[tokenId] = TokenPosition({owner: recipient, range: range}); } else { bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector(this.mint.selector, range, liquidity, deadline, recipient, hookData); - delta = unlockAndExecute(data); + bytes memory result = unlockAndExecute(data); + (tokenId, delta) = abi.decode(result, (uint256, BalanceDelta)); } - - // mint receipt token - _mint(recipient, (tokenId = _nextId++)); - tokenPositions[tokenId] = TokenPosition({owner: recipient, range: range}); } // NOTE: more expensive since LiquidityAmounts is used onchain @@ -125,7 +126,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit } else { bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector(this.increaseLiquidity.selector, tokenId, liquidity, hookData, claims); - delta = unlockAndExecute(data); + bytes memory result = unlockAndExecute(data); + delta = abi.decode(result, (BalanceDelta)); } } @@ -145,7 +147,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit } else { bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector(this.decreaseLiquidity.selector, tokenId, liquidity, hookData, claims); - delta = unlockAndExecute(data); + bytes memory result = unlockAndExecute(data); + (delta, thisDelta) = abi.decode(result, (BalanceDelta, BalanceDelta)); } } @@ -189,7 +192,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit } else { bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector(this.collect.selector, tokenId, recipient, hookData, claims); - delta = unlockAndExecute(data); + bytes memory result = unlockAndExecute(data); + delta = abi.decode(result, (BalanceDelta)); } } diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index b937f10b..6b029fd0 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -68,7 +68,7 @@ interface INonfungiblePositionManager { /// @notice Execute a batch of external calls by unlocking the PoolManager /// @param data an array of abi.encodeWithSelector(, ) for each call /// @return delta The final delta changes of the caller - function unlockAndExecute(bytes[] memory data) external returns (BalanceDelta delta); + function unlockAndExecute(bytes[] memory data) external returns (bytes memory); /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees /// @param tokenId The ID of the position diff --git a/contracts/libraries/TransientDemo.sol b/contracts/libraries/TransientDemo.sol new file mode 100644 index 00000000..2845878d --- /dev/null +++ b/contracts/libraries/TransientDemo.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; + +/// @title a library to store callers' currency deltas in transient storage +/// @dev this library implements the equivalent of a mapping, as transient storage can only be accessed in assembly +library TransientLiquidityDelta { + /// @notice calculates which storage slot a delta should be stored in for a given caller and currency + function _computeSlot(address caller_, Currency currency) internal pure returns (bytes32 hashSlot) { + assembly { + mstore(0, caller_) + mstore(32, currency) + hashSlot := keccak256(0, 64) + } + } + + /// @notice Flush a BalanceDelta into transient storage for a given holder + function flush(BalanceDelta delta, address holder, Currency currency0, Currency currency1) internal { + setDelta(currency0, holder, delta.amount0()); + setDelta(currency1, holder, delta.amount1()); + } + + function addDelta(Currency currency, address caller, int128 delta) internal { + bytes32 hashSlot = _computeSlot(caller, currency); + assembly { + let oldValue := tload(hashSlot) + let newValue := add(oldValue, delta) + tstore(hashSlot, newValue) + } + } + + function subDelta(Currency currency, address caller, int128 delta) internal { + bytes32 hashSlot = _computeSlot(caller, currency); + assembly { + let oldValue := tload(hashSlot) + let newValue := sub(oldValue, delta) + tstore(hashSlot, newValue) + } + } + + /// @notice sets a new currency delta for a given caller and currency + function setDelta(Currency currency, address caller, int256 delta) internal { + bytes32 hashSlot = _computeSlot(caller, currency); + + assembly { + tstore(hashSlot, delta) + } + } + + /// @notice gets a new currency delta for a given caller and currency + function getDelta(Currency currency, address caller) internal view returns (int256 delta) { + bytes32 hashSlot = _computeSlot(caller, currency); + + assembly { + delta := tload(hashSlot) + } + } +} From fae83dcfc6e2c7c6383b3a64029764042db6c0f2 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Sat, 29 Jun 2024 00:02:36 -0400 Subject: [PATCH 59/98] all operations only return a BalanceDelta type (#136) --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 16 +++++----- contracts/base/BaseLiquidityManagement.sol | 2 +- .../INonfungiblePositionManager.sol | 9 +++--- test/position-managers/Execute.t.sol | 6 ++-- test/position-managers/FeeCollection.t.sol | 31 +++++++++---------- test/position-managers/Gas.t.sol | 27 ++++++++++------ .../position-managers/IncreaseLiquidity.t.sol | 30 ++++++++++++------ .../NonfungiblePositionManager.t.sol | 6 ++-- test/shared/fuzz/LiquidityFuzzers.sol | 3 +- 17 files changed, 83 insertions(+), 63 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 2e536b9f..9418d155 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -262501 \ No newline at end of file +262456 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 553b43e9..17341d57 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -194874 \ No newline at end of file +194829 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index ac41e55d..51e59477 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -283040 \ No newline at end of file +282995 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index d780cfa9..f8eacac5 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -180891 \ No newline at end of file +180479 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index d968e558..cf69dc0a 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -180903 \ No newline at end of file +180491 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 6691256e..0a6e9003 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -175258 \ No newline at end of file +175213 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index babfbeeb..41b75c0b 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -150847 \ No newline at end of file +150802 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 33617d43..5d47c788 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -472846 \ No newline at end of file +472424 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 4f4056f1..e98d9abd 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -29,7 +29,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit using SafeCast for uint256; /// @dev The ID of the next token that will be minted. Skips 0 - uint256 private _nextId = 1; + uint256 public nextTokenId = 1; // maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range) mapping(uint256 tokenId => TokenPosition position) public tokenPositions; @@ -66,7 +66,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit uint256 deadline, address recipient, bytes calldata hookData - ) public payable returns (uint256 tokenId, BalanceDelta delta) { + ) public payable returns (BalanceDelta delta) { // TODO: optimization, read/write manager.isUnlocked to avoid repeated external calls for batched execution if (manager.isUnlocked()) { BalanceDelta thisDelta; @@ -77,13 +77,14 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); // mint receipt token - _mint(recipient, (tokenId = _nextId++)); + uint256 tokenId; + _mint(recipient, (tokenId = nextTokenId++)); tokenPositions[tokenId] = TokenPosition({owner: recipient, range: range}); } else { bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector(this.mint.selector, range, liquidity, deadline, recipient, hookData); bytes memory result = unlockAndExecute(data); - (tokenId, delta) = abi.decode(result, (uint256, BalanceDelta)); + delta = abi.decode(result, (BalanceDelta)); } } @@ -134,11 +135,12 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) public isAuthorizedForToken(tokenId) - returns (BalanceDelta delta, BalanceDelta thisDelta) + returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; if (manager.isUnlocked()) { + BalanceDelta thisDelta; (delta, thisDelta) = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); _closeCallerDeltas( delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims @@ -148,7 +150,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector(this.decreaseLiquidity.selector, tokenId, liquidity, hookData, claims); bytes memory result = unlockAndExecute(data); - (delta, thisDelta) = abi.decode(result, (BalanceDelta, BalanceDelta)); + delta = abi.decode(result, (BalanceDelta)); } } @@ -164,7 +166,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit LiquidityRangeId rangeId = tokenPosition.range.toId(); Position storage position = positions[msg.sender][rangeId]; if (position.liquidity > 0) { - (delta,) = decreaseLiquidity(tokenId, position.liquidity, hookData, claims); + delta = decreaseLiquidity(tokenId, position.liquidity, hookData, claims); } collect(tokenId, recipient, hookData, claims); diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 63d325de..530a94fc 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -152,7 +152,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb return (tokensOwed, callerDelta, thisDelta); } - + /// Any outstanding amounts owed to the caller from the underlying modify call must be collected explicitly with `collect`. function _decreaseLiquidity( address owner, diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 6b029fd0..6dbce1dc 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -17,7 +17,7 @@ interface INonfungiblePositionManager { uint256 deadline, address recipient, bytes calldata hookData - ) external payable returns (uint256 tokenId, BalanceDelta delta); + ) external payable returns (BalanceDelta delta); // NOTE: more expensive since LiquidityAmounts is used onchain // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); @@ -37,11 +37,10 @@ interface INonfungiblePositionManager { /// @param liquidity The amount of liquidity to remove /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of decreasing liquidity applied to user - /// @return thisDelta Corresponding balance changes as a result of decreasing liquidity applied to lpm + /// @return delta Corresponding balance changes as a result of decreasing liquidity applied to user (number of tokens credited to tokensOwed) function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external - returns (BalanceDelta delta, BalanceDelta thisDelta); + returns (BalanceDelta delta); /// @notice Burn a position and delete the tokenId /// @dev It removes liquidity and collects fees if the position is not empty @@ -75,4 +74,6 @@ interface INonfungiblePositionManager { /// @return token0Owed The amount of token0 owed /// @return token1Owed The amount of token1 owed function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed); + + function nextTokenId() external view returns (uint256); } diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 78ab07c0..1c1144d8 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -79,7 +79,8 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - (uint256 tokenId,) = lpm.mint(range, initialLiquidity, 0, address(this), ZERO_BYTES); + lpm.mint(range, initialLiquidity, 0, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector( @@ -100,7 +101,8 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { initialiLiquidity = bound(initialiLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); - (uint256 tokenId,) = lpm.mint(range, initialiLiquidity, 0, address(this), ZERO_BYTES); + lpm.mint(range, initialiLiquidity, 0, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; bytes[] memory data = new bytes[](2); data[0] = abi.encodeWithSelector( diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index e89ff68a..1a8071a6 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -116,8 +116,6 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) public { - uint256 tokenIdAlice; - uint256 tokenIdBob; params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity @@ -127,10 +125,12 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); - (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(bob); - (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees uint256 swapAmount = 0.01e18; @@ -158,8 +158,6 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) public { - uint256 tokenIdAlice; - uint256 tokenIdBob; params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity @@ -169,10 +167,12 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); - (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(bob); - (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // confirm the positions are same range (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); @@ -228,12 +228,12 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { uint256 liquidityAlice = 3000e18; uint256 liquidityBob = 1000e18; vm.prank(alice); - (uint256 tokenIdAlice, BalanceDelta lpDeltaAlice) = - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + BalanceDelta lpDeltaAlice = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(bob); - (uint256 tokenIdBob, BalanceDelta lpDeltaBob) = - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + BalanceDelta lpDeltaBob = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees uint256 swapAmount = 0.001e18; @@ -242,9 +242,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice decreases liquidity vm.prank(alice); - BalanceDelta aliceDelta; - BalanceDelta thisDelta; - (aliceDelta, thisDelta) = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); + BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); uint256 tolerance = 0.000000001 ether; @@ -261,8 +259,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // bob decreases half of his liquidity vm.prank(bob); - BalanceDelta bobDelta; - (bobDelta, thisDelta) = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); + BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); // lpm collects half of bobs principal // the fee amount has already been collected with alice's calls diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index fe2005e2..e25d85f7 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -103,14 +103,16 @@ contract GasTest is Test, Deployers, GasSnapshot { } function test_gas_increaseLiquidity_erc20() public { - (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, false); snapLastCall("increaseLiquidity_erc20"); } function test_gas_increaseLiquidity_erc6909() public { - (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, true); snapLastCall("increaseLiquidity_erc6909"); @@ -125,7 +127,8 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); @@ -159,11 +162,13 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -203,11 +208,13 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -230,14 +237,16 @@ contract GasTest is Test, Deployers, GasSnapshot { } function test_gas_decreaseLiquidity_erc20() public { - (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, false); snapLastCall("decreaseLiquidity_erc20"); } function test_gas_decreaseLiquidity_erc6909() public { - (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, true); snapLastCall("decreaseLiquidity_erc6909"); diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 1fa62382..2f6a8a7b 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -85,7 +85,8 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); @@ -134,7 +135,8 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); @@ -180,11 +182,13 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees uint256 swapAmount = 0.001e18; @@ -257,11 +261,13 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees uint256 swapAmount = 0.001e18; @@ -321,11 +327,13 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees uint256 swapAmount = 0.001e18; @@ -385,11 +393,13 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - (uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - (uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 3d59572b..5330b731 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -56,12 +56,11 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - (uint256 tokenId, BalanceDelta delta) = + BalanceDelta delta = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); - assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta)); @@ -247,8 +246,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - (BalanceDelta delta, BalanceDelta thisDelta) = - lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index cc401555..e118e062 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -20,13 +20,14 @@ contract LiquidityFuzzers is Fuzzers { ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); - (uint256 tokenId, BalanceDelta delta) = lpm.mint( + BalanceDelta delta = lpm.mint( LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}), uint256(params.liquidityDelta), block.timestamp, recipient, hookData ); + uint256 tokenId = lpm.nextTokenId() - 1; return (tokenId, params, delta); } } From 7db4e142b5bcdc929a50fa3f41f04d0bc8b58767 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:03:44 -0400 Subject: [PATCH 60/98] temp-dev-update (#135) * checkpointing * move decrease and collect to transient storage * remove returns since they are now saved to transient storage * draft: delta closing * wip * Sra/edits (#137) * consolidate using owner, update burn * fix: accrue deltas to caller in increase * Rip Out Vanilla (#138) * rip out vanilla and benchmark * fix gas benchmark * check posm is the locker before allowing access to external functions * restore execute tests * posm takes as 6909; remove legacy deadcode * restore tests * move helpers to the same file * fix: cleanup --------- Co-authored-by: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Co-authored-by: Sara Reynolds --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 176 ++++++------------ contracts/base/BaseLiquidityManagement.sol | 78 ++++---- .../interfaces/IBaseLiquidityManagement.sol | 3 + .../INonfungiblePositionManager.sol | 34 ++-- .../libraries/LiquidityDeltaAccounting.sol | 1 + contracts/libraries/TransientDemo.sol | 60 ------ .../libraries/TransientLiquidityDelta.sol | 108 +++++++++++ test/position-managers/Execute.t.sol | 25 ++- test/position-managers/FeeCollection.t.sol | 149 +++++++-------- test/position-managers/Gas.t.sol | 105 ++++++++--- .../position-managers/IncreaseLiquidity.t.sol | 95 +++++----- .../NonfungiblePositionManager.t.sol | 43 +++-- test/shared/LiquidityOperations.sol | 72 +++++++ test/shared/fuzz/LiquidityFuzzers.sol | 23 ++- 22 files changed, 568 insertions(+), 420 deletions(-) delete mode 100644 contracts/libraries/TransientDemo.sol create mode 100644 contracts/libraries/TransientLiquidityDelta.sol create mode 100644 test/shared/LiquidityOperations.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 9418d155..7c5efba1 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -262456 \ No newline at end of file +293336 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 17341d57..aad1fd07 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -194829 \ No newline at end of file +225695 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 51e59477..bfd89eca 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -282995 \ No newline at end of file +313875 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index f8eacac5..8335b197 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -180479 \ No newline at end of file +211756 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index cf69dc0a..043cac00 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -180491 \ No newline at end of file +211766 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 0a6e9003..031afb54 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -175213 \ No newline at end of file +196952 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 41b75c0b..55c77716 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -150802 \ No newline at end of file +196964 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 5d47c788..671b63ca 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -472424 \ No newline at end of file +493415 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index e98d9abd..ac34f27e 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -18,6 +18,7 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {TransientLiquidityDelta} from "./libraries/TransientLiquidityDelta.sol"; contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit { using CurrencyLibrary for Currency; @@ -27,6 +28,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit using StateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager; using SafeCast for uint256; + using TransientLiquidityDelta for Currency; /// @dev The ID of the next token that will be minted. Skips 0 uint256 public nextTokenId = 1; @@ -34,169 +36,97 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range) mapping(uint256 tokenId => TokenPosition position) public tokenPositions; + // TODO: We won't need this once we move to internal calls. + address internal msgSender; + + function _msgSenderInternal() internal view override returns (address) { + return msgSender; + } + constructor(IPoolManager _manager) BaseLiquidityManagement(_manager) ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} - function unlockAndExecute(bytes[] memory data) public returns (bytes memory) { - return manager.unlock(abi.encode(data)); + function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) + public + returns (int128[] memory returnData) + { + // TODO: This will be removed when we use internal calls. Otherwise we need to prevent calls to other code paths and prevent reentrancy or add a queue. + msgSender = msg.sender; + returnData = abi.decode(manager.unlock(abi.encode(data, currencies)), (int128[])); + msgSender = address(0); } function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { - bytes[] memory data = abi.decode(payload, (bytes[])); + (bytes[] memory data, Currency[] memory currencies) = abi.decode(payload, (bytes[], Currency[])); bool success; - bytes memory returnData; + for (uint256 i; i < data.length; i++) { - // TODO: bubble up the return - (success, returnData) = address(this).call(data[i]); + // TODO: Move to internal call and bubble up all call return data. + (success,) = address(this).call(data[i]); if (!success) revert("EXECUTE_FAILED"); } - // zeroOut(); - return returnData; + // close the final deltas + int128[] memory returnData = new int128[](currencies.length); + for (uint256 i; i < currencies.length; i++) { + returnData[i] = currencies[i].close(manager, _msgSenderInternal(), false); // TODO: support claims + currencies[i].close(manager, address(this), true); // position manager always takes 6909 + } + + return abi.encode(returnData); } - // NOTE: more gas efficient as LiquidityAmounts is used offchain - // TODO: deadline check function mint( LiquidityRange calldata range, uint256 liquidity, uint256 deadline, - address recipient, + address owner, bytes calldata hookData - ) public payable returns (BalanceDelta delta) { - // TODO: optimization, read/write manager.isUnlocked to avoid repeated external calls for batched execution - if (manager.isUnlocked()) { - BalanceDelta thisDelta; - (delta, thisDelta) = _increaseLiquidity(recipient, range, liquidity, hookData); - - // TODO: should be triggered by zeroOut in _execute... - _closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, recipient, false); - _closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1); - - // mint receipt token - uint256 tokenId; - _mint(recipient, (tokenId = nextTokenId++)); - tokenPositions[tokenId] = TokenPosition({owner: recipient, range: range}); - } else { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.mint.selector, range, liquidity, deadline, recipient, hookData); - bytes memory result = unlockAndExecute(data); - delta = abi.decode(result, (BalanceDelta)); - } - } + ) external payable checkDeadline(deadline) { + _increaseLiquidity(owner, range, liquidity, hookData); - // NOTE: more expensive since LiquidityAmounts is used onchain - // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { - // (uint160 sqrtPriceX96,,,) = manager.getSlot0(params.range.poolKey.toId()); - // (tokenId, delta) = mint( - // params.range, - // LiquidityAmounts.getLiquidityForAmounts( - // sqrtPriceX96, - // TickMath.getSqrtPriceAtTick(params.range.tickLower), - // TickMath.getSqrtPriceAtTick(params.range.tickUpper), - // params.amount0Desired, - // params.amount1Desired - // ), - // params.deadline, - // params.recipient, - // params.hookData - // ); - // require(params.amount0Min <= uint256(uint128(delta.amount0())), "INSUFFICIENT_AMOUNT0"); - // require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); - // } + // mint receipt token + uint256 tokenId; + _mint(owner, (tokenId = nextTokenId++)); + tokenPositions[tokenId] = TokenPosition({owner: owner, range: range}); + } function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external isAuthorizedForToken(tokenId) - returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - if (manager.isUnlocked()) { - BalanceDelta thisDelta; - (delta, thisDelta) = _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); - - // TODO: should be triggered by zeroOut in _execute... - _closeCallerDeltas( - delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims - ); - _closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1); - } else { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.increaseLiquidity.selector, tokenId, liquidity, hookData, claims); - bytes memory result = unlockAndExecute(data); - delta = abi.decode(result, (BalanceDelta)); - } + _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - public + external isAuthorizedForToken(tokenId) - returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - if (manager.isUnlocked()) { - BalanceDelta thisDelta; - (delta, thisDelta) = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); - _closeCallerDeltas( - delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims - ); - _closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1); - } else { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.decreaseLiquidity.selector, tokenId, liquidity, hookData, claims); - bytes memory result = unlockAndExecute(data); - delta = abi.decode(result, (BalanceDelta)); - } + _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } - function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) - external - isAuthorizedForToken(tokenId) - returns (BalanceDelta delta) - { - // TODO: Burn currently decreases and collects. However its done under different locks. - // Replace once we have the execute multicall. - // remove liquidity - TokenPosition storage tokenPosition = tokenPositions[tokenId]; - LiquidityRangeId rangeId = tokenPosition.range.toId(); - Position storage position = positions[msg.sender][rangeId]; - if (position.liquidity > 0) { - delta = decreaseLiquidity(tokenId, position.liquidity, hookData, claims); - } - - collect(tokenId, recipient, hookData, claims); - require(position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "NOT_EMPTY"); - delete positions[msg.sender][rangeId]; + function burn(uint256 tokenId) public isAuthorizedForToken(tokenId) { + // We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId. + TokenPosition memory tokenPos = tokenPositions[tokenId]; + // Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. + _validateBurn(tokenPos.owner, tokenPos.range); delete tokenPositions[tokenId]; - - // burn the token + // Burn the token. _burn(tokenId); } // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) - public - returns (BalanceDelta delta) - { + function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external { TokenPosition memory tokenPos = tokenPositions[tokenId]; - if (manager.isUnlocked()) { - BalanceDelta thisDelta; - (delta, thisDelta) = _collect(tokenPos.owner, tokenPos.range, hookData); - _closeCallerDeltas( - delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims - ); - _closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1); - } else { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.collect.selector, tokenId, recipient, hookData, claims); - bytes memory result = unlockAndExecute(data); - delta = abi.decode(result, (BalanceDelta)); - } + + _collect(recipient, tokenPos.owner, tokenPos.range, hookData); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { @@ -204,6 +134,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit return feesOwed(tokenPosition.owner, tokenPosition.range); } + // TODO: Bug - Positions are overrideable unless we can allow two of the same users to have distinct positions. function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override { TokenPosition storage tokenPosition = tokenPositions[tokenId]; LiquidityRangeId rangeId = tokenPosition.range.toId(); @@ -224,7 +155,12 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit } modifier isAuthorizedForToken(uint256 tokenId) { - require(msg.sender == address(this) || _isApprovedOrOwner(msg.sender, tokenId), "Not approved"); + require(_isApprovedOrOwner(_msgSenderInternal(), tokenId), "Not approved"); + _; + } + + modifier checkDeadline(uint256 deadline) { + if (block.timestamp > deadline) revert DeadlinePassed(); _; } } diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 530a94fc..a6bfaf0f 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -24,6 +24,7 @@ import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.s import {PositionLibrary} from "../libraries/Position.sol"; import {BalanceDeltaExtensionLibrary} from "../libraries/BalanceDeltaExtensionLibrary.sol"; import {LiquidityDeltaAccounting} from "../libraries/LiquidityDeltaAccounting.sol"; +import {TransientLiquidityDelta} from "../libraries/TransientLiquidityDelta.sol"; import "forge-std/console2.sol"; @@ -40,29 +41,15 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb using PositionLibrary for IBaseLiquidityManagement.Position; using BalanceDeltaExtensionLibrary for BalanceDelta; using LiquidityDeltaAccounting for BalanceDelta; + using TransientLiquidityDelta for BalanceDelta; + using TransientLiquidityDelta for Currency; + using TransientLiquidityDelta for address; mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; constructor(IPoolManager _manager) ImmutableState(_manager) {} - function _closeCallerDeltas( - BalanceDelta callerDeltas, - Currency currency0, - Currency currency1, - address owner, - bool claims - ) internal { - int128 callerDelta0 = callerDeltas.amount0(); - int128 callerDelta1 = callerDeltas.amount1(); - // On liquidity increase, the deltas should never be > 0. - // We always 0 out a caller positive delta because it is instead accounted for in position.tokensOwed. - - if (callerDelta0 < 0) currency0.settle(manager, owner, uint256(int256(-callerDelta0)), claims); - else if (callerDelta0 > 0) currency0.take(manager, owner, uint128(callerDelta0), claims); - - if (callerDelta1 < 0) currency1.settle(manager, owner, uint256(int256(-callerDelta1)), claims); - else if (callerDelta1 > 0) currency1.take(manager, owner, uint128(callerDelta1), claims); - } + function _msgSenderInternal() internal virtual returns (address); function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) internal @@ -87,7 +74,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb LiquidityRange memory range, uint256 liquidityToAdd, bytes memory hookData - ) internal returns (BalanceDelta callerDelta, BalanceDelta thisDelta) { + ) internal { // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData); @@ -100,7 +87,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb // Calculate the portion of the liquidityDelta that is attributable to the caller. // We must account for fees that might be owed to other users on the same range. - (callerDelta, thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); + (BalanceDelta callerDelta, BalanceDelta thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); // Update position storage, flushing the callerDelta value to tokensOwed first if necessary. // If callerDelta > 0, then even after investing callerFeesAccrued, the caller still has some amount to collect that were not added into the position so they are accounted to tokensOwed and removed from the final callerDelta returned. @@ -115,30 +102,20 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb _moveCallerDeltaToTokensOwed(false, tokensOwed, callerDelta, thisDelta); } + // Accrue all deltas to the caller. + callerDelta.flush(_msgSenderInternal(), range.poolKey.currency0, range.poolKey.currency1); + thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); + position.addTokensOwed(tokensOwed); position.addLiquidity(liquidityToAdd); } - // When chaining many actions, this should be called at the very end to close out any open deltas owed to or by this contract for other users on the same range. - // This is safe because any amounts the caller should not pay or take have already been accounted for in closeCallerDeltas. - function _closeThisDeltas(BalanceDelta delta, Currency currency0, Currency currency1) internal { - int128 delta0 = delta.amount0(); - int128 delta1 = delta.amount1(); - - // Mint a receipt for the tokens owed to this address. - if (delta0 > 0) currency0.take(manager, address(this), uint128(delta0), true); - if (delta1 > 0) currency1.take(manager, address(this), uint128(delta1), true); - // Burn the receipt for tokens owed to this address. - if (delta0 < 0) currency0.settle(manager, address(this), uint256(int256(-delta0)), true); - if (delta1 < 0) currency1.settle(manager, address(this), uint256(int256(-delta1)), true); - } - function _moveCallerDeltaToTokensOwed( bool useAmount0, BalanceDelta tokensOwed, BalanceDelta callerDelta, BalanceDelta thisDelta - ) private returns (BalanceDelta, BalanceDelta, BalanceDelta) { + ) private pure returns (BalanceDelta, BalanceDelta, BalanceDelta) { // credit the excess tokens to the position's tokensOwed tokensOwed = useAmount0 ? tokensOwed.setAmount0(callerDelta.amount0()) : tokensOwed.setAmount1(callerDelta.amount1()); @@ -159,7 +136,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb LiquidityRange memory range, uint256 liquidityToRemove, bytes memory hookData - ) internal returns (BalanceDelta callerDelta, BalanceDelta thisDelta) { + ) internal { (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); @@ -170,7 +147,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb (BalanceDelta callerFeesAccrued) = _updateFeeGrowth(range, position); // Account for fees accrued to other users on the same range. - (callerDelta, thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); + (BalanceDelta callerDelta, BalanceDelta thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); BalanceDelta tokensOwed; @@ -184,15 +161,17 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb (tokensOwed, callerDelta, thisDelta) = _moveCallerDeltaToTokensOwed(false, tokensOwed, callerDelta, thisDelta); } + callerDelta.flush(owner, range.poolKey.currency0, range.poolKey.currency1); + thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.addTokensOwed(tokensOwed); position.subtractLiquidity(liquidityToRemove); } - function _collect(address owner, LiquidityRange memory range, bytes memory hookData) - internal - returns (BalanceDelta callerDelta, BalanceDelta thisDelta) - { + // The recipient may not be the original owner. + function _collect(address recipient, address owner, LiquidityRange memory range, bytes memory hookData) internal { + BalanceDelta callerDelta; + BalanceDelta thisDelta; Position storage position = positions[owner][range.toId()]; // Only call modify if there is still liquidty in this position. @@ -217,9 +196,26 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb callerDelta = callerDelta + tokensOwed; thisDelta = thisDelta - tokensOwed; + if (recipient == _msgSenderInternal()) { + callerDelta.flush(recipient, range.poolKey.currency0, range.poolKey.currency1); + } else { + TransientLiquidityDelta.closeDelta( + manager, recipient, range.poolKey.currency0, range.poolKey.currency1, false + ); // TODO: allow recipient to receive claims, and add test! + } + thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); + position.clearTokensOwed(); } + function _validateBurn(address owner, LiquidityRange memory range) internal { + LiquidityRangeId rangeId = range.toId(); + Position storage position = positions[owner][rangeId]; + if (position.liquidity > 0) revert PositionMustBeEmpty(); + if (position.tokensOwed0 != 0 && position.tokensOwed1 != 0) revert TokensMustBeCollected(); + delete positions[owner][rangeId]; + } + function _updateFeeGrowth(LiquidityRange memory range, Position storage position) internal returns (BalanceDelta callerFeesAccrued) diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol index b5c07dd8..6bcb6e5b 100644 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -6,6 +6,9 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityRange, LiquidityRangeId} from "../types/LiquidityRange.sol"; interface IBaseLiquidityManagement { + error PositionMustBeEmpty(); + error TokensMustBeCollected(); + // details about the liquidity position struct Position { // the nonce for permits diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 6dbce1dc..62acbfd9 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; interface INonfungiblePositionManager { @@ -10,6 +11,9 @@ interface INonfungiblePositionManager { LiquidityRange range; } + error MustBeUnlockedByThisContract(); + error DeadlinePassed(); + // NOTE: more gas efficient as LiquidityAmounts is used offchain function mint( LiquidityRange calldata position, @@ -17,7 +21,7 @@ interface INonfungiblePositionManager { uint256 deadline, address recipient, bytes calldata hookData - ) external payable returns (BalanceDelta delta); + ) external payable; // NOTE: more expensive since LiquidityAmounts is used onchain // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); @@ -27,31 +31,20 @@ interface INonfungiblePositionManager { /// @param liquidity The amount of liquidity to add /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the liquidity increase uses ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of increasing liquidity - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - returns (BalanceDelta delta); + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; /// @notice Decrease liquidity for an existing position /// @param tokenId The ID of the position /// @param liquidity The amount of liquidity to remove /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of decreasing liquidity applied to user (number of tokens credited to tokensOwed) - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - returns (BalanceDelta delta); + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; + // TODO Can decide if we want burn to auto encode a decrease/collect. /// @notice Burn a position and delete the tokenId - /// @dev It removes liquidity and collects fees if the position is not empty + /// @dev It enforces that there is no open liquidity or tokens to be collected /// @param tokenId The ID of the position - /// @param recipient The address to send the collected tokens to - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of burning the position - function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) - external - returns (BalanceDelta delta); + function burn(uint256 tokenId) external; // TODO: in v3, we can partially collect fees, but what was the usecase here? /// @notice Collect fees for a position @@ -59,15 +52,12 @@ interface INonfungiblePositionManager { /// @param recipient The address to send the collected tokens to /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the collected fees are sent as ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of collecting fees - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) - external - returns (BalanceDelta delta); + function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external; /// @notice Execute a batch of external calls by unlocking the PoolManager /// @param data an array of abi.encodeWithSelector(, ) for each call /// @return delta The final delta changes of the caller - function unlockAndExecute(bytes[] memory data) external returns (bytes memory); + function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) external returns (int128[] memory); /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees /// @param tokenId The ID of the position diff --git a/contracts/libraries/LiquidityDeltaAccounting.sol b/contracts/libraries/LiquidityDeltaAccounting.sol index b6c99b10..9c82d1c9 100644 --- a/contracts/libraries/LiquidityDeltaAccounting.sol +++ b/contracts/libraries/LiquidityDeltaAccounting.sol @@ -8,6 +8,7 @@ import "forge-std/console2.sol"; library LiquidityDeltaAccounting { function split(BalanceDelta liquidityDelta, BalanceDelta callerFeesAccrued, BalanceDelta totalFeesAccrued) internal + pure returns (BalanceDelta callerDelta, BalanceDelta thisDelta) { if (totalFeesAccrued == callerFeesAccrued) { diff --git a/contracts/libraries/TransientDemo.sol b/contracts/libraries/TransientDemo.sol deleted file mode 100644 index 2845878d..00000000 --- a/contracts/libraries/TransientDemo.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.24; - -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; - -/// @title a library to store callers' currency deltas in transient storage -/// @dev this library implements the equivalent of a mapping, as transient storage can only be accessed in assembly -library TransientLiquidityDelta { - /// @notice calculates which storage slot a delta should be stored in for a given caller and currency - function _computeSlot(address caller_, Currency currency) internal pure returns (bytes32 hashSlot) { - assembly { - mstore(0, caller_) - mstore(32, currency) - hashSlot := keccak256(0, 64) - } - } - - /// @notice Flush a BalanceDelta into transient storage for a given holder - function flush(BalanceDelta delta, address holder, Currency currency0, Currency currency1) internal { - setDelta(currency0, holder, delta.amount0()); - setDelta(currency1, holder, delta.amount1()); - } - - function addDelta(Currency currency, address caller, int128 delta) internal { - bytes32 hashSlot = _computeSlot(caller, currency); - assembly { - let oldValue := tload(hashSlot) - let newValue := add(oldValue, delta) - tstore(hashSlot, newValue) - } - } - - function subDelta(Currency currency, address caller, int128 delta) internal { - bytes32 hashSlot = _computeSlot(caller, currency); - assembly { - let oldValue := tload(hashSlot) - let newValue := sub(oldValue, delta) - tstore(hashSlot, newValue) - } - } - - /// @notice sets a new currency delta for a given caller and currency - function setDelta(Currency currency, address caller, int256 delta) internal { - bytes32 hashSlot = _computeSlot(caller, currency); - - assembly { - tstore(hashSlot, delta) - } - } - - /// @notice gets a new currency delta for a given caller and currency - function getDelta(Currency currency, address caller) internal view returns (int256 delta) { - bytes32 hashSlot = _computeSlot(caller, currency); - - assembly { - delta := tload(hashSlot) - } - } -} diff --git a/contracts/libraries/TransientLiquidityDelta.sol b/contracts/libraries/TransientLiquidityDelta.sol new file mode 100644 index 00000000..df7608ba --- /dev/null +++ b/contracts/libraries/TransientLiquidityDelta.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; + +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; + +import "forge-std/console2.sol"; + +/// @title a library to store callers' currency deltas in transient storage +/// @dev this library implements the equivalent of a mapping, as transient storage can only be accessed in assembly +library TransientLiquidityDelta { + using CurrencySettleTake for Currency; + using TransientStateLibrary for IPoolManager; + + /// @notice calculates which storage slot a delta should be stored in for a given caller and currency + function _computeSlot(address caller_, Currency currency) internal pure returns (bytes32 hashSlot) { + assembly { + mstore(0, caller_) + mstore(32, currency) + hashSlot := keccak256(0, 64) + } + } + + /// @notice Flush a BalanceDelta into transient storage for a given holder + function flush(BalanceDelta delta, address holder, Currency currency0, Currency currency1) internal { + addDelta(currency0, holder, delta.amount0()); + addDelta(currency1, holder, delta.amount1()); + } + + function addDelta(Currency currency, address caller, int128 delta) internal { + bytes32 hashSlot = _computeSlot(caller, currency); + assembly { + let oldValue := tload(hashSlot) + let newValue := add(oldValue, delta) + tstore(hashSlot, newValue) + } + } + + function subtractDelta(Currency currency, address caller, int128 delta) internal { + bytes32 hashSlot = _computeSlot(caller, currency); + assembly { + let oldValue := tload(hashSlot) + let newValue := sub(oldValue, delta) + tstore(hashSlot, newValue) + } + } + + function close(Currency currency, IPoolManager manager, address holder, bool claims) + internal + returns (int128 delta) + { + // getDelta(currency, holder); + bytes32 hashSlot = _computeSlot(holder, currency); + assembly { + delta := tload(hashSlot) + } + + if (delta < 0) { + currency.settle(manager, holder, uint256(-int256(delta)), claims); + } else { + currency.take(manager, holder, uint256(int256(delta)), claims); + } + + // setDelta(0); + assembly { + tstore(hashSlot, 0) + } + } + + function closeDelta(IPoolManager manager, address holder, Currency currency0, Currency currency1, bool claims) + internal + { + close(currency0, manager, holder, claims); + close(currency1, manager, holder, claims); + } + + function getBalanceDelta(address holder, Currency currency0, Currency currency1) + internal + view + returns (BalanceDelta delta) + { + delta = toBalanceDelta(getDelta(currency0, holder), getDelta(currency1, holder)); + } + + /// Copied from v4-core/src/libraries/CurrencyDelta.sol: + /// @notice sets a new currency delta for a given caller and currency + function setDelta(Currency currency, address caller, int128 delta) internal { + bytes32 hashSlot = _computeSlot(caller, currency); + + assembly { + tstore(hashSlot, delta) + } + } + + /// @notice gets a new currency delta for a given caller and currency + // TODO: is returning 128 bits safe? + function getDelta(Currency currency, address caller) internal view returns (int128 delta) { + bytes32 hashSlot = _computeSlot(caller, currency); + + assembly { + delta := tload(hashSlot) + } + } +} diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 1c1144d8..b3f9f393 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -27,15 +27,15 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; -contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using SafeCast for uint256; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); @@ -79,7 +79,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - lpm.mint(range, initialLiquidity, 0, address(this), ZERO_BYTES); + _mint(range, initialLiquidity, block.timestamp, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; bytes[] memory data = new bytes[](1); @@ -87,7 +87,10 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false ); - lpm.unlockAndExecute(data); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.modifyLiquidities(data, currencies); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialLiquidity + liquidityToAdd); @@ -101,7 +104,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { initialiLiquidity = bound(initialiLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); - lpm.mint(range, initialiLiquidity, 0, address(this), ZERO_BYTES); + _mint(range, initialiLiquidity, block.timestamp, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; bytes[] memory data = new bytes[](2); @@ -112,7 +115,10 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd2, ZERO_BYTES, false ); - lpm.unlockAndExecute(data); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.modifyLiquidities(data, currencies); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialiLiquidity + liquidityToAdd + liquidityToAdd2); @@ -137,7 +143,10 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false ); - lpm.unlockAndExecute(data); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.modifyLiquidities(data, currencies); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, intialLiquidity + liquidityToAdd); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 1a8071a6..fe85b408 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -10,7 +10,7 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; @@ -24,22 +24,19 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; -contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18) uint256 FEE_WAD; @@ -69,25 +66,26 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { vm.stopPrank(); } - function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { - params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + // TODO: we dont accept collecting fees as 6909 yet + // function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { + // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + // uint256 tokenId; + // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, -int256(swapAmount), ZERO_BYTES); + // // swap to create fees + // uint256 swapAmount = 0.01e18; + // swap(key, false, -int256(swapAmount), ZERO_BYTES); - // collect fees - BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, true); + // // collect fees + // BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES, true); - assertEq(delta.amount0(), 0); + // assertEq(delta.amount0(), 0); - assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); + // assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); - assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); - } + // assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); + // } function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); @@ -102,7 +100,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // collect fees uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, false); + BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES, false); assertEq(delta.amount0(), 0); @@ -112,48 +110,49 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); } + // TODO: we dont accept collecting fees as 6909 yet // two users with the same range; one user cannot collect the other's fees - function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) - public - { - params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); - vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); - - LiquidityRange memory range = - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - vm.prank(alice); - lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - vm.prank(bob); - lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, -int256(swapAmount), ZERO_BYTES); - - // alice collects only her fees - vm.prank(alice); - BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, true); - assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); - assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); - assertTrue(delta.amount1() != 0); - - // bob collects only his fees - vm.prank(bob); - delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, true); - assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); - assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); - assertTrue(delta.amount1() != 0); - - // position manager holds no fees now - assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); - assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); - } + // function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) + // public + // { + // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + // params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + // liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + // LiquidityRange memory range = + // LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + // vm.prank(alice); + // _mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + // uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // vm.prank(bob); + // _mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + // uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // // swap to create fees + // uint256 swapAmount = 0.01e18; + // swap(key, false, -int256(swapAmount), ZERO_BYTES); + + // // alice collects only her fees + // vm.prank(alice); + // BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES, true); + // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); + // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); + // assertTrue(delta.amount1() != 0); + + // // bob collects only his fees + // vm.prank(bob); + // delta = _collect(tokenIdBob, bob, ZERO_BYTES, true); + // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); + // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); + // assertTrue(delta.amount1() != 0); + + // // position manager holds no fees now + // assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); + // assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); + // } function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) public @@ -167,11 +166,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); - lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(bob); - lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // confirm the positions are same range @@ -187,8 +186,9 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice collects only her fees uint256 balance0AliceBefore = currency0.balanceOf(alice); uint256 balance1AliceBefore = currency1.balanceOf(alice); - vm.prank(alice); - BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); + vm.startPrank(alice); + BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AliceAfter = currency0.balanceOf(alice); uint256 balance1AliceAfter = currency1.balanceOf(alice); @@ -199,8 +199,9 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // bob collects only his fees uint256 balance0BobBefore = currency0.balanceOf(bob); uint256 balance1BobBefore = currency1.balanceOf(bob); - vm.prank(bob); - delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.startPrank(bob); + delta = _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0BobAfter = currency0.balanceOf(bob); uint256 balance1BobAfter = currency1.balanceOf(bob); @@ -228,11 +229,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { uint256 liquidityAlice = 3000e18; uint256 liquidityBob = 1000e18; vm.prank(alice); - BalanceDelta lpDeltaAlice = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + BalanceDelta lpDeltaAlice = _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(bob); - BalanceDelta lpDeltaBob = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + BalanceDelta lpDeltaBob = _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees @@ -242,7 +243,8 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice decreases liquidity vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); + lpm.approve(address(this), tokenIdAlice); + _decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); uint256 tolerance = 0.000000001 ether; @@ -259,7 +261,8 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // bob decreases half of his liquidity vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); + lpm.approve(address(this), tokenIdBob); + _decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); // lpm collects half of bobs principal // the fee amount has already been collected with alice's calls diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index e25d85f7..81616e2e 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -23,23 +23,20 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; -contract GasTest is Test, Deployers, GasSnapshot { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) uint256 FEE_WAD; @@ -98,23 +95,42 @@ contract GasTest is Test, Deployers, GasSnapshot { // } function test_gas_mintWithLiquidity() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector( + lpm.mint.selector, range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES + ); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.modifyLiquidities(calls, currencies); snapLastCall("mintWithLiquidity"); } function test_gas_increaseLiquidity_erc20() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, false); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.modifyLiquidities(calls, currencies); snapLastCall("increaseLiquidity_erc20"); } function test_gas_increaseLiquidity_erc6909() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, true); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.modifyLiquidities(calls, currencies); snapLastCall("increaseLiquidity_erc6909"); } @@ -127,12 +143,12 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // donate to create fees donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); @@ -149,8 +165,15 @@ contract GasTest is Test, Deployers, GasSnapshot { token1Owed ); + bytes[] memory calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + lpm.modifyLiquidities(calls, currencies); snapLastCall("autocompound_exactUnclaimedFees"); } @@ -162,20 +185,26 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); // bob collects fees so some of alice's fees are now cached + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenIdBob, bob, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + lpm.modifyLiquidities(calls, currencies); // donate to create more fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -193,8 +222,15 @@ contract GasTest is Test, Deployers, GasSnapshot { newToken1Owed ); + calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + lpm.modifyLiquidities(calls, currencies); snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); } } @@ -208,12 +244,12 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees @@ -231,24 +267,43 @@ contract GasTest is Test, Deployers, GasSnapshot { token1Owed / 2 ); + bytes[] memory calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + lpm.modifyLiquidities(calls, currencies); snapLastCall("autocompound_excessFeesCredit"); } function test_gas_decreaseLiquidity_erc20() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, false); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.modifyLiquidities(calls, currencies); snapLastCall("decreaseLiquidity_erc20"); } function test_gas_decreaseLiquidity_erc6909() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, true); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.modifyLiquidities(calls, currencies); snapLastCall("decreaseLiquidity_erc6909"); } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 2f6a8a7b..39a6e329 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -10,7 +10,7 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; @@ -25,23 +25,20 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) uint256 FEE_WAD; @@ -85,12 +82,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // swap to create fees uint256 swapAmount = 0.001e18; @@ -112,8 +109,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.startPrank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); // alice did not spend any tokens assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); @@ -135,12 +133,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // donate to create fees donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); @@ -160,8 +158,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.startPrank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); // alice did not spend any tokens assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); @@ -182,12 +181,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees @@ -207,16 +206,18 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { token1Owed / 2 ); - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.startPrank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); } { // bob collects his fees uint256 balance0BeforeBob = currency0.balanceOf(bob); uint256 balance1BeforeBob = currency1.balanceOf(bob); - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.startPrank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterBob = currency0.balanceOf(bob); uint256 balance1AfterBob = currency1.balanceOf(bob); assertApproxEqAbs( @@ -235,8 +236,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice collects her fees, which should be about half of the fees uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); - lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); + vm.startPrank(alice); + _collect(tokenIdAlice, alice, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); assertApproxEqAbs( @@ -261,12 +263,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees @@ -288,8 +290,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.startPrank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); @@ -301,8 +304,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // bob collects his fees uint256 balance0BeforeBob = currency0.balanceOf(bob); uint256 balance1BeforeBob = currency1.balanceOf(bob); - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.startPrank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterBob = currency0.balanceOf(bob); uint256 balance1AfterBob = currency1.balanceOf(bob); assertApproxEqAbs( @@ -327,12 +331,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees @@ -343,8 +347,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); // bob collects fees so some of alice's fees are now cached - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.startPrank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); // swap to create more fees swap(key, true, -int256(swapAmount), ZERO_BYTES); @@ -369,8 +374,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { newToken1Owed ); - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.startPrank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); } // alice did not spend any tokens @@ -393,12 +399,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees @@ -407,8 +413,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); // bob collects fees so some of alice's fees are now cached - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.startPrank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); // donate to create more fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -432,8 +439,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { newToken1Owed ); - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.startPrank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); } // alice did not spend any tokens @@ -449,8 +457,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { assertApproxEqAbs(token0Owed, 5e18, 1 wei); assertApproxEqAbs(token1Owed, 5e18, 1 wei); - vm.prank(bob); - BalanceDelta result = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.startPrank(bob); + BalanceDelta result = _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); assertApproxEqAbs(result.amount0(), 5e18, 1 wei); assertApproxEqAbs(result.amount1(), 5e18, 1 wei); } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 5330b731..f652bc93 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -10,7 +10,7 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; @@ -24,19 +24,16 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; -contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - function setUp() public { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); @@ -56,8 +53,17 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = - lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES); + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector( + lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES + ); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.modifyLiquidities(calls, currencies); + BalanceDelta delta = toBalanceDelta(result[0], result[1]); + uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); @@ -215,13 +221,24 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // burn liquidity uint256 balance0BeforeBurn = currency0.balanceOfSelf(); uint256 balance1BeforeBurn = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.burn(tokenId, address(this), ZERO_BYTES, false); + // TODO, encode this under one call + BalanceDelta deltaDecrease = _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); + BalanceDelta deltaCollect = _collect(tokenId, address(this), ZERO_BYTES, false); + lpm.burn(tokenId); (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, 0); // TODO: slightly off by 1 bip (0.0001%) - assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(delta.amount0())), 0.0001e18); - assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(delta.amount1())), 0.0001e18); + assertApproxEqRel( + currency0.balanceOfSelf(), + balance0BeforeBurn + uint256(uint128(deltaDecrease.amount0())) + uint256(uint128(deltaCollect.amount0())), + 0.0001e18 + ); + assertApproxEqRel( + currency1.balanceOfSelf(), + balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1())) + uint256(uint128(deltaCollect.amount1())), + 0.0001e18 + ); // OZ 721 will revert if the token does not exist vm.expectRevert(); @@ -246,7 +263,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + BalanceDelta delta = _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol new file mode 100644 index 00000000..38867ea9 --- /dev/null +++ b/test/shared/LiquidityOperations.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; + +contract LiquidityOperations { + NonfungiblePositionManager lpm; + + function _mint( + LiquidityRange memory _range, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes memory hookData + ) internal returns (BalanceDelta) { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + int128[] memory result = lpm.modifyLiquidities(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); + + (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + lpm.modifyLiquidities(calls, currencies); + } + + function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); + + (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + int128[] memory result = lpm.modifyLiquidities(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); + + (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + int128[] memory result = lpm.modifyLiquidities(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } +} diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index e118e062..fd22c3b2 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.24; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; @@ -20,13 +21,21 @@ contract LiquidityFuzzers is Fuzzers { ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); - BalanceDelta delta = lpm.mint( - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}), - uint256(params.liquidityDelta), - block.timestamp, - recipient, - hookData + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector( + lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData ); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = key.currency0; + currencies[1] = key.currency1; + + int128[] memory result = lpm.modifyLiquidities(calls, currencies); + BalanceDelta delta = toBalanceDelta(result[0], result[1]); + uint256 tokenId = lpm.nextTokenId() - 1; return (tokenId, params, delta); } From a565e0f4768ce8ff756610f0a81cead36d09777a Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:05:33 -0400 Subject: [PATCH 61/98] using internal calls, first pass (#143) * using internal calls, first pass * fix gas tests * fix execute test * fix tests * edit mint gas test * fix mint test * fix warnings --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mint.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 1 - contracts/NonfungiblePositionManager.sol | 117 +++++++---- contracts/base/BaseLiquidityManagement.sol | 24 ++- contracts/base/SelfPermit.sol | 2 +- .../INonfungiblePositionManager.sol | 56 ++--- test/position-managers/Execute.t.sol | 58 +++--- test/position-managers/FeeCollection.t.sol | 8 +- test/position-managers/Gas.t.sol | 92 ++++---- .../position-managers/IncreaseLiquidity.t.sol | 6 +- .../NonfungiblePositionManager.t.sol | 196 +++++++++--------- test/shared/LiquidityOperations.sol | 43 ++-- test/shared/fuzz/LiquidityFuzzers.sol | 18 +- test/utils/Planner.sol | 31 +++ 21 files changed, 353 insertions(+), 315 deletions(-) delete mode 100644 .forge-snapshots/mintWithLiquidity.snap create mode 100644 test/utils/Planner.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 7c5efba1..021404a1 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -293336 \ No newline at end of file +291244 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index aad1fd07..10b683f2 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -225695 \ No newline at end of file +223603 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index bfd89eca..3aa28ddf 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -313875 \ No newline at end of file +311783 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 8335b197..c2b3a62d 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -211756 \ No newline at end of file +209314 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 043cac00..a164d000 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -211766 \ No newline at end of file +209326 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 031afb54..a2041485 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -196952 \ No newline at end of file +194862 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 55c77716..553b43e9 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -196964 \ No newline at end of file +194874 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index 5d250ba5..b0807c1b 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -422785 \ No newline at end of file +493163 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap deleted file mode 100644 index 671b63ca..00000000 --- a/.forge-snapshots/mintWithLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -493415 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index ac34f27e..fb12aca9 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {ERC721Permit} from "./base/ERC721Permit.sol"; -import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -20,6 +20,8 @@ import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientSta import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {TransientLiquidityDelta} from "./libraries/TransientLiquidityDelta.sol"; +import "forge-std/console2.sol"; + contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; @@ -36,83 +38,105 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range) mapping(uint256 tokenId => TokenPosition position) public tokenPositions; - // TODO: We won't need this once we move to internal calls. - address internal msgSender; - - function _msgSenderInternal() internal view override returns (address) { - return msgSender; - } - constructor(IPoolManager _manager) BaseLiquidityManagement(_manager) ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} - function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) - public - returns (int128[] memory returnData) - { - // TODO: This will be removed when we use internal calls. Otherwise we need to prevent calls to other code paths and prevent reentrancy or add a queue. - msgSender = msg.sender; - returnData = abi.decode(manager.unlock(abi.encode(data, currencies)), (int128[])); - msgSender = address(0); + /// @param unlockData is an encoding of actions, params, and currencies + /// @return returnData is the endocing of each actions return information + function modifyLiquidities(bytes calldata unlockData) public returns (bytes[] memory) { + // TODO: Edit the encoding/decoding. + return abi.decode(manager.unlock(abi.encode(unlockData, msg.sender)), (bytes[])); } function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { - (bytes[] memory data, Currency[] memory currencies) = abi.decode(payload, (bytes[], Currency[])); + // TODO: Fix double encode/decode + (bytes memory unlockData, address sender) = abi.decode(payload, (bytes, address)); - bool success; + (Actions[] memory actions, bytes[] memory params, Currency[] memory currencies) = + abi.decode(unlockData, (Actions[], bytes[], Currency[])); - for (uint256 i; i < data.length; i++) { - // TODO: Move to internal call and bubble up all call return data. - (success,) = address(this).call(data[i]); - if (!success) revert("EXECUTE_FAILED"); - } + bytes[] memory returnData = _dispatch(actions, params, sender); - // close the final deltas - int128[] memory returnData = new int128[](currencies.length); for (uint256 i; i < currencies.length; i++) { - returnData[i] = currencies[i].close(manager, _msgSenderInternal(), false); // TODO: support claims + currencies[i].close(manager, sender, false); // TODO: support claims currencies[i].close(manager, address(this), true); // position manager always takes 6909 } return abi.encode(returnData); } + function _dispatch(Actions[] memory actions, bytes[] memory params, address sender) + internal + returns (bytes[] memory returnData) + { + if (actions.length != params.length) revert MismatchedLengths(); + + returnData = new bytes[](actions.length); + for (uint256 i; i < actions.length; i++) { + if (actions[i] == Actions.INCREASE) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, uint256, bytes, bool)); + returnData[i] = abi.encode(increaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + } else if (actions[i] == Actions.DECREASE) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, uint256, bytes, bool)); + returnData[i] = abi.encode(decreaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + } else if (actions[i] == Actions.MINT) { + (LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, bytes memory hookData) + = abi.decode(params[i], (LiquidityRange, uint256, uint256, address, bytes)); + (BalanceDelta delta, uint256 tokenId) = mint(range, liquidity, deadline, owner, hookData, sender); + returnData[i] = abi.encode(delta, tokenId); + } else if (actions[i] == Actions.BURN) { + (uint256 tokenId) = abi.decode(params[i], (uint256)); + burn(tokenId, sender); + } else if (actions[i] == Actions.COLLECT) { + (uint256 tokenId, address recipient, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, address, bytes, bool)); + returnData[i] = abi.encode(collect(tokenId, recipient, hookData, claims, sender)); + } else { + revert UnsupportedAction(); + } + } + } + function mint( - LiquidityRange calldata range, + LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, - bytes calldata hookData - ) external payable checkDeadline(deadline) { - _increaseLiquidity(owner, range, liquidity, hookData); + bytes memory hookData, + address sender + ) internal checkDeadline(deadline) returns (BalanceDelta delta, uint256 tokenId) { + delta = _increaseLiquidity(owner, range, liquidity, hookData, sender); // mint receipt token - uint256 tokenId; _mint(owner, (tokenId = nextTokenId++)); tokenPositions[tokenId] = TokenPosition({owner: owner, range: range}); } - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - isAuthorizedForToken(tokenId) + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + delta = _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, sender); } - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - isAuthorizedForToken(tokenId) + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + delta = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } - function burn(uint256 tokenId) public isAuthorizedForToken(tokenId) { + function burn(uint256 tokenId, address sender) internal isAuthorizedForToken(tokenId, sender) { // We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId. TokenPosition memory tokenPos = tokenPositions[tokenId]; // Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. @@ -122,11 +146,14 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit _burn(tokenId); } - // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external { + function collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) + { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _collect(recipient, tokenPos.owner, tokenPos.range, hookData); + delta = _collect(recipient, tokenPos.owner, tokenPos.range, hookData, sender); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { @@ -154,8 +181,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit return uint256(positions[tokenPosition.owner][tokenPosition.range.toId()].nonce++); } - modifier isAuthorizedForToken(uint256 tokenId) { - require(_isApprovedOrOwner(_msgSenderInternal(), tokenId), "Not approved"); + modifier isAuthorizedForToken(uint256 tokenId, address sender) { + require(_isApprovedOrOwner(sender, tokenId), "Not approved"); _; } diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index a6bfaf0f..a72cbad3 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -49,8 +49,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb constructor(IPoolManager _manager) ImmutableState(_manager) {} - function _msgSenderInternal() internal virtual returns (address); - function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) internal returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) @@ -73,8 +71,9 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb address owner, LiquidityRange memory range, uint256 liquidityToAdd, - bytes memory hookData - ) internal { + bytes memory hookData, + address sender + ) internal returns (BalanceDelta) { // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData); @@ -103,11 +102,12 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb } // Accrue all deltas to the caller. - callerDelta.flush(_msgSenderInternal(), range.poolKey.currency0, range.poolKey.currency1); + callerDelta.flush(sender, range.poolKey.currency0, range.poolKey.currency1); thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.addTokensOwed(tokensOwed); position.addLiquidity(liquidityToAdd); + return liquidityDelta; } function _moveCallerDeltaToTokensOwed( @@ -136,7 +136,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb LiquidityRange memory range, uint256 liquidityToRemove, bytes memory hookData - ) internal { + ) internal returns (BalanceDelta) { (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); @@ -166,10 +166,17 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb position.addTokensOwed(tokensOwed); position.subtractLiquidity(liquidityToRemove); + return liquidityDelta; } // The recipient may not be the original owner. - function _collect(address recipient, address owner, LiquidityRange memory range, bytes memory hookData) internal { + function _collect( + address recipient, + address owner, + LiquidityRange memory range, + bytes memory hookData, + address sender + ) internal returns (BalanceDelta) { BalanceDelta callerDelta; BalanceDelta thisDelta; Position storage position = positions[owner][range.toId()]; @@ -196,7 +203,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb callerDelta = callerDelta + tokensOwed; thisDelta = thisDelta - tokensOwed; - if (recipient == _msgSenderInternal()) { + if (recipient == sender) { callerDelta.flush(recipient, range.poolKey.currency0, range.poolKey.currency1); } else { TransientLiquidityDelta.closeDelta( @@ -206,6 +213,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.clearTokensOwed(); + return callerDelta; } function _validateBurn(address owner, LiquidityRange memory range) internal { diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol index 60ae6762..2f626496 100644 --- a/contracts/base/SelfPermit.sol +++ b/contracts/base/SelfPermit.sol @@ -10,7 +10,7 @@ import {ISelfPermit} from "../interfaces/ISelfPermit.sol"; /// @title Self Permit /// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route /// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function -/// that requires an approval in a single transaction. +/// that requires an approval in a single transactions. abstract contract SelfPermit is ISelfPermit { /// @inheritdoc ISelfPermit function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 62acbfd9..92e0f35a 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -4,8 +4,19 @@ pragma solidity ^0.8.24; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; +// TODO: ADD/REMOVE ACTIONS + +enum Actions { + MINT, + BURN, + COLLECT, + INCREASE, + DECREASE +} interface INonfungiblePositionManager { + error MismatchedLengths(); + struct TokenPosition { address owner; LiquidityRange range; @@ -13,51 +24,18 @@ interface INonfungiblePositionManager { error MustBeUnlockedByThisContract(); error DeadlinePassed(); + error UnsupportedAction(); - // NOTE: more gas efficient as LiquidityAmounts is used offchain - function mint( - LiquidityRange calldata position, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes calldata hookData - ) external payable; - - // NOTE: more expensive since LiquidityAmounts is used onchain - // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); - - /// @notice Increase liquidity for an existing position - /// @param tokenId The ID of the position - /// @param liquidity The amount of liquidity to add - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the liquidity increase uses ERC-6909 claim tokens - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; - - /// @notice Decrease liquidity for an existing position - /// @param tokenId The ID of the position - /// @param liquidity The amount of liquidity to remove - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; + /// @notice Batches many liquidity modification calls to pool manager + /// @param payload is an encoding of actions, params, and currencies + /// @return returnData is the endocing of each actions return information + function modifyLiquidities(bytes calldata payload) external returns (bytes[] memory); // TODO Can decide if we want burn to auto encode a decrease/collect. /// @notice Burn a position and delete the tokenId /// @dev It enforces that there is no open liquidity or tokens to be collected /// @param tokenId The ID of the position - function burn(uint256 tokenId) external; - - // TODO: in v3, we can partially collect fees, but what was the usecase here? - /// @notice Collect fees for a position - /// @param tokenId The ID of the position - /// @param recipient The address to send the collected tokens to - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the collected fees are sent as ERC-6909 claim tokens - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external; - - /// @notice Execute a batch of external calls by unlocking the PoolManager - /// @param data an array of abi.encodeWithSelector(, ) for each call - /// @return delta The final delta changes of the caller - function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) external returns (int128[] memory); + // function burn(uint256 tokenId) external; /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees /// @param tokenId The ID of the position diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index b3f9f393..1d2ccc77 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -21,13 +21,16 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; + +import "forge-std/console2.sol"; contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; @@ -35,6 +38,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using SafeCast for uint256; + using Planner for Planner.Plan; PoolId poolId; address alice = makeAddr("ALICE"); @@ -82,15 +86,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit _mint(range, initialLiquidity, block.timestamp, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false - ); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); + _increaseLiquidity(tokenId, liquidityToAdd, ZERO_BYTES, false); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialLiquidity + liquidityToAdd); @@ -107,49 +103,45 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit _mint(range, initialiLiquidity, block.timestamp, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false - ); - data[1] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd2, ZERO_BYTES, false - ); + Planner.Plan memory planner = Planner.init(); + + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES, false)); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd2, ZERO_BYTES, false)); + + console2.log("action"); + console2.log(uint8(planner.actions[0])); + console2.log(uint8(planner.actions[1])); Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialiLiquidity + liquidityToAdd + liquidityToAdd2); } // this case doesnt make sense in real world usage, so it doesnt have a cool name. but its a good test case - function test_execute_mintAndIncrease(uint256 intialLiquidity, uint256 liquidityToAdd) public { - intialLiquidity = bound(intialLiquidity, 1e18, 1000e18); + function test_execute_mintAndIncrease(uint256 initialLiquidity, uint256 liquidityToAdd) public { + initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); uint256 tokenId = 1; // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity - bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.mint.selector, - range, - intialLiquidity, - block.timestamp + 1, - address(this), - ZERO_BYTES - ); - data[1] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false + + Planner.Plan memory planner = Planner.init(); + + planner = planner.add( + Actions.MINT, abi.encode(range, initialLiquidity, block.timestamp + 1, address(this), ZERO_BYTES) ); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES, false)); Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, intialLiquidity + liquidityToAdd); + assertEq(liquidity, initialLiquidity + liquidityToAdd); } // rebalance: burn and mint diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index fe85b408..22d99a05 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -70,7 +70,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // // swap to create fees @@ -90,7 +90,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees @@ -102,11 +102,9 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance1Before = currency1.balanceOfSelf(); BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES, false); - assertEq(delta.amount0(), 0); - // express key.fee as wad (i.e. 3000 = 0.003e18) assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); - + assertEq(uint256(int256(delta.amount0())), currency0.balanceOfSelf() - balance0Before); assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); } diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 81616e2e..ce9989f0 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -20,16 +20,19 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; + using Planner for Planner.Plan; PoolId poolId; address alice = makeAddr("ALICE"); @@ -76,47 +79,29 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } - // function test_gas_mint() public { - // uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - // uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // snapStart("mint"); - // lpm.mint(params); - // snapLastCall(); - // } - - function test_gas_mintWithLiquidity() public { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES + function test_gas_mint() public { + Planner.Plan memory plan = Planner.init().add( + Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES) ); Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); - snapLastCall("mintWithLiquidity"); + lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies, currencies)); + snapLastCall("mint"); } function test_gas_increaseLiquidity_erc20() public { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("increaseLiquidity_erc20"); } @@ -124,13 +109,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, true)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("increaseLiquidity_erc6909"); } @@ -165,15 +151,15 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { token1Owed ); - bytes[] memory calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("autocompound_exactUnclaimedFees"); } @@ -197,14 +183,16 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); // bob collects fees so some of alice's fees are now cached - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenIdBob, bob, ZERO_BYTES, false); + + Planner.Plan memory planner = + Planner.init().add(Actions.COLLECT, abi.encode(tokenIdBob, bob, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(bob); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); // donate to create more fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -222,15 +210,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { newToken1Owed ); - calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); } } @@ -250,7 +237,6 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // bob provides liquidity vm.prank(bob); _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -267,15 +253,15 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { token1Owed / 2 ); - bytes[] memory calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("autocompound_excessFeesCredit"); } @@ -283,13 +269,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("decreaseLiquidity_erc20"); } @@ -297,13 +284,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Planner.Plan memory planner = + Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, true)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("decreaseLiquidity_erc6909"); } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 39a6e329..9f0bec24 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -27,6 +27,8 @@ import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import "forge-std/console2.sol"; + contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -327,7 +329,6 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity uint256 liquidityAlice = 3_000e18; uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; // alice provides liquidity vm.prank(alice); @@ -347,10 +348,10 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); // bob collects fees so some of alice's fees are now cached + vm.startPrank(bob); _collect(tokenIdBob, bob, ZERO_BYTES, false); vm.stopPrank(); - // swap to create more fees swap(key, true, -int256(swapAmount), ZERO_BYTES); swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back @@ -395,7 +396,6 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity uint256 liquidityAlice = 3_000e18; uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; // alice provides liquidity vm.prank(alice); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index f652bc93..959f5d4d 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -14,22 +14,27 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; + using Planner for Planner.Plan; PoolId poolId; address alice = makeAddr("ALICE"); @@ -46,117 +51,117 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); } + function test_modifyLiquidities_reverts_mismatchedLengths() public { + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.MINT, abi.encode("test")); + planner = planner.add(Actions.BURN, abi.encode("test")); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + bytes[] memory badParams = new bytes[](1); + + vm.expectRevert(INonfungiblePositionManager.MismatchedLengths.selector); + lpm.modifyLiquidities(abi.encode(planner.actions, badParams, currencies)); + } + function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + // liquidity is a uint + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) + ); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + + (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); + + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(tokenId), address(this)); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta)); + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())), "incorrect amount1"); + } + + function test_mint_exactTokenRatios() public { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0Desired, + amount1Desired ); + + LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - BalanceDelta delta = toBalanceDelta(result[0], result[1]); + + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) + ); + + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); + assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, uint256(params.liquidityDelta)); - assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0())), "incorrect amount0"); - assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); + assertEq(uint256(int256(-delta.amount0())), amount0Desired); + assertEq(uint256(int256(-delta.amount1())), amount1Desired); + assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); } - // function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + function test_mint_recipient(IPoolManager.ModifyLiquidityParams memory seedParams) public { + IPoolManager.ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // uint256 balance0After = currency0.balanceOfSelf(); - // uint256 balance1After = currency1.balanceOfSelf(); - - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - // // minting with perfect token ratios will use all of the tokens - // function test_mint_perfect() public { - // int24 tickLower = -int24(key.tickSpacing); - // int24 tickUpper = int24(key.tickSpacing); - // uint256 amount0Desired = 100e18; - // uint256 amount1Desired = 100e18; - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), alice, ZERO_BYTES) + ); - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: amount0Desired, - // amount1Min: amount1Desired, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // uint256 balance0After = currency0.balanceOfSelf(); - // uint256 balance1After = currency1.balanceOfSelf(); - - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(uint256(int256(-delta.amount0())), amount0Desired); - // assertEq(uint256(int256(-delta.amount1())), amount1Desired); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; - // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - // public - // { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + bytes[] memory results = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: alice, - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId,) = lpm.mint(params); - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(tokenId), alice); - // } + (, uint256 tokenId) = abi.decode(results[0], (BalanceDelta, uint256)); + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(tokenId), alice); + } // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) // public @@ -210,7 +215,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // create liquidity we can burn uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); @@ -224,7 +229,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // TODO, encode this under one call BalanceDelta deltaDecrease = _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); BalanceDelta deltaCollect = _collect(tokenId, address(this), ZERO_BYTES, false); - lpm.burn(tokenId); + _burn(tokenId); (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, 0); @@ -249,11 +254,11 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + function test_decreaseLiquidity1(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) public { uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(0 < decreaseLiquidityDelta); vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); @@ -263,13 +268,14 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); + // On decrease, balance doesn't change (currenct functionality). + assertEq(currency0.balanceOfSelf() - balance0Before, 0); + assertEq(currency1.balanceOfSelf() - balance1Before, 0); } // function test_decreaseLiquidity_collectFees( @@ -277,7 +283,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // uint256 decreaseLiquidityDelta // ) public { // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // vm.assume(0 < decreaseLiquidityDelta); // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 38867ea9..c122ea9b 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -4,12 +4,15 @@ pragma solidity ^0.8.24; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {NonfungiblePositionManager, Actions} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; +import {Planner} from "../utils/Planner.sol"; contract LiquidityOperations { NonfungiblePositionManager lpm; + using Planner for Planner.Plan; + function _mint( LiquidityRange memory _range, uint256 liquidity, @@ -17,56 +20,64 @@ contract LiquidityOperations { address recipient, bytes memory hookData ) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, deadline, recipient, hookData)); + Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); } function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); } function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); } function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.COLLECT, abi.encode(tokenId, recipient, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); + } + + function _burn(uint256 tokenId) internal { + Currency[] memory currencies = new Currency[](0); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.BURN, abi.encode(tokenId)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); } } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index fd22c3b2..5def37bc 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -7,10 +7,13 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; +import {Planner} from "../../utils/Planner.sol"; contract LiquidityFuzzers is Fuzzers { + using Planner for Planner.Plan; + function createFuzzyLiquidity( INonfungiblePositionManager lpm, address recipient, @@ -18,25 +21,22 @@ contract LiquidityFuzzers is Fuzzers { IPoolManager.ModifyLiquidityParams memory params, uint160 sqrtPriceX96, bytes memory hookData - ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { + ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory) { params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); - LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData + Planner.Plan memory plan = Planner.init().add( + Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData) ); Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - BalanceDelta delta = toBalanceDelta(result[0], result[1]); + lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies)); uint256 tokenId = lpm.nextTokenId() - 1; - return (tokenId, params, delta); + return (tokenId, params); } } diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol new file mode 100644 index 00000000..788622c5 --- /dev/null +++ b/test/utils/Planner.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; + +library Planner { + struct Plan { + Actions[] actions; + bytes[] params; + } + + function init() public pure returns (Plan memory plan) { + return Plan({actions: new Actions[](0), params: new bytes[](0)}); + } + + function add(Plan memory plan, Actions action, bytes memory param) public pure returns (Plan memory) { + Actions[] memory actions = new Actions[](plan.actions.length + 1); + bytes[] memory params = new bytes[](plan.params.length + 1); + + for (uint256 i; i < actions.length - 1; i++) { + // Copy from plan. + actions[i] = plan.actions[i]; + params[i] = plan.params[i]; + } + + actions[actions.length - 1] = action; + params[params.length - 1] = param; + + return Plan({actions: actions, params: params}); + } +} From 4cec450ab46dba697ef5a0e8afd1d607a7c9e718 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Fri, 12 Jul 2024 19:27:12 -0400 Subject: [PATCH 62/98] dumb fix to test ci (#146) * dumb fix to test ci * memory limit * update gas limit to pass tests --------- Co-authored-by: gretzke --- .github/workflows/lint.yml | 2 +- foundry.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9d16a66b..b0eb346e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,5 +21,5 @@ jobs: with: version: nightly - - name: Run tests + - name: Check format run: forge fmt --check diff --git a/foundry.toml b/foundry.toml index 4e95a213..f9da8541 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,7 @@ optimizer_runs = 1000000 ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] evm_version = "cancun" +gas_limit = "3000000000" [profile.ci] fuzz_runs = 100000 From f4793e0fe9d6f6f96b7403505164fa189a400a39 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:30:02 -0400 Subject: [PATCH 63/98] some more gas snapshots (#150) --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/collect_erc20.snap | 1 + .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mint.snap | 2 +- .forge-snapshots/sameRange_collect.snap | 1 + .../sameRange_decreaseAllLiquidity.snap | 1 + .forge-snapshots/sameRange_mint.snap | 1 + test/position-managers/Gas.t.sol | 77 ++++++++++++++++++- 13 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 .forge-snapshots/collect_erc20.snap create mode 100644 .forge-snapshots/sameRange_collect.snap create mode 100644 .forge-snapshots/sameRange_decreaseAllLiquidity.snap create mode 100644 .forge-snapshots/sameRange_mint.snap diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 021404a1..f3ea78cd 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -291244 \ No newline at end of file +291256 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 10b683f2..84be2bcd 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -223603 \ No newline at end of file +223615 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 3aa28ddf..6975d63a 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -311783 \ No newline at end of file +311795 \ No newline at end of file diff --git a/.forge-snapshots/collect_erc20.snap b/.forge-snapshots/collect_erc20.snap new file mode 100644 index 00000000..27c17f0f --- /dev/null +++ b/.forge-snapshots/collect_erc20.snap @@ -0,0 +1 @@ +236784 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index c2b3a62d..a164d000 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -209314 \ No newline at end of file +209326 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index a164d000..6f8ef9c6 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -209326 \ No newline at end of file +209338 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index a2041485..553b43e9 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -194862 \ No newline at end of file +194874 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 553b43e9..e4d9ffe7 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -194874 \ No newline at end of file +194886 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index b0807c1b..bd2905ff 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -493163 \ No newline at end of file +493199 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_collect.snap b/.forge-snapshots/sameRange_collect.snap new file mode 100644 index 00000000..27c17f0f --- /dev/null +++ b/.forge-snapshots/sameRange_collect.snap @@ -0,0 +1 @@ +236784 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap new file mode 100644 index 00000000..303ab70e --- /dev/null +++ b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap @@ -0,0 +1 @@ +231686 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_mint.snap b/.forge-snapshots/sameRange_mint.snap new file mode 100644 index 00000000..dafff532 --- /dev/null +++ b/.forge-snapshots/sameRange_mint.snap @@ -0,0 +1 @@ +345049 \ No newline at end of file diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index ce9989f0..e5cf68b8 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -297,5 +297,80 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { function test_gas_burn() public {} function test_gas_burnEmpty() public {} - function test_gas_collect() public {} + + function test_gas_collect_erc20() public { + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; + + // donate to create fee revenue + donateRouter.donate(range.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); + + Planner.Plan memory planner = + Planner.init().add(Actions.COLLECT, abi.encode(tokenId, address(this), ZERO_BYTES, false)); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + snapLastCall("collect_erc20"); + } + + // same-range gas tests + function test_gas_sameRange_mint() public { + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + + Planner.Plan memory plan = Planner.init().add( + Actions.MINT, abi.encode(range, 10_001 ether, block.timestamp + 1, address(this), ZERO_BYTES) + ); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(alice); + lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies, currencies)); + snapLastCall("sameRange_mint"); + } + + function test_gas_sameRange_decrease() public { + // two positions of the same range, one of them decreases the entirety of the liquidity + vm.startPrank(alice); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + vm.stopPrank(); + + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; + + Planner.Plan memory planner = + Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + snapLastCall("sameRange_decreaseAllLiquidity"); + } + + function test_gas_sameRange_collect() public { + // two positions of the same range, one of them collects all their fees + vm.startPrank(alice); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + vm.stopPrank(); + + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + uint256 tokenId = lpm.nextTokenId() - 1; + + // donate to create fee revenue + donateRouter.donate(range.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); + + Planner.Plan memory planner = + Planner.init().add(Actions.COLLECT, abi.encode(tokenId, address(this), ZERO_BYTES, false)); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + snapLastCall("sameRange_collect"); + } } From 77ff368e03fdd2fb8d9e827f91045c393cfc6ec6 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:35:09 -0400 Subject: [PATCH 64/98] feat: posm, use salts for all positions & update permit (#148) * use position salts * use fees owed in some tests * remove claims from increase,decrease * increment token id before reading it * Revert "increment token id before reading it" This reverts commit d366d75fd6f19349e1fea8f4951cae3ef43714dd. * owner to alice * add more mint gas tests * update comment * Owner-level ERC721 Permit (#153) * checkpointing * move decrease and collect to transient storage * remove returns since they are now saved to transient storage * draft: delta closing * wip * Sra/edits (#137) * consolidate using owner, update burn * fix: accrue deltas to caller in increase * Rip Out Vanilla (#138) * rip out vanilla and benchmark * fix gas benchmark * check posm is the locker before allowing access to external functions * restore execute tests * posm takes as 6909; remove legacy deadcode * restore tests * move helpers to the same file * move operator to NFTposm; move nonce to ERC721Permit; owner-level nonce * tests for operator/permit * snapshots * gas benchmarks for permit * test fixes * unordered nonces * fix tests / cheatcode usage * fix tests --------- Co-authored-by: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> --------- Co-authored-by: gretzke Co-authored-by: saucepoint <98790946+saucepoint@users.noreply.github.com> --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/collect_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mint.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 1 + .forge-snapshots/mint_differentRanges.snap | 1 + .forge-snapshots/mint_same_tickLower.snap | 1 + .forge-snapshots/mint_same_tickUpper.snap | 1 + .forge-snapshots/permit.snap | 1 + .forge-snapshots/permit_secondPosition.snap | 1 + .forge-snapshots/permit_twice.snap | 1 + .forge-snapshots/sameRange_collect.snap | 2 +- .../sameRange_decreaseAllLiquidity.snap | 2 +- .forge-snapshots/sameRange_mint.snap | 2 +- contracts/NonfungiblePositionManager.sol | 108 +++-- contracts/base/BaseLiquidityManagement.sol | 199 +-------- contracts/base/ERC721Permit.sol | 48 ++- .../interfaces/IBaseLiquidityManagement.sol | 14 - contracts/interfaces/IERC721Permit.sol | 4 +- .../INonfungiblePositionManager.sol | 25 +- contracts/libraries/FeeMath.sol | 32 -- .../libraries/LiquidityDeltaAccounting.sol | 29 -- .../libraries/TransientLiquidityDelta.sol | 108 ----- test/position-managers/Execute.t.sol | 40 +- test/position-managers/FeeCollection.t.sol | 65 +-- test/position-managers/Gas.t.sol | 274 +++++++++---- .../position-managers/IncreaseLiquidity.t.sol | 381 ++++++------------ .../NonfungiblePositionManager.t.sol | 90 ++--- test/position-managers/Permit.t.sol | 282 +++++++++++++ test/shared/FeeMath.sol | 70 ++++ test/shared/LiquidityOperations.sol | 131 ++++-- test/shared/fuzz/LiquidityFuzzers.sol | 9 +- test/utils/Planner.sol | 17 +- 38 files changed, 1005 insertions(+), 952 deletions(-) create mode 100644 .forge-snapshots/mintWithLiquidity.snap create mode 100644 .forge-snapshots/mint_differentRanges.snap create mode 100644 .forge-snapshots/mint_same_tickLower.snap create mode 100644 .forge-snapshots/mint_same_tickUpper.snap create mode 100644 .forge-snapshots/permit.snap create mode 100644 .forge-snapshots/permit_secondPosition.snap create mode 100644 .forge-snapshots/permit_twice.snap delete mode 100644 contracts/libraries/FeeMath.sol delete mode 100644 contracts/libraries/LiquidityDeltaAccounting.sol delete mode 100644 contracts/libraries/TransientLiquidityDelta.sol create mode 100644 test/position-managers/Permit.t.sol create mode 100644 test/shared/FeeMath.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index f3ea78cd..ff43b36e 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -291256 \ No newline at end of file +179921 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 84be2bcd..31456f87 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -223615 \ No newline at end of file +194724 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 6975d63a..f7b70cac 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -311795 \ No newline at end of file +194425 \ No newline at end of file diff --git a/.forge-snapshots/collect_erc20.snap b/.forge-snapshots/collect_erc20.snap index 27c17f0f..bdbaf724 100644 --- a/.forge-snapshots/collect_erc20.snap +++ b/.forge-snapshots/collect_erc20.snap @@ -1 +1 @@ -236784 \ No newline at end of file +172148 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index a164d000..f499ab6a 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -209326 \ No newline at end of file +137197 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 6f8ef9c6..f499ab6a 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -209338 \ No newline at end of file +137197 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 553b43e9..f7e2fbd0 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -194874 \ No newline at end of file +167226 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index e4d9ffe7..f7e2fbd0 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -194886 \ No newline at end of file +167226 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index bd2905ff..334c9f54 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -493199 \ No newline at end of file +441402 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap new file mode 100644 index 00000000..ebad633c --- /dev/null +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -0,0 +1 @@ +490196 \ No newline at end of file diff --git a/.forge-snapshots/mint_differentRanges.snap b/.forge-snapshots/mint_differentRanges.snap new file mode 100644 index 00000000..f07f5fd3 --- /dev/null +++ b/.forge-snapshots/mint_differentRanges.snap @@ -0,0 +1 @@ +407202 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickLower.snap b/.forge-snapshots/mint_same_tickLower.snap new file mode 100644 index 00000000..5bb35423 --- /dev/null +++ b/.forge-snapshots/mint_same_tickLower.snap @@ -0,0 +1 @@ +401206 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickUpper.snap b/.forge-snapshots/mint_same_tickUpper.snap new file mode 100644 index 00000000..7317abb2 --- /dev/null +++ b/.forge-snapshots/mint_same_tickUpper.snap @@ -0,0 +1 @@ +401848 \ No newline at end of file diff --git a/.forge-snapshots/permit.snap b/.forge-snapshots/permit.snap new file mode 100644 index 00000000..f51e74ce --- /dev/null +++ b/.forge-snapshots/permit.snap @@ -0,0 +1 @@ +75049 \ No newline at end of file diff --git a/.forge-snapshots/permit_secondPosition.snap b/.forge-snapshots/permit_secondPosition.snap new file mode 100644 index 00000000..8977b204 --- /dev/null +++ b/.forge-snapshots/permit_secondPosition.snap @@ -0,0 +1 @@ +57937 \ No newline at end of file diff --git a/.forge-snapshots/permit_twice.snap b/.forge-snapshots/permit_twice.snap new file mode 100644 index 00000000..f1519e1c --- /dev/null +++ b/.forge-snapshots/permit_twice.snap @@ -0,0 +1 @@ +40849 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_collect.snap b/.forge-snapshots/sameRange_collect.snap index 27c17f0f..bdbaf724 100644 --- a/.forge-snapshots/sameRange_collect.snap +++ b/.forge-snapshots/sameRange_collect.snap @@ -1 +1 @@ -236784 \ No newline at end of file +172148 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap index 303ab70e..5f87c079 100644 --- a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap +++ b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap @@ -1 +1 @@ -231686 \ No newline at end of file +150250 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_mint.snap b/.forge-snapshots/sameRange_mint.snap index dafff532..37cd9a21 100644 --- a/.forge-snapshots/sameRange_mint.snap +++ b/.forge-snapshots/sameRange_mint.snap @@ -1 +1 @@ -345049 \ No newline at end of file +344552 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index fb12aca9..b73e6178 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -18,9 +18,6 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {TransientLiquidityDelta} from "./libraries/TransientLiquidityDelta.sol"; - -import "forge-std/console2.sol"; contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit { using CurrencyLibrary for Currency; @@ -30,7 +27,6 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit using StateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager; using SafeCast for uint256; - using TransientLiquidityDelta for Currency; /// @dev The ID of the next token that will be minted. Skips 0 uint256 public nextTokenId = 1; @@ -54,16 +50,10 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // TODO: Fix double encode/decode (bytes memory unlockData, address sender) = abi.decode(payload, (bytes, address)); - (Actions[] memory actions, bytes[] memory params, Currency[] memory currencies) = - abi.decode(unlockData, (Actions[], bytes[], Currency[])); + (Actions[] memory actions, bytes[] memory params) = abi.decode(unlockData, (Actions[], bytes[])); bytes[] memory returnData = _dispatch(actions, params, sender); - for (uint256 i; i < currencies.length; i++) { - currencies[i].close(manager, sender, false); // TODO: support claims - currencies[i].close(manager, address(this), true); // position manager always takes 6909 - } - return abi.encode(returnData); } @@ -76,25 +66,23 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit returnData = new bytes[](actions.length); for (uint256 i; i < actions.length; i++) { if (actions[i] == Actions.INCREASE) { - (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = - abi.decode(params[i], (uint256, uint256, bytes, bool)); - returnData[i] = abi.encode(increaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + (uint256 tokenId, uint256 liquidity, bytes memory hookData) = + abi.decode(params[i], (uint256, uint256, bytes)); + returnData[i] = abi.encode(increaseLiquidity(tokenId, liquidity, hookData, sender)); } else if (actions[i] == Actions.DECREASE) { - (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = - abi.decode(params[i], (uint256, uint256, bytes, bool)); - returnData[i] = abi.encode(decreaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + (uint256 tokenId, uint256 liquidity, bytes memory hookData) = + abi.decode(params[i], (uint256, uint256, bytes)); + returnData[i] = abi.encode(decreaseLiquidity(tokenId, liquidity, hookData, sender)); } else if (actions[i] == Actions.MINT) { (LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, bytes memory hookData) = abi.decode(params[i], (LiquidityRange, uint256, uint256, address, bytes)); - (BalanceDelta delta, uint256 tokenId) = mint(range, liquidity, deadline, owner, hookData, sender); - returnData[i] = abi.encode(delta, tokenId); + returnData[i] = abi.encode(mint(range, liquidity, deadline, owner, hookData)); + } else if (actions[i] == Actions.CLOSE_CURRENCY) { + (Currency currency) = abi.decode(params[i], (Currency)); + returnData[i] = abi.encode(close(currency, sender)); } else if (actions[i] == Actions.BURN) { (uint256 tokenId) = abi.decode(params[i], (uint256)); burn(tokenId, sender); - } else if (actions[i] == Actions.COLLECT) { - (uint256 tokenId, address recipient, bytes memory hookData, bool claims) = - abi.decode(params[i], (uint256, address, bytes, bool)); - returnData[i] = abi.encode(collect(tokenId, recipient, hookData, claims, sender)); } else { revert UnsupportedAction(); } @@ -106,34 +94,54 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit uint256 liquidity, uint256 deadline, address owner, - bytes memory hookData, - address sender - ) internal checkDeadline(deadline) returns (BalanceDelta delta, uint256 tokenId) { - delta = _increaseLiquidity(owner, range, liquidity, hookData, sender); - + bytes memory hookData + ) internal checkDeadline(deadline) returns (BalanceDelta delta) { // mint receipt token - _mint(owner, (tokenId = nextTokenId++)); - tokenPositions[tokenId] = TokenPosition({owner: owner, range: range}); + uint256 tokenId; + unchecked { + tokenId = nextTokenId++; + } + _mint(owner, tokenId); + + (delta,) = _modifyLiquidity(range, liquidity.toInt256(), bytes32(tokenId), hookData); + + tokenPositions[tokenId] = TokenPosition({owner: owner, range: range, operator: address(0x0)}); } - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + // Note: Calling increase with 0 will accrue any underlying fees. + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, address sender) internal isAuthorizedForToken(tokenId, sender) returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - - delta = _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, sender); + // Note: The tokenId is used as the salt for this position, so every minted liquidity has unique storage in the pool manager. + (delta,) = _modifyLiquidity(tokenPos.range, liquidity.toInt256(), bytes32(tokenId), hookData); } - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + // Note: Calling decrease with 0 will accrue any underlying fees. + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, address sender) internal isAuthorizedForToken(tokenId, sender) returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; + (delta,) = _modifyLiquidity(tokenPos.range, -(liquidity.toInt256()), bytes32(tokenId), hookData); + } - delta = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + // there is no authorization scheme because the payer/recipient is always the sender + // TODO: Add more advanced functionality for other payers/recipients, needs auth scheme. + function close(Currency currency, address sender) internal returns (int256 currencyDelta) { + // this address has applied all deltas on behalf of the user/owner + // it is safe to close this entire delta because of slippage checks throughout the batched calls. + currencyDelta = manager.currencyDelta(address(this), currency); + + // the sender is the payer or receiver + if (currencyDelta < 0) { + currency.settle(manager, sender, uint256(-int256(currencyDelta)), false); + } else { + currency.take(manager, sender, uint256(int256(currencyDelta)), false); + } } function burn(uint256 tokenId, address sender) internal isAuthorizedForToken(tokenId, sender) { @@ -146,39 +154,29 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit _burn(tokenId); } - function collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims, address sender) - internal - isAuthorizedForToken(tokenId, sender) - returns (BalanceDelta delta) - { - TokenPosition memory tokenPos = tokenPositions[tokenId]; - - delta = _collect(recipient, tokenPos.owner, tokenPos.range, hookData, sender); - } - - function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { - TokenPosition memory tokenPosition = tokenPositions[tokenId]; - return feesOwed(tokenPosition.owner, tokenPosition.range); - } - // TODO: Bug - Positions are overrideable unless we can allow two of the same users to have distinct positions. function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override { TokenPosition storage tokenPosition = tokenPositions[tokenId]; LiquidityRangeId rangeId = tokenPosition.range.toId(); Position storage position = positions[from][rangeId]; - position.operator = address(0x0); // transfer position data to destination positions[to][rangeId] = position; delete positions[from][rangeId]; // update token position - tokenPositions[tokenId] = TokenPosition({owner: to, range: tokenPosition.range}); + tokenPositions[tokenId] = TokenPosition({owner: to, range: tokenPosition.range, operator: address(0x0)}); } - function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256) { - TokenPosition memory tokenPosition = tokenPositions[tokenId]; - return uint256(positions[tokenPosition.owner][tokenPosition.range.toId()].nonce++); + // override ERC721 approval by setting operator + function _approve(address spender, uint256 tokenId) internal override { + tokenPositions[tokenId].operator = spender; + } + + function getApproved(uint256 tokenId) public view override returns (address) { + require(_exists(tokenId), "ERC721: approved query for nonexistent token"); + + return tokenPositions[tokenId].operator; } modifier isAuthorizedForToken(uint256 tokenId, address sender) { diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index a72cbad3..1d453703 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -18,15 +18,10 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; -import {FeeMath} from "../libraries/FeeMath.sol"; import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {PositionLibrary} from "../libraries/Position.sol"; import {BalanceDeltaExtensionLibrary} from "../libraries/BalanceDeltaExtensionLibrary.sol"; -import {LiquidityDeltaAccounting} from "../libraries/LiquidityDeltaAccounting.sol"; -import {TransientLiquidityDelta} from "../libraries/TransientLiquidityDelta.sol"; - -import "forge-std/console2.sol"; abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { using LiquidityRangeIdLibrary for LiquidityRange; @@ -40,16 +35,12 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb using LiquiditySaltLibrary for IHooks; using PositionLibrary for IBaseLiquidityManagement.Position; using BalanceDeltaExtensionLibrary for BalanceDelta; - using LiquidityDeltaAccounting for BalanceDelta; - using TransientLiquidityDelta for BalanceDelta; - using TransientLiquidityDelta for Currency; - using TransientLiquidityDelta for address; mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; constructor(IPoolManager _manager) ImmutableState(_manager) {} - function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) + function _modifyLiquidity(LiquidityRange memory range, int256 liquidityChange, bytes32 salt, bytes memory hookData) internal returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) { @@ -59,163 +50,12 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb tickLower: range.tickLower, tickUpper: range.tickUpper, liquidityDelta: liquidityChange, - salt: range.poolKey.hooks.getLiquiditySalt(owner) + salt: salt }), hookData ); } - /// @dev The delta returned from this call must be settled by the caller. - /// Zeroing out the full balance of open deltas accounted to this address is unsafe until the callerDeltas are handled. - function _increaseLiquidity( - address owner, - LiquidityRange memory range, - uint256 liquidityToAdd, - bytes memory hookData, - address sender - ) internal returns (BalanceDelta) { - // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. - (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = - _modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData); - - Position storage position = positions[owner][range.toId()]; - - // Calculates the fee growth since the last time the positions feeGrowthInside was updated. - // Also updates the feeGrowthInsideLast variables in storage. - (BalanceDelta callerFeesAccrued) = _updateFeeGrowth(range, position); - - // Calculate the portion of the liquidityDelta that is attributable to the caller. - // We must account for fees that might be owed to other users on the same range. - (BalanceDelta callerDelta, BalanceDelta thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); - - // Update position storage, flushing the callerDelta value to tokensOwed first if necessary. - // If callerDelta > 0, then even after investing callerFeesAccrued, the caller still has some amount to collect that were not added into the position so they are accounted to tokensOwed and removed from the final callerDelta returned. - BalanceDelta tokensOwed; - if (callerDelta.amount0() > 0) { - (tokensOwed, callerDelta, thisDelta) = - _moveCallerDeltaToTokensOwed(true, tokensOwed, callerDelta, thisDelta); - } - - if (callerDelta.amount1() > 0) { - (tokensOwed, callerDelta, thisDelta) = - _moveCallerDeltaToTokensOwed(false, tokensOwed, callerDelta, thisDelta); - } - - // Accrue all deltas to the caller. - callerDelta.flush(sender, range.poolKey.currency0, range.poolKey.currency1); - thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); - - position.addTokensOwed(tokensOwed); - position.addLiquidity(liquidityToAdd); - return liquidityDelta; - } - - function _moveCallerDeltaToTokensOwed( - bool useAmount0, - BalanceDelta tokensOwed, - BalanceDelta callerDelta, - BalanceDelta thisDelta - ) private pure returns (BalanceDelta, BalanceDelta, BalanceDelta) { - // credit the excess tokens to the position's tokensOwed - tokensOwed = - useAmount0 ? tokensOwed.setAmount0(callerDelta.amount0()) : tokensOwed.setAmount1(callerDelta.amount1()); - - // this contract is responsible for custodying the excess tokens - thisDelta = - useAmount0 ? thisDelta.addAmount0(callerDelta.amount0()) : thisDelta.addAmount1(callerDelta.amount1()); - - // the caller is not expected to collect the excess tokens - callerDelta = useAmount0 ? callerDelta.setAmount0(0) : callerDelta.setAmount1(0); - - return (tokensOwed, callerDelta, thisDelta); - } - - /// Any outstanding amounts owed to the caller from the underlying modify call must be collected explicitly with `collect`. - function _decreaseLiquidity( - address owner, - LiquidityRange memory range, - uint256 liquidityToRemove, - bytes memory hookData - ) internal returns (BalanceDelta) { - (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = - _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); - - Position storage position = positions[owner][range.toId()]; - - // Calculates the fee growth since the last time the positions feeGrowthInside was updated - // Also updates the position's the feeGrowthInsideLast variables in storage. - (BalanceDelta callerFeesAccrued) = _updateFeeGrowth(range, position); - - // Account for fees accrued to other users on the same range. - (BalanceDelta callerDelta, BalanceDelta thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); - - BalanceDelta tokensOwed; - - // Flush the callerDelta, incrementing the tokensOwed to the user and the amount claimable to this contract. - if (callerDelta.amount0() > 0) { - (tokensOwed, callerDelta, thisDelta) = - _moveCallerDeltaToTokensOwed(true, tokensOwed, callerDelta, thisDelta); - } - - if (callerDelta.amount1() > 0) { - (tokensOwed, callerDelta, thisDelta) = - _moveCallerDeltaToTokensOwed(false, tokensOwed, callerDelta, thisDelta); - } - callerDelta.flush(owner, range.poolKey.currency0, range.poolKey.currency1); - thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); - - position.addTokensOwed(tokensOwed); - position.subtractLiquidity(liquidityToRemove); - return liquidityDelta; - } - - // The recipient may not be the original owner. - function _collect( - address recipient, - address owner, - LiquidityRange memory range, - bytes memory hookData, - address sender - ) internal returns (BalanceDelta) { - BalanceDelta callerDelta; - BalanceDelta thisDelta; - Position storage position = positions[owner][range.toId()]; - - // Only call modify if there is still liquidty in this position. - if (position.liquidity != 0) { - // Do not add or decrease liquidity, just trigger fee updates. - (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, 0, hookData); - - // Also updates the position's the feeGrowthInsideLast variables in storage. - (BalanceDelta callerFeesAccrued) = _updateFeeGrowth(range, position); - - // Account for fees accrued to other users on the same range. - // TODO: Opt when liquidityDelta == 0 - (callerDelta, thisDelta) = liquidityDelta.split(callerFeesAccrued, totalFeesAccrued); - } - - // Allow the caller to collect the tokens owed. - // Tokens owed that the caller collects is paid for by this contract. - // ie. Transfer the tokensOwed amounts to the caller from the position manager through the pool manager. - // TODO case where this contract does not have enough credits to pay the caller? - BalanceDelta tokensOwed = - toBalanceDelta(uint256(position.tokensOwed0).toInt128(), uint256(position.tokensOwed1).toInt128()); - callerDelta = callerDelta + tokensOwed; - thisDelta = thisDelta - tokensOwed; - - if (recipient == sender) { - callerDelta.flush(recipient, range.poolKey.currency0, range.poolKey.currency1); - } else { - TransientLiquidityDelta.closeDelta( - manager, recipient, range.poolKey.currency0, range.poolKey.currency1, false - ); // TODO: allow recipient to receive claims, and add test! - } - thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); - - position.clearTokensOwed(); - return callerDelta; - } - function _validateBurn(address owner, LiquidityRange memory range) internal { LiquidityRangeId rangeId = range.toId(); Position storage position = positions[owner][rangeId]; @@ -223,39 +63,4 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb if (position.tokensOwed0 != 0 && position.tokensOwed1 != 0) revert TokensMustBeCollected(); delete positions[owner][rangeId]; } - - function _updateFeeGrowth(LiquidityRange memory range, Position storage position) - internal - returns (BalanceDelta callerFeesAccrued) - { - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = - manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); - - callerFeesAccrued = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); - - position.updateFeeGrowthInside(feeGrowthInside0X128, feeGrowthInside1X128); - } - - // --- View Functions --- // - function feesOwed(address owner, LiquidityRange memory range) - public - view - returns (uint256 token0Owed, uint256 token1Owed) - { - Position memory position = positions[owner][range.toId()]; - - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = - manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); - - (token0Owed) = FeeMath.getFeeOwed(feeGrowthInside0X128, position.feeGrowthInside0LastX128, position.liquidity); - (token1Owed) = FeeMath.getFeeOwed(feeGrowthInside1X128, position.feeGrowthInside1LastX128, position.liquidity); - token0Owed += position.tokensOwed0; - token1Owed += position.tokensOwed1; - } } diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol index 8eb86521..4668f2c5 100644 --- a/contracts/base/ERC721Permit.sol +++ b/contracts/base/ERC721Permit.sol @@ -11,8 +11,7 @@ import {IERC1271} from "../interfaces/external/IERC1271.sol"; /// @title ERC721 with permit /// @notice Nonfungible tokens that support an approve via signature, i.e. permit abstract contract ERC721Permit is ERC721, IERC721Permit { - /// @dev Gets the current nonce for a token ID and then increments it, returning the original value - function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256); + mapping(address owner => mapping(uint256 word => uint256 bitmap)) public nonces; /// @dev The hash of the name used in the permit signature verification bytes32 private immutable nameHash; @@ -46,23 +45,18 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad; /// @inheritdoc IERC721Permit - function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, uint8 v, bytes32 r, bytes32 s) external payable override { require(block.timestamp <= deadline, "Permit expired"); - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline)) - ) - ); address owner = ownerOf(tokenId); require(spender != owner, "ERC721Permit: approval to current owner"); + bytes32 digest = getDigest(spender, tokenId, nonce, deadline); + if (Address.isContract(owner)) { require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, "Unauthorized"); } else { @@ -71,6 +65,40 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { require(recoveredAddress == owner, "Unauthorized"); } + _useUnorderedNonce(owner, nonce); approve(spender, tokenId); } + + function getDigest(address spender, uint256 tokenId, uint256 _nonce, uint256 deadline) + public + view + returns (bytes32 digest) + { + digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _nonce, deadline)) + ) + ); + } + + /// @notice Returns the index of the bitmap and the bit position within the bitmap. Used for unordered nonces + /// @param nonce The nonce to get the associated word and bit positions + /// @return wordPos The word position or index into the nonceBitmap + /// @return bitPos The bit position + /// @dev The first 248 bits of the nonce value is the index of the desired bitmap + /// @dev The last 8 bits of the nonce value is the position of the bit in the bitmap + function bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) { + wordPos = uint248(nonce >> 8); + bitPos = uint8(nonce); + } + + function _useUnorderedNonce(address from, uint256 nonce) internal { + (uint256 wordPos, uint256 bitPos) = bitmapPositions(nonce); + uint256 bit = 1 << bitPos; + uint256 flipped = nonces[from][wordPos] ^= bit; + + if (flipped & bit == 0) revert NonceAlreadyUsed(); + } } diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol index 6bcb6e5b..05424934 100644 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ b/contracts/interfaces/IBaseLiquidityManagement.sol @@ -11,10 +11,6 @@ interface IBaseLiquidityManagement { // details about the liquidity position struct Position { - // the nonce for permits - uint96 nonce; - // the address that is approved for spending this token - address operator; uint256 liquidity; // the fee growth of the aggregate position as of the last action on the individual position uint256 feeGrowthInside0LastX128; @@ -25,14 +21,4 @@ interface IBaseLiquidityManagement { } error LockFailure(); - - /// @notice Fees owed for a given liquidity position. Includes materialized fees and uncollected fees. - /// @param owner The owner of the liquidity position - /// @param range The range of the liquidity position - /// @return token0Owed The amount of token0 owed to the owner - /// @return token1Owed The amount of token1 owed to the owner - function feesOwed(address owner, LiquidityRange memory range) - external - view - returns (uint256 token0Owed, uint256 token1Owed); } diff --git a/contracts/interfaces/IERC721Permit.sol b/contracts/interfaces/IERC721Permit.sol index daa27030..213bca2a 100644 --- a/contracts/interfaces/IERC721Permit.sol +++ b/contracts/interfaces/IERC721Permit.sol @@ -4,6 +4,8 @@ pragma solidity >=0.7.5; /// @title ERC721 with permit /// @notice Extension to ERC721 that includes a permit function for signature based approvals interface IERC721Permit { + error NonceAlreadyUsed(); + /// @notice The permit typehash used in the permit signature /// @return The typehash for the permit function PERMIT_TYPEHASH() external pure returns (bytes32); @@ -19,7 +21,7 @@ interface IERC721Permit { /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, uint8 v, bytes32 r, bytes32 s) external payable; } diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 92e0f35a..a13380f2 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.24; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; -// TODO: ADD/REMOVE ACTIONS enum Actions { MINT, BURN, - COLLECT, INCREASE, - DECREASE + DECREASE, + CLOSE_CURRENCY // Any positive delta on a currency will be sent to specified address + } interface INonfungiblePositionManager { @@ -20,28 +20,25 @@ interface INonfungiblePositionManager { struct TokenPosition { address owner; LiquidityRange range; + address operator; } error MustBeUnlockedByThisContract(); error DeadlinePassed(); error UnsupportedAction(); + function tokenPositions(uint256 tokenId) external view returns (address, LiquidityRange memory, address); + /// @notice Batches many liquidity modification calls to pool manager /// @param payload is an encoding of actions, params, and currencies /// @return returnData is the endocing of each actions return information function modifyLiquidities(bytes calldata payload) external returns (bytes[] memory); - // TODO Can decide if we want burn to auto encode a decrease/collect. - /// @notice Burn a position and delete the tokenId - /// @dev It enforces that there is no open liquidity or tokens to be collected - /// @param tokenId The ID of the position - // function burn(uint256 tokenId) external; - - /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees - /// @param tokenId The ID of the position - /// @return token0Owed The amount of token0 owed - /// @return token1Owed The amount of token1 owed - function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed); + /// TODO Can decide if we want burn to auto encode a decrease/collect. + //// @notice Burn a position and delete the tokenId + //// @dev It enforces that there is no open liquidity or tokens to be collected + //// @param tokenId The ID of the position + //// function burn(uint256 tokenId) external; function nextTokenId() external view returns (uint256); } diff --git a/contracts/libraries/FeeMath.sol b/contracts/libraries/FeeMath.sol deleted file mode 100644 index 9a459252..00000000 --- a/contracts/libraries/FeeMath.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - -library FeeMath { - using SafeCast for uint256; - - function getFeesOwed( - uint256 feeGrowthInside0X128, - uint256 feeGrowthInside1X128, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint256 liquidity - ) internal pure returns (BalanceDelta feesOwed) { - uint128 token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); - uint128 token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); - feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); - } - - function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint256 liquidity) - internal - pure - returns (uint128 tokenOwed) - { - tokenOwed = - (FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)).toUint128(); - } -} diff --git a/contracts/libraries/LiquidityDeltaAccounting.sol b/contracts/libraries/LiquidityDeltaAccounting.sol deleted file mode 100644 index 9c82d1c9..00000000 --- a/contracts/libraries/LiquidityDeltaAccounting.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; - -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - -import "forge-std/console2.sol"; - -library LiquidityDeltaAccounting { - function split(BalanceDelta liquidityDelta, BalanceDelta callerFeesAccrued, BalanceDelta totalFeesAccrued) - internal - pure - returns (BalanceDelta callerDelta, BalanceDelta thisDelta) - { - if (totalFeesAccrued == callerFeesAccrued) { - // when totalFeesAccrued == callerFeesAccrued, the caller is not sharing the range - // therefore, the caller is responsible for the entire liquidityDelta - callerDelta = liquidityDelta; - } else { - // the delta for increasing liquidity assuming that totalFeesAccrued was not applied - BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued; - - // outstanding deltas the caller is responsible for, after their fees are credited to the principal delta - callerDelta = principalDelta + callerFeesAccrued; - - // outstanding deltas this contract is responsible for, intuitively the contract is responsible for taking fees external to the caller's accrued fees - thisDelta = totalFeesAccrued - callerFeesAccrued; - } - } -} diff --git a/contracts/libraries/TransientLiquidityDelta.sol b/contracts/libraries/TransientLiquidityDelta.sol deleted file mode 100644 index df7608ba..00000000 --- a/contracts/libraries/TransientLiquidityDelta.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.24; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; - -import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; -import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; - -import "forge-std/console2.sol"; - -/// @title a library to store callers' currency deltas in transient storage -/// @dev this library implements the equivalent of a mapping, as transient storage can only be accessed in assembly -library TransientLiquidityDelta { - using CurrencySettleTake for Currency; - using TransientStateLibrary for IPoolManager; - - /// @notice calculates which storage slot a delta should be stored in for a given caller and currency - function _computeSlot(address caller_, Currency currency) internal pure returns (bytes32 hashSlot) { - assembly { - mstore(0, caller_) - mstore(32, currency) - hashSlot := keccak256(0, 64) - } - } - - /// @notice Flush a BalanceDelta into transient storage for a given holder - function flush(BalanceDelta delta, address holder, Currency currency0, Currency currency1) internal { - addDelta(currency0, holder, delta.amount0()); - addDelta(currency1, holder, delta.amount1()); - } - - function addDelta(Currency currency, address caller, int128 delta) internal { - bytes32 hashSlot = _computeSlot(caller, currency); - assembly { - let oldValue := tload(hashSlot) - let newValue := add(oldValue, delta) - tstore(hashSlot, newValue) - } - } - - function subtractDelta(Currency currency, address caller, int128 delta) internal { - bytes32 hashSlot = _computeSlot(caller, currency); - assembly { - let oldValue := tload(hashSlot) - let newValue := sub(oldValue, delta) - tstore(hashSlot, newValue) - } - } - - function close(Currency currency, IPoolManager manager, address holder, bool claims) - internal - returns (int128 delta) - { - // getDelta(currency, holder); - bytes32 hashSlot = _computeSlot(holder, currency); - assembly { - delta := tload(hashSlot) - } - - if (delta < 0) { - currency.settle(manager, holder, uint256(-int256(delta)), claims); - } else { - currency.take(manager, holder, uint256(int256(delta)), claims); - } - - // setDelta(0); - assembly { - tstore(hashSlot, 0) - } - } - - function closeDelta(IPoolManager manager, address holder, Currency currency0, Currency currency1, bool claims) - internal - { - close(currency0, manager, holder, claims); - close(currency1, manager, holder, claims); - } - - function getBalanceDelta(address holder, Currency currency0, Currency currency1) - internal - view - returns (BalanceDelta delta) - { - delta = toBalanceDelta(getDelta(currency0, holder), getDelta(currency1, holder)); - } - - /// Copied from v4-core/src/libraries/CurrencyDelta.sol: - /// @notice sets a new currency delta for a given caller and currency - function setDelta(Currency currency, address caller, int128 delta) internal { - bytes32 hashSlot = _computeSlot(caller, currency); - - assembly { - tstore(hashSlot, delta) - } - } - - /// @notice gets a new currency delta for a given caller and currency - // TODO: is returning 128 bits safe? - function getDelta(Currency currency, address caller) internal view returns (int128 delta) { - bytes32 hashSlot = _computeSlot(caller, currency); - - assembly { - delta := tload(hashSlot) - } - } -} diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 1d2ccc77..b738d02e 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -30,8 +30,6 @@ import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; -import "forge-std/console2.sol"; - contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -39,6 +37,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit using PoolIdLibrary for PoolKey; using SafeCast for uint256; using Planner for Planner.Plan; + using StateLibrary for IPoolManager; PoolId poolId; address alice = makeAddr("ALICE"); @@ -86,9 +85,12 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit _mint(range, initialLiquidity, block.timestamp, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - _increaseLiquidity(tokenId, liquidityToAdd, ZERO_BYTES, false); + _increaseLiquidity(tokenId, liquidityToAdd, ZERO_BYTES); + + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialLiquidity + liquidityToAdd); } @@ -105,19 +107,16 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES, false)); - planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd2, ZERO_BYTES, false)); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES)); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd2, ZERO_BYTES)); - console2.log("action"); - console2.log(uint8(planner.actions[0])); - console2.log(uint8(planner.actions[1])); + planner = planner.finalize(range); + lpm.modifyLiquidities(planner.zip()); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialiLiquidity + liquidityToAdd + liquidityToAdd2); } @@ -133,14 +132,15 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit planner = planner.add( Actions.MINT, abi.encode(range, initialLiquidity, block.timestamp + 1, address(this), ZERO_BYTES) ); - planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES, false)); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES)); + + planner = planner.finalize(range); + lpm.modifyLiquidities(planner.zip()); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialLiquidity + liquidityToAdd); } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 22d99a05..9e608ef0 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -26,6 +26,8 @@ import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; + contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -100,7 +102,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // collect fees uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES, false); + BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES); // express key.fee as wad (i.e. 3000 = 0.003e18) assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); @@ -172,8 +174,8 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 tokenIdBob = lpm.nextTokenId() - 1; // confirm the positions are same range - (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); - (, LiquidityRange memory rangeBob) = lpm.tokenPositions(tokenIdBob); + (, LiquidityRange memory rangeAlice,) = lpm.tokenPositions(tokenIdAlice); + (, LiquidityRange memory rangeBob,) = lpm.tokenPositions(tokenIdBob); assertEq(rangeAlice.tickLower, rangeBob.tickLower); assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); @@ -185,7 +187,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance0AliceBefore = currency0.balanceOf(alice); uint256 balance1AliceBefore = currency1.balanceOf(alice); vm.startPrank(alice); - BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES, false); + BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES); vm.stopPrank(); uint256 balance0AliceAfter = currency0.balanceOf(alice); uint256 balance1AliceAfter = currency1.balanceOf(alice); @@ -198,7 +200,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance0BobBefore = currency0.balanceOf(bob); uint256 balance1BobBefore = currency1.balanceOf(bob); vm.startPrank(bob); - delta = _collect(tokenIdBob, bob, ZERO_BYTES, false); + delta = _collect(tokenIdBob, bob, ZERO_BYTES); vm.stopPrank(); uint256 balance0BobAfter = currency0.balanceOf(bob); uint256 balance1BobAfter = currency1.balanceOf(bob); @@ -215,10 +217,8 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li function test_collect_donate() public {} function test_collect_donate_sameRange() public {} - /// @dev Alice and bob create liquidity on the same range - /// when alice decreases liquidity, she should only collect her fees - /// TODO Add back fuzz test on liquidityDeltaBob - /// TODO Assert state changes for lpm balance, position state, and return values + /// @dev Alice and Bob create liquidity on the same range, and decrease their liquidity + // Even though their positions are the same range, they are unique positions in pool manager. function test_decreaseLiquidity_sameRange_exact() public { // alice and bob create liquidity on the same range [-120, 120] LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120}); @@ -226,52 +226,63 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // alice provisions 3x the amount of liquidity as bob uint256 liquidityAlice = 3000e18; uint256 liquidityBob = 1000e18; + vm.prank(alice); BalanceDelta lpDeltaAlice = _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; + uint256 aliceBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)); + uint256 aliceBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)); + vm.prank(bob); BalanceDelta lpDeltaBob = _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; + uint256 bobBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)); + uint256 bobBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)); + // swap to create fees uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + swap(key, true, -int256(swapAmount), ZERO_BYTES); // zeroForOne is true, so zero is the input + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back, // zeroForOne is false, so one is the input // alice decreases liquidity - vm.prank(alice); + vm.startPrank(alice); lpm.approve(address(this), tokenIdAlice); - _decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); + _decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES); + vm.stopPrank(); uint256 tolerance = 0.000000001 ether; - uint256 lpmBalance0 = manager.balanceOf(address(lpm), currency0.toId()); - uint256 lpmBalance1 = manager.balanceOf(address(lpm), currency1.toId()); - - // lpm collects alice's principal + all fees accrued on the range + // alice has accrued her principle liquidity + any fees in token0 assertApproxEqAbs( - lpmBalance0, uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD), tolerance + IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)) - aliceBalance0Before, + uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD) * 3 / 4, + tolerance ); + // alice has accrued her principle liquidity + any fees in token1 assertApproxEqAbs( - lpmBalance1, uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD), tolerance + IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)) - aliceBalance1Before, + uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD) * 3 / 4, + tolerance ); // bob decreases half of his liquidity - vm.prank(bob); + vm.startPrank(bob); lpm.approve(address(this), tokenIdBob); - _decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); + _decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES); + vm.stopPrank(); - // lpm collects half of bobs principal - // the fee amount has already been collected with alice's calls + // bob has accrued half his principle liquidity + any fees in token0 assertApproxEqAbs( - manager.balanceOf(address(lpm), currency0.toId()) - lpmBalance0, - uint256(int256(-lpDeltaBob.amount0()) / 2), + IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)) - bobBalance0Before, + uint256(int256(-lpDeltaBob.amount0()) / 2) + swapAmount.mulWadDown(FEE_WAD) * 1 / 4, tolerance ); + // bob has accrued half his principle liquidity + any fees in token0 assertApproxEqAbs( - manager.balanceOf(address(lpm), currency1.toId()) - lpmBalance1, - uint256(int256(-lpDeltaBob.amount1()) / 2), + IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)) - bobBalance1Before, + uint256(int256(-lpDeltaBob.amount1()) / 2) + swapAmount.mulWadDown(FEE_WAD) * 1 / 4, tolerance ); } diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index e5cf68b8..af45f538 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -16,6 +16,7 @@ import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {FeeMath} from "../shared/FeeMath.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; @@ -33,10 +34,13 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using Planner for Planner.Plan; + using FeeMath for INonfungiblePositionManager; PoolId poolId; - address alice = makeAddr("ALICE"); - address bob = makeAddr("BOB"); + address alice; + uint256 alicePK; + address bob; + uint256 bobPK; uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; @@ -46,6 +50,9 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { LiquidityRange range; function setUp() public { + (alice, alicePK) = makeAddrAndKey("ALICE"); + (bob, bobPK) = makeAddrAndKey("BOB"); + Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); @@ -80,28 +87,72 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { } function test_gas_mint() public { - Planner.Plan memory plan = Planner.init().add( + Planner.Plan memory planner = Planner.init().add( Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES) ); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies, currencies)); + planner = planner.finalize(range); + lpm.modifyLiquidities(planner.zip()); snapLastCall("mint"); } + function test_gas_mint_differentRanges() public { + // Explicitly mint to a new range on the same pool. + LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 0, tickUpper: 60}); + vm.startPrank(bob); + _mint(bob_mint, 10_000 ether, block.timestamp + 1, address(bob), ZERO_BYTES); + vm.stopPrank(); + // Mint to a diff range, diff user. + Planner.Plan memory planner = Planner.init().add( + Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(alice), ZERO_BYTES) + ); + planner = planner.finalize(range); + vm.prank(alice); + lpm.modifyLiquidities(planner.zip()); + snapLastCall("mint_differentRanges"); + } + + function test_gas_mint_sameTickLower() public { + // Explicitly mint to range whos tickLower is the same. + LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: -60}); + vm.startPrank(bob); + _mint(bob_mint, 10_000 ether, block.timestamp + 1, address(bob), ZERO_BYTES); + vm.stopPrank(); + // Mint to a diff range, diff user. + Planner.Plan memory planner = Planner.init().add( + Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(alice), ZERO_BYTES) + ); + planner = planner.finalize(range); + vm.prank(alice); + lpm.modifyLiquidities(planner.zip()); + snapLastCall("mint_same_tickLower"); + } + + function test_gas_mint_sameTickUpper() public { + // Explicitly mint to range whos tickUpperis the same. + LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 60, tickUpper: 300}); + vm.startPrank(bob); + _mint(bob_mint, 10_000 ether, block.timestamp + 1, address(bob), ZERO_BYTES); + vm.stopPrank(); + // Mint to a diff range, diff user. + Planner.Plan memory planner = Planner.init().add( + Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(alice), ZERO_BYTES) + ); + planner = planner.finalize(range); + vm.prank(alice); + lpm.modifyLiquidities(planner.zip()); + snapLastCall("mint_same_tickUpper"); + } + function test_gas_increaseLiquidity_erc20() public { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = - Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); + Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); snapLastCall("increaseLiquidity_erc20"); } @@ -110,13 +161,11 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = - Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, true)); + Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); snapLastCall("increaseLiquidity_erc6909"); } @@ -137,29 +186,28 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // donate to create fees - donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); + uint256 amountDonate = 0.2e18; + donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); // alice uses her exact fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + uint256 tokensOwedAlice = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed, - token1Owed + tokensOwedAlice, + tokensOwedAlice ); Planner.Plan memory planner = - Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); snapLastCall("autocompound_exactUnclaimedFees"); } @@ -180,24 +228,23 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + uint256 amountDonate = 20e18; + donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); + uint256 tokensOwedAlice = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; // bob collects fees so some of alice's fees are now cached - Planner.Plan memory planner = - Planner.init().add(Actions.COLLECT, abi.encode(tokenIdBob, bob, ZERO_BYTES, false)); + Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenIdBob, 0, ZERO_BYTES)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); vm.prank(bob); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); // donate to create more fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); - (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + tokensOwedAlice = tokensOwedAlice + amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; // alice will use ALL of her fees to increase liquidity { @@ -206,18 +253,16 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), TickMath.getSqrtPriceAtTick(range.tickUpper), - newToken0Owed, - newToken1Owed + tokensOwedAlice, + tokensOwedAlice ); - planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); } } @@ -239,29 +284,28 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // donate to create fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + uint256 amountDonate = 20e18; + donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); // alice will use half of her fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + uint256 halfTokensOwedAlice = (amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1) / 2; (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed / 2, - token1Owed / 2 + halfTokensOwedAlice, + halfTokensOwedAlice ); Planner.Plan memory planner = - Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); snapLastCall("autocompound_excessFeesCredit"); } @@ -270,13 +314,11 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = - Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); + Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); snapLastCall("decreaseLiquidity_erc20"); } @@ -285,18 +327,92 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = - Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, true)); + Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(planner.zip()); snapLastCall("decreaseLiquidity_erc6909"); } function test_gas_burn() public {} function test_gas_burnEmpty() public {} + function test_gas_collect() public {} + + function test_gas_permit() public { + // alice permits for the first time + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // alice gives operator permission to bob + uint256 nonce = 1; + bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); + + vm.prank(alice); + lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); + snapLastCall("permit"); + } + + function test_gas_permit_secondPosition() public { + // alice permits for her two tokens, benchmark the 2nd permit + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // alice gives operator permission to bob + uint256 nonce = 1; + bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); + + vm.prank(alice); + lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); + + // alice creates another position + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + tokenIdAlice = lpm.nextTokenId() - 1; + + // alice gives operator permission to bob + nonce = 2; + digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); + (v, r, s) = vm.sign(alicePK, digest); + + vm.prank(alice); + lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); + snapLastCall("permit_secondPosition"); + } + + function test_gas_permit_twice() public { + // alice permits the same token, twice + address charlie = makeAddr("CHARLIE"); + + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // alice gives operator permission to bob + uint256 nonce = 1; + bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); + + vm.prank(alice); + lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); + + // alice gives operator permission to charlie + nonce = 2; + digest = lpm.getDigest(charlie, tokenIdAlice, nonce, block.timestamp + 1); + (v, r, s) = vm.sign(alicePK, digest); + + vm.prank(alice); + lpm.permit(charlie, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); + snapLastCall("permit_twice"); + } function test_gas_collect_erc20() public { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); @@ -305,14 +421,11 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // donate to create fee revenue donateRouter.donate(range.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); - Planner.Plan memory planner = - Planner.init().add(Actions.COLLECT, abi.encode(tokenId, address(this), ZERO_BYTES, false)); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + // Collect by calling decrease with 0. + Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 0, ZERO_BYTES, false)); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + planner = planner.finalize(range); + lpm.modifyLiquidities(planner.zip()); snapLastCall("collect_erc20"); } @@ -320,14 +433,12 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { function test_gas_sameRange_mint() public { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - Planner.Plan memory plan = Planner.init().add( - Actions.MINT, abi.encode(range, 10_001 ether, block.timestamp + 1, address(this), ZERO_BYTES) + Planner.Plan memory planner = Planner.init().add( + Actions.MINT, abi.encode(range, 10_001 ether, block.timestamp + 1, address(alice), ZERO_BYTES) ); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies, currencies)); + lpm.modifyLiquidities(planner.zip()); snapLastCall("sameRange_mint"); } @@ -343,11 +454,9 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(planner.zip()); snapLastCall("sameRange_decreaseAllLiquidity"); } @@ -363,14 +472,11 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // donate to create fee revenue donateRouter.donate(range.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); - Planner.Plan memory planner = - Planner.init().add(Actions.COLLECT, abi.encode(tokenId, address(this), ZERO_BYTES, false)); + Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 0, ZERO_BYTES, false)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; + planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + lpm.modifyLiquidities(planner.zip()); snapLastCall("sameRange_collect"); } } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 9f0bec24..1975a882 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -16,24 +16,25 @@ import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; - -import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; - +import {Actions, INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; - -import "forge-std/console2.sol"; +import {Planner} from "../utils/Planner.sol"; +import {FeeMath} from "../shared/FeeMath.sol"; contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; + using Planner for Planner.Plan; + using FeeMath for INonfungiblePositionManager; PoolId poolId; address alice = makeAddr("ALICE"); @@ -97,32 +98,38 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back // alice uses her exact fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + // Slight error in this calculation vs. actual fees.. TODO: Fix this. + BalanceDelta feesOwedAlice = INonfungiblePositionManager(lpm).getFeesOwed(manager, tokenIdAlice); + // Note: You can alternatively calculate Alice's fees owed from the swap amount, fee on the pool, and total liquidity in that range. + // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob); (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed, - token1Owed + uint256(int256(feesOwedAlice.amount0())), + uint256(int256(feesOwedAlice.amount1())) ); uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); + // TODO: Can we make this easier to re-invest fees, so that you don't need to know the exact collect amount? + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); + planner = planner.finalize(range); vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + lpm.modifyLiquidities(planner.zip()); vm.stopPrank(); - // alice did not spend any tokens - assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); - assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + // It is not exact because of the error in the fee calculation and error in the + uint256 tolerance = 0.00000000001 ether; - // alice spent all of the fees, approximately - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertApproxEqAbs(token0Owed, 0, 20 wei); - assertApproxEqAbs(token1Owed, 0, 20 wei); + // alice barely spent any tokens + // TODO: This is a case for not caring about dust left in pool manager :/ + assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance); + assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), tolerance); } // uses donate to simulate fee revenue @@ -143,118 +150,118 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // donate to create fees + uint256 amountDonate = 0.2e18; donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); - // alice uses her exact fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + // subtract 1 cause we'd rather take than pay + uint256 feesAmount = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed, - token1Owed + feesAmount, + feesAmount ); uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); vm.stopPrank(); - // alice did not spend any tokens - assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); - assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + // It is not exact because of the error in the fee calculation and error in the + uint256 tolerance = 0.00000000001 ether; - // alice spent all of the fees - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertEq(token0Owed, 0); - assertEq(token1Owed, 0); + // alice barely spent any tokens + // TODO: This is a case for not caring about dust left in pool manager :/ + assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance); + assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), tolerance); } - function test_increaseLiquidity_withExcessFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Excess fees are accounted to alice - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; - - // alice provides liquidity - vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - // alice will use half of her fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed / 2, - token1Owed / 2 - ); - - vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - vm.stopPrank(); - } - - { - // bob collects his fees - uint256 balance0BeforeBob = currency0.balanceOf(bob); - uint256 balance1BeforeBob = currency1.balanceOf(bob); - vm.startPrank(bob); - _collect(tokenIdBob, bob, ZERO_BYTES, false); - vm.stopPrank(); - uint256 balance0AfterBob = currency0.balanceOf(bob); - uint256 balance1AfterBob = currency1.balanceOf(bob); - assertApproxEqAbs( - balance0AfterBob - balance0BeforeBob, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei - ); - assertApproxEqAbs( - balance1AfterBob - balance1BeforeBob, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei - ); - } - - { - // alice collects her fees, which should be about half of the fees - uint256 balance0BeforeAlice = currency0.balanceOf(alice); - uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.startPrank(alice); - _collect(tokenIdAlice, alice, ZERO_BYTES, false); - vm.stopPrank(); - uint256 balance0AfterAlice = currency0.balanceOf(alice); - uint256 balance1AfterAlice = currency1.balanceOf(alice); - assertApproxEqAbs( - balance0AfterAlice - balance0BeforeAlice, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, - 9 wei - ); - assertApproxEqAbs( - balance1AfterAlice - balance1BeforeAlice, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, - 1 wei - ); - } - } + // function test_increaseLiquidity_withExcessFees() public { + // // Alice and Bob provide liquidity on the range + // // Alice uses her fees to increase liquidity. Excess fees are accounted to alice + // uint256 liquidityAlice = 3_000e18; + // uint256 liquidityBob = 1_000e18; + // uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // // alice provides liquidity + // vm.prank(alice); + // _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + // uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // // bob provides liquidity + // vm.prank(bob); + // _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + // uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // // swap to create fees + // uint256 swapAmount = 0.001e18; + // swap(key, true, -int256(swapAmount), ZERO_BYTES); + // swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // // alice will use half of her fees to increase liquidity + // (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + // { + // (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + // uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + // sqrtPriceX96, + // TickMath.getSqrtPriceAtTick(range.tickLower), + // TickMath.getSqrtPriceAtTick(range.tickUpper), + // token0Owed / 2, + // token1Owed / 2 + // ); + + // vm.startPrank(alice); + // _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + // vm.stopPrank(); + // } + + // { + // // bob collects his fees + // uint256 balance0BeforeBob = currency0.balanceOf(bob); + // uint256 balance1BeforeBob = currency1.balanceOf(bob); + // vm.startPrank(bob); + // _collect(tokenIdBob, bob, ZERO_BYTES, false); + // vm.stopPrank(); + // uint256 balance0AfterBob = currency0.balanceOf(bob); + // uint256 balance1AfterBob = currency1.balanceOf(bob); + // assertApproxEqAbs( + // balance0AfterBob - balance0BeforeBob, + // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + // 1 wei + // ); + // assertApproxEqAbs( + // balance1AfterBob - balance1BeforeBob, + // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + // 1 wei + // ); + // } + + // { + // // alice collects her fees, which should be about half of the fees + // uint256 balance0BeforeAlice = currency0.balanceOf(alice); + // uint256 balance1BeforeAlice = currency1.balanceOf(alice); + // vm.startPrank(alice); + // _collect(tokenIdAlice, alice, ZERO_BYTES, false); + // vm.stopPrank(); + // uint256 balance0AfterAlice = currency0.balanceOf(alice); + // uint256 balance1AfterAlice = currency1.balanceOf(alice); + // assertApproxEqAbs( + // balance0AfterAlice - balance0BeforeAlice, + // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + // 9 wei + // ); + // assertApproxEqAbs( + // balance1AfterAlice - balance1BeforeAlice, + // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + // 1 wei + // ); + // } + // } function test_increaseLiquidity_withInsufficientFees() public { // Alice and Bob provide liquidity on the range @@ -279,27 +286,29 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back // alice will use all of her fees + additional capital to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + BalanceDelta feesOwed = INonfungiblePositionManager(lpm).getFeesOwed(manager, tokenIdAlice); + { (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(range.tickLower), TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed * 2, - token1Owed * 2 + uint256(int256(feesOwed.amount0())) * 2, + uint256(int256(feesOwed.amount1())) * 2 ); uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); vm.stopPrank(); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); - assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, token0Owed, 37 wei); - assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, token1Owed, 1 wei); + // Alice owed feesOwed amount in 0 and 1 because she places feesOwed * 2 back into the pool. + assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, uint256(int256(feesOwed.amount0())), 37 wei); + assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, uint256(int256(feesOwed.amount1())), 1 wei); } { @@ -307,7 +316,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi uint256 balance0BeforeBob = currency0.balanceOf(bob); uint256 balance1BeforeBob = currency1.balanceOf(bob); vm.startPrank(bob); - _collect(tokenIdBob, bob, ZERO_BYTES, false); + _collect(tokenIdBob, bob, ZERO_BYTES); vm.stopPrank(); uint256 balance0AfterBob = currency0.balanceOf(bob); uint256 balance1AfterBob = currency1.balanceOf(bob); @@ -323,144 +332,4 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi ); } } - - function test_increaseLiquidity_withExactFees_withExactCachedFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - // bob collects fees so some of alice's fees are now cached - - vm.startPrank(bob); - _collect(tokenIdBob, bob, ZERO_BYTES, false); - vm.stopPrank(); - // swap to create more fees - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); - // alice's fees should be doubled - assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); - assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); - - uint256 balance0AliceBefore = currency0.balanceOf(alice); - uint256 balance1AliceBefore = currency1.balanceOf(alice); - - // alice will use ALL of her fees to increase liquidity - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - newToken0Owed, - newToken1Owed - ); - - vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - vm.stopPrank(); - } - - // alice did not spend any tokens - assertEq(balance0AliceBefore, currency0.balanceOf(alice)); - assertEq(balance1AliceBefore, currency1.balanceOf(alice)); - - // some dust was credited to alice's tokensOwed - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertApproxEqAbs(token0Owed, 0, 80 wei); - assertApproxEqAbs(token1Owed, 0, 80 wei); - } - - // uses donate to simulate fee revenue - function test_increaseLiquidity_withExactFees_withExactCachedFees_donate() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // donate to create fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - // bob collects fees so some of alice's fees are now cached - vm.startPrank(bob); - _collect(tokenIdBob, bob, ZERO_BYTES, false); - vm.stopPrank(); - - // donate to create more fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - - (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); - // alice's fees should be doubled - assertApproxEqAbs(newToken0Owed, token0Owed * 2, 1 wei); - assertApproxEqAbs(newToken1Owed, token1Owed * 2, 1 wei); - - uint256 balance0AliceBefore = currency0.balanceOf(alice); - uint256 balance1AliceBefore = currency1.balanceOf(alice); - - // alice will use ALL of her fees to increase liquidity - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - newToken0Owed, - newToken1Owed - ); - - vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - vm.stopPrank(); - } - - // alice did not spend any tokens - assertEq(balance0AliceBefore, currency0.balanceOf(alice), "alice spent token0"); - assertEq(balance1AliceBefore, currency1.balanceOf(alice), "alice spent token1"); - - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertEq(token0Owed, 0); - assertEq(token1Owed, 0); - - // bob still collects 5 - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdBob); - assertApproxEqAbs(token0Owed, 5e18, 1 wei); - assertApproxEqAbs(token1Owed, 5e18, 1 wei); - - vm.startPrank(bob); - BalanceDelta result = _collect(tokenIdBob, bob, ZERO_BYTES, false); - vm.stopPrank(); - assertApproxEqAbs(result.amount0(), 5e18, 1 wei); - assertApproxEqAbs(result.amount1(), 5e18, 1 wei); - } } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 959f5d4d..23452fa2 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -17,6 +17,7 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; @@ -35,6 +36,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using Planner for Planner.Plan; + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; PoolId poolId; address alice = makeAddr("ALICE"); @@ -74,25 +77,19 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - Planner.Plan memory planner = Planner.init(); - planner = planner.add( - Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) - ); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); - - (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); + uint256 tokenId = lpm.nextTokenId(); + BalanceDelta delta = _mint(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES); assertEq(tokenId, 1); assertEq(lpm.ownerOf(tokenId), address(this)); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta)); assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0"); assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())), "incorrect amount1"); @@ -116,23 +113,15 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - Planner.Plan memory planner = Planner.init(); - planner = planner.add( - Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) - ); - - bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); - (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); + uint256 tokenId = lpm.nextTokenId(); + BalanceDelta delta = _mint(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); + assertEq(uint256(int256(-delta.amount0())), amount0Desired); assertEq(uint256(int256(-delta.amount1())), amount1Desired); assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); @@ -147,18 +136,9 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - Planner.Plan memory planner = Planner.init(); - planner = planner.add( - Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), alice, ZERO_BYTES) - ); + uint256 tokenId = lpm.nextTokenId(); + _mint(range, liquidityToAdd, uint256(block.timestamp + 1), address(alice), ZERO_BYTES); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - bytes[] memory results = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); - - (, uint256 tokenId) = abi.decode(results[0], (BalanceDelta, uint256)); assertEq(tokenId, 1); assertEq(lpm.ownerOf(tokenId), alice); } @@ -220,29 +200,30 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta)); // burn liquidity uint256 balance0BeforeBurn = currency0.balanceOfSelf(); uint256 balance1BeforeBurn = currency1.balanceOfSelf(); - // TODO, encode this under one call - BalanceDelta deltaDecrease = _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); - BalanceDelta deltaCollect = _collect(tokenId, address(this), ZERO_BYTES, false); + + BalanceDelta deltaDecrease = _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES); _burn(tokenId); - (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); + + (liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); + assertEq(liquidity, 0); // TODO: slightly off by 1 bip (0.0001%) assertApproxEqRel( - currency0.balanceOfSelf(), - balance0BeforeBurn + uint256(uint128(deltaDecrease.amount0())) + uint256(uint128(deltaCollect.amount0())), - 0.0001e18 + currency0.balanceOfSelf(), balance0BeforeBurn + uint256(uint128(deltaDecrease.amount0())), 0.0001e18 ); assertApproxEqRel( - currency1.balanceOfSelf(), - balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1())) + uint256(uint128(deltaCollect.amount1())), - 0.0001e18 + currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1())), 0.0001e18 ); // OZ 721 will revert if the token does not exist @@ -254,7 +235,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_decreaseLiquidity1(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) public { uint256 tokenId; @@ -266,16 +247,13 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); - // On decrease, balance doesn't change (currenct functionality). - assertEq(currency0.balanceOfSelf() - balance0Before, 0); - assertEq(currency1.balanceOfSelf() - balance1Before, 0); + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); } // function test_decreaseLiquidity_collectFees( @@ -298,7 +276,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // uint256 balance0Before = currency0.balanceOfSelf(); // uint256 balance1Before = currency1.balanceOfSelf(); // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + // (uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); // // express key.fee as wad (i.e. 3000 = 0.003e18) diff --git a/test/position-managers/Permit.t.sol b/test/position-managers/Permit.t.sol new file mode 100644 index 00000000..d51717d9 --- /dev/null +++ b/test/position-managers/Permit.t.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IERC721Permit} from "../../contracts/interfaces/IERC721Permit.sol"; + +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; + +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + PoolId poolId; + address alice; + uint256 alicePK; + address bob; + uint256 bobPK; + + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + LiquidityRange range; + + function setUp() public { + (alice, alicePK) = makeAddrAndKey("ALICE"); + (bob, bobPK) = makeAddrAndKey("BOB"); + + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + lpm = new NonfungiblePositionManager(manager); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + + // Give tokens to Alice and Bob, with approvals + IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); + vm.startPrank(alice); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + vm.startPrank(bob); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + + // define a reusable range + range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); + } + + function test_permit_increaseLiquidity() public { + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // alice gives bob operator permissions + _permit(alice, alicePK, tokenIdAlice, bob, 1); + + // bob can increase liquidity on alice's token + uint256 newLiquidity = 2e18; + uint256 balance0BobBefore = currency0.balanceOf(bob); + uint256 balance1BobBefore = currency1.balanceOf(bob); + vm.startPrank(bob); + _increaseLiquidity(tokenIdAlice, newLiquidity, ZERO_BYTES); + vm.stopPrank(); + + // alice's position has new liquidity + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenIdAlice))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); + assertEq(liquidity, liquidityAlice + newLiquidity); + + // bob used his tokens to increase liquidity + assertGt(balance0BobBefore, currency0.balanceOf(bob)); + assertGt(balance1BobBefore, currency1.balanceOf(bob)); + } + + function test_permit_decreaseLiquidity() public { + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // alice gives bob operator permissions + _permit(alice, alicePK, tokenIdAlice, bob, 1); + + // bob can decrease liquidity on alice's token + uint256 liquidityToRemove = 0.4444e18; + vm.startPrank(bob); + _decreaseLiquidity(tokenIdAlice, liquidityToRemove, ZERO_BYTES); + vm.stopPrank(); + + // alice's position decreased liquidity + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenIdAlice))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); + + assertEq(liquidity, liquidityAlice - liquidityToRemove); + } + + function test_permit_collect() public { + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // donate to create fee revenue + uint256 currency0Revenue = 0.4444e18; + uint256 currency1Revenue = 0.2222e18; + donateRouter.donate(key, currency0Revenue, currency1Revenue, ZERO_BYTES); + + // alice gives bob operator permissions + _permit(alice, alicePK, tokenIdAlice, bob, 1); + + // TODO: test collection to recipient with a permissioned operator + + // bob collects fees to himself + address recipient = bob; + uint256 balance0BobBefore = currency0.balanceOf(bob); + uint256 balance1BobBefore = currency1.balanceOf(bob); + vm.startPrank(bob); + _collect(tokenIdAlice, recipient, ZERO_BYTES); + vm.stopPrank(); + + assertApproxEqAbs(currency0.balanceOf(recipient), balance0BobBefore + currency0Revenue, 1 wei); + assertApproxEqAbs(currency1.balanceOf(recipient), balance1BobBefore + currency1Revenue, 1 wei); + } + + // --- Fail Scenarios --- // + function test_permit_notOwnerRevert() public { + // calling permit on a token that is not owned will fail + + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob cannot permit himself on alice's token + bytes32 digest = lpm.getDigest(bob, tokenIdAlice, 0, block.timestamp + 1); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPK, digest); + + vm.startPrank(bob); + vm.expectRevert("Unauthorized"); + lpm.permit(bob, tokenIdAlice, block.timestamp + 1, 0, v, r, s); + vm.stopPrank(); + } + + function test_noPermit_increaseLiquidityRevert() public { + // increaseLiquidity fails if the owner did not permit + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob cannot increase liquidity on alice's token + uint256 newLiquidity = 2e18; + bytes memory increase = LiquidityOperations.getIncreaseEncoded(tokenIdAlice, newLiquidity, ZERO_BYTES); + vm.startPrank(bob); + vm.expectRevert("Not approved"); + lpm.modifyLiquidities(increase); + vm.stopPrank(); + } + + function test_noPermit_decreaseLiquidityRevert() public { + // decreaseLiquidity fails if the owner did not permit + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob cannot decrease liquidity on alice's token + uint256 liquidityToRemove = 0.4444e18; + bytes memory decrease = LiquidityOperations.getDecreaseEncoded(tokenIdAlice, 0.4444e18, ZERO_BYTES); + vm.startPrank(bob); + vm.expectRevert("Not approved"); + lpm.modifyLiquidities(decrease); + vm.stopPrank(); + } + + function test_noPermit_collectRevert() public { + // collect fails if the owner did not permit + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // donate to create fee revenue + uint256 currency0Revenue = 0.4444e18; + uint256 currency1Revenue = 0.2222e18; + donateRouter.donate(key, currency0Revenue, currency1Revenue, ZERO_BYTES); + + // bob cannot collect fees to a recipient + address recipient = address(0x00444400); + bytes memory collect = LiquidityOperations.getCollectEncoded(tokenIdAlice, recipient, ZERO_BYTES); + vm.startPrank(bob); + vm.expectRevert("Not approved"); + lpm.modifyLiquidities(collect); + vm.stopPrank(); + } + + function test_permit_nonceAlreadyUsed() public { + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // alice gives bob operator permissions + uint256 nonce = 1; + _permit(alice, alicePK, tokenIdAlice, bob, nonce); + + // alice cannot reuse the nonce + bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); + + vm.startPrank(alice); + vm.expectRevert(IERC721Permit.NonceAlreadyUsed.selector); + lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); + vm.stopPrank(); + } + + function test_permit_nonceAlreadyUsed_twoPositions() public { + uint256 liquidityAlice = 1e18; + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + vm.prank(alice); + range.tickLower = -600; + range.tickUpper = 600; + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice2 = lpm.nextTokenId() - 1; + + // alice gives bob operator permissions for first token + uint256 nonce = 1; + _permit(alice, alicePK, tokenIdAlice, bob, nonce); + + // alice cannot reuse the nonce for the second token + bytes32 digest = lpm.getDigest(bob, tokenIdAlice2, nonce, block.timestamp + 1); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); + + vm.startPrank(alice); + vm.expectRevert(IERC721Permit.NonceAlreadyUsed.selector); + lpm.permit(bob, tokenIdAlice2, block.timestamp + 1, nonce, v, r, s); + vm.stopPrank(); + } +} diff --git a/test/shared/FeeMath.sol b/test/shared/FeeMath.sol new file mode 100644 index 00000000..ad8746c0 --- /dev/null +++ b/test/shared/FeeMath.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; + +library FeeMath { + using SafeCast for uint256; + using StateLibrary for IPoolManager; + using PoolIdLibrary for PoolKey; + using PoolIdLibrary for PoolKey; + + /// @notice Calculates the fees accrued to a position. Used for testing purposes. + function getFeesOwed(INonfungiblePositionManager posm, IPoolManager manager, uint256 tokenId) + internal + view + returns (BalanceDelta feesOwed) + { + (, LiquidityRange memory range,) = posm.tokenPositions(tokenId); + + // getPosition(poolId, owner, tL, tU, salt) + // owner is the position manager + // salt is the tokenId + Position.Info memory position = + manager.getPosition(range.poolKey.toId(), address(posm), range.tickLower, range.tickUpper, bytes32(tokenId)); + + (uint256 feeGrowthInside0X218, uint256 feeGrowthInside1X128) = + manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); + + feesOwed = getFeesOwed( + feeGrowthInside0X218, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity + ); + } + + function getFeesOwed( + uint256 feeGrowthInside0X128, + uint256 feeGrowthInside1X128, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint256 liquidity + ) internal pure returns (BalanceDelta feesOwed) { + uint128 token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); + uint128 token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); + feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); + } + + function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint256 liquidity) + internal + pure + returns (uint128 tokenOwed) + { + tokenOwed = + (FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)).toUint128(); + } +} diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index c122ea9b..5e74db07 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; +import {Vm} from "forge-std/Vm.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; @@ -9,6 +10,7 @@ import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; import {Planner} from "../utils/Planner.sol"; contract LiquidityOperations { + Vm internal constant _vm1 = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); NonfungiblePositionManager lpm; using Planner for Planner.Plan; @@ -22,62 +24,133 @@ contract LiquidityOperations { ) internal returns (BalanceDelta) { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, deadline, recipient, hookData)); + planner = planner.finalize(_range); // Close the currencies. - Currency[] memory currencies = new Currency[](2); - currencies[0] = _range.poolKey.currency0; - currencies[1] = _range.poolKey.currency1; - bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + bytes[] memory result = lpm.modifyLiquidities(planner.zip()); return abi.decode(result[0], (BalanceDelta)); } - function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { - Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData, claims)); + // we overloaded this function because vm.prank was hitting .tokenPositions() + // TODO: now that vm.prank is hitting Planner, we can probably consolidate to a single function + function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData) + internal + returns (BalanceDelta) + { + (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + return _increaseLiquidity(_range, tokenId, liquidityToAdd, hookData); + } - (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + function _increaseLiquidity( + LiquidityRange memory _range, + uint256 tokenId, + uint256 liquidityToAdd, + bytes memory hookData + ) internal returns (BalanceDelta) { + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = _range.poolKey.currency0; - currencies[1] = _range.poolKey.currency1; - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + planner = planner.finalize(_range); // Close the currencies. + bytes[] memory result = lpm.modifyLiquidities(planner.zip()); + return abi.decode(result[0], (BalanceDelta)); } - function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) + function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData) internal returns (BalanceDelta) { - Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData, claims)); + (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + + return _decreaseLiquidity(_range, tokenId, liquidityToRemove, hookData); + } - (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + // do not make external call before unlockAndExecute, allows us to test reverts + function _decreaseLiquidity( + LiquidityRange memory _range, + uint256 tokenId, + uint256 liquidityToRemove, + bytes memory hookData + ) internal returns (BalanceDelta) { + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData)); - Currency[] memory currencies = new Currency[](2); - currencies[0] = _range.poolKey.currency0; - currencies[1] = _range.poolKey.currency1; - bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + planner = planner.finalize(_range); // Close the currencies. + bytes[] memory result = lpm.modifyLiquidities(planner.zip()); return abi.decode(result[0], (BalanceDelta)); } - function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) + function _collect(uint256 tokenId, address recipient, bytes memory hookData) internal returns (BalanceDelta) { + (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + return _collect(_range, tokenId, recipient, hookData); + } + + // do not make external call before unlockAndExecute, allows us to test reverts + function _collect(LiquidityRange memory _range, uint256 tokenId, address recipient, bytes memory hookData) internal returns (BalanceDelta) { Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.COLLECT, abi.encode(tokenId, recipient, hookData, claims)); + planner = planner.add(Actions.DECREASE, abi.encode(tokenId, 0, hookData)); - (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + planner = planner.finalize(_range); // Close the currencies. - Currency[] memory currencies = new Currency[](2); - currencies[0] = _range.poolKey.currency0; - currencies[1] = _range.poolKey.currency1; - bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + bytes[] memory result = lpm.modifyLiquidities(planner.zip()); return abi.decode(result[0], (BalanceDelta)); } function _burn(uint256 tokenId) internal { - Currency[] memory currencies = new Currency[](0); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.BURN, abi.encode(tokenId)); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + // No close needed on burn. + lpm.modifyLiquidities(planner.zip()); + } + + // TODO: organize somewhere else, or rename this file to NFTLiquidityHelpers? + function _permit(address signer, uint256 privateKey, uint256 tokenId, address operator, uint256 nonce) internal { + bytes32 digest = lpm.getDigest(operator, tokenId, 1, block.timestamp + 1); + + (uint8 v, bytes32 r, bytes32 s) = _vm1.sign(privateKey, digest); + + _vm1.prank(signer); + lpm.permit(operator, tokenId, block.timestamp + 1, nonce, v, r, s); + } + + // Helper functions for getting encoded calldata for .modifyLiquidities + function getIncreaseEncoded(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData) + internal + view + returns (bytes memory) + { + (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData)); + planner = planner.finalize(_range); + return planner.zip(); + } + + function getDecreaseEncoded(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData) + internal + view + returns (bytes memory) + { + (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData)); + planner = planner.finalize(_range); + return planner.zip(); + } + + function getCollectEncoded(uint256 tokenId, address recipient, bytes memory hookData) + internal + view + returns (bytes memory) + { + (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.DECREASE, abi.encode(tokenId, 0, hookData)); + + // TODO: allow recipient when supported on CLOSE_CURRENCY? + planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(_range.poolKey.currency0)); + planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(_range.poolKey.currency1)); + return planner.zip(); } } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 5def37bc..29755ce0 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -26,15 +26,12 @@ contract LiquidityFuzzers is Fuzzers { LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - Planner.Plan memory plan = Planner.init().add( + Planner.Plan memory planner = Planner.init().add( Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData) ); - Currency[] memory currencies = new Currency[](2); - currencies[0] = key.currency0; - currencies[1] = key.currency1; - - lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies)); + planner = planner.finalize(range); + lpm.modifyLiquidities(planner.zip()); uint256 tokenId = lpm.nextTokenId() - 1; return (tokenId, params); diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol index 788622c5..4d9a87b8 100644 --- a/test/utils/Planner.sol +++ b/test/utils/Planner.sol @@ -2,18 +2,21 @@ pragma solidity ^0.8.20; import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; library Planner { + using Planner for Plan; + struct Plan { Actions[] actions; bytes[] params; } - function init() public pure returns (Plan memory plan) { + function init() internal pure returns (Plan memory plan) { return Plan({actions: new Actions[](0), params: new bytes[](0)}); } - function add(Plan memory plan, Actions action, bytes memory param) public pure returns (Plan memory) { + function add(Plan memory plan, Actions action, bytes memory param) internal pure returns (Plan memory) { Actions[] memory actions = new Actions[](plan.actions.length + 1); bytes[] memory params = new bytes[](plan.params.length + 1); @@ -28,4 +31,14 @@ library Planner { return Plan({actions: actions, params: params}); } + + function finalize(Plan memory plan, LiquidityRange memory range) internal pure returns (Plan memory) { + plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(range.poolKey.currency0)); + plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(range.poolKey.currency1)); + return plan; + } + + function zip(Plan memory plan) internal pure returns (bytes memory) { + return abi.encode(plan.actions, plan.params); + } } From 07a3e63c02032252974ef5930a5f36dcdc72c179 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:37:48 -0400 Subject: [PATCH 65/98] Multicall & initialize (#154) * add multicall and an external function for initialization, with tests * test multicall contract * gas snapshot multicall * fix ci test * fix tests * forge fmt * change how msg.value is used in multicall mock --------- Co-authored-by: Sara Reynolds --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/collect_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mint.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 1 - .forge-snapshots/mint_differentRanges.snap | 2 +- .forge-snapshots/mint_same_tickLower.snap | 2 +- .forge-snapshots/mint_same_tickUpper.snap | 2 +- .../multicall_initialize_mint.snap | 1 + .forge-snapshots/permit.snap | 2 +- .forge-snapshots/permit_secondPosition.snap | 2 +- .forge-snapshots/permit_twice.snap | 2 +- .forge-snapshots/sameRange_collect.snap | 2 +- .../sameRange_decreaseAllLiquidity.snap | 2 +- .forge-snapshots/sameRange_mint.snap | 2 +- contracts/NonfungiblePositionManager.sol | 10 ++- contracts/base/PoolInitializer.sol | 15 ++++ test/base/Multicall.t.sol | 87 +++++++++++++++++++ test/mock/MockMulticall.sol | 29 +++++++ test/position-managers/Gas.t.sol | 25 ++++++ test/position-managers/Multicall.t.sol | 80 +++++++++++++++++ .../NonfungiblePositionManager.t.sol | 15 ++++ 27 files changed, 279 insertions(+), 20 deletions(-) delete mode 100644 .forge-snapshots/mintWithLiquidity.snap create mode 100644 .forge-snapshots/multicall_initialize_mint.snap create mode 100644 contracts/base/PoolInitializer.sol create mode 100644 test/base/Multicall.t.sol create mode 100644 test/mock/MockMulticall.sol create mode 100644 test/position-managers/Multicall.t.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index ea1b386d..3b193dac 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -174403 \ No newline at end of file +174839 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index ac2b256d..61a67212 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -189102 \ No newline at end of file +189538 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 8177e70b..982ad312 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -188803 \ No newline at end of file +189239 \ No newline at end of file diff --git a/.forge-snapshots/collect_erc20.snap b/.forge-snapshots/collect_erc20.snap index d0c885cd..172b522e 100644 --- a/.forge-snapshots/collect_erc20.snap +++ b/.forge-snapshots/collect_erc20.snap @@ -1 +1 @@ -166233 \ No newline at end of file +166669 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 25b57eed..39849f0b 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -131611 \ No newline at end of file +132047 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 25b57eed..39849f0b 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -131611 \ No newline at end of file +132047 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index b5c9ff46..a2940c3b 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -161077 \ No newline at end of file +161513 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index b5c9ff46..a2940c3b 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -161077 \ No newline at end of file +161513 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index 8e99e7bf..8abb4612 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -434266 \ No newline at end of file +434774 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap deleted file mode 100644 index ebad633c..00000000 --- a/.forge-snapshots/mintWithLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -490196 \ No newline at end of file diff --git a/.forge-snapshots/mint_differentRanges.snap b/.forge-snapshots/mint_differentRanges.snap index 567f4897..b9c27716 100644 --- a/.forge-snapshots/mint_differentRanges.snap +++ b/.forge-snapshots/mint_differentRanges.snap @@ -1 +1 @@ -400066 \ No newline at end of file +400574 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickLower.snap b/.forge-snapshots/mint_same_tickLower.snap index c78e18e3..64f913b1 100644 --- a/.forge-snapshots/mint_same_tickLower.snap +++ b/.forge-snapshots/mint_same_tickLower.snap @@ -1 +1 @@ -394048 \ No newline at end of file +394556 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickUpper.snap b/.forge-snapshots/mint_same_tickUpper.snap index 43b574ef..b3f23dae 100644 --- a/.forge-snapshots/mint_same_tickUpper.snap +++ b/.forge-snapshots/mint_same_tickUpper.snap @@ -1 +1 @@ -394690 \ No newline at end of file +395198 \ No newline at end of file diff --git a/.forge-snapshots/multicall_initialize_mint.snap b/.forge-snapshots/multicall_initialize_mint.snap new file mode 100644 index 00000000..a046c25a --- /dev/null +++ b/.forge-snapshots/multicall_initialize_mint.snap @@ -0,0 +1 @@ +478674 \ No newline at end of file diff --git a/.forge-snapshots/permit.snap b/.forge-snapshots/permit.snap index f51e74ce..269d80a6 100644 --- a/.forge-snapshots/permit.snap +++ b/.forge-snapshots/permit.snap @@ -1 +1 @@ -75049 \ No newline at end of file +75071 \ No newline at end of file diff --git a/.forge-snapshots/permit_secondPosition.snap b/.forge-snapshots/permit_secondPosition.snap index 0925e7d6..15e35dee 100644 --- a/.forge-snapshots/permit_secondPosition.snap +++ b/.forge-snapshots/permit_secondPosition.snap @@ -1 +1 @@ -57949 \ No newline at end of file +57971 \ No newline at end of file diff --git a/.forge-snapshots/permit_twice.snap b/.forge-snapshots/permit_twice.snap index f1519e1c..6ef9d761 100644 --- a/.forge-snapshots/permit_twice.snap +++ b/.forge-snapshots/permit_twice.snap @@ -1 +1 @@ -40849 \ No newline at end of file +40871 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_collect.snap b/.forge-snapshots/sameRange_collect.snap index d0c885cd..172b522e 100644 --- a/.forge-snapshots/sameRange_collect.snap +++ b/.forge-snapshots/sameRange_collect.snap @@ -1 +1 @@ -166233 \ No newline at end of file +166669 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap index b7718b47..20cc0ae9 100644 --- a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap +++ b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap @@ -1 +1 @@ -144492 \ No newline at end of file +144928 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_mint.snap b/.forge-snapshots/sameRange_mint.snap index 2202c681..3eb8d842 100644 --- a/.forge-snapshots/sameRange_mint.snap +++ b/.forge-snapshots/sameRange_mint.snap @@ -1 +1 @@ -337372 \ No newline at end of file +337880 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index b73e6178..d85936bb 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.24; import {ERC721Permit} from "./base/ERC721Permit.sol"; import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; +import {Multicall} from "./base/Multicall.sol"; +import {PoolInitializer} from "./base/PoolInitializer.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -19,7 +21,13 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit { +contract NonfungiblePositionManager is + INonfungiblePositionManager, + BaseLiquidityManagement, + ERC721Permit, + PoolInitializer, + Multicall +{ using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; diff --git a/contracts/base/PoolInitializer.sol b/contracts/base/PoolInitializer.sol new file mode 100644 index 00000000..caba2a51 --- /dev/null +++ b/contracts/base/PoolInitializer.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {ImmutableState} from "./ImmutableState.sol"; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +abstract contract PoolInitializer is ImmutableState { + function initializePool(PoolKey calldata key, uint160 sqrtPriceX96, bytes calldata hookData) + external + returns (int24) + { + return manager.initialize(key, sqrtPriceX96, hookData); + } +} diff --git a/test/base/Multicall.t.sol b/test/base/Multicall.t.sol new file mode 100644 index 00000000..c7c6fa78 --- /dev/null +++ b/test/base/Multicall.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import {MockMulticall} from "../mock/MockMulticall.sol"; + +contract MulticallTest is Test { + MockMulticall multicall; + + function setUp() public { + multicall = new MockMulticall(); + } + + function test_multicall() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 10, 20); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); + + bytes[] memory results = multicall.multicall(calls); + + (uint256 a, uint256 b) = abi.decode(results[0], (uint256, uint256)); + assertEq(a, 10); + assertEq(b, 20); + + (a, b) = abi.decode(results[1], (uint256, uint256)); + assertEq(a, 1); + assertEq(b, 2); + } + + function test_multicall_firstRevert() public { + bytes[] memory calls = new bytes[](2); + calls[0] = + abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithError.selector, "First call failed"); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); + + vm.expectRevert("First call failed"); + multicall.multicall(calls); + } + + function test_multicall_secondRevert() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); + calls[1] = + abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithError.selector, "Second call failed"); + + vm.expectRevert("Second call failed"); + multicall.multicall(calls); + } + + function test_multicall_pays() public { + assertEq(address(multicall).balance, 0); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).pays.selector); + multicall.multicall{value: 100}(calls); + assertEq(address(multicall).balance, 100); + assertEq(multicall.paid(), 100); + } + + function test_multicall_returnSender() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).returnSender.selector); + bytes[] memory results = multicall.multicall(calls); + address sender = abi.decode(results[0], (address)); + assertEq(sender, address(this)); + } + + function test_multicall_returnSender_prank() public { + address alice = makeAddr("ALICE"); + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).returnSender.selector, alice); + vm.prank(alice); + bytes[] memory results = multicall.multicall(calls); + address sender = abi.decode(results[0], (address)); + assertEq(sender, alice); + } + + function test_multicall_double_send() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).pays.selector); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).pays.selector); + + multicall.multicall{value: 100}(calls); + assertEq(address(multicall).balance, 100); + assertEq(multicall.paid(), 100); + } +} diff --git a/test/mock/MockMulticall.sol b/test/mock/MockMulticall.sol new file mode 100644 index 00000000..0c854c27 --- /dev/null +++ b/test/mock/MockMulticall.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import "../../contracts/base/Multicall.sol"; + +contract MockMulticall is Multicall { + function functionThatRevertsWithError(string memory error) external pure { + revert(error); + } + + struct Tuple { + uint256 a; + uint256 b; + } + + function functionThatReturnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) { + tuple = Tuple({a: a, b: b}); + } + + uint256 public paid; + + function pays() external payable { + paid = msg.value; + } + + function returnSender() external view returns (address) { + return msg.sender; + } +} diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index af45f538..99691490 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -339,6 +339,31 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { function test_gas_burnEmpty() public {} function test_gas_collect() public {} + function test_gas_multicall_initialize_mint() public { + key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); + + // Use multicall to initialize a pool and mint liquidity + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector( + NonfungiblePositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES + ); + + range = LiquidityRange({ + poolKey: key, + tickLower: TickMath.minUsableTick(key.tickSpacing), + tickUpper: TickMath.maxUsableTick(key.tickSpacing) + }); + + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.MINT, abi.encode(range, 100e18, block.timestamp + 1, address(this), ZERO_BYTES)); + planner = planner.finalize(range); + + calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); + + lpm.multicall(calls); + snapLastCall("multicall_initialize_mint"); + } + function test_gas_permit() public { // alice permits for the first time uint256 liquidityAlice = 1e18; diff --git a/test/position-managers/Multicall.t.sol b/test/position-managers/Multicall.t.sol new file mode 100644 index 00000000..abf93bb5 --- /dev/null +++ b/test/position-managers/Multicall.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; + +contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + using Planner for Planner.Plan; + + PoolId poolId; + address alice = makeAddr("ALICE"); + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + + lpm = new NonfungiblePositionManager(manager); + + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + } + + function test_multicall_initializePool_mint() public { + key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); + + // Use multicall to initialize a pool and mint liquidity + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector( + NonfungiblePositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES + ); + + LiquidityRange memory range = LiquidityRange({ + poolKey: key, + tickLower: TickMath.minUsableTick(key.tickSpacing), + tickUpper: TickMath.maxUsableTick(key.tickSpacing) + }); + + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.MINT, abi.encode(range, 100e18, block.timestamp + 1, address(this), ZERO_BYTES)); + planner = planner.finalize(range); + + calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); + + lpm.multicall(calls); + + // test swap, doesn't revert + swap(key, true, -1e18, ZERO_BYTES); + } +} diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 23452fa2..a6460d88 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -290,4 +290,19 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mintTransferCollect() public {} function test_mintTransferIncrease() public {} function test_mintTransferDecrease() public {} + + function test_initialize(IPoolManager.ModifyLiquidityParams memory params) public { + // initialize a new pool and add liquidity + key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); + lpm.initializePool(key, SQRT_PRICE_1_1, ZERO_BYTES); + + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + + // add liquidity to verify pool initialized + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + _mint(range, 100e18, block.timestamp + 1, address(this), ZERO_BYTES); + + assertEq(lpm.ownerOf(1), address(this)); + } } From cae6ed66de53dfd2ed9a0d64f5708034e7baf62a Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:24:49 -0400 Subject: [PATCH 66/98] prep shared actions (#158) --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/collect_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mint.snap | 2 +- .forge-snapshots/mint_differentRanges.snap | 2 +- .forge-snapshots/mint_same_tickLower.snap | 2 +- .forge-snapshots/mint_same_tickUpper.snap | 2 +- .../multicall_initialize_mint.snap | 2 +- .forge-snapshots/sameRange_collect.snap | 2 +- .../sameRange_decreaseAllLiquidity.snap | 2 +- .forge-snapshots/sameRange_mint.snap | 2 +- contracts/NonfungiblePositionManager.sol | 110 ++++++++-------- .../INonfungiblePositionManager.sol | 4 +- test/position-managers/Execute.t.sol | 14 +- test/position-managers/FeeCollection.t.sol | 8 +- test/position-managers/Gas.t.sol | 121 ++++++++++-------- .../position-managers/IncreaseLiquidity.t.sol | 17 +-- test/position-managers/Multicall.t.sol | 2 +- .../NonfungiblePositionManager.t.sol | 14 +- test/position-managers/Permit.t.sol | 35 ++--- test/shared/LiquidityOperations.sol | 30 +++-- test/shared/fuzz/LiquidityFuzzers.sol | 8 +- 27 files changed, 210 insertions(+), 185 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 3b193dac..6e06986c 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -174839 \ No newline at end of file +175084 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 61a67212..0d9d33ce 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -189538 \ No newline at end of file +189783 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 982ad312..81eabc96 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -189239 \ No newline at end of file +189484 \ No newline at end of file diff --git a/.forge-snapshots/collect_erc20.snap b/.forge-snapshots/collect_erc20.snap index 172b522e..56edd0e5 100644 --- a/.forge-snapshots/collect_erc20.snap +++ b/.forge-snapshots/collect_erc20.snap @@ -1 +1 @@ -166669 \ No newline at end of file +166914 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 39849f0b..f266306b 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -132047 \ No newline at end of file +132292 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 39849f0b..f266306b 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -132047 \ No newline at end of file +132292 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index a2940c3b..0f56b6ff 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -161513 \ No newline at end of file +161758 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index a2940c3b..0f56b6ff 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -161513 \ No newline at end of file +161758 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index 8abb4612..7c7fb5bc 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -434774 \ No newline at end of file +433657 \ No newline at end of file diff --git a/.forge-snapshots/mint_differentRanges.snap b/.forge-snapshots/mint_differentRanges.snap index b9c27716..ffc31809 100644 --- a/.forge-snapshots/mint_differentRanges.snap +++ b/.forge-snapshots/mint_differentRanges.snap @@ -1 +1 @@ -400574 \ No newline at end of file +399457 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickLower.snap b/.forge-snapshots/mint_same_tickLower.snap index 64f913b1..64f78087 100644 --- a/.forge-snapshots/mint_same_tickLower.snap +++ b/.forge-snapshots/mint_same_tickLower.snap @@ -1 +1 @@ -394556 \ No newline at end of file +393439 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickUpper.snap b/.forge-snapshots/mint_same_tickUpper.snap index b3f23dae..45062e45 100644 --- a/.forge-snapshots/mint_same_tickUpper.snap +++ b/.forge-snapshots/mint_same_tickUpper.snap @@ -1 +1 @@ -395198 \ No newline at end of file +394081 \ No newline at end of file diff --git a/.forge-snapshots/multicall_initialize_mint.snap b/.forge-snapshots/multicall_initialize_mint.snap index a046c25a..a5adbe1a 100644 --- a/.forge-snapshots/multicall_initialize_mint.snap +++ b/.forge-snapshots/multicall_initialize_mint.snap @@ -1 +1 @@ -478674 \ No newline at end of file +477392 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_collect.snap b/.forge-snapshots/sameRange_collect.snap index 172b522e..56edd0e5 100644 --- a/.forge-snapshots/sameRange_collect.snap +++ b/.forge-snapshots/sameRange_collect.snap @@ -1 +1 @@ -166669 \ No newline at end of file +166914 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap index 20cc0ae9..bb30d07b 100644 --- a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap +++ b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap @@ -1 +1 @@ -144928 \ No newline at end of file +145173 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_mint.snap b/.forge-snapshots/sameRange_mint.snap index 3eb8d842..2601c0bd 100644 --- a/.forge-snapshots/sameRange_mint.snap +++ b/.forge-snapshots/sameRange_mint.snap @@ -1 +1 @@ -337880 \ No newline at end of file +336763 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index d85936bb..32318dbf 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -49,7 +49,11 @@ contract NonfungiblePositionManager is /// @param unlockData is an encoding of actions, params, and currencies /// @return returnData is the endocing of each actions return information - function modifyLiquidities(bytes calldata unlockData) public returns (bytes[] memory) { + function modifyLiquidities(bytes calldata unlockData, uint256 deadline) + external + checkDeadline(deadline) + returns (bytes[] memory) + { // TODO: Edit the encoding/decoding. return abi.decode(manager.unlock(abi.encode(unlockData, msg.sender)), (bytes[])); } @@ -70,25 +74,18 @@ contract NonfungiblePositionManager is returns (bytes[] memory returnData) { if (actions.length != params.length) revert MismatchedLengths(); - returnData = new bytes[](actions.length); for (uint256 i; i < actions.length; i++) { if (actions[i] == Actions.INCREASE) { - (uint256 tokenId, uint256 liquidity, bytes memory hookData) = - abi.decode(params[i], (uint256, uint256, bytes)); - returnData[i] = abi.encode(increaseLiquidity(tokenId, liquidity, hookData, sender)); + returnData[i] = _increase(params[i], sender); } else if (actions[i] == Actions.DECREASE) { - (uint256 tokenId, uint256 liquidity, bytes memory hookData) = - abi.decode(params[i], (uint256, uint256, bytes)); - returnData[i] = abi.encode(decreaseLiquidity(tokenId, liquidity, hookData, sender)); + returnData[i] = _decrease(params[i], sender); } else if (actions[i] == Actions.MINT) { - (LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, bytes memory hookData) - = abi.decode(params[i], (LiquidityRange, uint256, uint256, address, bytes)); - returnData[i] = abi.encode(mint(range, liquidity, deadline, owner, hookData)); + returnData[i] = _mint(params[i]); } else if (actions[i] == Actions.CLOSE_CURRENCY) { - (Currency currency) = abi.decode(params[i], (Currency)); - returnData[i] = abi.encode(close(currency, sender)); + returnData[i] = _close(params[i], sender); } else if (actions[i] == Actions.BURN) { + // TODO: Burn will just be moved outside of this.. or coupled with a decrease.. (uint256 tokenId) = abi.decode(params[i], (uint256)); burn(tokenId, sender); } else { @@ -97,13 +94,42 @@ contract NonfungiblePositionManager is } } - function mint( - LiquidityRange memory range, - uint256 liquidity, - uint256 deadline, - address owner, - bytes memory hookData - ) internal checkDeadline(deadline) returns (BalanceDelta delta) { + /// @param param is an encoding of uint256 tokenId, uint256 liquidity, bytes hookData + /// @param sender the msg.sender, set by the `modifyLiquidities` function before the `unlockCallback`. Using msg.sender directly inside + /// the _unlockCallback will be the pool manager. + /// @return returns an encoding of the BalanceDelta applied by this increase call, including credited fees. + /// @dev Calling increase with 0 liquidity will credit the caller with any underlying fees of the position + function _increase(bytes memory param, address sender) internal returns (bytes memory) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData) = abi.decode(param, (uint256, uint256, bytes)); + + _requireApprovedOrOwner(tokenId, sender); + + TokenPosition memory tokenPos = tokenPositions[tokenId]; + // Note: The tokenId is used as the salt for this position, so every minted liquidity has unique storage in the pool manager. + (BalanceDelta delta,) = _modifyLiquidity(tokenPos.range, liquidity.toInt256(), bytes32(tokenId), hookData); + return abi.encode(delta); + } + + /// @param params is an encoding of uint256 tokenId, uint256 liquidity, bytes hookData + /// @param sender the msg.sender, set by the `modifyLiquidities` function before the `unlockCallback`. Using msg.sender directly inside + /// the _unlockCallback will be the pool manager. + /// @return returns an encoding of the BalanceDelta applied by this increase call, including credited fees. + /// @dev Calling decrease with 0 liquidity will credit the caller with any underlying fees of the position + function _decrease(bytes memory params, address sender) internal returns (bytes memory) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData) = abi.decode(params, (uint256, uint256, bytes)); + + _requireApprovedOrOwner(tokenId, sender); + + TokenPosition memory tokenPos = tokenPositions[tokenId]; + // Note: the tokenId is used as the salt. + (BalanceDelta delta,) = _modifyLiquidity(tokenPos.range, -(liquidity.toInt256()), bytes32(tokenId), hookData); + return abi.encode(delta); + } + + function _mint(bytes memory param) internal returns (bytes memory) { + (LiquidityRange memory range, uint256 liquidity, address owner, bytes memory hookData) = + abi.decode(param, (LiquidityRange, uint256, address, bytes)); + // mint receipt token uint256 tokenId; unchecked { @@ -111,38 +137,20 @@ contract NonfungiblePositionManager is } _mint(owner, tokenId); - (delta,) = _modifyLiquidity(range, liquidity.toInt256(), bytes32(tokenId), hookData); + (BalanceDelta delta,) = _modifyLiquidity(range, liquidity.toInt256(), bytes32(tokenId), hookData); tokenPositions[tokenId] = TokenPosition({owner: owner, range: range, operator: address(0x0)}); + return abi.encode(delta); } - // Note: Calling increase with 0 will accrue any underlying fees. - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, address sender) - internal - isAuthorizedForToken(tokenId, sender) - returns (BalanceDelta delta) - { - TokenPosition memory tokenPos = tokenPositions[tokenId]; - // Note: The tokenId is used as the salt for this position, so every minted liquidity has unique storage in the pool manager. - (delta,) = _modifyLiquidity(tokenPos.range, liquidity.toInt256(), bytes32(tokenId), hookData); - } - - // Note: Calling decrease with 0 will accrue any underlying fees. - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, address sender) - internal - isAuthorizedForToken(tokenId, sender) - returns (BalanceDelta delta) - { - TokenPosition memory tokenPos = tokenPositions[tokenId]; - (delta,) = _modifyLiquidity(tokenPos.range, -(liquidity.toInt256()), bytes32(tokenId), hookData); - } - - // there is no authorization scheme because the payer/recipient is always the sender - // TODO: Add more advanced functionality for other payers/recipients, needs auth scheme. - function close(Currency currency, address sender) internal returns (int256 currencyDelta) { + /// @param params is an encoding of the Currency to close + /// @param sender is the msg.sender encoded by the `modifyLiquidities` function before the `unlockCallback`. + /// @return int256 the balance of the currency being settled by this call + function _close(bytes memory params, address sender) internal returns (bytes memory) { + (Currency currency) = abi.decode(params, (Currency)); // this address has applied all deltas on behalf of the user/owner // it is safe to close this entire delta because of slippage checks throughout the batched calls. - currencyDelta = manager.currencyDelta(address(this), currency); + int256 currencyDelta = manager.currencyDelta(address(this), currency); // the sender is the payer or receiver if (currencyDelta < 0) { @@ -150,9 +158,12 @@ contract NonfungiblePositionManager is } else { currency.take(manager, sender, uint256(int256(currencyDelta)), false); } + + return abi.encode(currencyDelta); } - function burn(uint256 tokenId, address sender) internal isAuthorizedForToken(tokenId, sender) { + function burn(uint256 tokenId, address sender) internal { + _requireApprovedOrOwner(tokenId, sender); // We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId. TokenPosition memory tokenPos = tokenPositions[tokenId]; // Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. @@ -187,9 +198,8 @@ contract NonfungiblePositionManager is return tokenPositions[tokenId].operator; } - modifier isAuthorizedForToken(uint256 tokenId, address sender) { - require(_isApprovedOrOwner(sender, tokenId), "Not approved"); - _; + function _requireApprovedOrOwner(uint256 tokenId, address sender) internal view { + if (!_isApprovedOrOwner(sender, tokenId)) revert NotApproved(sender); } modifier checkDeadline(uint256 deadline) { diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index a13380f2..0fe32240 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -16,6 +16,7 @@ enum Actions { interface INonfungiblePositionManager { error MismatchedLengths(); + error NotApproved(address caller); struct TokenPosition { address owner; @@ -31,8 +32,9 @@ interface INonfungiblePositionManager { /// @notice Batches many liquidity modification calls to pool manager /// @param payload is an encoding of actions, params, and currencies + /// @param deadline is the deadline for the batched actions to be executed /// @return returnData is the endocing of each actions return information - function modifyLiquidities(bytes calldata payload) external returns (bytes[] memory); + function modifyLiquidities(bytes calldata payload, uint256 deadline) external returns (bytes[] memory); /// TODO Can decide if we want burn to auto encode a decrease/collect. //// @notice Burn a position and delete the tokenId diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index b738d02e..64effe2a 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -82,7 +82,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - _mint(range, initialLiquidity, block.timestamp, address(this), ZERO_BYTES); + _mint(range, initialLiquidity, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; _increaseLiquidity(tokenId, liquidityToAdd, ZERO_BYTES); @@ -102,7 +102,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit initialiLiquidity = bound(initialiLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); - _mint(range, initialiLiquidity, block.timestamp, address(this), ZERO_BYTES); + _mint(range, initialiLiquidity, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = Planner.init(); @@ -111,7 +111,8 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd2, ZERO_BYTES)); planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + (bytes memory actions) = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); bytes32 positionId = keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); @@ -129,13 +130,12 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit Planner.Plan memory planner = Planner.init(); - planner = planner.add( - Actions.MINT, abi.encode(range, initialLiquidity, block.timestamp + 1, address(this), ZERO_BYTES) - ); + planner = planner.add(Actions.MINT, abi.encode(range, initialLiquidity, address(this), ZERO_BYTES)); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES)); planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); bytes32 positionId = keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 9e608ef0..2a46136a 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -166,11 +166,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); - _mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, uint256(params.liquidityDelta), alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(bob); - _mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityDeltaBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // confirm the positions are same range @@ -228,14 +228,14 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 liquidityBob = 1000e18; vm.prank(alice); - BalanceDelta lpDeltaAlice = _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + BalanceDelta lpDeltaAlice = _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; uint256 aliceBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)); uint256 aliceBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)); vm.prank(bob); - BalanceDelta lpDeltaBob = _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + BalanceDelta lpDeltaBob = _mint(range, liquidityBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; uint256 bobBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)); diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 99691490..11636a8a 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -87,11 +87,11 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { } function test_gas_mint() public { - Planner.Plan memory planner = Planner.init().add( - Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES) - ); + Planner.Plan memory planner = + Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(this), ZERO_BYTES)); planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("mint"); } @@ -99,15 +99,15 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Explicitly mint to a new range on the same pool. LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 0, tickUpper: 60}); vm.startPrank(bob); - _mint(bob_mint, 10_000 ether, block.timestamp + 1, address(bob), ZERO_BYTES); + _mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff range, diff user. - Planner.Plan memory planner = Planner.init().add( - Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(alice), ZERO_BYTES) - ); + Planner.Plan memory planner = + Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("mint_differentRanges"); } @@ -115,15 +115,15 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Explicitly mint to range whos tickLower is the same. LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: -60}); vm.startPrank(bob); - _mint(bob_mint, 10_000 ether, block.timestamp + 1, address(bob), ZERO_BYTES); + _mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff range, diff user. - Planner.Plan memory planner = Planner.init().add( - Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(alice), ZERO_BYTES) - ); + Planner.Plan memory planner = + Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("mint_same_tickLower"); } @@ -131,20 +131,20 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Explicitly mint to range whos tickUpperis the same. LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 60, tickUpper: 300}); vm.startPrank(bob); - _mint(bob_mint, 10_000 ether, block.timestamp + 1, address(bob), ZERO_BYTES); + _mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff range, diff user. - Planner.Plan memory planner = Planner.init().add( - Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(alice), ZERO_BYTES) - ); + Planner.Plan memory planner = + Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("mint_same_tickUpper"); } function test_gas_increaseLiquidity_erc20() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = @@ -152,12 +152,13 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("increaseLiquidity_erc20"); } function test_gas_increaseLiquidity_erc6909() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = @@ -165,7 +166,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("increaseLiquidity_erc6909"); } @@ -178,12 +180,12 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, bob, ZERO_BYTES); // donate to create fees uint256 amountDonate = 0.2e18; @@ -207,7 +209,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("autocompound_exactUnclaimedFees"); } @@ -219,12 +222,12 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees @@ -239,7 +242,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); vm.prank(bob); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); // donate to create more fees donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); @@ -262,7 +266,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); + actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); } } @@ -276,12 +281,12 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, bob, ZERO_BYTES); // donate to create fees uint256 amountDonate = 20e18; @@ -305,12 +310,13 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("autocompound_excessFeesCredit"); } function test_gas_decreaseLiquidity_erc20() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = @@ -318,12 +324,13 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); - lpm.modifyLiquidities(abi.encode(planner.actions, planner.params)); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("decreaseLiquidity_erc20"); } function test_gas_decreaseLiquidity_erc6909() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = @@ -331,7 +338,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("decreaseLiquidity_erc6909"); } @@ -355,7 +363,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { }); Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.MINT, abi.encode(range, 100e18, block.timestamp + 1, address(this), ZERO_BYTES)); + planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); planner = planner.finalize(range); calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); @@ -368,7 +376,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice permits for the first time uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -386,7 +394,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice permits for her two tokens, benchmark the 2nd permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -399,7 +407,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice creates another position vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -418,7 +426,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -440,7 +448,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { } function test_gas_collect_erc20() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -450,30 +458,31 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 0, ZERO_BYTES, false)); planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("collect_erc20"); } // same-range gas tests function test_gas_sameRange_mint() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); - Planner.Plan memory planner = Planner.init().add( - Actions.MINT, abi.encode(range, 10_001 ether, block.timestamp + 1, address(alice), ZERO_BYTES) - ); + Planner.Plan memory planner = + Planner.init().add(Actions.MINT, abi.encode(range, 10_001 ether, address(alice), ZERO_BYTES)); planner = planner.finalize(range); vm.prank(alice); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("sameRange_mint"); } function test_gas_sameRange_decrease() public { // two positions of the same range, one of them decreases the entirety of the liquidity vm.startPrank(alice); - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); vm.stopPrank(); - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = @@ -481,17 +490,18 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("sameRange_decreaseAllLiquidity"); } function test_gas_sameRange_collect() public { // two positions of the same range, one of them collects all their fees vm.startPrank(alice); - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); vm.stopPrank(); - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -501,7 +511,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); snapLastCall("sameRange_collect"); } } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 1975a882..c955cae5 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -76,7 +76,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } - function test_increaseLiquidity_withExactFees() public { + function test_increaseLiquidity_withExactFees1() public { // Alice and Bob provide liquidity on the range // Alice uses her exact fees to increase liquidity (compounding) @@ -85,12 +85,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, bob, ZERO_BYTES); // swap to create fees uint256 swapAmount = 0.001e18; @@ -120,7 +120,8 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi planner = planner.add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); planner = planner.finalize(range); vm.startPrank(alice); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); vm.stopPrank(); // It is not exact because of the error in the fee calculation and error in the @@ -142,12 +143,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, bob, ZERO_BYTES); // donate to create fees uint256 amountDonate = 0.2e18; @@ -272,12 +273,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees diff --git a/test/position-managers/Multicall.t.sol b/test/position-managers/Multicall.t.sol index abf93bb5..d753a007 100644 --- a/test/position-managers/Multicall.t.sol +++ b/test/position-managers/Multicall.t.sol @@ -67,7 +67,7 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid }); Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.MINT, abi.encode(range, 100e18, block.timestamp + 1, address(this), ZERO_BYTES)); + planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); planner = planner.finalize(range); calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index a6460d88..a707a7cf 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -59,14 +59,10 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi planner = planner.add(Actions.MINT, abi.encode("test")); planner = planner.add(Actions.BURN, abi.encode("test")); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - bytes[] memory badParams = new bytes[](1); vm.expectRevert(INonfungiblePositionManager.MismatchedLengths.selector); - lpm.modifyLiquidities(abi.encode(planner.actions, badParams, currencies)); + lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1); } function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { @@ -81,7 +77,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance1Before = currency1.balanceOfSelf(); uint256 tokenId = lpm.nextTokenId(); - BalanceDelta delta = _mint(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES); + BalanceDelta delta = _mint(range, liquidityToAdd, address(this), ZERO_BYTES); assertEq(tokenId, 1); assertEq(lpm.ownerOf(tokenId), address(this)); @@ -114,7 +110,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance1Before = currency1.balanceOfSelf(); uint256 tokenId = lpm.nextTokenId(); - BalanceDelta delta = _mint(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES); + BalanceDelta delta = _mint(range, liquidityToAdd, address(this), ZERO_BYTES); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); @@ -137,7 +133,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 tokenId = lpm.nextTokenId(); - _mint(range, liquidityToAdd, uint256(block.timestamp + 1), address(alice), ZERO_BYTES); + _mint(range, liquidityToAdd, address(alice), ZERO_BYTES); assertEq(tokenId, 1); assertEq(lpm.ownerOf(tokenId), alice); @@ -301,7 +297,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // add liquidity to verify pool initialized LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - _mint(range, 100e18, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 100e18, address(this), ZERO_BYTES); assertEq(lpm.ownerOf(1), address(this)); } diff --git a/test/position-managers/Permit.t.sol b/test/position-managers/Permit.t.sol index d51717d9..660e4479 100644 --- a/test/position-managers/Permit.t.sol +++ b/test/position-managers/Permit.t.sol @@ -22,6 +22,7 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IERC721Permit} from "../../contracts/interfaces/IERC721Permit.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; @@ -83,7 +84,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_increaseLiquidity() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives bob operator permissions @@ -111,7 +112,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_decreaseLiquidity() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives bob operator permissions @@ -134,7 +135,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_collect() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -165,7 +166,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob cannot permit himself on alice's token @@ -183,15 +184,15 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation // increaseLiquidity fails if the owner did not permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob cannot increase liquidity on alice's token uint256 newLiquidity = 2e18; bytes memory increase = LiquidityOperations.getIncreaseEncoded(tokenIdAlice, newLiquidity, ZERO_BYTES); vm.startPrank(bob); - vm.expectRevert("Not approved"); - lpm.modifyLiquidities(increase); + vm.expectRevert(abi.encodeWithSelector(INonfungiblePositionManager.NotApproved.selector, address(bob))); + lpm.modifyLiquidities(increase, _deadline); vm.stopPrank(); } @@ -199,15 +200,15 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation // decreaseLiquidity fails if the owner did not permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob cannot decrease liquidity on alice's token uint256 liquidityToRemove = 0.4444e18; - bytes memory decrease = LiquidityOperations.getDecreaseEncoded(tokenIdAlice, 0.4444e18, ZERO_BYTES); + bytes memory decrease = LiquidityOperations.getDecreaseEncoded(tokenIdAlice, liquidityToRemove, ZERO_BYTES); vm.startPrank(bob); - vm.expectRevert("Not approved"); - lpm.modifyLiquidities(decrease); + vm.expectRevert(abi.encodeWithSelector(INonfungiblePositionManager.NotApproved.selector, address(bob))); + lpm.modifyLiquidities(decrease, _deadline); vm.stopPrank(); } @@ -215,7 +216,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation // collect fails if the owner did not permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -227,15 +228,15 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation address recipient = address(0x00444400); bytes memory collect = LiquidityOperations.getCollectEncoded(tokenIdAlice, recipient, ZERO_BYTES); vm.startPrank(bob); - vm.expectRevert("Not approved"); - lpm.modifyLiquidities(collect); + vm.expectRevert(abi.encodeWithSelector(INonfungiblePositionManager.NotApproved.selector, address(bob))); + lpm.modifyLiquidities(collect, block.timestamp + 1); vm.stopPrank(); } function test_permit_nonceAlreadyUsed() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives bob operator permissions @@ -256,13 +257,13 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_nonceAlreadyUsed_twoPositions() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(alice); range.tickLower = -600; range.tickUpper = 600; - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice2 = lpm.nextTokenId() - 1; // alice gives bob operator permissions for first token diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 5e74db07..20d4905f 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -15,18 +15,18 @@ contract LiquidityOperations { using Planner for Planner.Plan; - function _mint( - LiquidityRange memory _range, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes memory hookData - ) internal returns (BalanceDelta) { + uint256 _deadline = block.timestamp + 1; + + function _mint(LiquidityRange memory _range, uint256 liquidity, address recipient, bytes memory hookData) + internal + returns (BalanceDelta) + { Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, deadline, recipient, hookData)); + planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, recipient, hookData)); planner = planner.finalize(_range); // Close the currencies. - bytes[] memory result = lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); return abi.decode(result[0], (BalanceDelta)); } @@ -50,7 +50,8 @@ contract LiquidityOperations { planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData)); planner = planner.finalize(_range); // Close the currencies. - bytes[] memory result = lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); return abi.decode(result[0], (BalanceDelta)); } @@ -74,7 +75,8 @@ contract LiquidityOperations { planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData)); planner = planner.finalize(_range); // Close the currencies. - bytes[] memory result = lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); return abi.decode(result[0], (BalanceDelta)); } @@ -93,7 +95,8 @@ contract LiquidityOperations { planner = planner.finalize(_range); // Close the currencies. - bytes[] memory result = lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); return abi.decode(result[0], (BalanceDelta)); } @@ -101,7 +104,8 @@ contract LiquidityOperations { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.BURN, abi.encode(tokenId)); // No close needed on burn. - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, _deadline); } // TODO: organize somewhere else, or rename this file to NFTLiquidityHelpers? diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 29755ce0..6b629e6d 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -26,12 +26,12 @@ contract LiquidityFuzzers is Fuzzers { LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - Planner.Plan memory planner = Planner.init().add( - Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData) - ); + Planner.Plan memory planner = + Planner.init().add(Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), recipient, hookData)); planner = planner.finalize(range); - lpm.modifyLiquidities(planner.zip()); + bytes memory actions = planner.zip(); + lpm.modifyLiquidities(actions, block.timestamp + 1); uint256 tokenId = lpm.nextTokenId() - 1; return (tokenId, params); From 08f15e7bf15ff8fca2037710f4bc3edb66e59d21 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:53:55 -0400 Subject: [PATCH 67/98] update main (#162) --- .env | 7 - .../FullOracleObserve0After5Seconds.snap | 1 - .../FullOracleObserve200By13.snap | 1 - .../FullOracleObserve200By13Plus5.snap | 1 - .../FullOracleObserve5After5Seconds.snap | 1 - .forge-snapshots/FullOracleObserveOldest.snap | 1 - .../FullOracleObserveOldestAfter5Seconds.snap | 1 - .forge-snapshots/FullOracleObserveZero.snap | 1 - .../FullRangeAddInitialLiquidity.snap | 1 - .forge-snapshots/FullRangeAddLiquidity.snap | 1 - .forge-snapshots/FullRangeFirstSwap.snap | 1 - .forge-snapshots/FullRangeInitialize.snap | 1 - .../FullRangeRemoveLiquidity.snap | 1 - .../FullRangeRemoveLiquidityAndRebalance.snap | 1 - .forge-snapshots/FullRangeSecondSwap.snap | 1 - .forge-snapshots/FullRangeSwap.snap | 1 - .forge-snapshots/OracleGrow10Slots.snap | 1 - .../OracleGrow10SlotsCardinalityGreater.snap | 1 - .forge-snapshots/OracleGrow1Slot.snap | 1 - .../OracleGrow1SlotCardinalityGreater.snap | 1 - .forge-snapshots/OracleInitialize.snap | 1 - ...eObserveBetweenOldestAndOldestPlusOne.snap | 1 - .../OracleObserveCurrentTime.snap | 1 - ...racleObserveCurrentTimeCounterfactual.snap | 1 - .../OracleObserveLast20Seconds.snap | 1 - .../OracleObserveLatestEqual.snap | 1 - .../OracleObserveLatestTransform.snap | 1 - .forge-snapshots/OracleObserveMiddle.snap | 1 - .forge-snapshots/OracleObserveOldest.snap | 1 - .../OracleObserveSinceMostRecent.snap | 1 - .forge-snapshots/TWAMMSubmitOrder.snap | 1 - README.md | 22 +- contracts/base/PeripheryPayments.sol | 41 - contracts/base/PeripheryValidation.sol | 11 - contracts/hooks/examples/FullRange.sol | 368 ---- contracts/hooks/examples/GeomeanOracle.sol | 186 -- contracts/hooks/examples/LimitOrder.sol | 418 ----- contracts/hooks/examples/TWAMM.sol | 654 ------- contracts/hooks/examples/VolatilityOracle.sol | 70 - contracts/interfaces/IPeripheryPayments.sol | 17 - contracts/interfaces/ITWAMM.sol | 136 -- contracts/interfaces/external/IERC1271.sol | 16 - contracts/libraries/LiquidityAmounts.sol | 134 -- contracts/libraries/Oracle.sol | 337 ---- contracts/libraries/PoolGetters.sol | 106 -- contracts/libraries/TWAMM/ABDKMathQuad.sol | 1546 ----------------- contracts/libraries/TWAMM/OrderPool.sol | 34 - contracts/libraries/TWAMM/TwammMath.sol | 179 -- contracts/libraries/TransferHelper.sol | 54 - contracts/libraries/UniswapV4ERC20.sol | 16 - foundry.toml | 2 +- .../NonfungiblePositionManager.sol | 2 +- .../base/BaseLiquidityManagement.sol | 0 {contracts => src}/base/ERC721Permit.sol | 2 +- {contracts => src}/base/ImmutableState.sol | 0 {contracts => src}/base/Multicall.sol | 0 {contracts => src}/base/PoolInitializer.sol | 0 {contracts => src}/base/SafeCallback.sol | 0 {contracts => src}/base/SelfPermit.sol | 0 {contracts => src/base/hooks}/BaseHook.sol | 4 +- .../interfaces/IBaseLiquidityManagement.sol | 0 .../interfaces/IERC721Permit.sol | 0 {contracts => src}/interfaces/IMulticall.sol | 0 .../INonfungiblePositionManager.sol | 0 {contracts => src}/interfaces/IQuoter.sol | 0 {contracts => src}/interfaces/ISelfPermit.sol | 0 .../external/IERC20PermitAllowed.sol | 0 {contracts => src}/lens/Quoter.sol | 0 .../BalanceDeltaExtensionLibrary.sol | 0 {contracts => src}/libraries/ChainId.sol | 0 .../libraries/CurrencyDeltas.sol | 0 .../libraries/CurrencySettleTake.sol | 0 .../libraries/LiquiditySaltLibrary.sol | 0 {contracts => src}/libraries/PathKey.sol | 0 .../libraries/PoolTicksCounter.sol | 1 - {contracts => src}/libraries/Position.sol | 0 {contracts => src}/types/LiquidityRange.sol | 0 test/FullRange.t.sol | 782 --------- test/GeomeanOracle.t.sol | 237 --- test/LimitOrder.t.sol | 222 --- test/Oracle.t.sol | 867 --------- test/Quoter.t.sol | 18 +- test/TWAMM.t.sol | 432 ----- test/mock/MockMulticall.sol | 2 +- test/position-managers/Execute.t.sol | 8 +- test/position-managers/FeeCollection.t.sol | 6 +- test/position-managers/Gas.t.sol | 8 +- .../position-managers/IncreaseLiquidity.t.sol | 8 +- test/position-managers/Multicall.t.sol | 8 +- .../NonfungiblePositionManager.t.sol | 8 +- test/position-managers/Permit.t.sol | 10 +- test/shared/FeeMath.sol | 6 +- test/shared/GetSender.sol | 8 - test/shared/LiquidityOperations.sol | 4 +- test/shared/fuzz/LiquidityFuzzers.sol | 4 +- .../FullRangeImplementation.sol | 16 - .../GeomeanOracleImplementation.sol | 26 - .../LimitOrderImplementation.sol | 16 - .../implementation/OracleImplementation.sol | 95 - .../implementation/TWAMMImplementation.sol | 16 - test/utils/HookEnabledSwapRouter.sol | 80 - test/utils/Planner.sol | 4 +- 102 files changed, 54 insertions(+), 7230 deletions(-) delete mode 100644 .env delete mode 100644 .forge-snapshots/FullOracleObserve0After5Seconds.snap delete mode 100644 .forge-snapshots/FullOracleObserve200By13.snap delete mode 100644 .forge-snapshots/FullOracleObserve200By13Plus5.snap delete mode 100644 .forge-snapshots/FullOracleObserve5After5Seconds.snap delete mode 100644 .forge-snapshots/FullOracleObserveOldest.snap delete mode 100644 .forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap delete mode 100644 .forge-snapshots/FullOracleObserveZero.snap delete mode 100644 .forge-snapshots/FullRangeAddInitialLiquidity.snap delete mode 100644 .forge-snapshots/FullRangeAddLiquidity.snap delete mode 100644 .forge-snapshots/FullRangeFirstSwap.snap delete mode 100644 .forge-snapshots/FullRangeInitialize.snap delete mode 100644 .forge-snapshots/FullRangeRemoveLiquidity.snap delete mode 100644 .forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap delete mode 100644 .forge-snapshots/FullRangeSecondSwap.snap delete mode 100644 .forge-snapshots/FullRangeSwap.snap delete mode 100644 .forge-snapshots/OracleGrow10Slots.snap delete mode 100644 .forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap delete mode 100644 .forge-snapshots/OracleGrow1Slot.snap delete mode 100644 .forge-snapshots/OracleGrow1SlotCardinalityGreater.snap delete mode 100644 .forge-snapshots/OracleInitialize.snap delete mode 100644 .forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap delete mode 100644 .forge-snapshots/OracleObserveCurrentTime.snap delete mode 100644 .forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap delete mode 100644 .forge-snapshots/OracleObserveLast20Seconds.snap delete mode 100644 .forge-snapshots/OracleObserveLatestEqual.snap delete mode 100644 .forge-snapshots/OracleObserveLatestTransform.snap delete mode 100644 .forge-snapshots/OracleObserveMiddle.snap delete mode 100644 .forge-snapshots/OracleObserveOldest.snap delete mode 100644 .forge-snapshots/OracleObserveSinceMostRecent.snap delete mode 100644 .forge-snapshots/TWAMMSubmitOrder.snap delete mode 100644 contracts/base/PeripheryPayments.sol delete mode 100644 contracts/base/PeripheryValidation.sol delete mode 100644 contracts/hooks/examples/FullRange.sol delete mode 100644 contracts/hooks/examples/GeomeanOracle.sol delete mode 100644 contracts/hooks/examples/LimitOrder.sol delete mode 100644 contracts/hooks/examples/TWAMM.sol delete mode 100644 contracts/hooks/examples/VolatilityOracle.sol delete mode 100644 contracts/interfaces/IPeripheryPayments.sol delete mode 100644 contracts/interfaces/ITWAMM.sol delete mode 100644 contracts/interfaces/external/IERC1271.sol delete mode 100644 contracts/libraries/LiquidityAmounts.sol delete mode 100644 contracts/libraries/Oracle.sol delete mode 100644 contracts/libraries/PoolGetters.sol delete mode 100644 contracts/libraries/TWAMM/ABDKMathQuad.sol delete mode 100644 contracts/libraries/TWAMM/OrderPool.sol delete mode 100644 contracts/libraries/TWAMM/TwammMath.sol delete mode 100644 contracts/libraries/TransferHelper.sol delete mode 100644 contracts/libraries/UniswapV4ERC20.sol rename {contracts => src}/NonfungiblePositionManager.sol (99%) rename {contracts => src}/base/BaseLiquidityManagement.sol (100%) rename {contracts => src}/base/ERC721Permit.sol (98%) rename {contracts => src}/base/ImmutableState.sol (100%) rename {contracts => src}/base/Multicall.sol (100%) rename {contracts => src}/base/PoolInitializer.sol (100%) rename {contracts => src}/base/SafeCallback.sol (100%) rename {contracts => src}/base/SelfPermit.sol (100%) rename {contracts => src/base/hooks}/BaseHook.sol (97%) rename {contracts => src}/interfaces/IBaseLiquidityManagement.sol (100%) rename {contracts => src}/interfaces/IERC721Permit.sol (100%) rename {contracts => src}/interfaces/IMulticall.sol (100%) rename {contracts => src}/interfaces/INonfungiblePositionManager.sol (100%) rename {contracts => src}/interfaces/IQuoter.sol (100%) rename {contracts => src}/interfaces/ISelfPermit.sol (100%) rename {contracts => src}/interfaces/external/IERC20PermitAllowed.sol (100%) rename {contracts => src}/lens/Quoter.sol (100%) rename {contracts => src}/libraries/BalanceDeltaExtensionLibrary.sol (100%) rename {contracts => src}/libraries/ChainId.sol (100%) rename {contracts => src}/libraries/CurrencyDeltas.sol (100%) rename {contracts => src}/libraries/CurrencySettleTake.sol (100%) rename {contracts => src}/libraries/LiquiditySaltLibrary.sol (100%) rename {contracts => src}/libraries/PathKey.sol (100%) rename {contracts => src}/libraries/PoolTicksCounter.sol (99%) rename {contracts => src}/libraries/Position.sol (100%) rename {contracts => src}/types/LiquidityRange.sol (100%) delete mode 100644 test/FullRange.t.sol delete mode 100644 test/GeomeanOracle.t.sol delete mode 100644 test/LimitOrder.t.sol delete mode 100644 test/Oracle.t.sol delete mode 100644 test/TWAMM.t.sol delete mode 100644 test/shared/GetSender.sol delete mode 100644 test/shared/implementation/FullRangeImplementation.sol delete mode 100644 test/shared/implementation/GeomeanOracleImplementation.sol delete mode 100644 test/shared/implementation/LimitOrderImplementation.sol delete mode 100644 test/shared/implementation/OracleImplementation.sol delete mode 100644 test/shared/implementation/TWAMMImplementation.sol delete mode 100644 test/utils/HookEnabledSwapRouter.sol diff --git a/.env b/.env deleted file mode 100644 index 7859e840..00000000 --- a/.env +++ /dev/null @@ -1,7 +0,0 @@ -FOUNDRY_FUZZ_SEED=0x4444 - -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - export FOUNDRY_SOLC="./lib/v4-core/bin/solc-static-linux" -elif [[ "$OSTYPE" == "darwin"* ]]; then - export FOUNDRY_SOLC="./lib/v4-core/bin/solc-mac" -fi diff --git a/.forge-snapshots/FullOracleObserve0After5Seconds.snap b/.forge-snapshots/FullOracleObserve0After5Seconds.snap deleted file mode 100644 index f5b9e8bf..00000000 --- a/.forge-snapshots/FullOracleObserve0After5Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -1912 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13.snap b/.forge-snapshots/FullOracleObserve200By13.snap deleted file mode 100644 index b47b8dc4..00000000 --- a/.forge-snapshots/FullOracleObserve200By13.snap +++ /dev/null @@ -1 +0,0 @@ -20210 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13Plus5.snap b/.forge-snapshots/FullOracleObserve200By13Plus5.snap deleted file mode 100644 index 46616951..00000000 --- a/.forge-snapshots/FullOracleObserve200By13Plus5.snap +++ /dev/null @@ -1 +0,0 @@ -20443 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve5After5Seconds.snap b/.forge-snapshots/FullOracleObserve5After5Seconds.snap deleted file mode 100644 index dba60802..00000000 --- a/.forge-snapshots/FullOracleObserve5After5Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -2024 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldest.snap b/.forge-snapshots/FullOracleObserveOldest.snap deleted file mode 100644 index c90bb2fe..00000000 --- a/.forge-snapshots/FullOracleObserveOldest.snap +++ /dev/null @@ -1 +0,0 @@ -19279 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap b/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap deleted file mode 100644 index 1d23504b..00000000 --- a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -19555 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveZero.snap b/.forge-snapshots/FullOracleObserveZero.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/FullOracleObserveZero.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap deleted file mode 100644 index 85fc8b74..00000000 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -353166 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap deleted file mode 100644 index 2e50efba..00000000 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -160448 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap deleted file mode 100644 index c0359c33..00000000 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ /dev/null @@ -1 +0,0 @@ -145169 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap deleted file mode 100644 index 021da508..00000000 --- a/.forge-snapshots/FullRangeInitialize.snap +++ /dev/null @@ -1 +0,0 @@ -1033164 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap deleted file mode 100644 index ecd0fc69..00000000 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -145564 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap deleted file mode 100644 index a7490283..00000000 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ /dev/null @@ -1 +0,0 @@ -281452 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap deleted file mode 100644 index 88c99350..00000000 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ /dev/null @@ -1 +0,0 @@ -114747 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap deleted file mode 100644 index c0145c0d..00000000 --- a/.forge-snapshots/FullRangeSwap.snap +++ /dev/null @@ -1 +0,0 @@ -144610 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10Slots.snap b/.forge-snapshots/OracleGrow10Slots.snap deleted file mode 100644 index 96c9f369..00000000 --- a/.forge-snapshots/OracleGrow10Slots.snap +++ /dev/null @@ -1 +0,0 @@ -254164 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap deleted file mode 100644 index 9fc5bce2..00000000 --- a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap +++ /dev/null @@ -1 +0,0 @@ -249653 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1Slot.snap b/.forge-snapshots/OracleGrow1Slot.snap deleted file mode 100644 index ced15d76..00000000 --- a/.forge-snapshots/OracleGrow1Slot.snap +++ /dev/null @@ -1 +0,0 @@ -54049 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap deleted file mode 100644 index 8ad5646e..00000000 --- a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap +++ /dev/null @@ -1 +0,0 @@ -49549 \ No newline at end of file diff --git a/.forge-snapshots/OracleInitialize.snap b/.forge-snapshots/OracleInitialize.snap deleted file mode 100644 index a9ee0288..00000000 --- a/.forge-snapshots/OracleInitialize.snap +++ /dev/null @@ -1 +0,0 @@ -72794 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap b/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap deleted file mode 100644 index 5996d53e..00000000 --- a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap +++ /dev/null @@ -1 +0,0 @@ -5368 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTime.snap b/.forge-snapshots/OracleObserveCurrentTime.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/OracleObserveCurrentTime.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap b/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLast20Seconds.snap b/.forge-snapshots/OracleObserveLast20Seconds.snap deleted file mode 100644 index 24efe8f4..00000000 --- a/.forge-snapshots/OracleObserveLast20Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -73037 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestEqual.snap b/.forge-snapshots/OracleObserveLatestEqual.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/OracleObserveLatestEqual.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestTransform.snap b/.forge-snapshots/OracleObserveLatestTransform.snap deleted file mode 100644 index f5b9e8bf..00000000 --- a/.forge-snapshots/OracleObserveLatestTransform.snap +++ /dev/null @@ -1 +0,0 @@ -1912 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveMiddle.snap b/.forge-snapshots/OracleObserveMiddle.snap deleted file mode 100644 index 76e5b53e..00000000 --- a/.forge-snapshots/OracleObserveMiddle.snap +++ /dev/null @@ -1 +0,0 @@ -5541 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveOldest.snap b/.forge-snapshots/OracleObserveOldest.snap deleted file mode 100644 index f124ce2d..00000000 --- a/.forge-snapshots/OracleObserveOldest.snap +++ /dev/null @@ -1 +0,0 @@ -5092 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveSinceMostRecent.snap b/.forge-snapshots/OracleObserveSinceMostRecent.snap deleted file mode 100644 index 9dab3404..00000000 --- a/.forge-snapshots/OracleObserveSinceMostRecent.snap +++ /dev/null @@ -1 +0,0 @@ -2522 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap deleted file mode 100644 index 49c010e4..00000000 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ /dev/null @@ -1 +0,0 @@ -156535 \ No newline at end of file diff --git a/README.md b/README.md index b5be65fa..2322db2f 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,6 @@ Uniswap v4 is a new automated market maker protocol that provides extensibility If you’re interested in contributing please see the [contribution guidelines](https://github.com/Uniswap/v4-periphery/blob/main/CONTRIBUTING.md)! -## Repository Structure - -```solidity -contracts/ -----hooks/ - ----examples/ - | GeomeanOracle.sol - | LimitOrder.sol - | TWAMM.sol - | VolatilityOracle.sol -----libraries/ - | Oracle.sol -BaseHook.sol -test/ -``` - -To showcase the power of hooks, this repository provides some interesting examples in the `/hooks/examples/` folder. Note that none of the contracts in this repository are fully production-ready, and the final design for some of the example hooks could look different. - -Eventually, some hooks that have been audited and are considered production-ready will be placed in the root `hooks` folder. Not all hooks will be safe or valuable to users. This repository will maintain a limited set of hook contracts. Even a well-designed and audited hook contract may not be accepted in this repo. - ## Local Deployment and Usage To utilize the contracts and deploy to a local testnet, you can install the code in your repo with forge: @@ -38,7 +18,7 @@ If you are building hooks, it may be useful to inherit from the `BaseHook` contr ```solidity -import {BaseHook} from 'v4-periphery/contracts/BaseHook.sol'; +import {BaseHook} from 'v4-periphery/src/base/hooks/BaseHook.sol'; contract CoolHook is BaseHook { // Override the hook callbacks you want on your hook diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol deleted file mode 100644 index 24466924..00000000 --- a/contracts/base/PeripheryPayments.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; - -abstract contract PeripheryPayments is IPeripheryPayments { - using CurrencyLibrary for Currency; - using SafeTransferLib for address; - using SafeTransferLib for ERC20; - - error InsufficientToken(); - error NativeTokenTransferFrom(); - - /// @inheritdoc IPeripheryPayments - function sweepToken(Currency currency, uint256 amountMinimum, address recipient) public payable override { - uint256 balanceCurrency = currency.balanceOfSelf(); - if (balanceCurrency < amountMinimum) revert InsufficientToken(); - - if (balanceCurrency > 0) { - currency.transfer(recipient, balanceCurrency); - } - } - - /// @param currency The currency to pay - /// @param payer The entity that must pay - /// @param recipient The entity that will receive payment - /// @param value The amount to pay - function pay(Currency currency, address payer, address recipient, uint256 value) internal { - if (payer == address(this)) { - // pay with tokens already in the contract (for the exact input multihop case) - currency.transfer(recipient, value); - } else { - if (currency.isNative()) revert NativeTokenTransferFrom(); - // pull payment - ERC20(Currency.unwrap(currency)).safeTransferFrom(payer, recipient, value); - } - } -} diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol deleted file mode 100644 index b8ea81d4..00000000 --- a/contracts/base/PeripheryValidation.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -abstract contract PeripheryValidation { - error TransactionTooOld(); - - modifier checkDeadline(uint256 deadline) { - if (block.timestamp > deadline) revert TransactionTooOld(); - _; - } -} diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol deleted file mode 100644 index 191593b8..00000000 --- a/contracts/hooks/examples/FullRange.sol +++ /dev/null @@ -1,368 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {UniswapV4ERC20} from "../../libraries/UniswapV4ERC20.sol"; -import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; - -import "../../libraries/LiquidityAmounts.sol"; - -contract FullRange is BaseHook { - using CurrencyLibrary for Currency; - using CurrencySettler for Currency; - using PoolIdLibrary for PoolKey; - using SafeCast for uint256; - using SafeCast for uint128; - using StateLibrary for IPoolManager; - - /// @notice Thrown when trying to interact with a non-initialized pool - error PoolNotInitialized(); - error TickSpacingNotDefault(); - error LiquidityDoesntMeetMinimum(); - error SenderMustBeHook(); - error ExpiredPastDeadline(); - error TooMuchSlippage(); - - bytes internal constant ZERO_BYTES = bytes(""); - - /// @dev Min tick for full range with tick spacing of 60 - int24 internal constant MIN_TICK = -887220; - /// @dev Max tick for full range with tick spacing of 60 - int24 internal constant MAX_TICK = -MIN_TICK; - - int256 internal constant MAX_INT = type(int256).max; - uint16 internal constant MINIMUM_LIQUIDITY = 1000; - - struct CallbackData { - address sender; - PoolKey key; - IPoolManager.ModifyLiquidityParams params; - } - - struct PoolInfo { - bool hasAccruedFees; - address liquidityToken; - } - - struct AddLiquidityParams { - Currency currency0; - Currency currency1; - uint24 fee; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address to; - uint256 deadline; - } - - struct RemoveLiquidityParams { - Currency currency0; - Currency currency1; - uint24 fee; - uint256 liquidity; - uint256 deadline; - } - - mapping(PoolId => PoolInfo) public poolInfo; - - constructor(IPoolManager _manager) BaseHook(_manager) {} - - modifier ensure(uint256 deadline) { - if (deadline < block.timestamp) revert ExpiredPastDeadline(); - _; - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: false, - beforeAddLiquidity: true, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function addLiquidity(AddLiquidityParams calldata params) - external - ensure(params.deadline) - returns (uint128 liquidity) - { - PoolKey memory key = PoolKey({ - currency0: params.currency0, - currency1: params.currency1, - fee: params.fee, - tickSpacing: 60, - hooks: IHooks(address(this)) - }); - - PoolId poolId = key.toId(); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - - if (sqrtPriceX96 == 0) revert PoolNotInitialized(); - - PoolInfo storage pool = poolInfo[poolId]; - - uint128 poolLiquidity = manager.getLiquidity(poolId); - - liquidity = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(MIN_TICK), - TickMath.getSqrtPriceAtTick(MAX_TICK), - params.amount0Desired, - params.amount1Desired - ); - - if (poolLiquidity == 0 && liquidity <= MINIMUM_LIQUIDITY) { - revert LiquidityDoesntMeetMinimum(); - } - BalanceDelta addedDelta = modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256(), - salt: 0 - }) - ); - - if (poolLiquidity == 0) { - // permanently lock the first MINIMUM_LIQUIDITY tokens - liquidity -= MINIMUM_LIQUIDITY; - UniswapV4ERC20(pool.liquidityToken).mint(address(0), MINIMUM_LIQUIDITY); - } - - UniswapV4ERC20(pool.liquidityToken).mint(params.to, liquidity); - - if (uint128(-addedDelta.amount0()) < params.amount0Min || uint128(-addedDelta.amount1()) < params.amount1Min) { - revert TooMuchSlippage(); - } - } - - function removeLiquidity(RemoveLiquidityParams calldata params) - public - virtual - ensure(params.deadline) - returns (BalanceDelta delta) - { - PoolKey memory key = PoolKey({ - currency0: params.currency0, - currency1: params.currency1, - fee: params.fee, - tickSpacing: 60, - hooks: IHooks(address(this)) - }); - - PoolId poolId = key.toId(); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - - if (sqrtPriceX96 == 0) revert PoolNotInitialized(); - - UniswapV4ERC20 erc20 = UniswapV4ERC20(poolInfo[poolId].liquidityToken); - - delta = modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: -(params.liquidity.toInt256()), - salt: 0 - }) - ); - - erc20.burn(msg.sender, params.liquidity); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - override - returns (bytes4) - { - if (key.tickSpacing != 60) revert TickSpacingNotDefault(); - - PoolId poolId = key.toId(); - - string memory tokenSymbol = string( - abi.encodePacked( - "UniV4", - "-", - IERC20Metadata(Currency.unwrap(key.currency0)).symbol(), - "-", - IERC20Metadata(Currency.unwrap(key.currency1)).symbol(), - "-", - Strings.toString(uint256(key.fee)) - ) - ); - address poolToken = address(new UniswapV4ERC20(tokenSymbol, tokenSymbol)); - - poolInfo[poolId] = PoolInfo({hasAccruedFees: false, liquidityToken: poolToken}); - - return FullRange.beforeInitialize.selector; - } - - function beforeAddLiquidity( - address sender, - PoolKey calldata, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external view override returns (bytes4) { - if (sender != address(this)) revert SenderMustBeHook(); - - return FullRange.beforeAddLiquidity.selector; - } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - returns (bytes4, BeforeSwapDelta, uint24) - { - PoolId poolId = key.toId(); - - if (!poolInfo[poolId].hasAccruedFees) { - PoolInfo storage pool = poolInfo[poolId]; - pool.hasAccruedFees = true; - } - - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - function modifyLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) - internal - returns (BalanceDelta delta) - { - delta = abi.decode(manager.unlock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); - } - - function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - key.currency0.settle(manager, sender, uint256(int256(-delta.amount0())), false); - key.currency1.settle(manager, sender, uint256(int256(-delta.amount1())), false); - } - - function _takeDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - manager.take(key.currency0, sender, uint256(uint128(delta.amount0()))); - manager.take(key.currency1, sender, uint256(uint128(delta.amount1()))); - } - - function _removeLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) - internal - returns (BalanceDelta delta) - { - PoolId poolId = key.toId(); - PoolInfo storage pool = poolInfo[poolId]; - - if (pool.hasAccruedFees) { - _rebalance(key); - } - - uint256 liquidityToRemove = FullMath.mulDiv( - uint256(-params.liquidityDelta), - manager.getLiquidity(poolId), - UniswapV4ERC20(pool.liquidityToken).totalSupply() - ); - - params.liquidityDelta = -(liquidityToRemove.toInt256()); - (delta,) = manager.modifyLiquidity(key, params, ZERO_BYTES); - pool.hasAccruedFees = false; - } - - function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { - CallbackData memory data = abi.decode(rawData, (CallbackData)); - BalanceDelta delta; - - if (data.params.liquidityDelta < 0) { - delta = _removeLiquidity(data.key, data.params); - _takeDeltas(data.sender, data.key, delta); - } else { - (delta,) = manager.modifyLiquidity(data.key, data.params, ZERO_BYTES); - _settleDeltas(data.sender, data.key, delta); - } - return abi.encode(delta); - } - - function _rebalance(PoolKey memory key) public { - PoolId poolId = key.toId(); - (BalanceDelta balanceDelta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: -(manager.getLiquidity(poolId).toInt256()), - salt: 0 - }), - ZERO_BYTES - ); - - uint160 newSqrtPriceX96 = ( - FixedPointMathLib.sqrt( - FullMath.mulDiv(uint128(balanceDelta.amount1()), FixedPoint96.Q96, uint128(balanceDelta.amount0())) - ) * FixedPointMathLib.sqrt(FixedPoint96.Q96) - ).toUint160(); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - - manager.swap( - key, - IPoolManager.SwapParams({ - zeroForOne: newSqrtPriceX96 < sqrtPriceX96, - amountSpecified: -MAX_INT - 1, // equivalent of type(int256).min - sqrtPriceLimitX96: newSqrtPriceX96 - }), - ZERO_BYTES - ); - - uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( - newSqrtPriceX96, - TickMath.getSqrtPriceAtTick(MIN_TICK), - TickMath.getSqrtPriceAtTick(MAX_TICK), - uint256(uint128(balanceDelta.amount0())), - uint256(uint128(balanceDelta.amount1())) - ); - - (BalanceDelta balanceDeltaAfter,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256(), - salt: 0 - }), - ZERO_BYTES - ); - - // Donate any "dust" from the sqrtRatio change as fees - uint128 donateAmount0 = uint128(balanceDelta.amount0() + balanceDeltaAfter.amount0()); - uint128 donateAmount1 = uint128(balanceDelta.amount1() + balanceDeltaAfter.amount1()); - - manager.donate(key, donateAmount0, donateAmount1, ZERO_BYTES); - } -} diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol deleted file mode 100644 index df5a9ad1..00000000 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {Oracle} from "../../libraries/Oracle.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; - -/// @notice A hook for a pool that allows a Uniswap pool to act as an oracle. Pools that use this hook must have full range -/// tick spacing and liquidity is always permanently locked in these pools. This is the suggested configuration -/// for protocols that wish to use a V3 style geomean oracle. -contract GeomeanOracle is BaseHook { - using Oracle for Oracle.Observation[65535]; - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - /// @notice Oracle pools do not have fees because they exist to serve as an oracle for a pair of tokens - error OnlyOneOraclePoolAllowed(); - - /// @notice Oracle positions must be full range - error OraclePositionsMustBeFullRange(); - - /// @notice Oracle pools must have liquidity locked so that they cannot become more susceptible to price manipulation - error OraclePoolMustLockLiquidity(); - - /// @member index The index of the last written observation for the pool - /// @member cardinality The cardinality of the observations array for the pool - /// @member cardinalityNext The cardinality target of the observations array for the pool, which will replace cardinality when enough observations are written - struct ObservationState { - uint16 index; - uint16 cardinality; - uint16 cardinalityNext; - } - - /// @notice The list of observations for a given pool ID - mapping(PoolId => Oracle.Observation[65535]) public observations; - /// @notice The current observation array state for the given pool ID - mapping(PoolId => ObservationState) public states; - - /// @notice Returns the observation for the given pool key and observation index - function getObservation(PoolKey calldata key, uint256 index) - external - view - returns (Oracle.Observation memory observation) - { - observation = observations[PoolId.wrap(keccak256(abi.encode(key)))][index]; - } - - /// @notice Returns the state for the given pool key - function getState(PoolKey calldata key) external view returns (ObservationState memory state) { - state = states[PoolId.wrap(keccak256(abi.encode(key)))]; - } - - /// @dev For mocking - function _blockTimestamp() internal view virtual returns (uint32) { - return uint32(block.timestamp); - } - - constructor(IPoolManager _manager) BaseHook(_manager) {} - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: true, - beforeAddLiquidity: true, - beforeRemoveLiquidity: true, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - view - override - onlyByManager - returns (bytes4) - { - // This is to limit the fragmentation of pools using this oracle hook. In other words, - // there may only be one pool per pair of tokens that use this hook. The tick spacing is set to the maximum - // because we only allow max range liquidity in this pool. - if (key.fee != 0 || key.tickSpacing != manager.MAX_TICK_SPACING()) revert OnlyOneOraclePoolAllowed(); - return GeomeanOracle.beforeInitialize.selector; - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) - external - override - onlyByManager - returns (bytes4) - { - PoolId id = key.toId(); - (states[id].cardinality, states[id].cardinalityNext) = observations[id].initialize(_blockTimestamp()); - return GeomeanOracle.afterInitialize.selector; - } - - /// @dev Called before any action that potentially modifies pool price or liquidity, such as swap or modify position - function _updatePool(PoolKey calldata key) private { - PoolId id = key.toId(); - (, int24 tick,,) = manager.getSlot0(id); - - uint128 liquidity = manager.getLiquidity(id); - - (states[id].index, states[id].cardinality) = observations[id].write( - states[id].index, _blockTimestamp(), tick, liquidity, states[id].cardinality, states[id].cardinalityNext - ); - } - - function beforeAddLiquidity( - address, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata - ) external override onlyByManager returns (bytes4) { - int24 maxTickSpacing = manager.MAX_TICK_SPACING(); - if ( - params.tickLower != TickMath.minUsableTick(maxTickSpacing) - || params.tickUpper != TickMath.maxUsableTick(maxTickSpacing) - ) revert OraclePositionsMustBeFullRange(); - _updatePool(key); - return GeomeanOracle.beforeAddLiquidity.selector; - } - - function beforeRemoveLiquidity( - address, - PoolKey calldata, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external view override onlyByManager returns (bytes4) { - revert OraclePoolMustLockLiquidity(); - } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - onlyByManager - returns (bytes4, BeforeSwapDelta, uint24) - { - _updatePool(key); - return (GeomeanOracle.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - /// @notice Observe the given pool for the timestamps - function observe(PoolKey calldata key, uint32[] calldata secondsAgos) - external - view - returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) - { - PoolId id = key.toId(); - - ObservationState memory state = states[id]; - - (, int24 tick,,) = manager.getSlot0(id); - - uint128 liquidity = manager.getLiquidity(id); - - return observations[id].observe(_blockTimestamp(), secondsAgos, tick, state.index, liquidity, state.cardinality); - } - - /// @notice Increase the cardinality target for the given pool - function increaseCardinalityNext(PoolKey calldata key, uint16 cardinalityNext) - external - returns (uint16 cardinalityNextOld, uint16 cardinalityNextNew) - { - PoolId id = PoolId.wrap(keccak256(abi.encode(key))); - - ObservationState storage state = states[id]; - - cardinalityNextOld = state.cardinalityNext; - cardinalityNextNew = observations[id].grow(cardinalityNextOld, cardinalityNext); - state.cardinalityNext = cardinalityNextNew; - } -} diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol deleted file mode 100644 index 2a8ca909..00000000 --- a/contracts/hooks/examples/LimitOrder.sol +++ /dev/null @@ -1,418 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -type Epoch is uint232; - -library EpochLibrary { - function equals(Epoch a, Epoch b) internal pure returns (bool) { - return Epoch.unwrap(a) == Epoch.unwrap(b); - } - - function unsafeIncrement(Epoch a) internal pure returns (Epoch) { - unchecked { - return Epoch.wrap(Epoch.unwrap(a) + 1); - } - } -} - -contract LimitOrder is BaseHook { - using EpochLibrary for Epoch; - using PoolIdLibrary for PoolKey; - using CurrencyLibrary for Currency; - using CurrencySettler for Currency; - using StateLibrary for IPoolManager; - - error ZeroLiquidity(); - error InRange(); - error CrossedRange(); - error Filled(); - error NotFilled(); - error NotPoolManagerToken(); - - event Place( - address indexed owner, Epoch indexed epoch, PoolKey key, int24 tickLower, bool zeroForOne, uint128 liquidity - ); - - event Fill(Epoch indexed epoch, PoolKey key, int24 tickLower, bool zeroForOne); - - event Kill( - address indexed owner, Epoch indexed epoch, PoolKey key, int24 tickLower, bool zeroForOne, uint128 liquidity - ); - - event Withdraw(address indexed owner, Epoch indexed epoch, uint128 liquidity); - - bytes internal constant ZERO_BYTES = bytes(""); - - Epoch private constant EPOCH_DEFAULT = Epoch.wrap(0); - - mapping(PoolId => int24) public tickLowerLasts; - Epoch public epochNext = Epoch.wrap(1); - - struct EpochInfo { - bool filled; - Currency currency0; - Currency currency1; - uint256 token0Total; - uint256 token1Total; - uint128 liquidityTotal; - mapping(address => uint128) liquidity; - } - - mapping(bytes32 => Epoch) public epochs; - mapping(Epoch => EpochInfo) public epochInfos; - - constructor(IPoolManager _manager) BaseHook(_manager) {} - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: false, - afterInitialize: true, - beforeAddLiquidity: false, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: false, - afterSwap: true, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function getTickLowerLast(PoolId poolId) public view returns (int24) { - return tickLowerLasts[poolId]; - } - - function setTickLowerLast(PoolId poolId, int24 tickLower) private { - tickLowerLasts[poolId] = tickLower; - } - - function getEpoch(PoolKey memory key, int24 tickLower, bool zeroForOne) public view returns (Epoch) { - return epochs[keccak256(abi.encode(key, tickLower, zeroForOne))]; - } - - function setEpoch(PoolKey memory key, int24 tickLower, bool zeroForOne, Epoch epoch) private { - epochs[keccak256(abi.encode(key, tickLower, zeroForOne))] = epoch; - } - - function getEpochLiquidity(Epoch epoch, address owner) external view returns (uint256) { - return epochInfos[epoch].liquidity[owner]; - } - - function getTick(PoolId poolId) private view returns (int24 tick) { - (, tick,,) = manager.getSlot0(poolId); - } - - function getTickLower(int24 tick, int24 tickSpacing) private pure returns (int24) { - int24 compressed = tick / tickSpacing; - if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity - return compressed * tickSpacing; - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24 tick, bytes calldata) - external - override - onlyByManager - returns (bytes4) - { - setTickLowerLast(key.toId(), getTickLower(tick, key.tickSpacing)); - return LimitOrder.afterInitialize.selector; - } - - function afterSwap( - address, - PoolKey calldata key, - IPoolManager.SwapParams calldata params, - BalanceDelta, - bytes calldata - ) external override onlyByManager returns (bytes4, int128) { - (int24 tickLower, int24 lower, int24 upper) = _getCrossedTicks(key.toId(), key.tickSpacing); - if (lower > upper) return (LimitOrder.afterSwap.selector, 0); - - // note that a zeroForOne swap means that the pool is actually gaining token0, so limit - // order fills are the opposite of swap fills, hence the inversion below - bool zeroForOne = !params.zeroForOne; - for (; lower <= upper; lower += key.tickSpacing) { - _fillEpoch(key, lower, zeroForOne); - } - - setTickLowerLast(key.toId(), tickLower); - return (LimitOrder.afterSwap.selector, 0); - } - - function _fillEpoch(PoolKey calldata key, int24 lower, bool zeroForOne) internal { - Epoch epoch = getEpoch(key, lower, zeroForOne); - if (!epoch.equals(EPOCH_DEFAULT)) { - EpochInfo storage epochInfo = epochInfos[epoch]; - - epochInfo.filled = true; - - (uint256 amount0, uint256 amount1) = - _unlockCallbackFill(key, lower, -int256(uint256(epochInfo.liquidityTotal))); - - unchecked { - epochInfo.token0Total += amount0; - epochInfo.token1Total += amount1; - } - - setEpoch(key, lower, zeroForOne, EPOCH_DEFAULT); - - emit Fill(epoch, key, lower, zeroForOne); - } - } - - function _getCrossedTicks(PoolId poolId, int24 tickSpacing) - internal - view - returns (int24 tickLower, int24 lower, int24 upper) - { - tickLower = getTickLower(getTick(poolId), tickSpacing); - int24 tickLowerLast = getTickLowerLast(poolId); - - if (tickLower < tickLowerLast) { - lower = tickLower + tickSpacing; - upper = tickLowerLast; - } else { - lower = tickLowerLast; - upper = tickLower - tickSpacing; - } - } - - function _unlockCallbackFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) - private - onlyByManager - returns (uint128 amount0, uint128 amount1) - { - (BalanceDelta delta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta, - salt: 0 - }), - ZERO_BYTES - ); - - if (delta.amount0() > 0) { - manager.mint(address(this), key.currency0.toId(), amount0 = uint128(delta.amount0())); - } - if (delta.amount1() > 0) { - manager.mint(address(this), key.currency1.toId(), amount1 = uint128(delta.amount1())); - } - } - - function place(PoolKey calldata key, int24 tickLower, bool zeroForOne, uint128 liquidity) - external - onlyValidPools(key.hooks) - { - if (liquidity == 0) revert ZeroLiquidity(); - - manager.unlock( - abi.encodeCall( - this.unlockCallbackPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender) - ) - ); - - EpochInfo storage epochInfo; - Epoch epoch = getEpoch(key, tickLower, zeroForOne); - if (epoch.equals(EPOCH_DEFAULT)) { - unchecked { - setEpoch(key, tickLower, zeroForOne, epoch = epochNext); - // since epoch was just assigned the current value of epochNext, - // this is equivalent to epochNext++, which is what's intended, - // and it saves an SLOAD - epochNext = epoch.unsafeIncrement(); - } - epochInfo = epochInfos[epoch]; - epochInfo.currency0 = key.currency0; - epochInfo.currency1 = key.currency1; - } else { - epochInfo = epochInfos[epoch]; - } - - unchecked { - epochInfo.liquidityTotal += liquidity; - epochInfo.liquidity[msg.sender] += liquidity; - } - - emit Place(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); - } - - function unlockCallbackPlace( - PoolKey calldata key, - int24 tickLower, - bool zeroForOne, - int256 liquidityDelta, - address owner - ) external selfOnly { - (BalanceDelta delta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta, - salt: 0 - }), - ZERO_BYTES - ); - - if (delta.amount0() < 0) { - if (delta.amount1() != 0) revert InRange(); - if (!zeroForOne) revert CrossedRange(); - key.currency0.settle(manager, owner, uint256(uint128(-delta.amount0())), false); - } else { - if (delta.amount0() != 0) revert InRange(); - if (zeroForOne) revert CrossedRange(); - key.currency1.settle(manager, owner, uint256(uint128(-delta.amount1())), false); - } - } - - function kill(PoolKey calldata key, int24 tickLower, bool zeroForOne, address to) external { - Epoch epoch = getEpoch(key, tickLower, zeroForOne); - EpochInfo storage epochInfo = epochInfos[epoch]; - - if (epochInfo.filled) revert Filled(); - - uint128 liquidity = epochInfo.liquidity[msg.sender]; - if (liquidity == 0) revert ZeroLiquidity(); - delete epochInfo.liquidity[msg.sender]; - - uint256 amount0Fee; - uint256 amount1Fee; - (amount0Fee, amount1Fee) = abi.decode( - manager.unlock( - abi.encodeCall( - this.unlockCallbackKill, - (key, tickLower, -int256(uint256(liquidity)), to, liquidity == epochInfo.liquidityTotal) - ) - ), - (uint256, uint256) - ); - epochInfo.liquidityTotal -= liquidity; - unchecked { - epochInfo.token0Total += amount0Fee; - epochInfo.token1Total += amount1Fee; - } - - emit Kill(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); - } - - function unlockCallbackKill( - PoolKey calldata key, - int24 tickLower, - int256 liquidityDelta, - address to, - bool removingAllLiquidity - ) external selfOnly returns (uint128 amount0Fee, uint128 amount1Fee) { - int24 tickUpper = tickLower + key.tickSpacing; - - // because `modifyPosition` includes not just principal value but also fees, we cannot allocate - // the proceeds pro-rata. if we were to do so, users who have been in a limit order that's partially filled - // could be unfairly diluted by a user sychronously placing then killing a limit order to skim off fees. - // to prevent this, we allocate all fee revenue to remaining limit order placers, unless this is the last order. - if (!removingAllLiquidity) { - (, BalanceDelta deltaFee) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickUpper, - liquidityDelta: 0, - salt: 0 - }), - ZERO_BYTES - ); - - if (deltaFee.amount0() > 0) { - manager.mint(address(this), key.currency0.toId(), amount0Fee = uint128(deltaFee.amount0())); - } - if (deltaFee.amount1() > 0) { - manager.mint(address(this), key.currency1.toId(), amount1Fee = uint128(deltaFee.amount1())); - } - } - - (BalanceDelta delta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickUpper, - liquidityDelta: liquidityDelta, - salt: 0 - }), - ZERO_BYTES - ); - - if (delta.amount0() > 0) { - key.currency0.take(manager, to, uint256(uint128(delta.amount0())), false); - } - if (delta.amount1() > 0) { - key.currency1.take(manager, to, uint256(uint128(delta.amount1())), false); - } - } - - function withdraw(Epoch epoch, address to) external returns (uint256 amount0, uint256 amount1) { - EpochInfo storage epochInfo = epochInfos[epoch]; - - if (!epochInfo.filled) revert NotFilled(); - - uint128 liquidity = epochInfo.liquidity[msg.sender]; - if (liquidity == 0) revert ZeroLiquidity(); - delete epochInfo.liquidity[msg.sender]; - - uint128 liquidityTotal = epochInfo.liquidityTotal; - - amount0 = FullMath.mulDiv(epochInfo.token0Total, liquidity, liquidityTotal); - amount1 = FullMath.mulDiv(epochInfo.token1Total, liquidity, liquidityTotal); - - epochInfo.token0Total -= amount0; - epochInfo.token1Total -= amount1; - epochInfo.liquidityTotal = liquidityTotal - liquidity; - - manager.unlock( - abi.encodeCall( - this.unlockCallbackWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to) - ) - ); - - emit Withdraw(msg.sender, epoch, liquidity); - } - - function unlockCallbackWithdraw( - Currency currency0, - Currency currency1, - uint256 token0Amount, - uint256 token1Amount, - address to - ) external selfOnly { - if (token0Amount > 0) { - manager.burn(address(this), currency0.toId(), token0Amount); - manager.take(currency0, to, token0Amount); - } - if (token1Amount > 0) { - manager.burn(address(this), currency1.toId(), token1Amount); - manager.take(currency1, to, token1Amount); - } - } - - function onERC1155Received(address, address, uint256, uint256, bytes calldata) external view returns (bytes4) { - if (msg.sender != address(manager)) revert NotPoolManagerToken(); - return IERC1155Receiver.onERC1155Received.selector; - } -} diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol deleted file mode 100644 index dc1f3b00..00000000 --- a/contracts/hooks/examples/TWAMM.sol +++ /dev/null @@ -1,654 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {TickBitmap} from "@uniswap/v4-core/src/libraries/TickBitmap.sol"; -import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol"; -import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {ITWAMM} from "../../interfaces/ITWAMM.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {TransferHelper} from "../../libraries/TransferHelper.sol"; -import {TwammMath} from "../../libraries/TWAMM/TwammMath.sol"; -import {OrderPool} from "../../libraries/TWAMM/OrderPool.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolGetters} from "../../libraries/PoolGetters.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; - -contract TWAMM is BaseHook, ITWAMM { - using TransferHelper for IERC20Minimal; - using CurrencyLibrary for Currency; - using CurrencySettler for Currency; - using OrderPool for OrderPool.State; - using PoolIdLibrary for PoolKey; - using TickMath for int24; - using TickMath for uint160; - using SafeCast for uint256; - using PoolGetters for IPoolManager; - using TickBitmap for mapping(int16 => uint256); - using StateLibrary for IPoolManager; - - bytes internal constant ZERO_BYTES = bytes(""); - - int256 internal constant MIN_DELTA = -1; - bool internal constant ZERO_FOR_ONE = true; - bool internal constant ONE_FOR_ZERO = false; - - /// @notice Contains full state related to the TWAMM - /// @member lastVirtualOrderTimestamp Last timestamp in which virtual orders were executed - /// @member orderPool0For1 Order pool trading token0 for token1 of pool - /// @member orderPool1For0 Order pool trading token1 for token0 of pool - /// @member orders Mapping of orderId to individual orders on pool - struct State { - uint256 lastVirtualOrderTimestamp; - OrderPool.State orderPool0For1; - OrderPool.State orderPool1For0; - mapping(bytes32 => Order) orders; - } - - /// @inheritdoc ITWAMM - uint256 public immutable expirationInterval; - // twammStates[poolId] => Twamm.State - mapping(PoolId => State) internal twammStates; - // tokensOwed[token][owner] => amountOwed - mapping(Currency => mapping(address => uint256)) public tokensOwed; - - constructor(IPoolManager _manager, uint256 _expirationInterval) BaseHook(_manager) { - expirationInterval = _expirationInterval; - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: false, - beforeAddLiquidity: true, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - virtual - override - onlyByManager - returns (bytes4) - { - // one-time initialization enforced in PoolManager - initialize(_getTWAMM(key)); - return BaseHook.beforeInitialize.selector; - } - - function beforeAddLiquidity( - address, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external override onlyByManager returns (bytes4) { - executeTWAMMOrders(key); - return BaseHook.beforeAddLiquidity.selector; - } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - onlyByManager - returns (bytes4, BeforeSwapDelta, uint24) - { - executeTWAMMOrders(key); - return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - function lastVirtualOrderTimestamp(PoolId key) external view returns (uint256) { - return twammStates[key].lastVirtualOrderTimestamp; - } - - function getOrder(PoolKey calldata poolKey, OrderKey calldata orderKey) external view returns (Order memory) { - return _getOrder(twammStates[PoolId.wrap(keccak256(abi.encode(poolKey)))], orderKey); - } - - function getOrderPool(PoolKey calldata key, bool zeroForOne) - external - view - returns (uint256 sellRateCurrent, uint256 earningsFactorCurrent) - { - State storage twamm = _getTWAMM(key); - return zeroForOne - ? (twamm.orderPool0For1.sellRateCurrent, twamm.orderPool0For1.earningsFactorCurrent) - : (twamm.orderPool1For0.sellRateCurrent, twamm.orderPool1For0.earningsFactorCurrent); - } - - /// @notice Initialize TWAMM state - function initialize(State storage self) internal { - self.lastVirtualOrderTimestamp = block.timestamp; - } - - /// @inheritdoc ITWAMM - function executeTWAMMOrders(PoolKey memory key) public { - PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - State storage twamm = twammStates[poolId]; - - (bool zeroForOne, uint160 sqrtPriceLimitX96) = - _executeTWAMMOrders(twamm, manager, key, PoolParamsOnExecute(sqrtPriceX96, manager.getLiquidity(poolId))); - - if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - manager.unlock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96))); - } - } - - /// @inheritdoc ITWAMM - function submitOrder(PoolKey calldata key, OrderKey memory orderKey, uint256 amountIn) - external - returns (bytes32 orderId) - { - PoolId poolId = PoolId.wrap(keccak256(abi.encode(key))); - State storage twamm = twammStates[poolId]; - executeTWAMMOrders(key); - - uint256 sellRate; - unchecked { - // checks done in TWAMM library - uint256 duration = orderKey.expiration - block.timestamp; - sellRate = amountIn / duration; - orderId = _submitOrder(twamm, orderKey, sellRate); - IERC20Minimal(orderKey.zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1)) - .safeTransferFrom(msg.sender, address(this), sellRate * duration); - } - - emit SubmitOrder( - poolId, - orderKey.owner, - orderKey.expiration, - orderKey.zeroForOne, - sellRate, - _getOrder(twamm, orderKey).earningsFactorLast - ); - } - - /// @notice Submits a new long term order into the TWAMM - /// @dev executeTWAMMOrders must be executed up to current timestamp before calling submitOrder - /// @param orderKey The OrderKey for the new order - function _submitOrder(State storage self, OrderKey memory orderKey, uint256 sellRate) - internal - returns (bytes32 orderId) - { - if (orderKey.owner != msg.sender) revert MustBeOwner(orderKey.owner, msg.sender); - if (self.lastVirtualOrderTimestamp == 0) revert NotInitialized(); - if (orderKey.expiration <= block.timestamp) revert ExpirationLessThanBlocktime(orderKey.expiration); - if (sellRate == 0) revert SellRateCannotBeZero(); - if (orderKey.expiration % expirationInterval != 0) revert ExpirationNotOnInterval(orderKey.expiration); - - orderId = _orderId(orderKey); - if (self.orders[orderId].sellRate != 0) revert OrderAlreadyExists(orderKey); - - OrderPool.State storage orderPool = orderKey.zeroForOne ? self.orderPool0For1 : self.orderPool1For0; - - unchecked { - orderPool.sellRateCurrent += sellRate; - orderPool.sellRateEndingAtInterval[orderKey.expiration] += sellRate; - } - - self.orders[orderId] = Order({sellRate: sellRate, earningsFactorLast: orderPool.earningsFactorCurrent}); - } - - /// @inheritdoc ITWAMM - function updateOrder(PoolKey memory key, OrderKey memory orderKey, int256 amountDelta) - external - returns (uint256 tokens0Owed, uint256 tokens1Owed) - { - PoolId poolId = PoolId.wrap(keccak256(abi.encode(key))); - State storage twamm = twammStates[poolId]; - - executeTWAMMOrders(key); - - // This call reverts if the caller is not the owner of the order - (uint256 buyTokensOwed, uint256 sellTokensOwed, uint256 newSellrate, uint256 newEarningsFactorLast) = - _updateOrder(twamm, orderKey, amountDelta); - - if (orderKey.zeroForOne) { - tokens0Owed += sellTokensOwed; - tokens1Owed += buyTokensOwed; - } else { - tokens0Owed += buyTokensOwed; - tokens1Owed += sellTokensOwed; - } - - tokensOwed[key.currency0][orderKey.owner] += tokens0Owed; - tokensOwed[key.currency1][orderKey.owner] += tokens1Owed; - - if (amountDelta > 0) { - IERC20Minimal(orderKey.zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1)) - .safeTransferFrom(msg.sender, address(this), uint256(amountDelta)); - } - - emit UpdateOrder( - poolId, orderKey.owner, orderKey.expiration, orderKey.zeroForOne, newSellrate, newEarningsFactorLast - ); - } - - function _updateOrder(State storage self, OrderKey memory orderKey, int256 amountDelta) - internal - returns (uint256 buyTokensOwed, uint256 sellTokensOwed, uint256 newSellRate, uint256 earningsFactorLast) - { - Order storage order = _getOrder(self, orderKey); - OrderPool.State storage orderPool = orderKey.zeroForOne ? self.orderPool0For1 : self.orderPool1For0; - - if (orderKey.owner != msg.sender) revert MustBeOwner(orderKey.owner, msg.sender); - if (order.sellRate == 0) revert OrderDoesNotExist(orderKey); - if (amountDelta != 0 && orderKey.expiration <= block.timestamp) revert CannotModifyCompletedOrder(orderKey); - - unchecked { - uint256 earningsFactor = orderPool.earningsFactorCurrent - order.earningsFactorLast; - buyTokensOwed = (earningsFactor * order.sellRate) >> FixedPoint96.RESOLUTION; - earningsFactorLast = orderPool.earningsFactorCurrent; - order.earningsFactorLast = earningsFactorLast; - - if (orderKey.expiration <= block.timestamp) { - delete self.orders[_orderId(orderKey)]; - } - - if (amountDelta != 0) { - uint256 duration = orderKey.expiration - block.timestamp; - uint256 unsoldAmount = order.sellRate * duration; - if (amountDelta == MIN_DELTA) amountDelta = -(unsoldAmount.toInt256()); - int256 newSellAmount = unsoldAmount.toInt256() + amountDelta; - if (newSellAmount < 0) revert InvalidAmountDelta(orderKey, unsoldAmount, amountDelta); - - newSellRate = uint256(newSellAmount) / duration; - - if (amountDelta < 0) { - uint256 sellRateDelta = order.sellRate - newSellRate; - orderPool.sellRateCurrent -= sellRateDelta; - orderPool.sellRateEndingAtInterval[orderKey.expiration] -= sellRateDelta; - sellTokensOwed = uint256(-amountDelta); - } else { - uint256 sellRateDelta = newSellRate - order.sellRate; - orderPool.sellRateCurrent += sellRateDelta; - orderPool.sellRateEndingAtInterval[orderKey.expiration] += sellRateDelta; - } - if (newSellRate == 0) { - delete self.orders[_orderId(orderKey)]; - } else { - order.sellRate = newSellRate; - } - } - } - } - - /// @inheritdoc ITWAMM - function claimTokens(Currency token, address to, uint256 amountRequested) - external - returns (uint256 amountTransferred) - { - uint256 currentBalance = token.balanceOfSelf(); - amountTransferred = tokensOwed[token][msg.sender]; - if (amountRequested != 0 && amountRequested < amountTransferred) amountTransferred = amountRequested; - if (currentBalance < amountTransferred) amountTransferred = currentBalance; // to catch precision errors - tokensOwed[token][msg.sender] -= amountTransferred; - IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); - } - - function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { - (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = - abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); - - BalanceDelta delta = manager.swap(key, swapParams, ZERO_BYTES); - - if (swapParams.zeroForOne) { - if (delta.amount0() < 0) { - key.currency0.settle(manager, address(this), uint256(uint128(-delta.amount0())), false); - } - if (delta.amount1() > 0) { - key.currency1.take(manager, address(this), uint256(uint128(delta.amount1())), false); - } - } else { - if (delta.amount1() < 0) { - key.currency1.settle(manager, address(this), uint256(uint128(-delta.amount1())), false); - } - if (delta.amount0() > 0) { - key.currency0.take(manager, address(this), uint256(uint128(delta.amount0())), false); - } - } - return bytes(""); - } - - function _getTWAMM(PoolKey memory key) private view returns (State storage) { - return twammStates[PoolId.wrap(keccak256(abi.encode(key)))]; - } - - struct PoolParamsOnExecute { - uint160 sqrtPriceX96; - uint128 liquidity; - } - - /// @notice Executes all existing long term orders in the TWAMM - /// @param pool The relevant state of the pool - function _executeTWAMMOrders( - State storage self, - IPoolManager manager, - PoolKey memory key, - PoolParamsOnExecute memory pool - ) internal returns (bool zeroForOne, uint160 newSqrtPriceX96) { - if (!_hasOutstandingOrders(self)) { - self.lastVirtualOrderTimestamp = block.timestamp; - return (false, 0); - } - - uint160 initialSqrtPriceX96 = pool.sqrtPriceX96; - uint256 prevTimestamp = self.lastVirtualOrderTimestamp; - uint256 nextExpirationTimestamp = prevTimestamp + (expirationInterval - (prevTimestamp % expirationInterval)); - - OrderPool.State storage orderPool0For1 = self.orderPool0For1; - OrderPool.State storage orderPool1For0 = self.orderPool1For0; - - unchecked { - while (nextExpirationTimestamp <= block.timestamp) { - if ( - orderPool0For1.sellRateEndingAtInterval[nextExpirationTimestamp] > 0 - || orderPool1For0.sellRateEndingAtInterval[nextExpirationTimestamp] > 0 - ) { - if (orderPool0For1.sellRateCurrent != 0 && orderPool1For0.sellRateCurrent != 0) { - pool = _advanceToNewTimestamp( - self, - manager, - key, - AdvanceParams( - expirationInterval, - nextExpirationTimestamp, - nextExpirationTimestamp - prevTimestamp, - pool - ) - ); - } else { - pool = _advanceTimestampForSinglePoolSell( - self, - manager, - key, - AdvanceSingleParams( - expirationInterval, - nextExpirationTimestamp, - nextExpirationTimestamp - prevTimestamp, - pool, - orderPool0For1.sellRateCurrent != 0 - ) - ); - } - prevTimestamp = nextExpirationTimestamp; - } - nextExpirationTimestamp += expirationInterval; - - if (!_hasOutstandingOrders(self)) break; - } - - if (prevTimestamp < block.timestamp && _hasOutstandingOrders(self)) { - if (orderPool0For1.sellRateCurrent != 0 && orderPool1For0.sellRateCurrent != 0) { - pool = _advanceToNewTimestamp( - self, - manager, - key, - AdvanceParams(expirationInterval, block.timestamp, block.timestamp - prevTimestamp, pool) - ); - } else { - pool = _advanceTimestampForSinglePoolSell( - self, - manager, - key, - AdvanceSingleParams( - expirationInterval, - block.timestamp, - block.timestamp - prevTimestamp, - pool, - orderPool0For1.sellRateCurrent != 0 - ) - ); - } - } - } - - self.lastVirtualOrderTimestamp = block.timestamp; - newSqrtPriceX96 = pool.sqrtPriceX96; - zeroForOne = initialSqrtPriceX96 > newSqrtPriceX96; - } - - struct AdvanceParams { - uint256 expirationInterval; - uint256 nextTimestamp; - uint256 secondsElapsed; - PoolParamsOnExecute pool; - } - - function _advanceToNewTimestamp( - State storage self, - IPoolManager manager, - PoolKey memory poolKey, - AdvanceParams memory params - ) private returns (PoolParamsOnExecute memory) { - uint160 finalSqrtPriceX96; - uint256 secondsElapsedX96 = params.secondsElapsed * FixedPoint96.Q96; - - OrderPool.State storage orderPool0For1 = self.orderPool0For1; - OrderPool.State storage orderPool1For0 = self.orderPool1For0; - - while (true) { - TwammMath.ExecutionUpdateParams memory executionParams = TwammMath.ExecutionUpdateParams( - secondsElapsedX96, - params.pool.sqrtPriceX96, - params.pool.liquidity, - orderPool0For1.sellRateCurrent, - orderPool1For0.sellRateCurrent - ); - - finalSqrtPriceX96 = TwammMath.getNewSqrtPriceX96(executionParams); - - (bool crossingInitializedTick, int24 tick) = - _isCrossingInitializedTick(params.pool, manager, poolKey, finalSqrtPriceX96); - unchecked { - if (crossingInitializedTick) { - uint256 secondsUntilCrossingX96; - (params.pool, secondsUntilCrossingX96) = _advanceTimeThroughTickCrossing( - self, - manager, - poolKey, - TickCrossingParams(tick, params.nextTimestamp, secondsElapsedX96, params.pool) - ); - secondsElapsedX96 = secondsElapsedX96 - secondsUntilCrossingX96; - } else { - (uint256 earningsFactorPool0, uint256 earningsFactorPool1) = - TwammMath.calculateEarningsUpdates(executionParams, finalSqrtPriceX96); - - if (params.nextTimestamp % params.expirationInterval == 0) { - orderPool0For1.advanceToInterval(params.nextTimestamp, earningsFactorPool0); - orderPool1For0.advanceToInterval(params.nextTimestamp, earningsFactorPool1); - } else { - orderPool0For1.advanceToCurrentTime(earningsFactorPool0); - orderPool1For0.advanceToCurrentTime(earningsFactorPool1); - } - params.pool.sqrtPriceX96 = finalSqrtPriceX96; - break; - } - } - } - - return params.pool; - } - - struct AdvanceSingleParams { - uint256 expirationInterval; - uint256 nextTimestamp; - uint256 secondsElapsed; - PoolParamsOnExecute pool; - bool zeroForOne; - } - - function _advanceTimestampForSinglePoolSell( - State storage self, - IPoolManager manager, - PoolKey memory poolKey, - AdvanceSingleParams memory params - ) private returns (PoolParamsOnExecute memory) { - OrderPool.State storage orderPool = params.zeroForOne ? self.orderPool0For1 : self.orderPool1For0; - uint256 sellRateCurrent = orderPool.sellRateCurrent; - uint256 amountSelling = sellRateCurrent * params.secondsElapsed; - uint256 totalEarnings; - - while (true) { - uint160 finalSqrtPriceX96 = SqrtPriceMath.getNextSqrtPriceFromInput( - params.pool.sqrtPriceX96, params.pool.liquidity, amountSelling, params.zeroForOne - ); - - (bool crossingInitializedTick, int24 tick) = - _isCrossingInitializedTick(params.pool, manager, poolKey, finalSqrtPriceX96); - - if (crossingInitializedTick) { - (, int128 liquidityNetAtTick) = manager.getTickLiquidity(poolKey.toId(), tick); - uint160 initializedSqrtPrice = TickMath.getSqrtPriceAtTick(tick); - - uint256 swapDelta0 = SqrtPriceMath.getAmount0Delta( - params.pool.sqrtPriceX96, initializedSqrtPrice, params.pool.liquidity, true - ); - uint256 swapDelta1 = SqrtPriceMath.getAmount1Delta( - params.pool.sqrtPriceX96, initializedSqrtPrice, params.pool.liquidity, true - ); - - params.pool.liquidity = params.zeroForOne - ? params.pool.liquidity - uint128(liquidityNetAtTick) - : params.pool.liquidity + uint128(-liquidityNetAtTick); - params.pool.sqrtPriceX96 = initializedSqrtPrice; - - unchecked { - totalEarnings += params.zeroForOne ? swapDelta1 : swapDelta0; - amountSelling -= params.zeroForOne ? swapDelta0 : swapDelta1; - } - } else { - if (params.zeroForOne) { - totalEarnings += SqrtPriceMath.getAmount1Delta( - params.pool.sqrtPriceX96, finalSqrtPriceX96, params.pool.liquidity, true - ); - } else { - totalEarnings += SqrtPriceMath.getAmount0Delta( - params.pool.sqrtPriceX96, finalSqrtPriceX96, params.pool.liquidity, true - ); - } - - uint256 accruedEarningsFactor = (totalEarnings * FixedPoint96.Q96) / sellRateCurrent; - - if (params.nextTimestamp % params.expirationInterval == 0) { - orderPool.advanceToInterval(params.nextTimestamp, accruedEarningsFactor); - } else { - orderPool.advanceToCurrentTime(accruedEarningsFactor); - } - params.pool.sqrtPriceX96 = finalSqrtPriceX96; - break; - } - } - - return params.pool; - } - - struct TickCrossingParams { - int24 initializedTick; - uint256 nextTimestamp; - uint256 secondsElapsedX96; - PoolParamsOnExecute pool; - } - - function _advanceTimeThroughTickCrossing( - State storage self, - IPoolManager manager, - PoolKey memory poolKey, - TickCrossingParams memory params - ) private returns (PoolParamsOnExecute memory, uint256) { - uint160 initializedSqrtPrice = params.initializedTick.getSqrtPriceAtTick(); - - uint256 secondsUntilCrossingX96 = TwammMath.calculateTimeBetweenTicks( - params.pool.liquidity, - params.pool.sqrtPriceX96, - initializedSqrtPrice, - self.orderPool0For1.sellRateCurrent, - self.orderPool1For0.sellRateCurrent - ); - - (uint256 earningsFactorPool0, uint256 earningsFactorPool1) = TwammMath.calculateEarningsUpdates( - TwammMath.ExecutionUpdateParams( - secondsUntilCrossingX96, - params.pool.sqrtPriceX96, - params.pool.liquidity, - self.orderPool0For1.sellRateCurrent, - self.orderPool1For0.sellRateCurrent - ), - initializedSqrtPrice - ); - - self.orderPool0For1.advanceToCurrentTime(earningsFactorPool0); - self.orderPool1For0.advanceToCurrentTime(earningsFactorPool1); - - unchecked { - // update pool - (, int128 liquidityNet) = manager.getTickLiquidity(poolKey.toId(), params.initializedTick); - if (initializedSqrtPrice < params.pool.sqrtPriceX96) liquidityNet = -liquidityNet; - params.pool.liquidity = liquidityNet < 0 - ? params.pool.liquidity - uint128(-liquidityNet) - : params.pool.liquidity + uint128(liquidityNet); - - params.pool.sqrtPriceX96 = initializedSqrtPrice; - } - return (params.pool, secondsUntilCrossingX96); - } - - function _isCrossingInitializedTick( - PoolParamsOnExecute memory pool, - IPoolManager manager, - PoolKey memory poolKey, - uint160 nextSqrtPriceX96 - ) internal view returns (bool crossingInitializedTick, int24 nextTickInit) { - // use current price as a starting point for nextTickInit - nextTickInit = pool.sqrtPriceX96.getTickAtSqrtPrice(); - int24 targetTick = nextSqrtPriceX96.getTickAtSqrtPrice(); - bool searchingLeft = nextSqrtPriceX96 < pool.sqrtPriceX96; - bool nextTickInitFurtherThanTarget = false; // initialize as false - - // nextTickInit returns the furthest tick within one word if no tick within that word is initialized - // so we must keep iterating if we haven't reached a tick further than our target tick - while (!nextTickInitFurtherThanTarget) { - unchecked { - if (searchingLeft) nextTickInit -= 1; - } - (nextTickInit, crossingInitializedTick) = manager.getNextInitializedTickWithinOneWord( - poolKey.toId(), nextTickInit, poolKey.tickSpacing, searchingLeft - ); - nextTickInitFurtherThanTarget = searchingLeft ? nextTickInit <= targetTick : nextTickInit > targetTick; - if (crossingInitializedTick == true) break; - } - if (nextTickInitFurtherThanTarget) crossingInitializedTick = false; - } - - function _getOrder(State storage self, OrderKey memory key) internal view returns (Order storage) { - return self.orders[_orderId(key)]; - } - - function _orderId(OrderKey memory key) private pure returns (bytes32) { - return keccak256(abi.encode(key)); - } - - function _hasOutstandingOrders(State storage self) internal view returns (bool) { - return self.orderPool0For1.sellRateCurrent != 0 || self.orderPool1For0.sellRateCurrent != 0; - } -} diff --git a/contracts/hooks/examples/VolatilityOracle.sol b/contracts/hooks/examples/VolatilityOracle.sol deleted file mode 100644 index 2900632f..00000000 --- a/contracts/hooks/examples/VolatilityOracle.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -contract VolatilityOracle is BaseHook { - using LPFeeLibrary for uint24; - - error MustUseDynamicFee(); - - uint32 immutable deployTimestamp; - - /// @dev For mocking - function _blockTimestamp() internal view virtual returns (uint32) { - return uint32(block.timestamp); - } - - constructor(IPoolManager _manager) BaseHook(_manager) { - deployTimestamp = _blockTimestamp(); - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: true, - beforeAddLiquidity: false, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: false, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - pure - override - returns (bytes4) - { - if (!key.fee.isDynamicFee()) revert MustUseDynamicFee(); - return VolatilityOracle.beforeInitialize.selector; - } - - function setFee(PoolKey calldata key) public { - uint24 startingFee = 3000; - uint32 lapsed = _blockTimestamp() - deployTimestamp; - uint24 fee = startingFee + (uint24(lapsed) * 100) / 60; // 100 bps a minute - manager.updateDynamicLPFee(key, fee); // initial fee 0.30% - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) - external - override - returns (bytes4) - { - setFee(key); - return BaseHook.afterInitialize.selector; - } -} diff --git a/contracts/interfaces/IPeripheryPayments.sol b/contracts/interfaces/IPeripheryPayments.sol deleted file mode 100644 index f3c24660..00000000 --- a/contracts/interfaces/IPeripheryPayments.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; - -/// @title Periphery Payments -/// @notice Functions to ease deposits and withdrawals of ETH -interface IPeripheryPayments { - // TODO: figure out if we still need unwrapWETH9 from v3? - - /// @notice Transfers the full amount of a token held by this contract to recipient - /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users - /// @param currency The contract address of the token which will be transferred to `recipient` - /// @param amountMinimum The minimum amount of token required for a transfer - /// @param recipient The destination address of the token - function sweepToken(Currency currency, uint256 amountMinimum, address recipient) external payable; -} diff --git a/contracts/interfaces/ITWAMM.sol b/contracts/interfaces/ITWAMM.sol deleted file mode 100644 index 3b932d3c..00000000 --- a/contracts/interfaces/ITWAMM.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -interface ITWAMM { - /// @notice Thrown when account other than owner attempts to interact with an order - /// @param owner The owner of the order - /// @param currentAccount The invalid account attempting to interact with the order - error MustBeOwner(address owner, address currentAccount); - - /// @notice Thrown when trying to cancel an already completed order - /// @param orderKey The orderKey - error CannotModifyCompletedOrder(OrderKey orderKey); - - /// @notice Thrown when trying to submit an order with an expiration that isn't on the interval. - /// @param expiration The expiration timestamp of the order - error ExpirationNotOnInterval(uint256 expiration); - - /// @notice Thrown when trying to submit an order with an expiration time in the past. - /// @param expiration The expiration timestamp of the order - error ExpirationLessThanBlocktime(uint256 expiration); - - /// @notice Thrown when trying to submit an order without initializing TWAMM state first - error NotInitialized(); - - /// @notice Thrown when trying to submit an order that's already ongoing. - /// @param orderKey The already existing orderKey - error OrderAlreadyExists(OrderKey orderKey); - - /// @notice Thrown when trying to interact with an order that does not exist. - /// @param orderKey The already existing orderKey - error OrderDoesNotExist(OrderKey orderKey); - - /// @notice Thrown when trying to subtract more value from a long term order than exists - /// @param orderKey The orderKey - /// @param unsoldAmount The amount still unsold - /// @param amountDelta The amount delta for the order - error InvalidAmountDelta(OrderKey orderKey, uint256 unsoldAmount, int256 amountDelta); - - /// @notice Thrown when submitting an order with a sellRate of 0 - error SellRateCannotBeZero(); - - /// @notice Information associated with a long term order - /// @member sellRate Amount of tokens sold per interval - /// @member earningsFactorLast The accrued earnings factor from which to start claiming owed earnings for this order - struct Order { - uint256 sellRate; - uint256 earningsFactorLast; - } - - /// @notice Information that identifies an order - /// @member owner Owner of the order - /// @member expiration Timestamp when the order expires - /// @member zeroForOne Bool whether the order is zeroForOne - struct OrderKey { - address owner; - uint160 expiration; - bool zeroForOne; - } - - /// @notice Emitted when a new long term order is submitted - /// @param poolId The id of the corresponding pool - /// @param owner The owner of the new order - /// @param expiration The expiration timestamp of the order - /// @param zeroForOne Whether the order is selling token 0 for token 1 - /// @param sellRate The sell rate of tokens per second being sold in the order - /// @param earningsFactorLast The current earningsFactor of the order pool - event SubmitOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - /// @notice Emitted when a long term order is updated - /// @param poolId The id of the corresponding pool - /// @param owner The owner of the existing order - /// @param expiration The expiration timestamp of the order - /// @param zeroForOne Whether the order is selling token 0 for token 1 - /// @param sellRate The updated sellRate of tokens per second being sold in the order - /// @param earningsFactorLast The current earningsFactor of the order pool - /// (since updated orders will claim existing earnings) - event UpdateOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - /// @notice Time interval on which orders are allowed to expire. Conserves processing needed on execute. - function expirationInterval() external view returns (uint256); - - /// @notice Submits a new long term order into the TWAMM. Also executes TWAMM orders if not up to date. - /// @param key The PoolKey for which to identify the amm pool of the order - /// @param orderKey The OrderKey for the new order - /// @param amountIn The amount of sell token to add to the order. Some precision on amountIn may be lost up to the - /// magnitude of (orderKey.expiration - block.timestamp) - /// @return orderId The bytes32 ID of the order - function submitOrder(PoolKey calldata key, OrderKey calldata orderKey, uint256 amountIn) - external - returns (bytes32 orderId); - - /// @notice Update an existing long term order with current earnings, optionally modify the amount selling. - /// @param key The PoolKey for which to identify the amm pool of the order - /// @param orderKey The OrderKey for which to identify the order - /// @param amountDelta The delta for the order sell amount. Negative to remove from order, positive to add, or - /// -1 to remove full amount from order. - function updateOrder(PoolKey calldata key, OrderKey calldata orderKey, int256 amountDelta) - external - returns (uint256 tokens0Owed, uint256 tokens1Owed); - - /// @notice Claim tokens owed from TWAMM contract - /// @param token The token to claim - /// @param to The receipient of the claim - /// @param amountRequested The amount of tokens requested to claim. Set to 0 to claim all. - /// @return amountTransferred The total token amount to be collected - function claimTokens(Currency token, address to, uint256 amountRequested) - external - returns (uint256 amountTransferred); - - /// @notice Executes TWAMM orders on the pool, swapping on the pool itself to make up the difference between the - /// two TWAMM pools swapping against each other - /// @param key The pool key associated with the TWAMM - function executeTWAMMOrders(PoolKey memory key) external; - - function tokensOwed(Currency token, address owner) external returns (uint256); -} diff --git a/contracts/interfaces/external/IERC1271.sol b/contracts/interfaces/external/IERC1271.sol deleted file mode 100644 index dcb30cb8..00000000 --- a/contracts/interfaces/external/IERC1271.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.0; - -/// @title Interface for verifying contract-based account signatures -/// @notice Interface that verifies provided signature for the data -/// @dev Interface defined by EIP-1271 -interface IERC1271 { - /// @notice Returns whether the provided signature is valid for the provided data - /// @dev MUST return the bytes4 magic value 0x1626ba7e when function passes. - /// MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5). - /// MUST allow external calls. - /// @param hash Hash of the data to be signed - /// @param signature Signature byte array associated with _data - /// @return magicValue The bytes4 magic value 0x1626ba7e - function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); -} diff --git a/contracts/libraries/LiquidityAmounts.sol b/contracts/libraries/LiquidityAmounts.sol deleted file mode 100644 index 742e48f5..00000000 --- a/contracts/libraries/LiquidityAmounts.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; - -import "@uniswap/v4-core/src/libraries/FullMath.sol"; -import "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; - -/// @title Liquidity amount functions -/// @notice Provides functions for computing liquidity amounts from token amounts and prices -library LiquidityAmounts { - /// @notice Downcasts uint256 to uint128 - /// @param x The uint258 to be downcasted - /// @return y The passed value, downcasted to uint128 - function toUint128(uint256 x) private pure returns (uint128 y) { - require((y = uint128(x)) == x); - } - - /// @notice Computes the amount of liquidity received for a given amount of token0 and price range - /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)) - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param amount0 The amount0 being sent in - /// @return liquidity The amount of returned liquidity - function getLiquidityForAmount0(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount0) - internal - pure - returns (uint128 liquidity) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96); - return toUint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96)); - } - - /// @notice Computes the amount of liquidity received for a given amount of token1 and price range - /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param amount1 The amount1 being sent in - /// @return liquidity The amount of returned liquidity - function getLiquidityForAmount1(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount1) - internal - pure - returns (uint128 liquidity) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - return toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96)); - } - - /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current - /// pool prices and the prices at the tick boundaries - /// @param sqrtRatioX96 A sqrt price representing the current pool prices - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param amount0 The amount of token0 being sent in - /// @param amount1 The amount of token1 being sent in - /// @return liquidity The maximum amount of liquidity received - function getLiquidityForAmounts( - uint160 sqrtRatioX96, - uint160 sqrtRatioAX96, - uint160 sqrtRatioBX96, - uint256 amount0, - uint256 amount1 - ) internal pure returns (uint128 liquidity) { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - if (sqrtRatioX96 <= sqrtRatioAX96) { - liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0); - } else if (sqrtRatioX96 < sqrtRatioBX96) { - uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0); - uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1); - - liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; - } else { - liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1); - } - } - - /// @notice Computes the amount of token0 for a given amount of liquidity and a price range - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param liquidity The liquidity being valued - /// @return amount0 The amount of token0 - function getAmount0ForLiquidity(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity) - internal - pure - returns (uint256 amount0) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - return FullMath.mulDiv( - uint256(liquidity) << FixedPoint96.RESOLUTION, sqrtRatioBX96 - sqrtRatioAX96, sqrtRatioBX96 - ) / sqrtRatioAX96; - } - - /// @notice Computes the amount of token1 for a given amount of liquidity and a price range - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param liquidity The liquidity being valued - /// @return amount1 The amount of token1 - function getAmount1ForLiquidity(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity) - internal - pure - returns (uint256 amount1) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - return FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96); - } - - /// @notice Computes the token0 and token1 value for a given amount of liquidity, the current - /// pool prices and the prices at the tick boundaries - /// @param sqrtRatioX96 A sqrt price representing the current pool prices - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param liquidity The liquidity being valued - /// @return amount0 The amount of token0 - /// @return amount1 The amount of token1 - function getAmountsForLiquidity( - uint160 sqrtRatioX96, - uint160 sqrtRatioAX96, - uint160 sqrtRatioBX96, - uint128 liquidity - ) internal pure returns (uint256 amount0, uint256 amount1) { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - if (sqrtRatioX96 <= sqrtRatioAX96) { - amount0 = getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); - } else if (sqrtRatioX96 < sqrtRatioBX96) { - amount0 = getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity); - amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity); - } else { - amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); - } - } -} diff --git a/contracts/libraries/Oracle.sol b/contracts/libraries/Oracle.sol deleted file mode 100644 index 822f356f..00000000 --- a/contracts/libraries/Oracle.sol +++ /dev/null @@ -1,337 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -/// @title Oracle -/// @notice Provides price and liquidity data useful for a wide variety of system designs -/// @dev Instances of stored oracle data, "observations", are collected in the oracle array -/// Every pool is initialized with an oracle array length of 1. Anyone can pay the SSTOREs to increase the -/// maximum length of the oracle array. New slots will be added when the array is fully populated. -/// Observations are overwritten when the full length of the oracle array is populated. -/// The most recent observation is available, independent of the length of the oracle array, by passing 0 to observe() -library Oracle { - /// @notice Thrown when trying to interact with an Oracle of a non-initialized pool - error OracleCardinalityCannotBeZero(); - - /// @notice Thrown when trying to observe a price that is older than the oldest recorded price - /// @param oldestTimestamp Timestamp of the oldest remaining observation - /// @param targetTimestamp Invalid timestamp targeted to be observed - error TargetPredatesOldestObservation(uint32 oldestTimestamp, uint32 targetTimestamp); - - struct Observation { - // the block timestamp of the observation - uint32 blockTimestamp; - // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized - int56 tickCumulative; - // the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized - uint160 secondsPerLiquidityCumulativeX128; - // whether or not the observation is initialized - bool initialized; - } - - /// @notice Transforms a previous observation into a new observation, given the passage of time and the current tick and liquidity values - /// @dev blockTimestamp _must_ be chronologically equal to or greater than last.blockTimestamp, safe for 0 or 1 overflows - /// @param last The specified observation to be transformed - /// @param blockTimestamp The timestamp of the new observation - /// @param tick The active tick at the time of the new observation - /// @param liquidity The total in-range liquidity at the time of the new observation - /// @return Observation The newly populated observation - function transform(Observation memory last, uint32 blockTimestamp, int24 tick, uint128 liquidity) - private - pure - returns (Observation memory) - { - unchecked { - uint32 delta = blockTimestamp - last.blockTimestamp; - return Observation({ - blockTimestamp: blockTimestamp, - tickCumulative: last.tickCumulative + int56(tick) * int56(uint56(delta)), - secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 - + ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)), - initialized: true - }); - } - } - - /// @notice Initialize the oracle array by writing the first slot. Called once for the lifecycle of the observations array - /// @param self The stored oracle array - /// @param time The time of the oracle initialization, via block.timestamp truncated to uint32 - /// @return cardinality The number of populated elements in the oracle array - /// @return cardinalityNext The new length of the oracle array, independent of population - function initialize(Observation[65535] storage self, uint32 time) - internal - returns (uint16 cardinality, uint16 cardinalityNext) - { - self[0] = Observation({ - blockTimestamp: time, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: true - }); - return (1, 1); - } - - /// @notice Writes an oracle observation to the array - /// @dev Writable at most once per block. Index represents the most recently written element. cardinality and index must be tracked externally. - /// If the index is at the end of the allowable array length (according to cardinality), and the next cardinality - /// is greater than the current one, cardinality may be increased. This restriction is created to preserve ordering. - /// @param self The stored oracle array - /// @param index The index of the observation that was most recently written to the observations array - /// @param blockTimestamp The timestamp of the new observation - /// @param tick The active tick at the time of the new observation - /// @param liquidity The total in-range liquidity at the time of the new observation - /// @param cardinality The number of populated elements in the oracle array - /// @param cardinalityNext The new length of the oracle array, independent of population - /// @return indexUpdated The new index of the most recently written element in the oracle array - /// @return cardinalityUpdated The new cardinality of the oracle array - function write( - Observation[65535] storage self, - uint16 index, - uint32 blockTimestamp, - int24 tick, - uint128 liquidity, - uint16 cardinality, - uint16 cardinalityNext - ) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) { - unchecked { - Observation memory last = self[index]; - - // early return if we've already written an observation this block - if (last.blockTimestamp == blockTimestamp) return (index, cardinality); - - // if the conditions are right, we can bump the cardinality - if (cardinalityNext > cardinality && index == (cardinality - 1)) { - cardinalityUpdated = cardinalityNext; - } else { - cardinalityUpdated = cardinality; - } - - indexUpdated = (index + 1) % cardinalityUpdated; - self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity); - } - } - - /// @notice Prepares the oracle array to store up to `next` observations - /// @param self The stored oracle array - /// @param current The current next cardinality of the oracle array - /// @param next The proposed next cardinality which will be populated in the oracle array - /// @return next The next cardinality which will be populated in the oracle array - function grow(Observation[65535] storage self, uint16 current, uint16 next) internal returns (uint16) { - unchecked { - if (current == 0) revert OracleCardinalityCannotBeZero(); - // no-op if the passed next value isn't greater than the current next value - if (next <= current) return current; - // store in each slot to prevent fresh SSTOREs in swaps - // this data will not be used because the initialized boolean is still false - for (uint16 i = current; i < next; i++) { - self[i].blockTimestamp = 1; - } - return next; - } - } - - /// @notice comparator for 32-bit timestamps - /// @dev safe for 0 or 1 overflows, a and b _must_ be chronologically before or equal to time - /// @param time A timestamp truncated to 32 bits - /// @param a A comparison timestamp from which to determine the relative position of `time` - /// @param b From which to determine the relative position of `time` - /// @return Whether `a` is chronologically <= `b` - function lte(uint32 time, uint32 a, uint32 b) private pure returns (bool) { - unchecked { - // if there hasn't been overflow, no need to adjust - if (a <= time && b <= time) return a <= b; - - uint256 aAdjusted = a > time ? a : a + 2 ** 32; - uint256 bAdjusted = b > time ? b : b + 2 ** 32; - - return aAdjusted <= bAdjusted; - } - } - - /// @notice Fetches the observations beforeOrAt and atOrAfter a target, i.e. where [beforeOrAt, atOrAfter] is satisfied. - /// The result may be the same observation, or adjacent observations. - /// @dev The answer must be contained in the array, used when the target is located within the stored observation - /// boundaries: older than the most recent observation and younger, or the same age as, the oldest observation - /// @param self The stored oracle array - /// @param time The current block.timestamp - /// @param target The timestamp at which the reserved observation should be for - /// @param index The index of the observation that was most recently written to the observations array - /// @param cardinality The number of populated elements in the oracle array - /// @return beforeOrAt The observation recorded before, or at, the target - /// @return atOrAfter The observation recorded at, or after, the target - function binarySearch(Observation[65535] storage self, uint32 time, uint32 target, uint16 index, uint16 cardinality) - private - view - returns (Observation memory beforeOrAt, Observation memory atOrAfter) - { - unchecked { - uint256 l = (index + 1) % cardinality; // oldest observation - uint256 r = l + cardinality - 1; // newest observation - uint256 i; - while (true) { - i = (l + r) / 2; - - beforeOrAt = self[i % cardinality]; - - // we've landed on an uninitialized tick, keep searching higher (more recently) - if (!beforeOrAt.initialized) { - l = i + 1; - continue; - } - - atOrAfter = self[(i + 1) % cardinality]; - - bool targetAtOrAfter = lte(time, beforeOrAt.blockTimestamp, target); - - // check if we've found the answer! - if (targetAtOrAfter && lte(time, target, atOrAfter.blockTimestamp)) break; - - if (!targetAtOrAfter) r = i - 1; - else l = i + 1; - } - } - } - - /// @notice Fetches the observations beforeOrAt and atOrAfter a given target, i.e. where [beforeOrAt, atOrAfter] is satisfied - /// @dev Assumes there is at least 1 initialized observation. - /// Used by observeSingle() to compute the counterfactual accumulator values as of a given block timestamp. - /// @param self The stored oracle array - /// @param time The current block.timestamp - /// @param target The timestamp at which the reserved observation should be for - /// @param tick The active tick at the time of the returned or simulated observation - /// @param index The index of the observation that was most recently written to the observations array - /// @param liquidity The total pool liquidity at the time of the call - /// @param cardinality The number of populated elements in the oracle array - /// @return beforeOrAt The observation which occurred at, or before, the given timestamp - /// @return atOrAfter The observation which occurred at, or after, the given timestamp - function getSurroundingObservations( - Observation[65535] storage self, - uint32 time, - uint32 target, - int24 tick, - uint16 index, - uint128 liquidity, - uint16 cardinality - ) private view returns (Observation memory beforeOrAt, Observation memory atOrAfter) { - unchecked { - // optimistically set before to the newest observation - beforeOrAt = self[index]; - - // if the target is chronologically at or after the newest observation, we can early return - if (lte(time, beforeOrAt.blockTimestamp, target)) { - if (beforeOrAt.blockTimestamp == target) { - // if newest observation equals target, we're in the same block, so we can ignore atOrAfter - return (beforeOrAt, atOrAfter); - } else { - // otherwise, we need to transform - return (beforeOrAt, transform(beforeOrAt, target, tick, liquidity)); - } - } - - // now, set before to the oldest observation - beforeOrAt = self[(index + 1) % cardinality]; - if (!beforeOrAt.initialized) beforeOrAt = self[0]; - - // ensure that the target is chronologically at or after the oldest observation - if (!lte(time, beforeOrAt.blockTimestamp, target)) { - revert TargetPredatesOldestObservation(beforeOrAt.blockTimestamp, target); - } - - // if we've reached this point, we have to binary search - return binarySearch(self, time, target, index, cardinality); - } - } - - /// @dev Reverts if an observation at or before the desired observation timestamp does not exist. - /// 0 may be passed as `secondsAgo' to return the current cumulative values. - /// If called with a timestamp falling between two observations, returns the counterfactual accumulator values - /// at exactly the timestamp between the two observations. - /// @param self The stored oracle array - /// @param time The current block timestamp - /// @param secondsAgo The amount of time to look back, in seconds, at which point to return an observation - /// @param tick The current tick - /// @param index The index of the observation that was most recently written to the observations array - /// @param liquidity The current in-range pool liquidity - /// @param cardinality The number of populated elements in the oracle array - /// @return tickCumulative The tick * time elapsed since the pool was first initialized, as of `secondsAgo` - /// @return secondsPerLiquidityCumulativeX128 The time elapsed / max(1, liquidity) since the pool was first initialized, as of `secondsAgo` - function observeSingle( - Observation[65535] storage self, - uint32 time, - uint32 secondsAgo, - int24 tick, - uint16 index, - uint128 liquidity, - uint16 cardinality - ) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) { - unchecked { - if (secondsAgo == 0) { - Observation memory last = self[index]; - if (last.blockTimestamp != time) last = transform(last, time, tick, liquidity); - return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128); - } - - uint32 target = time - secondsAgo; - - (Observation memory beforeOrAt, Observation memory atOrAfter) = - getSurroundingObservations(self, time, target, tick, index, liquidity, cardinality); - - if (target == beforeOrAt.blockTimestamp) { - // we're at the left boundary - return (beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128); - } else if (target == atOrAfter.blockTimestamp) { - // we're at the right boundary - return (atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128); - } else { - // we're in the middle - uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp; - uint32 targetDelta = target - beforeOrAt.blockTimestamp; - return ( - beforeOrAt.tickCumulative - + ((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / int56(uint56(observationTimeDelta))) - * int56(uint56(targetDelta)), - beforeOrAt.secondsPerLiquidityCumulativeX128 - + uint160( - ( - uint256( - atOrAfter.secondsPerLiquidityCumulativeX128 - - beforeOrAt.secondsPerLiquidityCumulativeX128 - ) * targetDelta - ) / observationTimeDelta - ) - ); - } - } - } - - /// @notice Returns the accumulator values as of each time seconds ago from the given time in the array of `secondsAgos` - /// @dev Reverts if `secondsAgos` > oldest observation - /// @param self The stored oracle array - /// @param time The current block.timestamp - /// @param secondsAgos Each amount of time to look back, in seconds, at which point to return an observation - /// @param tick The current tick - /// @param index The index of the observation that was most recently written to the observations array - /// @param liquidity The current in-range pool liquidity - /// @param cardinality The number of populated elements in the oracle array - /// @return tickCumulatives The tick * time elapsed since the pool was first initialized, as of each `secondsAgo` - /// @return secondsPerLiquidityCumulativeX128s The cumulative seconds / max(1, liquidity) since the pool was first initialized, as of each `secondsAgo` - function observe( - Observation[65535] storage self, - uint32 time, - uint32[] memory secondsAgos, - int24 tick, - uint16 index, - uint128 liquidity, - uint16 cardinality - ) internal view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) { - unchecked { - if (cardinality == 0) revert OracleCardinalityCannotBeZero(); - - tickCumulatives = new int56[](secondsAgos.length); - secondsPerLiquidityCumulativeX128s = new uint160[](secondsAgos.length); - for (uint256 i = 0; i < secondsAgos.length; i++) { - (tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = - observeSingle(self, time, secondsAgos[i], tick, index, liquidity, cardinality); - } - } - } -} diff --git a/contracts/libraries/PoolGetters.sol b/contracts/libraries/PoolGetters.sol deleted file mode 100644 index df31f3c1..00000000 --- a/contracts/libraries/PoolGetters.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -/// @title Helper functions to access pool information -/// TODO: Expose other getters on core with extsload. Only use when extsload is available and storage layout is frozen. -library PoolGetters { - uint256 constant POOL_SLOT = 10; - uint256 constant TICKS_OFFSET = 4; - uint256 constant TICK_BITMAP_OFFSET = 5; - - using StateLibrary for IPoolManager; - - function getNetLiquidityAtTick(IPoolManager poolManager, PoolId poolId, int24 tick) - internal - view - returns (int128 l) - { - bytes32 value = poolManager.extsload( - keccak256(abi.encode(tick, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICKS_OFFSET)) - ); - - assembly { - l := shr(128, and(value, shl(128, sub(shl(128, 1), 1)))) - } - } - - function getTickBitmapAtWord(IPoolManager poolManager, PoolId poolId, int16 word) - internal - view - returns (uint256 bm) - { - bm = uint256( - poolManager.extsload( - keccak256(abi.encode(word, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICK_BITMAP_OFFSET)) - ) - ); - } - - /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either - /// to the left (less than or equal to) or right (greater than) of the given tick - /// @param poolManager The mapping in which to compute the next initialized tick - /// @param tick The starting tick - /// @param tickSpacing The spacing between usable ticks - /// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) - /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick - /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks - function getNextInitializedTickWithinOneWord( - IPoolManager poolManager, - PoolId poolId, - int24 tick, - int24 tickSpacing, - bool lte - ) internal view returns (int24 next, bool initialized) { - unchecked { - int24 compressed = tick / tickSpacing; - if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity - - if (lte) { - (int16 wordPos, uint8 bitPos) = position(compressed); - // all the 1s at or to the right of the current bitPos - uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); - // uint256 masked = self[wordPos] & mask; - uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); - uint256 masked = tickBitmap & mask; - - // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word - initialized = masked != 0; - // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick - next = initialized - ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing - : (compressed - int24(uint24(bitPos))) * tickSpacing; - } else { - // start from the word of the next tick, since the current tick state doesn't matter - (int16 wordPos, uint8 bitPos) = position(compressed + 1); - // all the 1s at or to the left of the bitPos - uint256 mask = ~((1 << bitPos) - 1); - uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); - uint256 masked = tickBitmap & mask; - - // if there are no initialized ticks to the left of the current tick, return leftmost in the word - initialized = masked != 0; - // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick - next = initialized - ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing - : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; - } - } - } - - /// @notice Computes the position in the mapping where the initialized bit for a tick lives - /// @param tick The tick for which to compute the position - /// @return wordPos The key in the mapping containing the word in which the bit is stored - /// @return bitPos The bit position in the word where the flag is stored - function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { - unchecked { - wordPos = int16(tick >> 8); - bitPos = uint8(int8(tick % 256)); - } - } -} diff --git a/contracts/libraries/TWAMM/ABDKMathQuad.sol b/contracts/libraries/TWAMM/ABDKMathQuad.sol deleted file mode 100644 index 00fa5a05..00000000 --- a/contracts/libraries/TWAMM/ABDKMathQuad.sol +++ /dev/null @@ -1,1546 +0,0 @@ -// SPDX-License-Identifier: BSD-4-Clause -/* - * ABDK Math Quad Smart Contract Library. Copyright © 2019 by ABDK Consulting. - * Author: Mikhail Vladimirov - */ -pragma solidity ^0.8.0; - -/** - * Smart contract library of mathematical functions operating with IEEE 754 - * quadruple-precision binary floating-point numbers (quadruple precision - * numbers). As long as quadruple precision numbers are 16-bytes long, they are - * represented by bytes16 type. - */ -library ABDKMathQuad { - /* - * 0. - */ - bytes16 private constant POSITIVE_ZERO = 0x00000000000000000000000000000000; - - /* - * -0. - */ - bytes16 private constant NEGATIVE_ZERO = 0x80000000000000000000000000000000; - - /* - * +Infinity. - */ - bytes16 private constant POSITIVE_INFINITY = 0x7FFF0000000000000000000000000000; - - /* - * -Infinity. - */ - bytes16 private constant NEGATIVE_INFINITY = 0xFFFF0000000000000000000000000000; - - /* - * Canonical NaN value. - */ - bytes16 private constant NaN = 0x7FFF8000000000000000000000000000; - - /** - * Convert signed 256-bit integer number into quadruple precision number. - * - * @param x signed 256-bit integer number - * @return quadruple precision number - */ - function fromInt(int256 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - // We rely on overflow behavior here - uint256 result = uint256(x > 0 ? x : -x); - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16383 + msb << 112; - if (x < 0) result |= 0x80000000000000000000000000000000; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into signed 256-bit integer number - * rounding towards zero. Revert on overflow. - * - * @param x quadruple precision number - * @return signed 256-bit integer number - */ - function toInt(bytes16 x) internal pure returns (int256) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - require(exponent <= 16638); // Overflow - if (exponent < 16383) return 0; // Underflow - - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16495) result >>= 16495 - exponent; - else if (exponent > 16495) result <<= exponent - 16495; - - if (uint128(x) >= 0x80000000000000000000000000000000) { - // Negative - require(result <= 0x8000000000000000000000000000000000000000000000000000000000000000); - return -int256(result); // We rely on overflow behavior here - } else { - require(result <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int256(result); - } - } - } - - /** - * Convert unsigned 256-bit integer number into quadruple precision number. - * - * @param x unsigned 256-bit integer number - * @return quadruple precision number - */ - function fromUInt(uint256 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - uint256 result = x; - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16383 + msb << 112; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into unsigned 256-bit integer number - * rounding towards zero. Revert on underflow. Note, that negative floating - * point numbers in range (-1.0 .. 0.0) may be converted to unsigned integer - * without error, because they are rounded to zero. - * - * @param x quadruple precision number - * @return unsigned 256-bit integer number - */ - function toUInt(bytes16 x) internal pure returns (uint256) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - if (exponent < 16383) return 0; // Underflow - - require(uint128(x) < 0x80000000000000000000000000000000); // Negative - - require(exponent <= 16638); // Overflow - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16495) result >>= 16495 - exponent; - else if (exponent > 16495) result <<= exponent - 16495; - - return result; - } - } - - /** - * Convert signed 128.128 bit fixed point number into quadruple precision - * number. - * - * @param x signed 128.128 bit fixed point number - * @return quadruple precision number - */ - function from128x128(int256 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - // We rely on overflow behavior here - uint256 result = uint256(x > 0 ? x : -x); - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16255 + msb << 112; - if (x < 0) result |= 0x80000000000000000000000000000000; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into signed 128.128 bit fixed point - * number. Revert on overflow. - * - * @param x quadruple precision number - * @return signed 128.128 bit fixed point number - */ - function to128x128(bytes16 x) internal pure returns (int256) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - require(exponent <= 16510); // Overflow - if (exponent < 16255) return 0; // Underflow - - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16367) result >>= 16367 - exponent; - else if (exponent > 16367) result <<= exponent - 16367; - - if (uint128(x) >= 0x80000000000000000000000000000000) { - // Negative - require(result <= 0x8000000000000000000000000000000000000000000000000000000000000000); - return -int256(result); // We rely on overflow behavior here - } else { - require(result <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int256(result); - } - } - } - - /** - * Convert signed 64.64 bit fixed point number into quadruple precision - * number. - * - * @param x signed 64.64 bit fixed point number - * @return quadruple precision number - */ - function from64x64(int128 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - // We rely on overflow behavior here - uint256 result = uint128(x > 0 ? x : -x); - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16319 + msb << 112; - if (x < 0) result |= 0x80000000000000000000000000000000; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into signed 64.64 bit fixed point - * number. Revert on overflow. - * - * @param x quadruple precision number - * @return signed 64.64 bit fixed point number - */ - function to64x64(bytes16 x) internal pure returns (int128) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - require(exponent <= 16446); // Overflow - if (exponent < 16319) return 0; // Underflow - - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16431) result >>= 16431 - exponent; - else if (exponent > 16431) result <<= exponent - 16431; - - if (uint128(x) >= 0x80000000000000000000000000000000) { - // Negative - require(result <= 0x80000000000000000000000000000000); - return -int128(int256(result)); // We rely on overflow behavior here - } else { - require(result <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int128(int256(result)); - } - } - } - - /** - * Convert octuple precision number into quadruple precision number. - * - * @param x octuple precision number - * @return quadruple precision number - */ - function fromOctuple(bytes32 x) internal pure returns (bytes16) { - unchecked { - bool negative = x & 0x8000000000000000000000000000000000000000000000000000000000000000 > 0; - - uint256 exponent = uint256(x) >> 236 & 0x7FFFF; - uint256 significand = uint256(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (exponent == 0x7FFFF) { - if (significand > 0) return NaN; - else return negative ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } - - if (exponent > 278526) { - return negative ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } else if (exponent < 245649) { - return negative ? NEGATIVE_ZERO : POSITIVE_ZERO; - } else if (exponent < 245761) { - significand = - (significand | 0x100000000000000000000000000000000000000000000000000000000000) >> 245885 - exponent; - exponent = 0; - } else { - significand >>= 124; - exponent -= 245760; - } - - uint128 result = uint128(significand | exponent << 112); - if (negative) result |= 0x80000000000000000000000000000000; - - return bytes16(result); - } - } - - /** - * Convert quadruple precision number into octuple precision number. - * - * @param x quadruple precision number - * @return octuple precision number - */ - function toOctuple(bytes16 x) internal pure returns (bytes32) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - uint256 result = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (exponent == 0x7FFF) { - exponent = 0x7FFFF; - } // Infinity or NaN - else if (exponent == 0) { - if (result > 0) { - uint256 msb = mostSignificantBit(result); - result = result << 236 - msb & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - exponent = 245649 + msb; - } - } else { - result <<= 124; - exponent += 245760; - } - - result |= exponent << 236; - if (uint128(x) >= 0x80000000000000000000000000000000) { - result |= 0x8000000000000000000000000000000000000000000000000000000000000000; - } - - return bytes32(result); - } - } - - /** - * Convert double precision number into quadruple precision number. - * - * @param x double precision number - * @return quadruple precision number - */ - function fromDouble(bytes8 x) internal pure returns (bytes16) { - unchecked { - uint256 exponent = uint64(x) >> 52 & 0x7FF; - - uint256 result = uint64(x) & 0xFFFFFFFFFFFFF; - - if (exponent == 0x7FF) { - exponent = 0x7FFF; - } // Infinity or NaN - else if (exponent == 0) { - if (result > 0) { - uint256 msb = mostSignificantBit(result); - result = result << 112 - msb & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - exponent = 15309 + msb; - } - } else { - result <<= 60; - exponent += 15360; - } - - result |= exponent << 112; - if (x & 0x8000000000000000 > 0) { - result |= 0x80000000000000000000000000000000; - } - - return bytes16(uint128(result)); - } - } - - /** - * Convert quadruple precision number into double precision number. - * - * @param x quadruple precision number - * @return double precision number - */ - function toDouble(bytes16 x) internal pure returns (bytes8) { - unchecked { - bool negative = uint128(x) >= 0x80000000000000000000000000000000; - - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - uint256 significand = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (exponent == 0x7FFF) { - if (significand > 0) { - return 0x7FF8000000000000; - } // NaN - else { - return negative - ? bytes8(0xFFF0000000000000) // -Infinity - : bytes8(0x7FF0000000000000); - } // Infinity - } - - if (exponent > 17406) { - return negative - ? bytes8(0xFFF0000000000000) // -Infinity - : bytes8(0x7FF0000000000000); - } // Infinity - else if (exponent < 15309) { - return negative - ? bytes8(0x8000000000000000) // -0 - : bytes8(0x0000000000000000); - } // 0 - else if (exponent < 15361) { - significand = (significand | 0x10000000000000000000000000000) >> 15421 - exponent; - exponent = 0; - } else { - significand >>= 60; - exponent -= 15360; - } - - uint64 result = uint64(significand | exponent << 52); - if (negative) result |= 0x8000000000000000; - - return bytes8(result); - } - } - - /** - * Test whether given quadruple precision number is NaN. - * - * @param x quadruple precision number - * @return true if x is NaN, false otherwise - */ - function isNaN(bytes16 x) internal pure returns (bool) { - unchecked { - return uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF > 0x7FFF0000000000000000000000000000; - } - } - - /** - * Test whether given quadruple precision number is positive or negative - * infinity. - * - * @param x quadruple precision number - * @return true if x is positive or negative infinity, false otherwise - */ - function isInfinity(bytes16 x) internal pure returns (bool) { - unchecked { - return uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0x7FFF0000000000000000000000000000; - } - } - - /** - * Calculate sign of x, i.e. -1 if x is negative, 0 if x if zero, and 1 if x - * is positive. Note that sign (-0) is zero. Revert if x is NaN. - * - * @param x quadruple precision number - * @return sign of x - */ - function sign(bytes16 x) internal pure returns (int8) { - unchecked { - uint128 absoluteX = uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - require(absoluteX <= 0x7FFF0000000000000000000000000000); // Not NaN - - if (absoluteX == 0) return 0; - else if (uint128(x) >= 0x80000000000000000000000000000000) return -1; - else return 1; - } - } - - /** - * Calculate sign (x - y). Revert if either argument is NaN, or both - * arguments are infinities of the same sign. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return sign (x - y) - */ - function gt(bytes16 x, bytes16 y) internal pure returns (int8) { - unchecked { - uint128 absoluteX = uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - require(absoluteX <= 0x7FFF0000000000000000000000000000); // Not NaN - - uint128 absoluteY = uint128(y) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - require(absoluteY <= 0x7FFF0000000000000000000000000000); // Not NaN - - // Not infinities of the same sign - require(x != y || absoluteX < 0x7FFF0000000000000000000000000000); - - if (x == y) { - return 0; - } else { - bool negativeX = uint128(x) >= 0x80000000000000000000000000000000; - bool negativeY = uint128(y) >= 0x80000000000000000000000000000000; - - if (negativeX) { - if (negativeY) return absoluteX > absoluteY ? -1 : int8(1); - else return -1; - } else { - if (negativeY) return 1; - else return absoluteX > absoluteY ? int8(1) : -1; - } - } - } - } - - /** - * Test whether x equals y. NaN, infinity, and -infinity are not equal to - * anything. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return true if x equals to y, false otherwise - */ - function eq(bytes16 x, bytes16 y) internal pure returns (bool) { - unchecked { - if (x == y) { - return uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF < 0x7FFF0000000000000000000000000000; - } else { - return false; - } - } - } - - /** - * Calculate x + y. Special values behave in the following way: - * - * NaN + x = NaN for any x. - * Infinity + x = Infinity for any finite x. - * -Infinity + x = -Infinity for any finite x. - * Infinity + Infinity = Infinity. - * -Infinity + -Infinity = -Infinity. - * Infinity + -Infinity = -Infinity + Infinity = NaN. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function add(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 yExponent = uint128(y) >> 112 & 0x7FFF; - - if (xExponent == 0x7FFF) { - if (yExponent == 0x7FFF) { - if (x == y) return x; - else return NaN; - } else { - return x; - } - } else if (yExponent == 0x7FFF) { - return y; - } else { - bool xSign = uint128(x) >= 0x80000000000000000000000000000000; - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - bool ySign = uint128(y) >= 0x80000000000000000000000000000000; - uint256 ySignifier = uint128(y) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (yExponent == 0) yExponent = 1; - else ySignifier |= 0x10000000000000000000000000000; - - if (xSignifier == 0) { - return y == NEGATIVE_ZERO ? POSITIVE_ZERO : y; - } else if (ySignifier == 0) { - return x == NEGATIVE_ZERO ? POSITIVE_ZERO : x; - } else { - int256 delta = int256(xExponent) - int256(yExponent); - - if (xSign == ySign) { - if (delta > 112) { - return x; - } else if (delta > 0) { - ySignifier >>= uint256(delta); - } else if (delta < -112) { - return y; - } else if (delta < 0) { - xSignifier >>= uint256(-delta); - xExponent = yExponent; - } - - xSignifier += ySignifier; - - if (xSignifier >= 0x20000000000000000000000000000) { - xSignifier >>= 1; - xExponent += 1; - } - - if (xExponent == 0x7FFF) { - return xSign ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } else { - if (xSignifier < 0x10000000000000000000000000000) xExponent = 0; - else xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - return bytes16( - uint128( - (xSign ? 0x80000000000000000000000000000000 : 0) | (xExponent << 112) | xSignifier - ) - ); - } - } else { - if (delta > 0) { - xSignifier <<= 1; - xExponent -= 1; - } else if (delta < 0) { - ySignifier <<= 1; - xExponent = yExponent - 1; - } - - if (delta > 112) ySignifier = 1; - else if (delta > 1) ySignifier = (ySignifier - 1 >> uint256(delta - 1)) + 1; - else if (delta < -112) xSignifier = 1; - else if (delta < -1) xSignifier = (xSignifier - 1 >> uint256(-delta - 1)) + 1; - - if (xSignifier >= ySignifier) { - xSignifier -= ySignifier; - } else { - xSignifier = ySignifier - xSignifier; - xSign = ySign; - } - - if (xSignifier == 0) { - return POSITIVE_ZERO; - } - - uint256 msb = mostSignificantBit(xSignifier); - - if (msb == 113) { - xSignifier = xSignifier >> 1 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - xExponent += 1; - } else if (msb < 112) { - uint256 shift = 112 - msb; - if (xExponent > shift) { - xSignifier = xSignifier << shift & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - xExponent -= shift; - } else { - xSignifier <<= xExponent - 1; - xExponent = 0; - } - } else { - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } - - if (xExponent == 0x7FFF) { - return xSign ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } else { - return bytes16( - uint128( - (xSign ? 0x80000000000000000000000000000000 : 0) | (xExponent << 112) | xSignifier - ) - ); - } - } - } - } - } - } - - /** - * Calculate x - y. Special values behave in the following way: - * - * NaN - x = NaN for any x. - * Infinity - x = Infinity for any finite x. - * -Infinity - x = -Infinity for any finite x. - * Infinity - -Infinity = Infinity. - * -Infinity - Infinity = -Infinity. - * Infinity - Infinity = -Infinity - -Infinity = NaN. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function sub(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - return add(x, y ^ 0x80000000000000000000000000000000); - } - } - - /** - * Calculate x * y. Special values behave in the following way: - * - * NaN * x = NaN for any x. - * Infinity * x = Infinity for any finite positive x. - * Infinity * x = -Infinity for any finite negative x. - * -Infinity * x = -Infinity for any finite positive x. - * -Infinity * x = Infinity for any finite negative x. - * Infinity * 0 = NaN. - * -Infinity * 0 = NaN. - * Infinity * Infinity = Infinity. - * Infinity * -Infinity = -Infinity. - * -Infinity * Infinity = -Infinity. - * -Infinity * -Infinity = Infinity. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function mul(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 yExponent = uint128(y) >> 112 & 0x7FFF; - - if (xExponent == 0x7FFF) { - if (yExponent == 0x7FFF) { - if (x == y) return x ^ y & 0x80000000000000000000000000000000; - else if (x ^ y == 0x80000000000000000000000000000000) return x | y; - else return NaN; - } else { - if (y & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) return NaN; - else return x ^ y & 0x80000000000000000000000000000000; - } - } else if (yExponent == 0x7FFF) { - if (x & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) return NaN; - else return y ^ x & 0x80000000000000000000000000000000; - } else { - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - uint256 ySignifier = uint128(y) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (yExponent == 0) yExponent = 1; - else ySignifier |= 0x10000000000000000000000000000; - - xSignifier *= ySignifier; - if (xSignifier == 0) { - return (x ^ y) & 0x80000000000000000000000000000000 > 0 ? NEGATIVE_ZERO : POSITIVE_ZERO; - } - - xExponent += yExponent; - - uint256 msb = xSignifier >= 0x200000000000000000000000000000000000000000000000000000000 - ? 225 - : xSignifier >= 0x100000000000000000000000000000000000000000000000000000000 - ? 224 - : mostSignificantBit(xSignifier); - - if (xExponent + msb < 16496) { - // Underflow - xExponent = 0; - xSignifier = 0; - } else if (xExponent + msb < 16608) { - // Subnormal - if (xExponent < 16496) { - xSignifier >>= 16496 - xExponent; - } else if (xExponent > 16496) { - xSignifier <<= xExponent - 16496; - } - xExponent = 0; - } else if (xExponent + msb > 49373) { - xExponent = 0x7FFF; - xSignifier = 0; - } else { - if (msb > 112) { - xSignifier >>= msb - 112; - } else if (msb < 112) { - xSignifier <<= 112 - msb; - } - - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - xExponent = xExponent + msb - 16607; - } - - return bytes16( - uint128(uint128((x ^ y) & 0x80000000000000000000000000000000) | xExponent << 112 | xSignifier) - ); - } - } - } - - /** - * Calculate x / y. Special values behave in the following way: - * - * NaN / x = NaN for any x. - * x / NaN = NaN for any x. - * Infinity / x = Infinity for any finite non-negative x. - * Infinity / x = -Infinity for any finite negative x including -0. - * -Infinity / x = -Infinity for any finite non-negative x. - * -Infinity / x = Infinity for any finite negative x including -0. - * x / Infinity = 0 for any finite non-negative x. - * x / -Infinity = -0 for any finite non-negative x. - * x / Infinity = -0 for any finite non-negative x including -0. - * x / -Infinity = 0 for any finite non-negative x including -0. - * - * Infinity / Infinity = NaN. - * Infinity / -Infinity = -NaN. - * -Infinity / Infinity = -NaN. - * -Infinity / -Infinity = NaN. - * - * Division by zero behaves in the following way: - * - * x / 0 = Infinity for any finite positive x. - * x / -0 = -Infinity for any finite positive x. - * x / 0 = -Infinity for any finite negative x. - * x / -0 = Infinity for any finite negative x. - * 0 / 0 = NaN. - * 0 / -0 = NaN. - * -0 / 0 = NaN. - * -0 / -0 = NaN. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function div(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 yExponent = uint128(y) >> 112 & 0x7FFF; - - if (xExponent == 0x7FFF) { - if (yExponent == 0x7FFF) return NaN; - else return x ^ y & 0x80000000000000000000000000000000; - } else if (yExponent == 0x7FFF) { - if (y & 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFF != 0) return NaN; - else return POSITIVE_ZERO | (x ^ y) & 0x80000000000000000000000000000000; - } else if (y & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) { - if (x & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) return NaN; - else return POSITIVE_INFINITY | (x ^ y) & 0x80000000000000000000000000000000; - } else { - uint256 ySignifier = uint128(y) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (yExponent == 0) yExponent = 1; - else ySignifier |= 0x10000000000000000000000000000; - - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) { - if (xSignifier != 0) { - uint256 shift = 226 - mostSignificantBit(xSignifier); - - xSignifier <<= shift; - - xExponent = 1; - yExponent += shift - 114; - } - } else { - xSignifier = (xSignifier | 0x10000000000000000000000000000) << 114; - } - - xSignifier = xSignifier / ySignifier; - if (xSignifier == 0) { - return (x ^ y) & 0x80000000000000000000000000000000 > 0 ? NEGATIVE_ZERO : POSITIVE_ZERO; - } - - assert(xSignifier >= 0x1000000000000000000000000000); - - uint256 msb = xSignifier >= 0x80000000000000000000000000000 - ? mostSignificantBit(xSignifier) - : xSignifier >= 0x40000000000000000000000000000 - ? 114 - : xSignifier >= 0x20000000000000000000000000000 ? 113 : 112; - - if (xExponent + msb > yExponent + 16497) { - // Overflow - xExponent = 0x7FFF; - xSignifier = 0; - } else if (xExponent + msb + 16380 < yExponent) { - // Underflow - xExponent = 0; - xSignifier = 0; - } else if (xExponent + msb + 16268 < yExponent) { - // Subnormal - if (xExponent + 16380 > yExponent) { - xSignifier <<= xExponent + 16380 - yExponent; - } else if (xExponent + 16380 < yExponent) { - xSignifier >>= yExponent - xExponent - 16380; - } - - xExponent = 0; - } else { - // Normal - if (msb > 112) { - xSignifier >>= msb - 112; - } - - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - xExponent = xExponent + msb + 16269 - yExponent; - } - - return bytes16( - uint128(uint128((x ^ y) & 0x80000000000000000000000000000000) | xExponent << 112 | xSignifier) - ); - } - } - } - - /** - * Calculate -x. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function neg(bytes16 x) internal pure returns (bytes16) { - unchecked { - return x ^ 0x80000000000000000000000000000000; - } - } - - /** - * Calculate |x|. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function abs(bytes16 x) internal pure returns (bytes16) { - unchecked { - return x & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } - } - - /** - * Calculate square root of x. Return NaN on negative x excluding -0. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function sqrt(bytes16 x) internal pure returns (bytes16) { - unchecked { - if (uint128(x) > 0x80000000000000000000000000000000) { - return NaN; - } else { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - if (xExponent == 0x7FFF) { - return x; - } else { - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - if (xSignifier == 0) return POSITIVE_ZERO; - - bool oddExponent = xExponent & 0x1 == 0; - xExponent = xExponent + 16383 >> 1; - - if (oddExponent) { - if (xSignifier >= 0x10000000000000000000000000000) { - xSignifier <<= 113; - } else { - uint256 msb = mostSignificantBit(xSignifier); - uint256 shift = (226 - msb) & 0xFE; - xSignifier <<= shift; - xExponent -= shift - 112 >> 1; - } - } else { - if (xSignifier >= 0x10000000000000000000000000000) { - xSignifier <<= 112; - } else { - uint256 msb = mostSignificantBit(xSignifier); - uint256 shift = (225 - msb) & 0xFE; - xSignifier <<= shift; - xExponent -= shift - 112 >> 1; - } - } - - uint256 r = 0x10000000000000000000000000000; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; // Seven iterations should be enough - uint256 r1 = xSignifier / r; - if (r1 < r) r = r1; - - return bytes16(uint128(xExponent << 112 | r & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF)); - } - } - } - } - - /** - * Calculate binary logarithm of x. Return NaN on negative x excluding -0. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function log_2(bytes16 x) internal pure returns (bytes16) { - unchecked { - if (uint128(x) > 0x80000000000000000000000000000000) { - return NaN; - } else if (x == 0x3FFF0000000000000000000000000000) { - return POSITIVE_ZERO; - } else { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - if (xExponent == 0x7FFF) { - return x; - } else { - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - if (xSignifier == 0) return NEGATIVE_INFINITY; - - bool resultNegative; - uint256 resultExponent = 16495; - uint256 resultSignifier; - - if (xExponent >= 0x3FFF) { - resultNegative = false; - resultSignifier = xExponent - 0x3FFF; - xSignifier <<= 15; - } else { - resultNegative = true; - if (xSignifier >= 0x10000000000000000000000000000) { - resultSignifier = 0x3FFE - xExponent; - xSignifier <<= 15; - } else { - uint256 msb = mostSignificantBit(xSignifier); - resultSignifier = 16493 - msb; - xSignifier <<= 127 - msb; - } - } - - if (xSignifier == 0x80000000000000000000000000000000) { - if (resultNegative) resultSignifier += 1; - uint256 shift = 112 - mostSignificantBit(resultSignifier); - resultSignifier <<= shift; - resultExponent -= shift; - } else { - uint256 bb = resultNegative ? 1 : 0; - while (resultSignifier < 0x10000000000000000000000000000) { - resultSignifier <<= 1; - resultExponent -= 1; - - xSignifier *= xSignifier; - uint256 b = xSignifier >> 255; - resultSignifier += b ^ bb; - xSignifier >>= 127 + b; - } - } - - return bytes16( - uint128( - (resultNegative ? 0x80000000000000000000000000000000 : 0) | resultExponent << 112 - | resultSignifier & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) - ); - } - } - } - } - - /** - * Calculate natural logarithm of x. Return NaN on negative x excluding -0. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function ln(bytes16 x) internal pure returns (bytes16) { - unchecked { - return mul(log_2(x), 0x3FFE62E42FEFA39EF35793C7673007E5); - } - } - - /** - * Calculate 2^x. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function pow_2(bytes16 x) internal pure returns (bytes16) { - unchecked { - bool xNegative = uint128(x) > 0x80000000000000000000000000000000; - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (xExponent == 0x7FFF && xSignifier != 0) { - return NaN; - } else if (xExponent > 16397) { - return xNegative ? POSITIVE_ZERO : POSITIVE_INFINITY; - } else if (xExponent < 16255) { - return 0x3FFF0000000000000000000000000000; - } else { - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - if (xExponent > 16367) { - xSignifier <<= xExponent - 16367; - } else if (xExponent < 16367) { - xSignifier >>= 16367 - xExponent; - } - - if (xNegative && xSignifier > 0x406E00000000000000000000000000000000) { - return POSITIVE_ZERO; - } - - if (!xNegative && xSignifier > 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) { - return POSITIVE_INFINITY; - } - - uint256 resultExponent = xSignifier >> 128; - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xNegative && xSignifier != 0) { - xSignifier = ~xSignifier; - resultExponent += 1; - } - - uint256 resultSignifier = 0x80000000000000000000000000000000; - if (xSignifier & 0x80000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x16A09E667F3BCC908B2FB1366EA957D3E >> 128; - } - if (xSignifier & 0x40000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1306FE0A31B7152DE8D5A46305C85EDEC >> 128; - } - if (xSignifier & 0x20000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1172B83C7D517ADCDF7C8C50EB14A791F >> 128; - } - if (xSignifier & 0x10000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10B5586CF9890F6298B92B71842A98363 >> 128; - } - if (xSignifier & 0x8000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1059B0D31585743AE7C548EB68CA417FD >> 128; - } - if (xSignifier & 0x4000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x102C9A3E778060EE6F7CACA4F7A29BDE8 >> 128; - } - if (xSignifier & 0x2000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10163DA9FB33356D84A66AE336DCDFA3F >> 128; - } - if (xSignifier & 0x1000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100B1AFA5ABCBED6129AB13EC11DC9543 >> 128; - } - if (xSignifier & 0x800000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10058C86DA1C09EA1FF19D294CF2F679B >> 128; - } - if (xSignifier & 0x400000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1002C605E2E8CEC506D21BFC89A23A00F >> 128; - } - if (xSignifier & 0x200000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100162F3904051FA128BCA9C55C31E5DF >> 128; - } - if (xSignifier & 0x100000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000B175EFFDC76BA38E31671CA939725 >> 128; - } - if (xSignifier & 0x80000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100058BA01FB9F96D6CACD4B180917C3D >> 128; - } - if (xSignifier & 0x40000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10002C5CC37DA9491D0985C348C68E7B3 >> 128; - } - if (xSignifier & 0x20000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000162E525EE054754457D5995292026 >> 128; - } - if (xSignifier & 0x10000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000B17255775C040618BF4A4ADE83FC >> 128; - } - if (xSignifier & 0x8000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000058B91B5BC9AE2EED81E9B7D4CFAB >> 128; - } - if (xSignifier & 0x4000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100002C5C89D5EC6CA4D7C8ACC017B7C9 >> 128; - } - if (xSignifier & 0x2000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000162E43F4F831060E02D839A9D16D >> 128; - } - if (xSignifier & 0x1000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000B1721BCFC99D9F890EA06911763 >> 128; - } - if (xSignifier & 0x800000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000058B90CF1E6D97F9CA14DBCC1628 >> 128; - } - if (xSignifier & 0x400000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000002C5C863B73F016468F6BAC5CA2B >> 128; - } - if (xSignifier & 0x200000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000162E430E5A18F6119E3C02282A5 >> 128; - } - if (xSignifier & 0x100000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000B1721835514B86E6D96EFD1BFE >> 128; - } - if (xSignifier & 0x80000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000058B90C0B48C6BE5DF846C5B2EF >> 128; - } - if (xSignifier & 0x40000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000002C5C8601CC6B9E94213C72737A >> 128; - } - if (xSignifier & 0x20000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000162E42FFF037DF38AA2B219F06 >> 128; - } - if (xSignifier & 0x10000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000B17217FBA9C739AA5819F44F9 >> 128; - } - if (xSignifier & 0x8000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000058B90BFCDEE5ACD3C1CEDC823 >> 128; - } - if (xSignifier & 0x4000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000002C5C85FE31F35A6A30DA1BE50 >> 128; - } - if (xSignifier & 0x2000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000162E42FF0999CE3541B9FFFCF >> 128; - } - if (xSignifier & 0x1000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000B17217F80F4EF5AADDA45554 >> 128; - } - if (xSignifier & 0x800000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000058B90BFBF8479BD5A81B51AD >> 128; - } - if (xSignifier & 0x400000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000002C5C85FDF84BD62AE30A74CC >> 128; - } - if (xSignifier & 0x200000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000162E42FEFB2FED257559BDAA >> 128; - } - if (xSignifier & 0x100000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000B17217F7D5A7716BBA4A9AE >> 128; - } - if (xSignifier & 0x80000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000058B90BFBE9DDBAC5E109CCE >> 128; - } - if (xSignifier & 0x40000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000002C5C85FDF4B15DE6F17EB0D >> 128; - } - if (xSignifier & 0x20000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000162E42FEFA494F1478FDE05 >> 128; - } - if (xSignifier & 0x10000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000B17217F7D20CF927C8E94C >> 128; - } - if (xSignifier & 0x8000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000058B90BFBE8F71CB4E4B33D >> 128; - } - if (xSignifier & 0x4000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000002C5C85FDF477B662B26945 >> 128; - } - if (xSignifier & 0x2000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000162E42FEFA3AE53369388C >> 128; - } - if (xSignifier & 0x1000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000B17217F7D1D351A389D40 >> 128; - } - if (xSignifier & 0x800000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000058B90BFBE8E8B2D3D4EDE >> 128; - } - if (xSignifier & 0x400000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000002C5C85FDF4741BEA6E77E >> 128; - } - if (xSignifier & 0x200000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000162E42FEFA39FE95583C2 >> 128; - } - if (xSignifier & 0x100000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000B17217F7D1CFB72B45E1 >> 128; - } - if (xSignifier & 0x80000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000058B90BFBE8E7CC35C3F0 >> 128; - } - if (xSignifier & 0x40000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000002C5C85FDF473E242EA38 >> 128; - } - if (xSignifier & 0x20000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000162E42FEFA39F02B772C >> 128; - } - if (xSignifier & 0x10000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000B17217F7D1CF7D83C1A >> 128; - } - if (xSignifier & 0x8000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000058B90BFBE8E7BDCBE2E >> 128; - } - if (xSignifier & 0x4000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000002C5C85FDF473DEA871F >> 128; - } - if (xSignifier & 0x2000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000162E42FEFA39EF44D91 >> 128; - } - if (xSignifier & 0x1000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000B17217F7D1CF79E949 >> 128; - } - if (xSignifier & 0x800000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000058B90BFBE8E7BCE544 >> 128; - } - if (xSignifier & 0x400000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000002C5C85FDF473DE6ECA >> 128; - } - if (xSignifier & 0x200000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000162E42FEFA39EF366F >> 128; - } - if (xSignifier & 0x100000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000B17217F7D1CF79AFA >> 128; - } - if (xSignifier & 0x80000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000058B90BFBE8E7BCD6D >> 128; - } - if (xSignifier & 0x40000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000002C5C85FDF473DE6B2 >> 128; - } - if (xSignifier & 0x20000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000162E42FEFA39EF358 >> 128; - } - if (xSignifier & 0x10000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000B17217F7D1CF79AB >> 128; - } - if (xSignifier & 0x8000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000058B90BFBE8E7BCD5 >> 128; - } - if (xSignifier & 0x4000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000002C5C85FDF473DE6A >> 128; - } - if (xSignifier & 0x2000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000162E42FEFA39EF34 >> 128; - } - if (xSignifier & 0x1000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000B17217F7D1CF799 >> 128; - } - if (xSignifier & 0x800000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000058B90BFBE8E7BCC >> 128; - } - if (xSignifier & 0x400000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000002C5C85FDF473DE5 >> 128; - } - if (xSignifier & 0x200000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000162E42FEFA39EF2 >> 128; - } - if (xSignifier & 0x100000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000B17217F7D1CF78 >> 128; - } - if (xSignifier & 0x80000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000058B90BFBE8E7BB >> 128; - } - if (xSignifier & 0x40000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000002C5C85FDF473DD >> 128; - } - if (xSignifier & 0x20000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000162E42FEFA39EE >> 128; - } - if (xSignifier & 0x10000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000B17217F7D1CF6 >> 128; - } - if (xSignifier & 0x8000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000058B90BFBE8E7A >> 128; - } - if (xSignifier & 0x4000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000002C5C85FDF473C >> 128; - } - if (xSignifier & 0x2000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000162E42FEFA39D >> 128; - } - if (xSignifier & 0x1000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000B17217F7D1CE >> 128; - } - if (xSignifier & 0x800000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000058B90BFBE8E6 >> 128; - } - if (xSignifier & 0x400000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000002C5C85FDF472 >> 128; - } - if (xSignifier & 0x200000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000162E42FEFA38 >> 128; - } - if (xSignifier & 0x100000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000B17217F7D1B >> 128; - } - if (xSignifier & 0x80000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000058B90BFBE8D >> 128; - } - if (xSignifier & 0x40000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000002C5C85FDF46 >> 128; - } - if (xSignifier & 0x20000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000162E42FEFA2 >> 128; - } - if (xSignifier & 0x10000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000B17217F7D0 >> 128; - } - if (xSignifier & 0x8000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000058B90BFBE7 >> 128; - } - if (xSignifier & 0x4000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000002C5C85FDF3 >> 128; - } - if (xSignifier & 0x2000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000162E42FEF9 >> 128; - } - if (xSignifier & 0x1000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000B17217F7C >> 128; - } - if (xSignifier & 0x800000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000058B90BFBD >> 128; - } - if (xSignifier & 0x400000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000002C5C85FDE >> 128; - } - if (xSignifier & 0x200000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000162E42FEE >> 128; - } - if (xSignifier & 0x100000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000B17217F6 >> 128; - } - if (xSignifier & 0x80000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000058B90BFA >> 128; - } - if (xSignifier & 0x40000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000002C5C85FC >> 128; - } - if (xSignifier & 0x20000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000162E42FD >> 128; - } - if (xSignifier & 0x10000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000B17217E >> 128; - } - if (xSignifier & 0x8000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000058B90BE >> 128; - } - if (xSignifier & 0x4000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000002C5C85E >> 128; - } - if (xSignifier & 0x2000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000162E42E >> 128; - } - if (xSignifier & 0x1000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000B17216 >> 128; - } - if (xSignifier & 0x800000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000058B90A >> 128; - } - if (xSignifier & 0x400000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000002C5C84 >> 128; - } - if (xSignifier & 0x200000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000162E41 >> 128; - } - if (xSignifier & 0x100000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000B1720 >> 128; - } - if (xSignifier & 0x80000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000058B8F >> 128; - } - if (xSignifier & 0x40000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000002C5C7 >> 128; - } - if (xSignifier & 0x20000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000162E3 >> 128; - } - if (xSignifier & 0x10000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000B171 >> 128; - } - if (xSignifier & 0x8000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000058B8 >> 128; - } - if (xSignifier & 0x4000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000002C5B >> 128; - } - if (xSignifier & 0x2000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000162D >> 128; - } - if (xSignifier & 0x1000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000B16 >> 128; - } - if (xSignifier & 0x800 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000058A >> 128; - } - if (xSignifier & 0x400 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000002C4 >> 128; - } - if (xSignifier & 0x200 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000161 >> 128; - } - if (xSignifier & 0x100 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000000B0 >> 128; - } - if (xSignifier & 0x80 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000057 >> 128; - } - if (xSignifier & 0x40 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000002B >> 128; - } - if (xSignifier & 0x20 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000015 >> 128; - } - if (xSignifier & 0x10 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000000A >> 128; - } - if (xSignifier & 0x8 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000004 >> 128; - } - if (xSignifier & 0x4 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000001 >> 128; - } - - if (!xNegative) { - resultSignifier = resultSignifier >> 15 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - resultExponent += 0x3FFF; - } else if (resultExponent <= 0x3FFE) { - resultSignifier = resultSignifier >> 15 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - resultExponent = 0x3FFF - resultExponent; - } else { - resultSignifier = resultSignifier >> resultExponent - 16367; - resultExponent = 0; - } - - return bytes16(uint128(resultExponent << 112 | resultSignifier)); - } - } - } - - /** - * Calculate e^x. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function exp(bytes16 x) internal pure returns (bytes16) { - unchecked { - return pow_2(mul(x, 0x3FFF71547652B82FE1777D0FFDA0D23A)); - } - } - - /** - * Get index of the most significant non-zero bit in binary representation of - * x. Reverts if x is zero. - * - * @return index of the most significant non-zero bit in binary representation - * of x - */ - function mostSignificantBit(uint256 x) private pure returns (uint256) { - unchecked { - require(x > 0); - - uint256 result = 0; - - if (x >= 0x100000000000000000000000000000000) { - x >>= 128; - result += 128; - } - if (x >= 0x10000000000000000) { - x >>= 64; - result += 64; - } - if (x >= 0x100000000) { - x >>= 32; - result += 32; - } - if (x >= 0x10000) { - x >>= 16; - result += 16; - } - if (x >= 0x100) { - x >>= 8; - result += 8; - } - if (x >= 0x10) { - x >>= 4; - result += 4; - } - if (x >= 0x4) { - x >>= 2; - result += 2; - } - if (x >= 0x2) result += 1; // No need to shift x anymore - - return result; - } - } -} diff --git a/contracts/libraries/TWAMM/OrderPool.sol b/contracts/libraries/TWAMM/OrderPool.sol deleted file mode 100644 index 4822dbff..00000000 --- a/contracts/libraries/TWAMM/OrderPool.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -/// @title TWAMM OrderPool - Represents an OrderPool inside of a TWAMM -library OrderPool { - /// @notice Information related to a long term order pool. - /// @member sellRateCurrent The total current sell rate (sellAmount / second) among all orders - /// @member sellRateEndingAtInterval Mapping (timestamp => sellRate) The amount of expiring sellRate at this interval - /// @member earningsFactor Sum of (salesEarnings_k / salesRate_k) over every period k. Stored as Fixed Point X96. - /// @member earningsFactorAtInterval Mapping (timestamp => sellRate) The earnings factor accrued by a certain time interval. Stored as Fixed Point X96. - struct State { - uint256 sellRateCurrent; - mapping(uint256 => uint256) sellRateEndingAtInterval; - // - uint256 earningsFactorCurrent; - mapping(uint256 => uint256) earningsFactorAtInterval; - } - - // Performs all updates on an OrderPool that must happen when hitting an expiration interval with expiring orders - function advanceToInterval(State storage self, uint256 expiration, uint256 earningsFactor) internal { - unchecked { - self.earningsFactorCurrent += earningsFactor; - self.earningsFactorAtInterval[expiration] = self.earningsFactorCurrent; - self.sellRateCurrent -= self.sellRateEndingAtInterval[expiration]; - } - } - - // Performs all the updates on an OrderPool that must happen when updating to the current time not on an interval - function advanceToCurrentTime(State storage self, uint256 earningsFactor) internal { - unchecked { - self.earningsFactorCurrent += earningsFactor; - } - } -} diff --git a/contracts/libraries/TWAMM/TwammMath.sol b/contracts/libraries/TWAMM/TwammMath.sol deleted file mode 100644 index a5994b51..00000000 --- a/contracts/libraries/TWAMM/TwammMath.sol +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -import {ABDKMathQuad} from "./ABDKMathQuad.sol"; -import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; - -/// @title TWAMM Math - Pure functions for TWAMM math calculations -library TwammMath { - using ABDKMathQuad for bytes16; - using ABDKMathQuad for uint256; - using ABDKMathQuad for uint160; - using ABDKMathQuad for uint128; - using SafeCast for uint256; - - // ABDKMathQuad FixedPoint96.Q96.fromUInt() - bytes16 internal constant Q96 = 0x405f0000000000000000000000000000; - - bytes16 internal constant ONE = 0x3fff0000000000000000000000000000; - //// @dev The minimum value that a pool price can equal, represented in bytes. - // (TickMath.MIN_SQRT_RATIO + 1).fromUInt() - bytes16 internal constant MIN_SQRT_RATIO_BYTES = 0x401f000276a400000000000000000000; - //// @dev The maximum value that a pool price can equal, represented in bytes. - // (TickMath.MAX_SQRT_RATIO - 1).fromUInt() - bytes16 internal constant MAX_SQRT_RATIO_BYTES = 0x409efffb12c7dfa3f8d4a0c91092bb2a; - - struct PriceParamsBytes16 { - bytes16 sqrtSellRatio; - bytes16 sqrtSellRate; - bytes16 secondsElapsed; - bytes16 sqrtPrice; - bytes16 liquidity; - } - - struct ExecutionUpdateParams { - uint256 secondsElapsedX96; - uint160 sqrtPriceX96; - uint128 liquidity; - uint256 sellRateCurrent0; - uint256 sellRateCurrent1; - } - - function getNewSqrtPriceX96(ExecutionUpdateParams memory params) internal pure returns (uint160 newSqrtPriceX96) { - bytes16 sellRateBytes0 = params.sellRateCurrent0.fromUInt(); - bytes16 sellRateBytes1 = params.sellRateCurrent1.fromUInt(); - bytes16 sqrtSellRateBytes = sellRateBytes0.mul(sellRateBytes1).sqrt(); - bytes16 sqrtSellRatioX96Bytes = sellRateBytes1.div(sellRateBytes0).sqrt().mul(Q96); - - PriceParamsBytes16 memory priceParams = PriceParamsBytes16({ - sqrtSellRatio: sqrtSellRatioX96Bytes.div(Q96), - sqrtSellRate: sqrtSellRateBytes, - secondsElapsed: params.secondsElapsedX96.fromUInt().div(Q96), - sqrtPrice: params.sqrtPriceX96.fromUInt().div(Q96), - liquidity: params.liquidity.fromUInt() - }); - - bytes16 newSqrtPriceBytesX96 = calculateNewSqrtPrice(priceParams).mul(Q96); - bool isOverflow = newSqrtPriceBytesX96.isInfinity() || newSqrtPriceBytesX96.isNaN(); - bytes16 newSqrtPriceX96Bytes = isOverflow ? sqrtSellRatioX96Bytes : newSqrtPriceBytesX96; - - newSqrtPriceX96 = getSqrtPriceWithinBounds( - params.sellRateCurrent0 > params.sellRateCurrent1, newSqrtPriceX96Bytes - ).toUInt().toUint160(); - } - - function getSqrtPriceWithinBounds(bool zeroForOne, bytes16 desiredPriceX96) - internal - pure - returns (bytes16 newSqrtPriceX96) - { - if (zeroForOne) { - newSqrtPriceX96 = MIN_SQRT_RATIO_BYTES.gt(desiredPriceX96) == 1 ? MIN_SQRT_RATIO_BYTES : desiredPriceX96; - } else { - newSqrtPriceX96 = desiredPriceX96.gt(MAX_SQRT_RATIO_BYTES) == 1 ? MAX_SQRT_RATIO_BYTES : desiredPriceX96; - } - } - - function calculateEarningsUpdates(ExecutionUpdateParams memory params, uint160 finalSqrtPriceX96) - internal - pure - returns (uint256 earningsFactorPool0, uint256 earningsFactorPool1) - { - bytes16 sellRateBytes0 = params.sellRateCurrent0.fromUInt(); - bytes16 sellRateBytes1 = params.sellRateCurrent1.fromUInt(); - - bytes16 sellRatio = sellRateBytes1.div(sellRateBytes0); - bytes16 sqrtSellRate = sellRateBytes0.mul(sellRateBytes1).sqrt(); - - EarningsFactorParams memory earningsFactorParams = EarningsFactorParams({ - secondsElapsed: params.secondsElapsedX96.fromUInt().div(Q96), - sellRatio: sellRatio, - sqrtSellRate: sqrtSellRate, - prevSqrtPrice: params.sqrtPriceX96.fromUInt().div(Q96), - newSqrtPrice: finalSqrtPriceX96.fromUInt().div(Q96), - liquidity: params.liquidity.fromUInt() - }); - - // Trade the amm orders. - // If liquidity is 0, it trades the twamm orders against each other for the time duration. - earningsFactorPool0 = getEarningsFactorPool0(earningsFactorParams).mul(Q96).toUInt(); - earningsFactorPool1 = getEarningsFactorPool1(earningsFactorParams).mul(Q96).toUInt(); - } - - struct calculateTimeBetweenTicksParams { - uint256 liquidity; - uint160 sqrtPriceStartX96; - uint160 sqrtPriceEndX96; - uint256 sellRate0; - uint256 sellRate1; - } - - /// @notice Used when crossing an initialized tick. Can extract the amount of seconds it took to cross - /// the tick, and recalibrate the calculation from there to accommodate liquidity changes - function calculateTimeBetweenTicks( - uint256 liquidity, - uint160 sqrtPriceStartX96, - uint160 sqrtPriceEndX96, - uint256 sellRate0, - uint256 sellRate1 - ) internal pure returns (uint256 secondsBetween) { - bytes16 sellRate0Bytes = sellRate0.fromUInt(); - bytes16 sellRate1Bytes = sellRate1.fromUInt(); - bytes16 sqrtPriceStartX96Bytes = sqrtPriceStartX96.fromUInt(); - bytes16 sqrtPriceEndX96Bytes = sqrtPriceEndX96.fromUInt(); - bytes16 sqrtSellRatioX96 = sellRate1Bytes.div(sellRate0Bytes).sqrt().mul(Q96); - bytes16 sqrtSellRate = sellRate0Bytes.mul(sellRate1Bytes).sqrt(); - - bytes16 multiple = getTimeBetweenTicksMultiple(sqrtSellRatioX96, sqrtPriceStartX96Bytes, sqrtPriceEndX96Bytes); - bytes16 numerator = multiple.mul(liquidity.fromUInt()); - bytes16 denominator = uint256(2).fromUInt().mul(sqrtSellRate); - return numerator.mul(Q96).div(denominator).toUInt(); - } - - function getTimeBetweenTicksMultiple(bytes16 sqrtSellRatioX96, bytes16 sqrtPriceStartX96, bytes16 sqrtPriceEndX96) - private - pure - returns (bytes16 multiple) - { - bytes16 multiple1 = sqrtSellRatioX96.add(sqrtPriceEndX96).div(sqrtSellRatioX96.sub(sqrtPriceEndX96)); - bytes16 multiple2 = sqrtSellRatioX96.sub(sqrtPriceStartX96).div(sqrtSellRatioX96.add(sqrtPriceStartX96)); - return multiple1.mul(multiple2).ln(); - } - - struct EarningsFactorParams { - bytes16 secondsElapsed; - bytes16 sellRatio; - bytes16 sqrtSellRate; - bytes16 prevSqrtPrice; - bytes16 newSqrtPrice; - bytes16 liquidity; - } - - function getEarningsFactorPool0(EarningsFactorParams memory params) private pure returns (bytes16 earningsFactor) { - bytes16 minuend = params.sellRatio.mul(params.secondsElapsed); - bytes16 subtrahend = params.liquidity.mul(params.sellRatio.sqrt()).mul( - params.newSqrtPrice.sub(params.prevSqrtPrice) - ).div(params.sqrtSellRate); - return minuend.sub(subtrahend); - } - - function getEarningsFactorPool1(EarningsFactorParams memory params) private pure returns (bytes16 earningsFactor) { - bytes16 minuend = params.secondsElapsed.div(params.sellRatio); - bytes16 subtrahend = params.liquidity.mul(reciprocal(params.sellRatio.sqrt())).mul( - reciprocal(params.newSqrtPrice).sub(reciprocal(params.prevSqrtPrice)) - ).div(params.sqrtSellRate); - return minuend.sub(subtrahend); - } - - function calculateNewSqrtPrice(PriceParamsBytes16 memory params) private pure returns (bytes16 newSqrtPrice) { - bytes16 pow = uint256(2).fromUInt().mul(params.sqrtSellRate).mul(params.secondsElapsed).div(params.liquidity); - bytes16 c = params.sqrtSellRatio.sub(params.sqrtPrice).div(params.sqrtSellRatio.add(params.sqrtPrice)); - newSqrtPrice = params.sqrtSellRatio.mul(pow.exp().sub(c)).div(pow.exp().add(c)); - } - - function reciprocal(bytes16 n) private pure returns (bytes16) { - return ONE.div(n); - } -} diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol deleted file mode 100644 index 9ab40d9e..00000000 --- a/contracts/libraries/TransferHelper.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.15; - -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; - -/// @title TransferHelper -/// @notice Contains helper methods for interacting with ERC20 tokens that do not consistently return true/false -/// @dev implementation from https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol#L63 -library TransferHelper { - /// @notice Transfers tokens from msg.sender to a recipient - /// @dev Calls transfer on token contract, errors with TF if transfer fails - /// @param token The contract address of the token which will be transferred - /// @param to The recipient of the transfer - /// @param value The value of the transfer - function safeTransfer(IERC20Minimal token, address to, uint256 value) internal { - bool success; - - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) - mstore(add(freeMemoryPointer, 4), to) // Append the "to" argument. - mstore(add(freeMemoryPointer, 36), value) // Append the "value" argument. - - success := - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), - // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. - // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. - // Counterintuitively, this call must be positioned second to the or() call in the - // surrounding and() call or else returndatasize() will be zero during the computation. - call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) - ) - } - - require(success, "TRANSFER_FAILED"); - } - - /// @notice Transfers tokens from from to a recipient - /// @dev Calls transferFrom on token contract, errors with TF if transfer fails - /// @param token The contract address of the token which will be transferred - /// @param from The origin of the transfer - /// @param to The recipient of the transfer - /// @param value The value of the transfer - function safeTransferFrom(IERC20Minimal token, address from, address to, uint256 value) internal { - (bool success, bytes memory data) = - address(token).call(abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool))), "STF"); - } -} diff --git a/contracts/libraries/UniswapV4ERC20.sol b/contracts/libraries/UniswapV4ERC20.sol deleted file mode 100644 index fdd93ba4..00000000 --- a/contracts/libraries/UniswapV4ERC20.sol +++ /dev/null @@ -1,16 +0,0 @@ -pragma solidity ^0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Owned} from "solmate/auth/Owned.sol"; - -contract UniswapV4ERC20 is ERC20, Owned { - constructor(string memory name, string memory symbol) ERC20(name, symbol, 18) Owned(msg.sender) {} - - function mint(address account, uint256 amount) external onlyOwner { - _mint(account, amount); - } - - function burn(address account, uint256 amount) external onlyOwner { - _burn(account, amount); - } -} diff --git a/foundry.toml b/foundry.toml index 7979bfb0..6d64d305 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,4 @@ [profile.default] -src = 'contracts' out = 'foundry-out' solc_version = '0.8.26' optimizer_runs = 1000000 @@ -7,6 +6,7 @@ ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] evm_version = "cancun" gas_limit = "3000000000" +fuzz_runs = 10000 [profile.ci] fuzz_runs = 100000 diff --git a/contracts/NonfungiblePositionManager.sol b/src/NonfungiblePositionManager.sol similarity index 99% rename from contracts/NonfungiblePositionManager.sol rename to src/NonfungiblePositionManager.sol index 32318dbf..5be13588 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/src/NonfungiblePositionManager.sol @@ -15,7 +15,7 @@ import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; diff --git a/contracts/base/BaseLiquidityManagement.sol b/src/base/BaseLiquidityManagement.sol similarity index 100% rename from contracts/base/BaseLiquidityManagement.sol rename to src/base/BaseLiquidityManagement.sol diff --git a/contracts/base/ERC721Permit.sol b/src/base/ERC721Permit.sol similarity index 98% rename from contracts/base/ERC721Permit.sol rename to src/base/ERC721Permit.sol index 4668f2c5..ee9c10f9 100644 --- a/contracts/base/ERC721Permit.sol +++ b/src/base/ERC721Permit.sol @@ -6,7 +6,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ChainId} from "../libraries/ChainId.sol"; import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; -import {IERC1271} from "../interfaces/external/IERC1271.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; /// @title ERC721 with permit /// @notice Nonfungible tokens that support an approve via signature, i.e. permit diff --git a/contracts/base/ImmutableState.sol b/src/base/ImmutableState.sol similarity index 100% rename from contracts/base/ImmutableState.sol rename to src/base/ImmutableState.sol diff --git a/contracts/base/Multicall.sol b/src/base/Multicall.sol similarity index 100% rename from contracts/base/Multicall.sol rename to src/base/Multicall.sol diff --git a/contracts/base/PoolInitializer.sol b/src/base/PoolInitializer.sol similarity index 100% rename from contracts/base/PoolInitializer.sol rename to src/base/PoolInitializer.sol diff --git a/contracts/base/SafeCallback.sol b/src/base/SafeCallback.sol similarity index 100% rename from contracts/base/SafeCallback.sol rename to src/base/SafeCallback.sol diff --git a/contracts/base/SelfPermit.sol b/src/base/SelfPermit.sol similarity index 100% rename from contracts/base/SelfPermit.sol rename to src/base/SelfPermit.sol diff --git a/contracts/BaseHook.sol b/src/base/hooks/BaseHook.sol similarity index 97% rename from contracts/BaseHook.sol rename to src/base/hooks/BaseHook.sol index 01fc4954..e1ed6a06 100644 --- a/contracts/BaseHook.sol +++ b/src/base/hooks/BaseHook.sol @@ -7,8 +7,8 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {SafeCallback} from "./base/SafeCallback.sol"; -import {ImmutableState} from "./base/ImmutableState.sol"; +import {SafeCallback} from "../SafeCallback.sol"; +import {ImmutableState} from "../ImmutableState.sol"; abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/src/interfaces/IBaseLiquidityManagement.sol similarity index 100% rename from contracts/interfaces/IBaseLiquidityManagement.sol rename to src/interfaces/IBaseLiquidityManagement.sol diff --git a/contracts/interfaces/IERC721Permit.sol b/src/interfaces/IERC721Permit.sol similarity index 100% rename from contracts/interfaces/IERC721Permit.sol rename to src/interfaces/IERC721Permit.sol diff --git a/contracts/interfaces/IMulticall.sol b/src/interfaces/IMulticall.sol similarity index 100% rename from contracts/interfaces/IMulticall.sol rename to src/interfaces/IMulticall.sol diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/src/interfaces/INonfungiblePositionManager.sol similarity index 100% rename from contracts/interfaces/INonfungiblePositionManager.sol rename to src/interfaces/INonfungiblePositionManager.sol diff --git a/contracts/interfaces/IQuoter.sol b/src/interfaces/IQuoter.sol similarity index 100% rename from contracts/interfaces/IQuoter.sol rename to src/interfaces/IQuoter.sol diff --git a/contracts/interfaces/ISelfPermit.sol b/src/interfaces/ISelfPermit.sol similarity index 100% rename from contracts/interfaces/ISelfPermit.sol rename to src/interfaces/ISelfPermit.sol diff --git a/contracts/interfaces/external/IERC20PermitAllowed.sol b/src/interfaces/external/IERC20PermitAllowed.sol similarity index 100% rename from contracts/interfaces/external/IERC20PermitAllowed.sol rename to src/interfaces/external/IERC20PermitAllowed.sol diff --git a/contracts/lens/Quoter.sol b/src/lens/Quoter.sol similarity index 100% rename from contracts/lens/Quoter.sol rename to src/lens/Quoter.sol diff --git a/contracts/libraries/BalanceDeltaExtensionLibrary.sol b/src/libraries/BalanceDeltaExtensionLibrary.sol similarity index 100% rename from contracts/libraries/BalanceDeltaExtensionLibrary.sol rename to src/libraries/BalanceDeltaExtensionLibrary.sol diff --git a/contracts/libraries/ChainId.sol b/src/libraries/ChainId.sol similarity index 100% rename from contracts/libraries/ChainId.sol rename to src/libraries/ChainId.sol diff --git a/contracts/libraries/CurrencyDeltas.sol b/src/libraries/CurrencyDeltas.sol similarity index 100% rename from contracts/libraries/CurrencyDeltas.sol rename to src/libraries/CurrencyDeltas.sol diff --git a/contracts/libraries/CurrencySettleTake.sol b/src/libraries/CurrencySettleTake.sol similarity index 100% rename from contracts/libraries/CurrencySettleTake.sol rename to src/libraries/CurrencySettleTake.sol diff --git a/contracts/libraries/LiquiditySaltLibrary.sol b/src/libraries/LiquiditySaltLibrary.sol similarity index 100% rename from contracts/libraries/LiquiditySaltLibrary.sol rename to src/libraries/LiquiditySaltLibrary.sol diff --git a/contracts/libraries/PathKey.sol b/src/libraries/PathKey.sol similarity index 100% rename from contracts/libraries/PathKey.sol rename to src/libraries/PathKey.sol diff --git a/contracts/libraries/PoolTicksCounter.sol b/src/libraries/PoolTicksCounter.sol similarity index 99% rename from contracts/libraries/PoolTicksCounter.sol rename to src/libraries/PoolTicksCounter.sol index 60fdbbe5..7420ffd5 100644 --- a/contracts/libraries/PoolTicksCounter.sol +++ b/src/libraries/PoolTicksCounter.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.8.20; -import {PoolGetters} from "./PoolGetters.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; diff --git a/contracts/libraries/Position.sol b/src/libraries/Position.sol similarity index 100% rename from contracts/libraries/Position.sol rename to src/libraries/Position.sol diff --git a/contracts/types/LiquidityRange.sol b/src/types/LiquidityRange.sol similarity index 100% rename from contracts/types/LiquidityRange.sol rename to src/types/LiquidityRange.sol diff --git a/test/FullRange.t.sol b/test/FullRange.t.sol deleted file mode 100644 index 8a35661b..00000000 --- a/test/FullRange.t.sol +++ /dev/null @@ -1,782 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FullRange} from "../contracts/hooks/examples/FullRange.sol"; -import {FullRangeImplementation} from "./shared/implementation/FullRangeImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {UniswapV4ERC20} from "../contracts/libraries/UniswapV4ERC20.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -contract TestFullRange is Test, Deployers, GasSnapshot { - using PoolIdLibrary for PoolKey; - using SafeCast for uint256; - using CurrencyLibrary for Currency; - using StateLibrary for IPoolManager; - - event Initialize( - PoolId indexed poolId, - Currency indexed currency0, - Currency indexed currency1, - uint24 fee, - int24 tickSpacing, - IHooks hooks - ); - event ModifyPosition( - PoolId indexed poolId, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta - ); - event Swap( - PoolId indexed id, - address indexed sender, - int128 amount0, - int128 amount1, - uint160 sqrtPriceX96, - uint128 liquidity, - int24 tick, - uint24 fee - ); - - HookEnabledSwapRouter router; - /// @dev Min tick for full range with tick spacing of 60 - int24 internal constant MIN_TICK = -887220; - /// @dev Max tick for full range with tick spacing of 60 - int24 internal constant MAX_TICK = -MIN_TICK; - - int24 constant TICK_SPACING = 60; - uint16 constant LOCKED_LIQUIDITY = 1000; - uint256 constant MAX_DEADLINE = 12329839823; - uint256 constant MAX_TICK_LIQUIDITY = 11505069308564788430434325881101412; - uint8 constant DUST = 30; - - MockERC20 token0; - MockERC20 token1; - MockERC20 token2; - - FullRangeImplementation fullRange = FullRangeImplementation( - address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG)) - ); - - PoolId id; - - PoolKey key2; - PoolId id2; - - // For a pool that gets initialized with liquidity in setUp() - PoolKey keyWithLiq; - PoolId idWithLiq; - - function setUp() public { - deployFreshManagerAndRouters(); - router = new HookEnabledSwapRouter(manager); - MockERC20[] memory tokens = deployTokens(3, 2 ** 128); - token0 = tokens[0]; - token1 = tokens[1]; - token2 = tokens[2]; - - FullRangeImplementation impl = new FullRangeImplementation(manager, fullRange); - vm.etch(address(fullRange), address(impl).code); - - key = createPoolKey(token0, token1); - id = key.toId(); - - key2 = createPoolKey(token1, token2); - id2 = key.toId(); - - keyWithLiq = createPoolKey(token0, token2); - idWithLiq = keyWithLiq.toId(); - - token0.approve(address(fullRange), type(uint256).max); - token1.approve(address(fullRange), type(uint256).max); - token2.approve(address(fullRange), type(uint256).max); - token0.approve(address(router), type(uint256).max); - token1.approve(address(router), type(uint256).max); - token2.approve(address(router), type(uint256).max); - - initPool(keyWithLiq.currency0, keyWithLiq.currency1, fullRange, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - keyWithLiq.currency0, - keyWithLiq.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - } - - function testFullRange_beforeInitialize_AllowsPoolCreation() public { - PoolKey memory testKey = key; - - vm.expectEmit(true, true, true, true); - emit Initialize(id, testKey.currency0, testKey.currency1, testKey.fee, testKey.tickSpacing, testKey.hooks); - - snapStart("FullRangeInitialize"); - manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); - snapEnd(); - - (, address liquidityToken) = fullRange.poolInfo(id); - - assertFalse(liquidityToken == address(0)); - } - - function testFullRange_beforeInitialize_RevertsIfWrongSpacing() public { - PoolKey memory wrongKey = PoolKey(key.currency0, key.currency1, 0, TICK_SPACING + 1, fullRange); - - vm.expectRevert( - abi.encodeWithSelector( - Hooks.FailedHookCall.selector, abi.encodeWithSelector(FullRange.TickSpacingNotDefault.selector) - ) - ); - - manager.initialize(wrongKey, SQRT_PRICE_1_1, ZERO_BYTES); - } - - function testFullRange_addLiquidity_InitialAddSucceeds() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOf(address(this)); - uint256 prevBalance1 = key.currency1.balanceOf(address(this)); - - FullRange.AddLiquidityParams memory addLiquidityParams = FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ); - - snapStart("FullRangeAddInitialLiquidity"); - fullRange.addLiquidity(addLiquidityParams); - snapEnd(); - - (bool hasAccruedFees, address liquidityToken) = fullRange.poolInfo(id); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether); - - assertEq(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, false); - } - - function testFullRange_addLiquidity_InitialAddFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - if (amount <= LOCKED_LIQUIDITY) { - vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else if (amount > MAX_TICK_LIQUIDITY) { - vm.expectRevert(); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else { - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, 0, 0, address(this), MAX_DEADLINE - ) - ); - - (bool hasAccruedFees, address liquidityToken) = fullRange.poolInfo(id); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, false); - } - } - - function testFullRange_addLiquidity_SubsequentAdd() public { - uint256 prevBalance0 = keyWithLiq.currency0.balanceOfSelf(); - uint256 prevBalance1 = keyWithLiq.currency1.balanceOfSelf(); - - (, address liquidityToken) = fullRange.poolInfo(idWithLiq); - uint256 prevLiquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - FullRange.AddLiquidityParams memory addLiquidityParams = FullRange.AddLiquidityParams( - keyWithLiq.currency0, - keyWithLiq.currency1, - 3000, - 10 ether, - 10 ether, - 9 ether, - 9 ether, - address(this), - MAX_DEADLINE - ); - - snapStart("FullRangeAddLiquidity"); - fullRange.addLiquidity(addLiquidityParams); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(idWithLiq); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(idWithLiq), liquidityTokenBal + LOCKED_LIQUIDITY); - - assertEq(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 - 10 ether); - assertEq(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 - 10 ether); - - assertEq(liquidityTokenBal, prevLiquidityTokenBal + 10 ether); - assertEq(hasAccruedFees, false); - } - - function testFullRange_addLiquidity_FailsIfNoPool() public { - vm.expectRevert(FullRange.PoolNotInitialized.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 0, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - } - - function testFullRange_addLiquidity_SwapThenAddSucceeds() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOf(address(this)); - uint256 prevBalance1 = key.currency1.balanceOf(address(this)); - (, address liquidityToken) = fullRange.poolInfo(id); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY); - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether); - - vm.expectEmit(true, true, true, true); - emit Swap( - id, address(router), -1 ether, 906610893880149131, 72045250990510446115798809072, 10 ether, -1901, 3000 - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - snapStart("FullRangeSwap"); - router.swap(key, params, settings, ZERO_BYTES); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether - 1 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 9093389106119850869); - assertEq(hasAccruedFees, true); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 5 ether, 5 ether, 4 ether, 4 ether, address(this), MAX_DEADLINE - ) - ); - - (hasAccruedFees,) = fullRange.poolInfo(id); - liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 14546694553059925434 - LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, true); - } - - function testFullRange_addLiquidity_FailsIfTooMuchSlippage() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether, 10 ether, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1000 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, settings, ZERO_BYTES); - - vm.expectRevert(FullRange.TooMuchSlippage.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether, 10 ether, address(this), MAX_DEADLINE - ) - ); - } - - function testFullRange_swap_TwoSwaps() public { - PoolKey memory testKey = key; - manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - snapStart("FullRangeFirstSwap"); - router.swap(testKey, params, settings, ZERO_BYTES); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - - snapStart("FullRangeSecondSwap"); - router.swap(testKey, params, settings, ZERO_BYTES); - snapEnd(); - - (hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - } - - function testFullRange_swap_TwoPools() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - manager.initialize(key2, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key2.currency0, key2.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10000000, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, testSettings, ZERO_BYTES); - router.swap(key2, params, testSettings, ZERO_BYTES); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - - (hasAccruedFees,) = fullRange.poolInfo(id2); - assertEq(hasAccruedFees, true); - } - - function testFullRange_removeLiquidity_InitialRemoveSucceeds() public { - uint256 prevBalance0 = keyWithLiq.currency0.balanceOfSelf(); - uint256 prevBalance1 = keyWithLiq.currency1.balanceOfSelf(); - - (, address liquidityToken) = fullRange.poolInfo(idWithLiq); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - FullRange.RemoveLiquidityParams memory removeLiquidityParams = - FullRange.RemoveLiquidityParams(keyWithLiq.currency0, keyWithLiq.currency1, 3000, 1 ether, MAX_DEADLINE); - - snapStart("FullRangeRemoveLiquidity"); - fullRange.removeLiquidity(removeLiquidityParams); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(idWithLiq); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(idWithLiq), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 99 ether - LOCKED_LIQUIDITY + 5); - assertEq(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 + 1 ether - 1); - assertEq(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 + 1 ether - 1); - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_InitialRemoveFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 1000 ether, - 1000 ether, - 999 ether, - 999 ether, - address(this), - MAX_DEADLINE - ) - ); - - (, address liquidityToken) = fullRange.poolInfo(id); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - if (amount > UniswapV4ERC20(liquidityToken).balanceOf(address(this))) { - vm.expectRevert(); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, amount, MAX_DEADLINE) - ); - } else { - uint256 prevLiquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, amount, MAX_DEADLINE) - ); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - (bool hasAccruedFees,) = fullRange.poolInfo(id); - - assertEq(prevLiquidityTokenBal - liquidityTokenBal, amount); - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, false); - } - } - - function testFullRange_removeLiquidity_FailsIfNoPool() public { - vm.expectRevert(FullRange.PoolNotInitialized.selector); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 0, 10 ether, MAX_DEADLINE) - ); - } - - function testFullRange_removeLiquidity_FailsIfNoLiquidity() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - (, address liquidityToken) = fullRange.poolInfo(id); - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - vm.expectRevert(); // Insufficient balance error from ERC20 contract - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, 10 ether, MAX_DEADLINE) - ); - } - - function testFullRange_removeLiquidity_SucceedsWithPartial() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOfSelf(); - uint256 prevBalance1 = key.currency1.balanceOfSelf(); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - (, address liquidityToken) = fullRange.poolInfo(id); - - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY); - - assertEq(key.currency0.balanceOfSelf(), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOfSelf(), prevBalance1 - 10 ether); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, 5 ether, MAX_DEADLINE) - ); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 5 ether - LOCKED_LIQUIDITY); - assertEq(key.currency0.balanceOfSelf(), prevBalance0 - 5 ether - 1); - assertEq(key.currency1.balanceOfSelf(), prevBalance1 - 5 ether - 1); - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_DiffRatios() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOf(address(this)); - uint256 prevBalance1 = key.currency1.balanceOf(address(this)); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether); - - (, address liquidityToken) = fullRange.poolInfo(id); - - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 5 ether, 2.5 ether, 2 ether, 2 ether, address(this), MAX_DEADLINE - ) - ); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 12.5 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 12.5 ether); - - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 12.5 ether - LOCKED_LIQUIDITY); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, 5 ether, MAX_DEADLINE) - ); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 7.5 ether - LOCKED_LIQUIDITY); - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 7.5 ether - 1); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 7.5 ether - 1); - } - - function testFullRange_removeLiquidity_SwapAndRebalance() public { - (, address liquidityToken) = fullRange.poolInfo(idWithLiq); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(keyWithLiq, params, testSettings, ZERO_BYTES); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - FullRange.RemoveLiquidityParams memory removeLiquidityParams = - FullRange.RemoveLiquidityParams(keyWithLiq.currency0, keyWithLiq.currency1, 3000, 5 ether, MAX_DEADLINE); - - snapStart("FullRangeRemoveLiquidityAndRebalance"); - fullRange.removeLiquidity(removeLiquidityParams); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(idWithLiq); - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_RemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - (, address liquidityToken) = fullRange.poolInfo(id); - - if (amount <= LOCKED_LIQUIDITY) { - vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else if (amount >= MAX_TICK_LIQUIDITY) { - vm.expectRevert(); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else { - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, 0, 0, address(this), MAX_DEADLINE - ) - ); - - // Test contract removes liquidity, succeeds - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, liquidityTokenBal, MAX_DEADLINE) - ); - - assertEq(manager.getLiquidity(id), LOCKED_LIQUIDITY); - } - } - - function testFullRange_removeLiquidity_ThreeLPsRemovePrincipalAndFees() public { - // Mint tokens for dummy addresses - token0.mint(address(1), 2 ** 128); - token1.mint(address(1), 2 ** 128); - token0.mint(address(2), 2 ** 128); - token1.mint(address(2), 2 ** 128); - - // Approve the hook - vm.prank(address(1)); - token0.approve(address(fullRange), type(uint256).max); - vm.prank(address(1)); - token1.approve(address(fullRange), type(uint256).max); - - vm.prank(address(2)); - token0.approve(address(fullRange), type(uint256).max); - vm.prank(address(2)); - token1.approve(address(fullRange), type(uint256).max); - - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - (, address liquidityToken) = fullRange.poolInfo(id); - - // Test contract adds liquidity - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - - // address(1) adds liquidity - vm.prank(address(1)); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - - // address(2) adds liquidity - vm.prank(address(2)); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: SQRT_PRICE_1_4}); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, testSettings, ZERO_BYTES); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - - // Test contract removes liquidity, succeeds - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams( - key.currency0, key.currency1, 3000, 300 ether - LOCKED_LIQUIDITY, MAX_DEADLINE - ) - ); - (hasAccruedFees,) = fullRange.poolInfo(id); - - // PoolManager does not have any liquidity left over - assertTrue(manager.getLiquidity(id) >= LOCKED_LIQUIDITY); - assertTrue(manager.getLiquidity(id) < LOCKED_LIQUIDITY + DUST); - - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_SwapRemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - (, address liquidityToken) = fullRange.poolInfo(id); - - if (amount <= LOCKED_LIQUIDITY) { - vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else if (amount >= MAX_TICK_LIQUIDITY) { - vm.expectRevert(); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else { - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, 0, 0, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ - zeroForOne: true, - amountSpecified: (FullMath.mulDiv(amount, 1, 4)).toInt256(), - sqrtPriceLimitX96: SQRT_PRICE_1_4 - }); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, testSettings, ZERO_BYTES); - - // Test contract removes liquidity, succeeds - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, liquidityTokenBal, MAX_DEADLINE) - ); - - assertTrue(manager.getLiquidity(id) <= LOCKED_LIQUIDITY + DUST); - } - } - - function testFullRange_BeforeModifyPositionFailsWithWrongMsgSender() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - vm.expectRevert( - abi.encodeWithSelector( - Hooks.FailedHookCall.selector, abi.encodeWithSelector(FullRange.SenderMustBeHook.selector) - ) - ); - - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100, salt: 0}), - ZERO_BYTES - ); - } - - function createPoolKey(MockERC20 tokenA, MockERC20 tokenB) internal view returns (PoolKey memory) { - if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); - return PoolKey(Currency.wrap(address(tokenA)), Currency.wrap(address(tokenB)), 3000, TICK_SPACING, fullRange); - } -} diff --git a/test/GeomeanOracle.t.sol b/test/GeomeanOracle.t.sol deleted file mode 100644 index 7fbcd995..00000000 --- a/test/GeomeanOracle.t.sol +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {GetSender} from "./shared/GetSender.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {GeomeanOracle} from "../contracts/hooks/examples/GeomeanOracle.sol"; -import {GeomeanOracleImplementation} from "./shared/implementation/GeomeanOracleImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {Oracle} from "../contracts/libraries/Oracle.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -contract TestGeomeanOracle is Test, Deployers { - using PoolIdLibrary for PoolKey; - - int24 constant MAX_TICK_SPACING = 32767; - - TestERC20 token0; - TestERC20 token1; - GeomeanOracleImplementation geomeanOracle = GeomeanOracleImplementation( - address( - uint160( - Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG - | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG - ) - ) - ); - PoolId id; - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - - token0 = TestERC20(Currency.unwrap(currency0)); - token1 = TestERC20(Currency.unwrap(currency1)); - - vm.record(); - GeomeanOracleImplementation impl = new GeomeanOracleImplementation(manager, geomeanOracle); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(geomeanOracle), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(geomeanOracle), slot, vm.load(address(impl), slot)); - } - } - geomeanOracle.setTime(1); - key = PoolKey(currency0, currency1, 0, MAX_TICK_SPACING, geomeanOracle); - id = key.toId(); - - modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); - - token0.approve(address(geomeanOracle), type(uint256).max); - token1.approve(address(geomeanOracle), type(uint256).max); - token0.approve(address(modifyLiquidityRouter), type(uint256).max); - token1.approve(address(modifyLiquidityRouter), type(uint256).max); - } - - function testBeforeInitializeAllowsPoolCreation() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - } - - function testBeforeInitializeRevertsIfFee() public { - vm.expectRevert( - abi.encodeWithSelector( - Hooks.FailedHookCall.selector, abi.encodeWithSelector(GeomeanOracle.OnlyOneOraclePoolAllowed.selector) - ) - ); - - manager.initialize( - PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 1, MAX_TICK_SPACING, geomeanOracle), - SQRT_PRICE_1_1, - ZERO_BYTES - ); - } - - function testBeforeInitializeRevertsIfNotMaxTickSpacing() public { - vm.expectRevert( - abi.encodeWithSelector( - Hooks.FailedHookCall.selector, abi.encodeWithSelector(GeomeanOracle.OnlyOneOraclePoolAllowed.selector) - ) - ); - - manager.initialize( - PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 0, 60, geomeanOracle), - SQRT_PRICE_1_1, - ZERO_BYTES - ); - } - - function testAfterInitializeState() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 1); - } - - function testAfterInitializeObservation() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 1); - assertEq(observation.tickCumulative, 0); - assertEq(observation.secondsPerLiquidityCumulativeX128, 0); - } - - function testAfterInitializeObserve0() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - uint32[] memory secondsAgo = new uint32[](1); - secondsAgo[0] = 0; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = - geomeanOracle.observe(key, secondsAgo); - assertEq(tickCumulatives.length, 1); - assertEq(secondsPerLiquidityCumulativeX128s.length, 1); - assertEq(tickCumulatives[0], 0); - assertEq(secondsPerLiquidityCumulativeX128s[0], 0); - } - - function testBeforeModifyPositionNoObservations() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 1); - - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 1); - assertEq(observation.tickCumulative, 0); - assertEq(observation.secondsPerLiquidityCumulativeX128, 0); - } - - function testBeforeModifyPositionObservation() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 1); - - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 3); - assertEq(observation.tickCumulative, 13862); - assertEq(observation.secondsPerLiquidityCumulativeX128, 680564733841876926926749214863536422912); - } - - function testBeforeModifyPositionObservationAndCardinality() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds - geomeanOracle.increaseCardinalityNext(key, 2); - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 2); - - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - // cardinality is updated - observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 1); - assertEq(observationState.cardinality, 2); - assertEq(observationState.cardinalityNext, 2); - - // index 0 is untouched - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 1); - assertEq(observation.tickCumulative, 0); - assertEq(observation.secondsPerLiquidityCumulativeX128, 0); - - // index 1 is written - observation = geomeanOracle.getObservation(key, 1); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 3); - assertEq(observation.tickCumulative, 13862); - assertEq(observation.secondsPerLiquidityCumulativeX128, 680564733841876926926749214863536422912); - } - - function testPermanentLiquidity() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - vm.expectRevert( - abi.encodeWithSelector( - Hooks.FailedHookCall.selector, - abi.encodeWithSelector(GeomeanOracle.OraclePoolMustLockLiquidity.selector) - ) - ); - - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), -1000, 0 - ), - ZERO_BYTES - ); - } -} diff --git a/test/LimitOrder.t.sol b/test/LimitOrder.t.sol deleted file mode 100644 index 29b1093f..00000000 --- a/test/LimitOrder.t.sol +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {GetSender} from "./shared/GetSender.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {LimitOrder, Epoch, EpochLibrary} from "../contracts/hooks/examples/LimitOrder.sol"; -import {LimitOrderImplementation} from "./shared/implementation/LimitOrderImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -contract TestLimitOrder is Test, Deployers { - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - uint160 constant SQRT_PRICE_10_1 = 250541448375047931186413801569; - - HookEnabledSwapRouter router; - TestERC20 token0; - TestERC20 token1; - LimitOrder limitOrder = LimitOrder(address(uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.AFTER_SWAP_FLAG))); - PoolId id; - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - - router = new HookEnabledSwapRouter(manager); - token0 = TestERC20(Currency.unwrap(currency0)); - token1 = TestERC20(Currency.unwrap(currency1)); - - vm.record(); - LimitOrderImplementation impl = new LimitOrderImplementation(manager, limitOrder); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(limitOrder), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(limitOrder), slot, vm.load(address(impl), slot)); - } - } - - // key = PoolKey(currency0, currency1, 3000, 60, limitOrder); - (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - - token0.approve(address(limitOrder), type(uint256).max); - token1.approve(address(limitOrder), type(uint256).max); - token0.approve(address(router), type(uint256).max); - token1.approve(address(router), type(uint256).max); - } - - function testGetTickLowerLast() public { - assertEq(limitOrder.getTickLowerLast(id), 0); - } - - function testGetTickLowerLastWithDifferentPrice() public { - PoolKey memory differentKey = - PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 61, limitOrder); - manager.initialize(differentKey, SQRT_PRICE_10_1, ZERO_BYTES); - assertEq(limitOrder.getTickLowerLast(differentKey.toId()), 22997); - } - - function testEpochNext() public { - assertTrue(EpochLibrary.equals(limitOrder.epochNext(), Epoch.wrap(1))); - } - - function testZeroLiquidityRevert() public { - vm.expectRevert(LimitOrder.ZeroLiquidity.selector); - limitOrder.place(key, 0, true, 0); - } - - function testZeroForOneRightBoundaryOfCurrentRange() public { - int24 tickLower = 60; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); - } - - function testZeroForOneLeftBoundaryOfCurrentRange() public { - int24 tickLower = 0; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); - } - - function testZeroForOneCrossedRangeRevert() public { - vm.expectRevert(LimitOrder.CrossedRange.selector); - limitOrder.place(key, -60, true, 1000000); - } - - function testZeroForOneInRangeRevert() public { - // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei - router.swap( - key, - IPoolManager.SwapParams(false, -1 ether, SQRT_PRICE_1_1 + 1), - HookEnabledSwapRouter.TestSettings(false, false), - ZERO_BYTES - ); - vm.expectRevert(LimitOrder.InRange.selector); - limitOrder.place(key, 0, true, 1000000); - } - - function testNotZeroForOneLeftBoundaryOfCurrentRange() public { - int24 tickLower = -60; - bool zeroForOne = false; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); - } - - function testNotZeroForOneCrossedRangeRevert() public { - vm.expectRevert(LimitOrder.CrossedRange.selector); - limitOrder.place(key, 0, false, 1000000); - } - - function testNotZeroForOneInRangeRevert() public { - // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei - router.swap( - key, - IPoolManager.SwapParams(true, -1 ether, SQRT_PRICE_1_1 - 1), - HookEnabledSwapRouter.TestSettings(false, false), - ZERO_BYTES - ); - vm.expectRevert(LimitOrder.InRange.selector); - limitOrder.place(key, -60, false, 1000000); - } - - function testMultipleLPs() public { - int24 tickLower = 60; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - address other = 0x1111111111111111111111111111111111111111; - token0.transfer(other, 1e18); - token1.transfer(other, 1e18); - vm.startPrank(other); - token0.approve(address(limitOrder), type(uint256).max); - token1.approve(address(limitOrder), type(uint256).max); - limitOrder.place(key, tickLower, zeroForOne, liquidity); - vm.stopPrank(); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity * 2); - - ( - bool filled, - Currency currency0, - Currency currency1, - uint256 token0Total, - uint256 token1Total, - uint128 liquidityTotal - ) = limitOrder.epochInfos(Epoch.wrap(1)); - assertFalse(filled); - assertTrue(currency0 == Currency.wrap(address(token0))); - assertTrue(currency1 == Currency.wrap(address(token1))); - assertEq(token0Total, 0); - assertEq(token1Total, 0); - assertEq(liquidityTotal, liquidity * 2); - assertEq(limitOrder.getEpochLiquidity(Epoch.wrap(1), new GetSender().sender()), liquidity); - assertEq(limitOrder.getEpochLiquidity(Epoch.wrap(1), other), liquidity); - } - - event Transfer(address indexed from, address indexed to, uint256 value); - - function testKill() public { - int24 tickLower = 0; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - vm.expectEmit(true, true, true, true, address(token0)); - emit Transfer(address(manager), new GetSender().sender(), 2995); - limitOrder.kill(key, tickLower, zeroForOne, new GetSender().sender()); - } - - function testSwapAcrossRange() public { - int24 tickLower = 0; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - - router.swap( - key, - IPoolManager.SwapParams(false, -1e18, TickMath.getSqrtPriceAtTick(60)), - HookEnabledSwapRouter.TestSettings(false, false), - ZERO_BYTES - ); - - assertEq(limitOrder.getTickLowerLast(id), 60); - (, int24 tick,,) = manager.getSlot0(id); - assertEq(tick, 60); - - (bool filled,,, uint256 token0Total, uint256 token1Total,) = limitOrder.epochInfos(Epoch.wrap(1)); - - assertTrue(filled); - assertEq(token0Total, 0); - assertEq(token1Total, 2996 + 17); // 3013, 2 wei of dust - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, 0); - - vm.expectEmit(true, true, true, true, address(token1)); - emit Transfer(address(manager), new GetSender().sender(), 2996 + 17); - limitOrder.withdraw(Epoch.wrap(1), new GetSender().sender()); - - (,,, token0Total, token1Total,) = limitOrder.epochInfos(Epoch.wrap(1)); - - assertEq(token0Total, 0); - assertEq(token1Total, 0); - } -} diff --git a/test/Oracle.t.sol b/test/Oracle.t.sol deleted file mode 100644 index 04157e16..00000000 --- a/test/Oracle.t.sol +++ /dev/null @@ -1,867 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {Test} from "forge-std/Test.sol"; -import {Oracle} from "../contracts/libraries/Oracle.sol"; -import {OracleImplementation} from "./shared/implementation/OracleImplementation.sol"; - -contract TestOracle is Test, GasSnapshot { - OracleImplementation initializedOracle; - OracleImplementation oracle; - - function setUp() public { - oracle = new OracleImplementation(); - initializedOracle = new OracleImplementation(); - initializedOracle.initialize(OracleImplementation.InitializeParams({time: 0, tick: 0, liquidity: 0})); - } - - function testInitialize() public { - snapStart("OracleInitialize"); - oracle.initialize(OracleImplementation.InitializeParams({time: 1, tick: 1, liquidity: 1})); - snapEnd(); - - assertEq(oracle.index(), 0); - assertEq(oracle.cardinality(), 1); - assertEq(oracle.cardinalityNext(), 1); - assertObservation( - oracle, - 0, - Oracle.Observation({ - blockTimestamp: 1, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: true - }) - ); - } - - function testGrow() public { - initializedOracle.grow(5); - assertEq(initializedOracle.index(), 0); - assertEq(initializedOracle.cardinality(), 1); - assertEq(initializedOracle.cardinalityNext(), 5); - - // does not touch first slot - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 0, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: true - }) - ); - - // adds data to all slots - for (uint64 i = 1; i < 5; i++) { - assertObservation( - initializedOracle, - i, - Oracle.Observation({ - blockTimestamp: 1, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: false - }) - ); - } - - // noop if initializedOracle is already gte size - initializedOracle.grow(3); - assertEq(initializedOracle.index(), 0); - assertEq(initializedOracle.cardinality(), 1); - assertEq(initializedOracle.cardinalityNext(), 5); - } - - function testGrowAfterWrap() public { - initializedOracle.grow(2); - // index is now 1 - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2, liquidity: 1, tick: 1})); - // index is now 0 again - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2, liquidity: 1, tick: 1})); - assertEq(initializedOracle.index(), 0); - initializedOracle.grow(3); - - assertEq(initializedOracle.index(), 0); - assertEq(initializedOracle.cardinality(), 2); - assertEq(initializedOracle.cardinalityNext(), 3); - } - - function testGas1Slot() public { - snapStart("OracleGrow1Slot"); - initializedOracle.grow(2); - snapEnd(); - } - - function testGas10Slots() public { - snapStart("OracleGrow10Slots"); - initializedOracle.grow(11); - snapEnd(); - } - - function testGas1SlotCardinalityGreater() public { - initializedOracle.grow(2); - snapStart("OracleGrow1SlotCardinalityGreater"); - initializedOracle.grow(3); - snapEnd(); - } - - function testGas10SlotCardinalityGreater() public { - initializedOracle.grow(2); - snapStart("OracleGrow10SlotsCardinalityGreater"); - initializedOracle.grow(12); - snapEnd(); - } - - function testWrite() public { - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 1, tick: 2, liquidity: 5})); - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 1, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 340282366920938463463374607431768211456, - initialized: true - }) - ); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 5, tick: -1, liquidity: 8})); - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 6, - tickCumulative: 10, - secondsPerLiquidityCumulativeX128: 680564733841876926926749214863536422912, - initialized: true - }) - ); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 2, liquidity: 3})); - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 9, - tickCumulative: 7, - secondsPerLiquidityCumulativeX128: 808170621437228850725514692650449502208, - initialized: true - }) - ); - } - - function testWriteAddsNothingIfTimeUnchanged() public { - initializedOracle.grow(2); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 1, tick: 3, liquidity: 2})); - assertEq(initializedOracle.index(), 1); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 0, tick: -5, liquidity: 9})); - assertEq(initializedOracle.index(), 1); - } - - function testWriteTimeChanged() public { - initializedOracle.grow(3); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 6, tick: 3, liquidity: 2})); - assertEq(initializedOracle.index(), 1); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: -5, liquidity: 9})); - assertEq(initializedOracle.index(), 2); - assertObservation( - initializedOracle, - 1, - Oracle.Observation({ - blockTimestamp: 6, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 2041694201525630780780247644590609268736, - initialized: true - }) - ); - } - - function testWriteGrowsCardinalityWritingPast() public { - initializedOracle.grow(2); - initializedOracle.grow(4); - assertEq(initializedOracle.cardinality(), 1); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 5, liquidity: 6})); - assertEq(initializedOracle.cardinality(), 4); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 6, liquidity: 4})); - assertEq(initializedOracle.cardinality(), 4); - assertEq(initializedOracle.index(), 2); - assertObservation( - initializedOracle, - 2, - Oracle.Observation({ - blockTimestamp: 7, - tickCumulative: 20, - secondsPerLiquidityCumulativeX128: 1247702012043441032699040227249816775338, - initialized: true - }) - ); - } - - function testWriteWrapsAround() public { - initializedOracle.grow(3); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 1, liquidity: 2})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 2, liquidity: 3})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 5, tick: 3, liquidity: 4})); - - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 12, - tickCumulative: 14, - secondsPerLiquidityCumulativeX128: 2268549112806256423089164049545121409706, - initialized: true - }) - ); - } - - function testWriteAccumulatesLiquidity() public { - initializedOracle.grow(4); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 3, liquidity: 2})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: -7, liquidity: 6})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 5, tick: -2, liquidity: 4})); - - assertEq(initializedOracle.index(), 3); - - assertObservation( - initializedOracle, - 1, - Oracle.Observation({ - blockTimestamp: 3, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 1020847100762815390390123822295304634368, - initialized: true - }) - ); - assertObservation( - initializedOracle, - 2, - Oracle.Observation({ - blockTimestamp: 7, - tickCumulative: 12, - secondsPerLiquidityCumulativeX128: 1701411834604692317316873037158841057280, - initialized: true - }) - ); - assertObservation( - initializedOracle, - 3, - Oracle.Observation({ - blockTimestamp: 12, - tickCumulative: -23, - secondsPerLiquidityCumulativeX128: 1984980473705474370203018543351981233493, - initialized: true - }) - ); - assertObservation( - initializedOracle, - 4, - Oracle.Observation({ - blockTimestamp: 0, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: false - }) - ); - } - - function testObserveFailsBeforeInitialize() public { - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - vm.expectRevert(Oracle.OracleCardinalityCannotBeZero.selector); - oracle.observe(secondsAgos); - } - - function testObserveFailsIfOlderDoesNotExist() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 1; - vm.expectRevert(abi.encodeWithSelector(Oracle.TargetPredatesOldestObservation.selector, 5, 4)); - oracle.observe(secondsAgos); - } - - function testDoesNotFailAcrossOverflowBoundary() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 2 ** 32 - 1})); - oracle.advanceTime(2); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 1); - assertEq(tickCumulative, 2); - assertEq(secondsPerLiquidityCumulativeX128, 85070591730234615865843651857942052864); - } - - function testInterpolationMaxLiquidity() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: type(uint128).max, tick: 0, time: 0})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 0, liquidity: 0})); - (, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, 13); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 6); - assertEq(secondsPerLiquidityCumulativeX128, 7); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 12); - assertEq(secondsPerLiquidityCumulativeX128, 1); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 13); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testInterpolatesSame0And1Liquidity() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 1, tick: 0, time: 0})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 0, liquidity: type(uint128).max})); - (, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, 13 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 6); - assertEq(secondsPerLiquidityCumulativeX128, 7 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 12); - assertEq(secondsPerLiquidityCumulativeX128, 1 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 13); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testInterpolatesAcrossChunkBoundaries() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 0, tick: 0, time: 0})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2 ** 32 - 6, tick: 0, liquidity: 0})); - (, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, (2 ** 32 - 6) << 128); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 0, liquidity: 0})); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, 7 << 128); - - // interpolation checks - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 3); - assertEq(secondsPerLiquidityCumulativeX128, 4 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 8); - assertEq(secondsPerLiquidityCumulativeX128, (2 ** 32 - 1) << 128); - } - - function testSingleObservationAtCurrentTime() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, 0); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testSingleObservationInRecentPast() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 4; - vm.expectRevert(abi.encodeWithSelector(Oracle.TargetPredatesOldestObservation.selector, 5, 4)); - oracle.observe(secondsAgos); - } - - function testSingleObservationSecondsAgo() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 3); - assertEq(tickCumulative, 0); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testSingleObservationInPastCounterfactualInPast() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 1); - assertEq(tickCumulative, 4); - assertEq(secondsPerLiquidityCumulativeX128, 170141183460469231731687303715884105728); - } - - function testSingleObservationInPastCounterfactualNow() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, 6); - assertEq(secondsPerLiquidityCumulativeX128, 255211775190703847597530955573826158592); - } - - function testTwoObservationsChronologicalZeroSecondsAgoExact() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -20); - assertEq(secondsPerLiquidityCumulativeX128, 272225893536750770770699685945414569164); - } - - function testTwoObservationsChronologicalZeroSecondsAgoCounterfactual() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -13); - assertEq(secondsPerLiquidityCumulativeX128, 1463214177760035392892510811956603309260); - } - - function testTwoObservationsChronologicalSecondsAgoExactlyFirstObservation() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 11); - assertEq(tickCumulative, 0); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testTwoObservationsChronologicalSecondsAgoBetween() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 9); - assertEq(tickCumulative, -10); - assertEq(secondsPerLiquidityCumulativeX128, 136112946768375385385349842972707284582); - } - - function testTwoObservationsReverseOrderZeroSecondsAgoExact() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -17); - assertEq(secondsPerLiquidityCumulativeX128, 782649443918158465965761597093066886348); - } - - function testTwoObservationsReverseOrderZeroSecondsAgoCounterfactual() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -52); - assertEq(secondsPerLiquidityCumulativeX128, 1378143586029800777026667160098661256396); - } - - function testTwoObservationsReverseOrderSecondsAgoExactlyOnFirstObservation() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 10); - assertEq(tickCumulative, -20); - assertEq(secondsPerLiquidityCumulativeX128, 272225893536750770770699685945414569164); - } - - function testTwoObservationsReverseOrderSecondsAgoBetween() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 9); - assertEq(tickCumulative, -19); - assertEq(secondsPerLiquidityCumulativeX128, 442367076997220002502386989661298674892); - } - - function testCanFetchMultipleObservations() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 2 ** 15, tick: 2, time: 5})); - oracle.grow(4); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 6, liquidity: 2 ** 12})); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](6); - secondsAgos[0] = 0; - secondsAgos[1] = 3; - secondsAgos[2] = 8; - secondsAgos[3] = 13; - secondsAgos[4] = 15; - secondsAgos[5] = 18; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = - oracle.observe(secondsAgos); - assertEq(tickCumulatives.length, 6); - assertEq(tickCumulatives[0], 56); - assertEq(tickCumulatives[1], 38); - assertEq(tickCumulatives[2], 20); - assertEq(tickCumulatives[3], 10); - assertEq(tickCumulatives[4], 6); - assertEq(tickCumulatives[5], 0); - assertEq(secondsPerLiquidityCumulativeX128s.length, 6); - assertEq(secondsPerLiquidityCumulativeX128s[0], 550383467004691728624232610897330176); - assertEq(secondsPerLiquidityCumulativeX128s[1], 301153217795020002454768787094765568); - assertEq(secondsPerLiquidityCumulativeX128s[2], 103845937170696552570609926584401920); - assertEq(secondsPerLiquidityCumulativeX128s[3], 51922968585348276285304963292200960); - assertEq(secondsPerLiquidityCumulativeX128s[4], 31153781151208965771182977975320576); - assertEq(secondsPerLiquidityCumulativeX128s[5], 0); - } - - function testObserveGasSinceMostRecent() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.advanceTime(2); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 1; - snap("OracleObserveSinceMostRecent", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testObserveGasCurrentTime() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveCurrentTime", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testObserveGasCurrentTimeCounterfactual() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - initializedOracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveCurrentTimeCounterfactual", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testManyObservationsSimpleReads(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - assertEq(oracle.index(), 1); - assertEq(oracle.cardinality(), 5); - assertEq(oracle.cardinalityNext(), 5); - } - - function testManyObservationsLatestObservationSameTimeAsLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -21); - assertEq(secondsPerLiquidityCumulativeX128, 2104079302127802832415199655953100107502); - } - - function testManyObservationsLatestObservation5SecondsAfterLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - // latest observation 5 seconds after latest - oracle.advanceTime(5); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 5); - assertEq(tickCumulative, -21); - assertEq(secondsPerLiquidityCumulativeX128, 2104079302127802832415199655953100107502); - } - - function testManyObservationsCurrentObservation5SecondsAfterLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - oracle.advanceTime(5); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, 9); - assertEq(secondsPerLiquidityCumulativeX128, 2347138135642758877746181518404363115684); - } - - function testManyObservationsBetweenLatestObservationAtLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 3); - assertEq(tickCumulative, -33); - assertEq(secondsPerLiquidityCumulativeX128, 1593655751746395137220137744805447790318); - } - - function testManyObservationsBetweenLatestObservationAfterLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - oracle.advanceTime(5); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 8); - assertEq(tickCumulative, -33); - assertEq(secondsPerLiquidityCumulativeX128, 1593655751746395137220137744805447790318); - } - - function testManyObservationsOlderThanOldestReverts(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - (uint32 oldestTimestamp,,,) = oracle.observations(oracle.index() + 1); - uint32 secondsAgo = 15; - // overflow desired here - uint32 target; - unchecked { - target = oracle.time() - secondsAgo; - } - - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = secondsAgo; - vm.expectRevert( - abi.encodeWithSelector( - Oracle.TargetPredatesOldestObservation.selector, oldestTimestamp, uint32(int32(target)) - ) - ); - oracle.observe(secondsAgos); - - oracle.advanceTime(5); - - secondsAgos[0] = 20; - vm.expectRevert( - abi.encodeWithSelector( - Oracle.TargetPredatesOldestObservation.selector, oldestTimestamp, uint32(int32(target)) - ) - ); - oracle.observe(secondsAgos); - } - - function testManyObservationsOldest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 14); - assertEq(tickCumulative, -13); - assertEq(secondsPerLiquidityCumulativeX128, 544451787073501541541399371890829138329); - } - - function testManyObservationsOldestAfterTime(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - oracle.advanceTime(6); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 20); - assertEq(tickCumulative, -13); - assertEq(secondsPerLiquidityCumulativeX128, 544451787073501541541399371890829138329); - } - - function testManyObservationsFetchManyValues(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - oracle.advanceTime(6); - uint32[] memory secondsAgos = new uint32[](7); - secondsAgos[0] = 20; - secondsAgos[1] = 17; - secondsAgos[2] = 13; - secondsAgos[3] = 10; - secondsAgos[4] = 5; - secondsAgos[5] = 1; - secondsAgos[6] = 0; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = - oracle.observe(secondsAgos); - assertEq(tickCumulatives[0], -13); - assertEq(secondsPerLiquidityCumulativeX128s[0], 544451787073501541541399371890829138329); - assertEq(tickCumulatives[1], -31); - assertEq(secondsPerLiquidityCumulativeX128s[1], 799663562264205389138930327464655296921); - assertEq(tickCumulatives[2], -43); - assertEq(secondsPerLiquidityCumulativeX128s[2], 1045423049484883168306923099498710116305); - assertEq(tickCumulatives[3], -37); - assertEq(secondsPerLiquidityCumulativeX128s[3], 1423514568285925905488450441089563684590); - assertEq(tickCumulatives[4], -15); - assertEq(secondsPerLiquidityCumulativeX128s[4], 2152691068830794041481396028443352709138); - assertEq(tickCumulatives[5], 9); - assertEq(secondsPerLiquidityCumulativeX128s[5], 2347138135642758877746181518404363115684); - assertEq(tickCumulatives[6], 15); - assertEq(secondsPerLiquidityCumulativeX128s[6], 2395749902345750086812377890894615717321); - } - - function testGasAllOfLast20Seconds() public { - setupOracleWithManyObservations(5); - oracle.advanceTime(6); - uint32[] memory secondsAgos = new uint32[](20); - for (uint32 i = 0; i < 20; i++) { - secondsAgos[i] = 20 - i; - } - snap("OracleObserveLast20Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasLatestEqual() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveLatestEqual", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasLatestTransform() public { - setupOracleWithManyObservations(5); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveLatestTransform", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasOldest() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 14; - snap("OracleObserveOldest", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasBetweenOldestAndOldestPlusOne() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 13; - snap("OracleObserveBetweenOldestAndOldestPlusOne", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasMiddle() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 5; - snap("OracleObserveMiddle", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracle() public { - setupFullOracle(); - - assertEq(oracle.cardinalityNext(), 65535); - assertEq(oracle.cardinality(), 65535); - assertEq(oracle.index(), 165); - - // can observe into the ordered portion with exact seconds ago - (int56 tickCumulative, uint160 secondsPerLiquidityCumulative) = observeSingle(oracle, 100 * 13); - assertEq(tickCumulative, -27970560813); - assertEq(secondsPerLiquidityCumulative, 60465049086512033878831623038233202591033); - - // can observe into the ordered portion with unexact seconds ago - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 100 * 13 + 5); - assertEq(tickCumulative, -27970232823); - assertEq(secondsPerLiquidityCumulative, 60465023149565257990964350912969670793706); - - // can observe at exactly the latest observation - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 0); - assertEq(tickCumulative, -28055903863); - assertEq(secondsPerLiquidityCumulative, 60471787506468701386237800669810720099776); - - // can observe into the unordered portion of array at exact seconds ago - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 200 * 13); - assertEq(tickCumulative, -27885347763); - assertEq(secondsPerLiquidityCumulative, 60458300386499273141628780395875293027404); - - // can observe into the unordered portion of array at seconds ago between observations - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 200 * 13 + 5); - assertEq(tickCumulative, -27885020273); - assertEq(secondsPerLiquidityCumulative, 60458274409952896081377821330361274907140); - - // can observe the oldest observation - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 13 * 65534); - assertEq(tickCumulative, -175890); - assertEq(secondsPerLiquidityCumulative, 33974356747348039873972993881117400879779); - - // can observe at exactly the latest observation after some time passes - oracle.advanceTime(5); - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 5); - assertEq(tickCumulative, -28055903863); - assertEq(secondsPerLiquidityCumulative, 60471787506468701386237800669810720099776); - - // can observe after the latest observation counterfactual - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 3); - assertEq(tickCumulative, -28056035261); - assertEq(secondsPerLiquidityCumulative, 60471797865298117996489508104462919730461); - - // can observe the oldest observation after time passes - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 13 * 65534 + 5); - assertEq(tickCumulative, -175890); - assertEq(secondsPerLiquidityCumulative, 33974356747348039873972993881117400879779); - } - - function testFullOracleGasCostObserveZero() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("FullOracleObserveZero", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve200By13() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 200 * 13; - snap("FullOracleObserve200By13", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve200By13Plus5() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 200 * 13 + 5; - snap("FullOracleObserve200By13Plus5", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve0After5Seconds() public { - setupFullOracle(); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("FullOracleObserve0After5Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve5After5Seconds() public { - setupFullOracle(); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 5; - snap("FullOracleObserve5After5Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserveOldest() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 13 * 65534; - snap("FullOracleObserveOldest", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserveOldestAfter5Seconds() public { - setupFullOracle(); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 13 * 65534; - snap("FullOracleObserveOldestAfter5Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - // fixtures and helpers - - function observeSingle(OracleImplementation _initializedOracle, uint32 secondsAgo) - internal - view - returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulative) - { - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = secondsAgo; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulatives) = - _initializedOracle.observe(secondsAgos); - return (tickCumulatives[0], secondsPerLiquidityCumulatives[0]); - } - - function assertObservation(OracleImplementation _initializedOracle, uint64 idx, Oracle.Observation memory expected) - internal - { - (uint32 blockTimestamp, int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128, bool initialized) = - _initializedOracle.observations(idx); - assertEq(blockTimestamp, expected.blockTimestamp); - assertEq(tickCumulative, expected.tickCumulative); - assertEq(secondsPerLiquidityCumulativeX128, expected.secondsPerLiquidityCumulativeX128); - assertEq(initialized, expected.initialized); - } - - function setupOracleWithManyObservations(uint32 startingTime) internal { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: startingTime})); - oracle.grow(5); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2, tick: -6, liquidity: 4})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: -2, liquidity: 4})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 1, tick: -2, liquidity: 9})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 4, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 6, tick: 6, liquidity: 7})); - } - - function setupFullOracle() internal { - uint16 BATCH_SIZE = 300; - oracle.initialize( - OracleImplementation.InitializeParams({ - liquidity: 0, - tick: 0, - // Monday, October 5, 2020 9:00:00 AM GMT-05:00 - time: 1601906400 - }) - ); - - uint16 cardinalityNext = oracle.cardinalityNext(); - while (cardinalityNext < 65535) { - uint16 growTo = cardinalityNext + BATCH_SIZE < 65535 ? 65535 : cardinalityNext + BATCH_SIZE; - oracle.grow(growTo); - cardinalityNext = growTo; - } - - for (int24 i = 0; i < 65535; i += int24(uint24(BATCH_SIZE))) { - OracleImplementation.UpdateParams[] memory batch = new OracleImplementation.UpdateParams[](BATCH_SIZE); - for (int24 j = 0; j < int24(uint24(BATCH_SIZE)); j++) { - batch[uint24(j)] = OracleImplementation.UpdateParams({ - advanceTimeBy: 13, - tick: -i - j, - liquidity: uint128(int128(i) + int128(j)) - }); - } - oracle.batchUpdate(batch); - } - } -} diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index f434fd19..5e27a1ce 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -3,24 +3,26 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {PathKey} from "../contracts/libraries/PathKey.sol"; -import {IQuoter} from "../contracts/interfaces/IQuoter.sol"; -import {Quoter} from "../contracts/lens/Quoter.sol"; -import {LiquidityAmounts} from "../contracts/libraries/LiquidityAmounts.sol"; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PathKey} from "../src/libraries/PathKey.sol"; +import {IQuoter} from "../src/interfaces/IQuoter.sol"; +import {Quoter} from "../src/lens/Quoter.sol"; + +// v4-core +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +// solmate +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + contract QuoterTest is Test, Deployers { using SafeCast for *; using PoolIdLibrary for PoolKey; diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol deleted file mode 100644 index 0f2f82e0..00000000 --- a/test/TWAMM.t.sol +++ /dev/null @@ -1,432 +0,0 @@ -pragma solidity ^0.8.15; - -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {TWAMMImplementation} from "./shared/implementation/TWAMMImplementation.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {PoolDonateTest} from "@uniswap/v4-core/src/test/PoolDonateTest.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {TWAMM} from "../contracts/hooks/examples/TWAMM.sol"; -import {ITWAMM} from "../contracts/interfaces/ITWAMM.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -contract TWAMMTest is Test, Deployers, GasSnapshot { - using PoolIdLibrary for PoolKey; - using CurrencyLibrary for Currency; - - event SubmitOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - event UpdateOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - TWAMM twamm = - TWAMM(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG))); - address hookAddress; - MockERC20 token0; - MockERC20 token1; - PoolKey poolKey; - PoolId poolId; - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - - token0 = MockERC20(Currency.unwrap(currency0)); - token1 = MockERC20(Currency.unwrap(currency1)); - - TWAMMImplementation impl = new TWAMMImplementation(manager, 10_000, twamm); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(twamm), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(twamm), slot, vm.load(address(impl), slot)); - } - } - - (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - - token0.approve(address(modifyLiquidityRouter), 100 ether); - token1.approve(address(modifyLiquidityRouter), 100 ether); - token0.mint(address(this), 100 ether); - token1.mint(address(this), 100 ether); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES - ); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES - ); - modifyLiquidityRouter.modifyLiquidity( - poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether, 0), - ZERO_BYTES - ); - } - - function testTWAMM_beforeInitialize_SetsLastVirtualOrderTimestamp() public { - (PoolKey memory initKey, PoolId initId) = newPoolKeyWithTWAMM(twamm); - assertEq(twamm.lastVirtualOrderTimestamp(initId), 0); - vm.warp(10000); - - manager.initialize(initKey, SQRT_PRICE_1_1, ZERO_BYTES); - assertEq(twamm.lastVirtualOrderTimestamp(initId), 10000); - } - - function testTWAMM_submitOrder_StoresOrderWithCorrectPoolAndOrderPoolInfo() public { - uint160 expiration = 30000; - uint160 submitTimestamp = 10000; - uint160 duration = expiration - submitTimestamp; - - ITWAMM.OrderKey memory orderKey = ITWAMM.OrderKey(address(this), expiration, true); - - ITWAMM.Order memory nullOrder = twamm.getOrder(poolKey, orderKey); - assertEq(nullOrder.sellRate, 0); - assertEq(nullOrder.earningsFactorLast, 0); - - vm.warp(10000); - token0.approve(address(twamm), 100 ether); - snapStart("TWAMMSubmitOrder"); - twamm.submitOrder(poolKey, orderKey, 1 ether); - snapEnd(); - - ITWAMM.Order memory submittedOrder = twamm.getOrder(poolKey, orderKey); - (uint256 sellRateCurrent0For1, uint256 earningsFactorCurrent0For1) = twamm.getOrderPool(poolKey, true); - (uint256 sellRateCurrent1For0, uint256 earningsFactorCurrent1For0) = twamm.getOrderPool(poolKey, false); - - assertEq(submittedOrder.sellRate, 1 ether / duration); - assertEq(submittedOrder.earningsFactorLast, 0); - assertEq(sellRateCurrent0For1, 1 ether / duration); - assertEq(sellRateCurrent1For0, 0); - assertEq(earningsFactorCurrent0For1, 0); - assertEq(earningsFactorCurrent1For0, 0); - } - - function TWAMMSingleSell0For1SellRateAndEarningsFactorGetsUpdatedProperly() public { - // TODO: fails with a bug for single pool sell, swap amount 3 wei above balance. - - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); - ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 40000, true); - - token0.approve(address(twamm), 100e18); - token1.approve(address(twamm), 100e18); - vm.warp(10000); - twamm.submitOrder(poolKey, orderKey1, 1e18); - vm.warp(30000); - twamm.submitOrder(poolKey, orderKey2, 1e18); - vm.warp(40000); - - ITWAMM.Order memory submittedOrder = twamm.getOrder(poolKey, orderKey2); - (, uint256 earningsFactorCurrent) = twamm.getOrderPool(poolKey, true); - assertEq(submittedOrder.sellRate, 1 ether / 10000); - assertEq(submittedOrder.earningsFactorLast, earningsFactorCurrent); - } - - function testTWAMM_submitOrder_StoresSellRatesEarningsFactorsProperly() public { - uint160 expiration1 = 30000; - uint160 expiration2 = 40000; - uint256 submitTimestamp1 = 10000; - uint256 submitTimestamp2 = 30000; - uint256 earningsFactor0For1; - uint256 earningsFactor1For0; - uint256 sellRate0For1; - uint256 sellRate1For0; - - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), expiration1, true); - ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), expiration2, true); - ITWAMM.OrderKey memory orderKey3 = ITWAMM.OrderKey(address(this), expiration2, false); - - token0.approve(address(twamm), 100e18); - token1.approve(address(twamm), 100e18); - - // Submit 2 TWAMM orders and test all information gets updated - vm.warp(submitTimestamp1); - twamm.submitOrder(poolKey, orderKey1, 1e18); - twamm.submitOrder(poolKey, orderKey3, 3e18); - - (sellRate0For1, earningsFactor0For1) = twamm.getOrderPool(poolKey, true); - (sellRate1For0, earningsFactor1For0) = twamm.getOrderPool(poolKey, false); - assertEq(sellRate0For1, 1e18 / (expiration1 - submitTimestamp1)); - assertEq(sellRate1For0, 3e18 / (expiration2 - submitTimestamp1)); - assertEq(earningsFactor0For1, 0); - assertEq(earningsFactor1For0, 0); - - // Warp time and submit 1 TWAMM order. Test that pool information is updated properly as one order expires and - // another order is added to the pool - vm.warp(submitTimestamp2); - twamm.submitOrder(poolKey, orderKey2, 2e18); - - (sellRate0For1, earningsFactor0For1) = twamm.getOrderPool(poolKey, true); - (sellRate1For0, earningsFactor1For0) = twamm.getOrderPool(poolKey, false); - - assertEq(sellRate0For1, 2e18 / (expiration2 - submitTimestamp2)); - assertEq(sellRate1For0, 3e18 / (expiration2 - submitTimestamp1)); - assertEq(earningsFactor0For1, 1712020976636017581269515821040000); - assertEq(earningsFactor1For0, 1470157410324350030712806974476955); - } - - function testTWAMM_submitOrder_EmitsEvent() public { - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); - - token0.approve(address(twamm), 100e18); - vm.warp(10000); - - vm.expectEmit(false, false, false, true); - emit SubmitOrder(poolId, address(this), 30000, true, 1 ether / 20000, 0); - twamm.submitOrder(poolKey, orderKey1, 1e18); - } - - function testTWAMM_updateOrder_EmitsEvent() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - // decrease order amount by 10% - int256 amountDelta = -1; - - // set timestamp to halfway through the order - vm.warp(20000); - - vm.expectEmit(true, true, true, true); - emit UpdateOrder(poolId, address(this), 30000, true, 0, 10000 << 96); - twamm.updateOrder(poolKey, orderKey1, amountDelta); - } - - function testTWAMM_updateOrder_ZeroForOne_DecreasesSellrateUpdatesSellTokensOwed() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - // decrease order amount by 10% - int256 amountDelta = -int256(orderAmount) / 10; - - // set timestamp to halfway through the order - vm.warp(20000); - - (uint256 originalSellRate,) = twamm.getOrderPool(poolKey, true); - twamm.updateOrder(poolKey, orderKey1, amountDelta); - (uint256 updatedSellRate,) = twamm.getOrderPool(poolKey, true); - - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, (originalSellRate * 80) / 100); - assertEq(token0Owed, uint256(-amountDelta)); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updateOrder_OneForZero_DecreasesSellrateUpdatesSellTokensOwed() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // decrease order amount by 10% - int256 amountDelta = -int256(orderAmount) / 10; - - // set timestamp to halfway through the order - vm.warp(20000); - - (uint256 originalSellRate,) = twamm.getOrderPool(poolKey, false); - twamm.updateOrder(poolKey, orderKey2, amountDelta); - (uint256 updatedSellRate,) = twamm.getOrderPool(poolKey, false); - - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, (originalSellRate * 80) / 100); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, uint256(-amountDelta)); - } - - function testTWAMM_updatedOrder_ZeroForOne_ClosesOrderIfEliminatingPosition() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - twamm.updateOrder(poolKey, orderKey1, -1); - ITWAMM.Order memory deletedOrder = twamm.getOrder(poolKey, orderKey1); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - assertEq(deletedOrder.sellRate, 0); - assertEq(deletedOrder.earningsFactorLast, 0); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updatedOrder_OneForZero_ClosesOrderIfEliminatingPosition() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - twamm.updateOrder(poolKey, orderKey2, -1); - ITWAMM.Order memory deletedOrder = twamm.getOrder(poolKey, orderKey2); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey2.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey2.owner); - - assertEq(deletedOrder.sellRate, 0); - assertEq(deletedOrder.earningsFactorLast, 0); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updatedOrder_ZeroForOne_IncreaseOrderAmount() public { - int256 amountDelta = 1 ether; - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - uint256 balance0TWAMMBefore = token0.balanceOf(address(twamm)); - token0.approve(address(twamm), uint256(amountDelta)); - twamm.updateOrder(poolKey, orderKey1, amountDelta); - uint256 balance0TWAMMAfter = token0.balanceOf(address(twamm)); - - ITWAMM.Order memory updatedOrder = twamm.getOrder(poolKey, orderKey1); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - assertEq(balance0TWAMMAfter - balance0TWAMMBefore, uint256(amountDelta)); - assertEq(updatedOrder.sellRate, 150000000000000); - assertEq(token0Owed, 0); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updatedOrder_OneForZero_IncreaseOrderAmount() public { - int256 amountDelta = 1 ether; - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - uint256 balance0TWAMMBefore = token1.balanceOf(address(twamm)); - token1.approve(address(twamm), uint256(amountDelta)); - twamm.updateOrder(poolKey, orderKey2, amountDelta); - uint256 balance0TWAMMAfter = token1.balanceOf(address(twamm)); - - ITWAMM.Order memory updatedOrder = twamm.getOrder(poolKey, orderKey2); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey2.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey2.owner); - - assertEq(balance0TWAMMAfter - balance0TWAMMBefore, uint256(amountDelta)); - assertEq(updatedOrder.sellRate, 150000000000000); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, 0); - } - - function testTWAMMEndToEndSimSymmetricalOrderPools() public { - uint256 orderAmount = 1e18; - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); - ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 30000, false); - - token0.approve(address(twamm), 100e18); - token1.approve(address(twamm), 100e18); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES - ); - - vm.warp(10000); - twamm.submitOrder(poolKey, orderKey1, orderAmount); - twamm.submitOrder(poolKey, orderKey2, orderAmount); - vm.warp(20000); - twamm.executeTWAMMOrders(poolKey); - twamm.updateOrder(poolKey, orderKey1, 0); - twamm.updateOrder(poolKey, orderKey2, 0); - - uint256 earningsToken0 = twamm.tokensOwed(poolKey.currency0, address(this)); - uint256 earningsToken1 = twamm.tokensOwed(poolKey.currency1, address(this)); - - assertEq(earningsToken0, orderAmount / 2); - assertEq(earningsToken1, orderAmount / 2); - - uint256 balance0BeforeTWAMM = MockERC20(Currency.unwrap(poolKey.currency0)).balanceOf(address(twamm)); - uint256 balance1BeforeTWAMM = MockERC20(Currency.unwrap(poolKey.currency1)).balanceOf(address(twamm)); - uint256 balance0BeforeThis = poolKey.currency0.balanceOfSelf(); - uint256 balance1BeforeThis = poolKey.currency1.balanceOfSelf(); - - vm.warp(30000); - twamm.executeTWAMMOrders(poolKey); - twamm.updateOrder(poolKey, orderKey1, 0); - twamm.updateOrder(poolKey, orderKey2, 0); - twamm.claimTokens(poolKey.currency0, address(this), 0); - twamm.claimTokens(poolKey.currency1, address(this), 0); - - assertEq(twamm.tokensOwed(poolKey.currency0, address(this)), 0); - assertEq(twamm.tokensOwed(poolKey.currency1, address(this)), 0); - - uint256 balance0AfterTWAMM = MockERC20(Currency.unwrap(poolKey.currency0)).balanceOf(address(twamm)); - uint256 balance1AfterTWAMM = MockERC20(Currency.unwrap(poolKey.currency1)).balanceOf(address(twamm)); - uint256 balance0AfterThis = poolKey.currency0.balanceOfSelf(); - uint256 balance1AfterThis = poolKey.currency1.balanceOfSelf(); - - assertEq(balance1AfterTWAMM, 0); - assertEq(balance0AfterTWAMM, 0); - assertEq(balance0BeforeTWAMM - balance0AfterTWAMM, orderAmount); - assertEq(balance0AfterThis - balance0BeforeThis, orderAmount); - assertEq(balance1BeforeTWAMM - balance1AfterTWAMM, orderAmount); - assertEq(balance1AfterThis - balance1BeforeThis, orderAmount); - } - - function newPoolKeyWithTWAMM(IHooks hooks) public returns (PoolKey memory, PoolId) { - (Currency _token0, Currency _token1) = deployMintAndApprove2Currencies(); - PoolKey memory key = PoolKey(_token0, _token1, 0, 60, hooks); - return (key, key.toId()); - } - - function submitOrdersBothDirections() - internal - returns (ITWAMM.OrderKey memory key1, ITWAMM.OrderKey memory key2, uint256 amount) - { - key1 = ITWAMM.OrderKey(address(this), 30000, true); - key2 = ITWAMM.OrderKey(address(this), 30000, false); - amount = 1 ether; - - token0.approve(address(twamm), amount); - token1.approve(address(twamm), amount); - - vm.warp(10000); - twamm.submitOrder(poolKey, key1, amount); - twamm.submitOrder(poolKey, key2, amount); - } -} diff --git a/test/mock/MockMulticall.sol b/test/mock/MockMulticall.sol index 0c854c27..351803cd 100644 --- a/test/mock/MockMulticall.sol +++ b/test/mock/MockMulticall.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.20; -import "../../contracts/base/Multicall.sol"; +import "../../src/base/Multicall.sol"; contract MockMulticall is Multicall { function functionThatRevertsWithError(string memory error) external pure { diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 64effe2a..70aaed5a 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -12,7 +12,7 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; @@ -21,9 +21,9 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; +import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 2a46136a..6e0adae3 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -12,15 +12,15 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 11636a8a..40cb014e 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -12,7 +12,7 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; @@ -21,9 +21,9 @@ import {FeeMath} from "../shared/FeeMath.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; +import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index c955cae5..6c889e09 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -12,7 +12,7 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; @@ -21,9 +21,9 @@ import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; -import {Actions, INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {Actions, INonfungiblePositionManager} from "../../src/interfaces/INonfungiblePositionManager.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; import {FeeMath} from "../shared/FeeMath.sol"; diff --git a/test/position-managers/Multicall.t.sol b/test/position-managers/Multicall.t.sol index d753a007..5a0cc4fa 100644 --- a/test/position-managers/Multicall.t.sol +++ b/test/position-managers/Multicall.t.sol @@ -12,7 +12,7 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; @@ -21,9 +21,9 @@ import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; +import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index a707a7cf..10b17b25 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -12,7 +12,7 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; @@ -22,9 +22,9 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; +import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; diff --git a/test/position-managers/Permit.t.sol b/test/position-managers/Permit.t.sol index 660e4479..5532520c 100644 --- a/test/position-managers/Permit.t.sol +++ b/test/position-managers/Permit.t.sol @@ -12,18 +12,18 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {IERC721Permit} from "../../contracts/interfaces/IERC721Permit.sol"; +import {IERC721Permit} from "../../src/interfaces/IERC721Permit.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {INonfungiblePositionManager} from "../../src/interfaces/INonfungiblePositionManager.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; diff --git a/test/shared/FeeMath.sol b/test/shared/FeeMath.sol index ad8746c0..d5339005 100644 --- a/test/shared/FeeMath.sol +++ b/test/shared/FeeMath.sol @@ -11,9 +11,9 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; +import {INonfungiblePositionManager} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; library FeeMath { using SafeCast for uint256; diff --git a/test/shared/GetSender.sol b/test/shared/GetSender.sol deleted file mode 100644 index d1709219..00000000 --- a/test/shared/GetSender.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -contract GetSender { - function sender() external view returns (address) { - return msg.sender; - } -} diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 20d4905f..ee4a8374 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -5,8 +5,8 @@ import {Vm} from "forge-std/Vm.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {NonfungiblePositionManager, Actions} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; +import {NonfungiblePositionManager, Actions} from "../../src/NonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {Planner} from "../utils/Planner.sol"; contract LiquidityOperations { diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 6b629e6d..929b6c71 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -7,8 +7,8 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -import {INonfungiblePositionManager, Actions} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; +import {INonfungiblePositionManager, Actions} from "../../../src/interfaces/INonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../../src/types/LiquidityRange.sol"; import {Planner} from "../../utils/Planner.sol"; contract LiquidityFuzzers is Fuzzers { diff --git a/test/shared/implementation/FullRangeImplementation.sol b/test/shared/implementation/FullRangeImplementation.sol deleted file mode 100644 index 2d4ce3cc..00000000 --- a/test/shared/implementation/FullRangeImplementation.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {FullRange} from "../../../contracts/hooks/examples/FullRange.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract FullRangeImplementation is FullRange { - constructor(IPoolManager _poolManager, FullRange addressToEtch) FullRange(_poolManager) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} diff --git a/test/shared/implementation/GeomeanOracleImplementation.sol b/test/shared/implementation/GeomeanOracleImplementation.sol deleted file mode 100644 index b953a3b6..00000000 --- a/test/shared/implementation/GeomeanOracleImplementation.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {GeomeanOracle} from "../../../contracts/hooks/examples/GeomeanOracle.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract GeomeanOracleImplementation is GeomeanOracle { - uint32 public time; - - constructor(IPoolManager _poolManager, GeomeanOracle addressToEtch) GeomeanOracle(_poolManager) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} - - function setTime(uint32 _time) external { - time = _time; - } - - function _blockTimestamp() internal view override returns (uint32) { - return time; - } -} diff --git a/test/shared/implementation/LimitOrderImplementation.sol b/test/shared/implementation/LimitOrderImplementation.sol deleted file mode 100644 index 11625771..00000000 --- a/test/shared/implementation/LimitOrderImplementation.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {LimitOrder} from "../../../contracts/hooks/examples/LimitOrder.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract LimitOrderImplementation is LimitOrder { - constructor(IPoolManager _poolManager, LimitOrder addressToEtch) LimitOrder(_poolManager) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} diff --git a/test/shared/implementation/OracleImplementation.sol b/test/shared/implementation/OracleImplementation.sol deleted file mode 100644 index 7eefe3d3..00000000 --- a/test/shared/implementation/OracleImplementation.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Oracle} from "../../../contracts/libraries/Oracle.sol"; - -contract OracleImplementation { - using Oracle for Oracle.Observation[65535]; - - Oracle.Observation[65535] public observations; - - uint32 public time; - int24 public tick; - uint128 public liquidity; - uint16 public index; - uint16 public cardinality; - uint16 public cardinalityNext; - - struct InitializeParams { - uint32 time; - int24 tick; - uint128 liquidity; - } - - function initialize(InitializeParams calldata params) external { - require(cardinality == 0, "already initialized"); - time = params.time; - tick = params.tick; - liquidity = params.liquidity; - (cardinality, cardinalityNext) = observations.initialize(params.time); - } - - function advanceTime(uint32 by) public { - unchecked { - time += by; - } - } - - struct UpdateParams { - uint32 advanceTimeBy; - int24 tick; - uint128 liquidity; - } - - // write an observation, then change tick and liquidity - function update(UpdateParams calldata params) external { - advanceTime(params.advanceTimeBy); - (index, cardinality) = observations.write(index, time, tick, liquidity, cardinality, cardinalityNext); - tick = params.tick; - liquidity = params.liquidity; - } - - function batchUpdate(UpdateParams[] calldata params) external { - // sload everything - int24 _tick = tick; - uint128 _liquidity = liquidity; - uint16 _index = index; - uint16 _cardinality = cardinality; - uint16 _cardinalityNext = cardinalityNext; - uint32 _time = time; - - for (uint256 i = 0; i < params.length; i++) { - _time += params[i].advanceTimeBy; - (_index, _cardinality) = - observations.write(_index, _time, _tick, _liquidity, _cardinality, _cardinalityNext); - _tick = params[i].tick; - _liquidity = params[i].liquidity; - } - - // sstore everything - tick = _tick; - liquidity = _liquidity; - index = _index; - cardinality = _cardinality; - time = _time; - } - - function grow(uint16 _cardinalityNext) external { - cardinalityNext = observations.grow(cardinalityNext, _cardinalityNext); - } - - function observe(uint32[] calldata secondsAgos) - external - view - returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) - { - return observations.observe(time, secondsAgos, tick, index, liquidity, cardinality); - } - - function getGasCostOfObserve(uint32[] calldata secondsAgos) external view returns (uint256) { - (uint32 _time, int24 _tick, uint128 _liquidity, uint16 _index) = (time, tick, liquidity, index); - uint256 gasBefore = gasleft(); - observations.observe(_time, secondsAgos, _tick, _index, _liquidity, cardinality); - return gasBefore - gasleft(); - } -} diff --git a/test/shared/implementation/TWAMMImplementation.sol b/test/shared/implementation/TWAMMImplementation.sol deleted file mode 100644 index f217db8c..00000000 --- a/test/shared/implementation/TWAMMImplementation.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {TWAMM} from "../../../contracts/hooks/examples/TWAMM.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract TWAMMImplementation is TWAMM { - constructor(IPoolManager poolManager, uint256 interval, TWAMM addressToEtch) TWAMM(poolManager, interval) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} diff --git a/test/utils/HookEnabledSwapRouter.sol b/test/utils/HookEnabledSwapRouter.sol deleted file mode 100644 index 4021f453..00000000 --- a/test/utils/HookEnabledSwapRouter.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; - -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolTestBase} from "@uniswap/v4-core/src/test/PoolTestBase.sol"; -import {Test} from "forge-std/Test.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; - -contract HookEnabledSwapRouter is PoolTestBase { - using CurrencyLibrary for Currency; - using CurrencySettler for Currency; - - error NoSwapOccurred(); - - constructor(IPoolManager _manager) PoolTestBase(_manager) {} - - struct CallbackData { - address sender; - TestSettings testSettings; - PoolKey key; - IPoolManager.SwapParams params; - bytes hookData; - } - - struct TestSettings { - bool takeClaims; - bool settleUsingBurn; - } - - function swap( - PoolKey memory key, - IPoolManager.SwapParams memory params, - TestSettings memory testSettings, - bytes memory hookData - ) external payable returns (BalanceDelta delta) { - delta = abi.decode( - manager.unlock(abi.encode(CallbackData(msg.sender, testSettings, key, params, hookData))), (BalanceDelta) - ); - - uint256 ethBalance = address(this).balance; - if (ethBalance > 0) CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); - } - - function unlockCallback(bytes calldata rawData) external returns (bytes memory) { - require(msg.sender == address(manager)); - - CallbackData memory data = abi.decode(rawData, (CallbackData)); - - BalanceDelta delta = manager.swap(data.key, data.params, data.hookData); - - // Make sure youve added liquidity to the test pool! - if (BalanceDelta.unwrap(delta) == 0) revert NoSwapOccurred(); - - if (data.params.zeroForOne) { - data.key.currency0.settle( - manager, data.sender, uint256(int256(-delta.amount0())), data.testSettings.settleUsingBurn - ); - if (delta.amount1() > 0) { - data.key.currency1.take( - manager, data.sender, uint256(int256(delta.amount1())), data.testSettings.takeClaims - ); - } - } else { - data.key.currency1.settle( - manager, data.sender, uint256(int256(-delta.amount1())), data.testSettings.settleUsingBurn - ); - if (delta.amount0() > 0) { - data.key.currency0.take( - manager, data.sender, uint256(int256(delta.amount0())), data.testSettings.takeClaims - ); - } - } - - return abi.encode(delta); - } -} diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol index 4d9a87b8..cf9042b9 100644 --- a/test/utils/Planner.sol +++ b/test/utils/Planner.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; -import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; +import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; library Planner { using Planner for Plan; From 85bab02ab9b95343ab83fdc931cf5632bb96acd5 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:11:26 -0400 Subject: [PATCH 68/98] ERC721Permit cleanup (#161) * wip Solmate ERC721 * the queens dead, you may put down your arms. with this commit, i hereby lay BaseLiquidityManagement and the ideals of fee accounting finally to rest * refactor tokenId => LiquidityRange; begin separating PoolKey * move comment --------- Co-authored-by: Sara Reynolds --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/collect_erc20.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mint.snap | 2 +- .forge-snapshots/mint_differentRanges.snap | 2 +- .forge-snapshots/mint_same_tickLower.snap | 2 +- .forge-snapshots/mint_same_tickUpper.snap | 2 +- .../multicall_initialize_mint.snap | 2 +- .forge-snapshots/permit.snap | 2 +- .forge-snapshots/permit_secondPosition.snap | 2 +- .forge-snapshots/permit_twice.snap | 2 +- .forge-snapshots/sameRange_collect.snap | 2 +- .../sameRange_decreaseAllLiquidity.snap | 2 +- .forge-snapshots/sameRange_mint.snap | 2 +- src/NonfungiblePositionManager.sol | 100 +++++++++++------- src/base/ERC721Permit.sol | 12 ++- .../INonfungiblePositionManager.sol | 14 ++- test/position-managers/Execute.t.sol | 4 +- test/position-managers/FeeCollection.t.sol | 8 +- test/position-managers/Gas.t.sol | 34 +++--- .../position-managers/IncreaseLiquidity.t.sol | 2 +- test/position-managers/Multicall.t.sol | 2 +- test/shared/FeeMath.sol | 6 +- test/shared/LiquidityOperations.sol | 35 +++--- test/shared/fuzz/LiquidityFuzzers.sol | 2 +- test/utils/Planner.sol | 8 +- 31 files changed, 147 insertions(+), 118 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 6e06986c..f35bf0f5 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -175084 \ No newline at end of file +170568 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 0d9d33ce..3b59a386 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -189783 \ No newline at end of file +185267 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index 81eabc96..7b170ca9 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -189484 \ No newline at end of file +184968 \ No newline at end of file diff --git a/.forge-snapshots/collect_erc20.snap b/.forge-snapshots/collect_erc20.snap index 56edd0e5..91ea58d8 100644 --- a/.forge-snapshots/collect_erc20.snap +++ b/.forge-snapshots/collect_erc20.snap @@ -1 +1 @@ -166914 \ No newline at end of file +162398 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index f266306b..9e97b505 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -132292 \ No newline at end of file +127776 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index f266306b..9e97b505 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -132292 \ No newline at end of file +127776 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 0f56b6ff..d5f6bc6f 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -161758 \ No newline at end of file +157242 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 0f56b6ff..d5f6bc6f 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -161758 \ No newline at end of file +157242 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index 7c7fb5bc..6521db2a 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -433657 \ No newline at end of file +384052 \ No newline at end of file diff --git a/.forge-snapshots/mint_differentRanges.snap b/.forge-snapshots/mint_differentRanges.snap index ffc31809..1e5ce948 100644 --- a/.forge-snapshots/mint_differentRanges.snap +++ b/.forge-snapshots/mint_differentRanges.snap @@ -1 +1 @@ -399457 \ No newline at end of file +349852 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickLower.snap b/.forge-snapshots/mint_same_tickLower.snap index 64f78087..3cc3aa84 100644 --- a/.forge-snapshots/mint_same_tickLower.snap +++ b/.forge-snapshots/mint_same_tickLower.snap @@ -1 +1 @@ -393439 \ No newline at end of file +343834 \ No newline at end of file diff --git a/.forge-snapshots/mint_same_tickUpper.snap b/.forge-snapshots/mint_same_tickUpper.snap index 45062e45..8c54f121 100644 --- a/.forge-snapshots/mint_same_tickUpper.snap +++ b/.forge-snapshots/mint_same_tickUpper.snap @@ -1 +1 @@ -394081 \ No newline at end of file +344476 \ No newline at end of file diff --git a/.forge-snapshots/multicall_initialize_mint.snap b/.forge-snapshots/multicall_initialize_mint.snap index a5adbe1a..ba481d52 100644 --- a/.forge-snapshots/multicall_initialize_mint.snap +++ b/.forge-snapshots/multicall_initialize_mint.snap @@ -1 +1 @@ -477392 \ No newline at end of file +427828 \ No newline at end of file diff --git a/.forge-snapshots/permit.snap b/.forge-snapshots/permit.snap index 269d80a6..0bd11bf5 100644 --- a/.forge-snapshots/permit.snap +++ b/.forge-snapshots/permit.snap @@ -1 +1 @@ -75071 \ No newline at end of file +76881 \ No newline at end of file diff --git a/.forge-snapshots/permit_secondPosition.snap b/.forge-snapshots/permit_secondPosition.snap index 15e35dee..78a29c3e 100644 --- a/.forge-snapshots/permit_secondPosition.snap +++ b/.forge-snapshots/permit_secondPosition.snap @@ -1 +1 @@ -57971 \ No newline at end of file +59781 \ No newline at end of file diff --git a/.forge-snapshots/permit_twice.snap b/.forge-snapshots/permit_twice.snap index 6ef9d761..78fdba19 100644 --- a/.forge-snapshots/permit_twice.snap +++ b/.forge-snapshots/permit_twice.snap @@ -1 +1 @@ -40871 \ No newline at end of file +42681 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_collect.snap b/.forge-snapshots/sameRange_collect.snap index 56edd0e5..91ea58d8 100644 --- a/.forge-snapshots/sameRange_collect.snap +++ b/.forge-snapshots/sameRange_collect.snap @@ -1 +1 @@ -166914 \ No newline at end of file +162398 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap index bb30d07b..28446f0c 100644 --- a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap +++ b/.forge-snapshots/sameRange_decreaseAllLiquidity.snap @@ -1 +1 @@ -145173 \ No newline at end of file +140657 \ No newline at end of file diff --git a/.forge-snapshots/sameRange_mint.snap b/.forge-snapshots/sameRange_mint.snap index 2601c0bd..3e347363 100644 --- a/.forge-snapshots/sameRange_mint.snap +++ b/.forge-snapshots/sameRange_mint.snap @@ -1 +1 @@ -336763 \ No newline at end of file +287158 \ No newline at end of file diff --git a/src/NonfungiblePositionManager.sol b/src/NonfungiblePositionManager.sol index 5be13588..fff8e665 100644 --- a/src/NonfungiblePositionManager.sol +++ b/src/NonfungiblePositionManager.sol @@ -1,32 +1,32 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {ERC721Permit} from "./base/ERC721Permit.sol"; -import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; -import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; -import {Multicall} from "./base/Multicall.sol"; -import {PoolInitializer} from "./base/PoolInitializer.sol"; - import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {ERC721Permit} from "./base/ERC721Permit.sol"; +import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; +import {SafeCallback} from "./base/SafeCallback.sol"; +import {ImmutableState} from "./base/ImmutableState.sol"; +import {Multicall} from "./base/Multicall.sol"; +import {PoolInitializer} from "./base/PoolInitializer.sol"; +import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; + contract NonfungiblePositionManager is INonfungiblePositionManager, - BaseLiquidityManagement, ERC721Permit, PoolInitializer, - Multicall + Multicall, + SafeCallback { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; @@ -39,11 +39,11 @@ contract NonfungiblePositionManager is /// @dev The ID of the next token that will be minted. Skips 0 uint256 public nextTokenId = 1; - // maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range) - mapping(uint256 tokenId => TokenPosition position) public tokenPositions; + // maps the ERC721 tokenId to its Range (poolKey, tick range) + mapping(uint256 tokenId => LiquidityRange range) public tokenRange; constructor(IPoolManager _manager) - BaseLiquidityManagement(_manager) + ImmutableState(_manager) ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} @@ -104,9 +104,8 @@ contract NonfungiblePositionManager is _requireApprovedOrOwner(tokenId, sender); - TokenPosition memory tokenPos = tokenPositions[tokenId]; // Note: The tokenId is used as the salt for this position, so every minted liquidity has unique storage in the pool manager. - (BalanceDelta delta,) = _modifyLiquidity(tokenPos.range, liquidity.toInt256(), bytes32(tokenId), hookData); + (BalanceDelta delta,) = _modifyLiquidity(tokenRange[tokenId], liquidity.toInt256(), bytes32(tokenId), hookData); return abi.encode(delta); } @@ -120,9 +119,9 @@ contract NonfungiblePositionManager is _requireApprovedOrOwner(tokenId, sender); - TokenPosition memory tokenPos = tokenPositions[tokenId]; // Note: the tokenId is used as the salt. - (BalanceDelta delta,) = _modifyLiquidity(tokenPos.range, -(liquidity.toInt256()), bytes32(tokenId), hookData); + (BalanceDelta delta,) = + _modifyLiquidity(tokenRange[tokenId], -(liquidity.toInt256()), bytes32(tokenId), hookData); return abi.encode(delta); } @@ -139,7 +138,8 @@ contract NonfungiblePositionManager is (BalanceDelta delta,) = _modifyLiquidity(range, liquidity.toInt256(), bytes32(tokenId), hookData); - tokenPositions[tokenId] = TokenPosition({owner: owner, range: range, operator: address(0x0)}); + tokenRange[tokenId] = range; + return abi.encode(delta); } @@ -165,37 +165,55 @@ contract NonfungiblePositionManager is function burn(uint256 tokenId, address sender) internal { _requireApprovedOrOwner(tokenId, sender); // We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId. - TokenPosition memory tokenPos = tokenPositions[tokenId]; + // Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. - _validateBurn(tokenPos.owner, tokenPos.range); - delete tokenPositions[tokenId]; + _validateBurn(tokenId); + + delete tokenRange[tokenId]; // Burn the token. _burn(tokenId); } - // TODO: Bug - Positions are overrideable unless we can allow two of the same users to have distinct positions. - function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override { - TokenPosition storage tokenPosition = tokenPositions[tokenId]; - LiquidityRangeId rangeId = tokenPosition.range.toId(); - Position storage position = positions[from][rangeId]; - - // transfer position data to destination - positions[to][rangeId] = position; - delete positions[from][rangeId]; - - // update token position - tokenPositions[tokenId] = TokenPosition({owner: to, range: tokenPosition.range, operator: address(0x0)}); + function _modifyLiquidity(LiquidityRange memory range, int256 liquidityChange, bytes32 salt, bytes memory hookData) + internal + returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) + { + (liquidityDelta, totalFeesAccrued) = manager.modifyLiquidity( + range.poolKey, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: liquidityChange, + salt: salt + }), + hookData + ); } - // override ERC721 approval by setting operator - function _approve(address spender, uint256 tokenId) internal override { - tokenPositions[tokenId].operator = spender; + function _validateBurn(uint256 tokenId) internal { + bytes32 positionId = getPositionIdFromTokenId(tokenId); + uint128 liquidity = manager.getPositionLiquidity(tokenRange[tokenId].poolKey.toId(), positionId); + if (liquidity > 0) revert PositionMustBeEmpty(); } - function getApproved(uint256 tokenId) public view override returns (address) { - require(_exists(tokenId), "ERC721: approved query for nonexistent token"); - - return tokenPositions[tokenId].operator; + // TODO: Move this to a posm state-view library. + function getPositionIdFromTokenId(uint256 tokenId) public view returns (bytes32 positionId) { + LiquidityRange memory range = tokenRange[tokenId]; + bytes32 salt = bytes32(tokenId); + int24 tickLower = range.tickLower; + int24 tickUpper = range.tickUpper; + address owner = address(this); + + // positionId = keccak256(abi.encodePacked(owner, tickLower, tickUpper, salt)) + /// @solidity memory-safe-assembly + assembly { + mstore(0x26, salt) // [0x26, 0x46) + mstore(0x06, tickUpper) // [0x23, 0x26) + mstore(0x03, tickLower) // [0x20, 0x23) + mstore(0, owner) // [0x0c, 0x20) + positionId := keccak256(0x0c, 0x3a) // len is 58 bytes + mstore(0x26, 0) // rewrite 0x26 to 0 + } } function _requireApprovedOrOwner(uint256 tokenId, address sender) internal view { diff --git a/src/base/ERC721Permit.sol b/src/base/ERC721Permit.sol index ee9c10f9..e1e3ee22 100644 --- a/src/base/ERC721Permit.sol +++ b/src/base/ERC721Permit.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.24; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {ERC721} from "solmate/tokens/ERC721.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ChainId} from "../libraries/ChainId.sol"; @@ -25,6 +25,11 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { versionHash = keccak256(bytes(version_)); } + // TODO: implement here, or in posm + function tokenURI(uint256) public pure override returns (string memory) { + return "https://example.com"; + } + /// @inheritdoc IERC721Permit function DOMAIN_SEPARATOR() public view override returns (bytes32) { return keccak256( @@ -83,6 +88,11 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { ); } + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { + return spender == ownerOf(tokenId) || getApproved[tokenId] == spender + || isApprovedForAll[ownerOf(tokenId)][spender]; + } + /// @notice Returns the index of the bitmap and the bit position within the bitmap. Used for unordered nonces /// @param nonce The nonce to get the associated word and bit positions /// @return wordPos The word position or index into the nonceBitmap diff --git a/src/interfaces/INonfungiblePositionManager.sol b/src/interfaces/INonfungiblePositionManager.sol index 0fe32240..ad2a97ce 100644 --- a/src/interfaces/INonfungiblePositionManager.sol +++ b/src/interfaces/INonfungiblePositionManager.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; enum Actions { @@ -17,18 +18,15 @@ enum Actions { interface INonfungiblePositionManager { error MismatchedLengths(); error NotApproved(address caller); - - struct TokenPosition { - address owner; - LiquidityRange range; - address operator; - } - error MustBeUnlockedByThisContract(); error DeadlinePassed(); error UnsupportedAction(); + error PositionMustBeEmpty(); - function tokenPositions(uint256 tokenId) external view returns (address, LiquidityRange memory, address); + function tokenRange(uint256 tokenId) + external + view + returns (PoolKey memory poolKey, int24 tickLower, int24 tickUpper); /// @notice Batches many liquidity modification calls to pool manager /// @param payload is an encoding of actions, params, and currencies diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 70aaed5a..9ac5da7d 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -110,7 +110,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES)); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd2, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); (bytes memory actions) = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -133,7 +133,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit planner = planner.add(Actions.MINT, abi.encode(range, initialLiquidity, address(this), ZERO_BYTES)); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 6e0adae3..f7a2b90e 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -174,10 +174,10 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 tokenIdBob = lpm.nextTokenId() - 1; // confirm the positions are same range - (, LiquidityRange memory rangeAlice,) = lpm.tokenPositions(tokenIdAlice); - (, LiquidityRange memory rangeBob,) = lpm.tokenPositions(tokenIdBob); - assertEq(rangeAlice.tickLower, rangeBob.tickLower); - assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); + (, int24 tickLowerAlice, int24 tickUpperAlice) = lpm.tokenRange(tokenIdAlice); + (, int24 tickLowerBob, int24 tickUpperBob) = lpm.tokenRange(tokenIdBob); + assertEq(tickLowerAlice, tickLowerBob); + assertEq(tickUpperAlice, tickUpperBob); // swap to create fees uint256 swapAmount = 0.01e18; diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 40cb014e..0a4f3981 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -89,7 +89,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { function test_gas_mint() public { Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(this), ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); snapLastCall("mint"); @@ -104,7 +104,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Mint to a diff range, diff user. Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(alice); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -120,7 +120,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Mint to a diff range, diff user. Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(alice); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -136,7 +136,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Mint to a diff range, diff user. Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(alice); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -150,7 +150,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -164,7 +164,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -206,7 +206,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(alice); bytes memory actions = planner.zip(); @@ -239,7 +239,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenIdBob, 0, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(bob); bytes memory actions = planner.zip(); @@ -263,7 +263,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(alice); actions = planner.zip(); @@ -307,7 +307,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(alice); bytes memory actions = planner.zip(); @@ -322,7 +322,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -336,7 +336,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -364,7 +364,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); @@ -457,7 +457,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Collect by calling decrease with 0. Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 0, ZERO_BYTES, false)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); snapLastCall("collect_erc20"); @@ -469,7 +469,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_001 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.prank(alice); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -488,7 +488,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); @@ -509,7 +509,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 0, ZERO_BYTES, false)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 6c889e09..39843f18 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -118,7 +118,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // TODO: Can we make this easier to re-invest fees, so that you don't need to know the exact collect amount? Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); vm.startPrank(alice); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, _deadline); diff --git a/test/position-managers/Multicall.t.sol b/test/position-managers/Multicall.t.sol index 5a0cc4fa..64a99289 100644 --- a/test/position-managers/Multicall.t.sol +++ b/test/position-managers/Multicall.t.sol @@ -68,7 +68,7 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); diff --git a/test/shared/FeeMath.sol b/test/shared/FeeMath.sol index d5339005..8812efa2 100644 --- a/test/shared/FeeMath.sol +++ b/test/shared/FeeMath.sol @@ -27,16 +27,16 @@ library FeeMath { view returns (BalanceDelta feesOwed) { - (, LiquidityRange memory range,) = posm.tokenPositions(tokenId); + (PoolKey memory poolKey, int24 tickLower, int24 tickUpper) = posm.tokenRange(tokenId); // getPosition(poolId, owner, tL, tU, salt) // owner is the position manager // salt is the tokenId Position.Info memory position = - manager.getPosition(range.poolKey.toId(), address(posm), range.tickLower, range.tickUpper, bytes32(tokenId)); + manager.getPosition(poolKey.toId(), address(posm), tickLower, tickUpper, bytes32(tokenId)); (uint256 feeGrowthInside0X218, uint256 feeGrowthInside1X128) = - manager.getFeeGrowthInside(range.poolKey.toId(), range.tickLower, range.tickUpper); + manager.getFeeGrowthInside(poolKey.toId(), tickLower, tickUpper); feesOwed = getFeesOwed( feeGrowthInside0X218, diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index ee4a8374..16679a73 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {Vm} from "forge-std/Vm.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {NonfungiblePositionManager, Actions} from "../../src/NonfungiblePositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; @@ -23,7 +24,7 @@ contract LiquidityOperations { { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, recipient, hookData)); - planner = planner.finalize(_range); // Close the currencies. + planner = planner.finalize(_range.poolKey); // Close the currencies. bytes memory actions = planner.zip(); bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); @@ -36,8 +37,8 @@ contract LiquidityOperations { internal returns (BalanceDelta) { - (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); - return _increaseLiquidity(_range, tokenId, liquidityToAdd, hookData); + (PoolKey memory key, int24 tickLower, int24 tickUpper) = lpm.tokenRange(tokenId); + return _increaseLiquidity(LiquidityRange(key, tickLower, tickUpper), tokenId, liquidityToAdd, hookData); } function _increaseLiquidity( @@ -49,7 +50,7 @@ contract LiquidityOperations { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData)); - planner = planner.finalize(_range); // Close the currencies. + planner = planner.finalize(_range.poolKey); // Close the currencies. bytes memory actions = planner.zip(); bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); return abi.decode(result[0], (BalanceDelta)); @@ -59,9 +60,9 @@ contract LiquidityOperations { internal returns (BalanceDelta) { - (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + (PoolKey memory key, int24 tickLower, int24 tickUpper) = lpm.tokenRange(tokenId); - return _decreaseLiquidity(_range, tokenId, liquidityToRemove, hookData); + return _decreaseLiquidity(LiquidityRange(key, tickLower, tickUpper), tokenId, liquidityToRemove, hookData); } // do not make external call before unlockAndExecute, allows us to test reverts @@ -74,15 +75,15 @@ contract LiquidityOperations { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData)); - planner = planner.finalize(_range); // Close the currencies. + planner = planner.finalize(_range.poolKey); // Close the currencies. bytes memory actions = planner.zip(); bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); return abi.decode(result[0], (BalanceDelta)); } function _collect(uint256 tokenId, address recipient, bytes memory hookData) internal returns (BalanceDelta) { - (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); - return _collect(_range, tokenId, recipient, hookData); + (PoolKey memory key, int24 tickLower, int24 tickUpper) = lpm.tokenRange(tokenId); + return _collect(LiquidityRange(key, tickLower, tickUpper), tokenId, recipient, hookData); } // do not make external call before unlockAndExecute, allows us to test reverts @@ -93,7 +94,7 @@ contract LiquidityOperations { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.DECREASE, abi.encode(tokenId, 0, hookData)); - planner = planner.finalize(_range); // Close the currencies. + planner = planner.finalize(_range.poolKey); // Close the currencies. bytes memory actions = planner.zip(); bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); @@ -124,10 +125,10 @@ contract LiquidityOperations { view returns (bytes memory) { - (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + (PoolKey memory key,,) = lpm.tokenRange(tokenId); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData)); - planner = planner.finalize(_range); + planner = planner.finalize(key); return planner.zip(); } @@ -136,10 +137,10 @@ contract LiquidityOperations { view returns (bytes memory) { - (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + (PoolKey memory key,,) = lpm.tokenRange(tokenId); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData)); - planner = planner.finalize(_range); + planner = planner.finalize(key); return planner.zip(); } @@ -148,13 +149,13 @@ contract LiquidityOperations { view returns (bytes memory) { - (, LiquidityRange memory _range,) = lpm.tokenPositions(tokenId); + (PoolKey memory poolKey,,) = lpm.tokenRange(tokenId); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.DECREASE, abi.encode(tokenId, 0, hookData)); // TODO: allow recipient when supported on CLOSE_CURRENCY? - planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(_range.poolKey.currency0)); - planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(_range.poolKey.currency1)); + planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency0)); + planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency1)); return planner.zip(); } } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 929b6c71..c95c0af5 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -29,7 +29,7 @@ contract LiquidityFuzzers is Fuzzers { Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), recipient, hookData)); - planner = planner.finalize(range); + planner = planner.finalize(range.poolKey); bytes memory actions = planner.zip(); lpm.modifyLiquidities(actions, block.timestamp + 1); diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol index cf9042b9..77329259 100644 --- a/test/utils/Planner.sol +++ b/test/utils/Planner.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; @@ -32,9 +34,9 @@ library Planner { return Plan({actions: actions, params: params}); } - function finalize(Plan memory plan, LiquidityRange memory range) internal pure returns (Plan memory) { - plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(range.poolKey.currency0)); - plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(range.poolKey.currency1)); + function finalize(Plan memory plan, PoolKey memory poolKey) internal pure returns (Plan memory) { + plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency0)); + plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency1)); return plan; } From 7c873443313549304a13d728607abeb62ea56fbc Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Thu, 18 Jul 2024 16:46:08 -0400 Subject: [PATCH 69/98] remove old files, imports --- .../decreaseLiquidity_erc6909.snap | 1 - .../increaseLiquidity_erc6909.snap | 1 - src/NonfungiblePositionManager.sol | 9 ++- src/base/BaseLiquidityManagement.sol | 66 ------------------- src/base/ERC721Permit.sol | 2 +- src/base/SelfPermit.sol | 52 --------------- src/interfaces/IBaseLiquidityManagement.sol | 24 ------- .../INonfungiblePositionManager.sol | 14 +--- src/interfaces/ISelfPermit.sol | 56 ---------------- .../BalanceDeltaExtensionLibrary.sol | 53 --------------- src/libraries/CurrencyDeltas.sol | 38 ----------- src/libraries/LiquiditySaltLibrary.sol | 21 ------ src/libraries/Position.sol | 44 ------------- test/position-managers/Gas.t.sol | 42 ++++++------ 14 files changed, 29 insertions(+), 394 deletions(-) delete mode 100644 .forge-snapshots/decreaseLiquidity_erc6909.snap delete mode 100644 .forge-snapshots/increaseLiquidity_erc6909.snap delete mode 100644 src/base/BaseLiquidityManagement.sol delete mode 100644 src/base/SelfPermit.sol delete mode 100644 src/interfaces/IBaseLiquidityManagement.sol delete mode 100644 src/interfaces/ISelfPermit.sol delete mode 100644 src/libraries/BalanceDeltaExtensionLibrary.sol delete mode 100644 src/libraries/CurrencyDeltas.sol delete mode 100644 src/libraries/LiquiditySaltLibrary.sol delete mode 100644 src/libraries/Position.sol diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap deleted file mode 100644 index 9e97b505..00000000 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ /dev/null @@ -1 +0,0 @@ -127776 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap deleted file mode 100644 index d5f6bc6f..00000000 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ /dev/null @@ -1 +0,0 @@ -157242 \ No newline at end of file diff --git a/src/NonfungiblePositionManager.sol b/src/NonfungiblePositionManager.sol index fff8e665..2fabebfe 100644 --- a/src/NonfungiblePositionManager.sol +++ b/src/NonfungiblePositionManager.sol @@ -5,12 +5,10 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {ERC721Permit} from "./base/ERC721Permit.sol"; import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; @@ -81,11 +79,12 @@ contract NonfungiblePositionManager is } else if (actions[i] == Actions.DECREASE) { returnData[i] = _decrease(params[i], sender); } else if (actions[i] == Actions.MINT) { + // TODO: Mint will be coupled with increase. returnData[i] = _mint(params[i]); } else if (actions[i] == Actions.CLOSE_CURRENCY) { returnData[i] = _close(params[i], sender); } else if (actions[i] == Actions.BURN) { - // TODO: Burn will just be moved outside of this.. or coupled with a decrease.. + // TODO: Burn will be coupled with decrease. (uint256 tokenId) = abi.decode(params[i], (uint256)); burn(tokenId, sender); } else { diff --git a/src/base/BaseLiquidityManagement.sol b/src/base/BaseLiquidityManagement.sol deleted file mode 100644 index 1d453703..00000000 --- a/src/base/BaseLiquidityManagement.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; -import {SafeCallback} from "./SafeCallback.sol"; -import {ImmutableState} from "./ImmutableState.sol"; -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; - -import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; -import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; - -import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; -import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; -import {PositionLibrary} from "../libraries/Position.sol"; -import {BalanceDeltaExtensionLibrary} from "../libraries/BalanceDeltaExtensionLibrary.sol"; - -abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback { - using LiquidityRangeIdLibrary for LiquidityRange; - using CurrencyLibrary for Currency; - using CurrencySettleTake for Currency; - using CurrencyDeltas for IPoolManager; - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - using TransientStateLibrary for IPoolManager; - using SafeCast for uint256; - using LiquiditySaltLibrary for IHooks; - using PositionLibrary for IBaseLiquidityManagement.Position; - using BalanceDeltaExtensionLibrary for BalanceDelta; - - mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; - - constructor(IPoolManager _manager) ImmutableState(_manager) {} - - function _modifyLiquidity(LiquidityRange memory range, int256 liquidityChange, bytes32 salt, bytes memory hookData) - internal - returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) - { - (liquidityDelta, totalFeesAccrued) = manager.modifyLiquidity( - range.poolKey, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: liquidityChange, - salt: salt - }), - hookData - ); - } - - function _validateBurn(address owner, LiquidityRange memory range) internal { - LiquidityRangeId rangeId = range.toId(); - Position storage position = positions[owner][rangeId]; - if (position.liquidity > 0) revert PositionMustBeEmpty(); - if (position.tokensOwed0 != 0 && position.tokensOwed1 != 0) revert TokensMustBeCollected(); - delete positions[owner][rangeId]; - } -} diff --git a/src/base/ERC721Permit.sol b/src/base/ERC721Permit.sol index e1e3ee22..27719643 100644 --- a/src/base/ERC721Permit.sol +++ b/src/base/ERC721Permit.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.24; import {ERC721} from "solmate/tokens/ERC721.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import {ChainId} from "../libraries/ChainId.sol"; import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; -import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; /// @title ERC721 with permit /// @notice Nonfungible tokens that support an approve via signature, i.e. permit diff --git a/src/base/SelfPermit.sol b/src/base/SelfPermit.sol deleted file mode 100644 index 2f626496..00000000 --- a/src/base/SelfPermit.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; - -import {IERC20PermitAllowed} from "../interfaces/external/IERC20PermitAllowed.sol"; -import {ISelfPermit} from "../interfaces/ISelfPermit.sol"; - -/// @title Self Permit -/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route -/// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function -/// that requires an approval in a single transactions. -abstract contract SelfPermit is ISelfPermit { - /// @inheritdoc ISelfPermit - function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - public - payable - override - { - IERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s); - } - - /// @inheritdoc ISelfPermit - function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable - override - { - if (IERC20(token).allowance(msg.sender, address(this)) < value) selfPermit(token, value, deadline, v, r, s); - } - - /// @inheritdoc ISelfPermit - function selfPermitAllowed(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - public - payable - override - { - IERC20PermitAllowed(token).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); - } - - /// @inheritdoc ISelfPermit - function selfPermitAllowedIfNecessary(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - external - payable - override - { - if (IERC20(token).allowance(msg.sender, address(this)) < type(uint256).max) { - selfPermitAllowed(token, nonce, expiry, v, r, s); - } - } -} diff --git a/src/interfaces/IBaseLiquidityManagement.sol b/src/interfaces/IBaseLiquidityManagement.sol deleted file mode 100644 index 05424934..00000000 --- a/src/interfaces/IBaseLiquidityManagement.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {LiquidityRange, LiquidityRangeId} from "../types/LiquidityRange.sol"; - -interface IBaseLiquidityManagement { - error PositionMustBeEmpty(); - error TokensMustBeCollected(); - - // details about the liquidity position - struct Position { - uint256 liquidity; - // the fee growth of the aggregate position as of the last action on the individual position - uint256 feeGrowthInside0LastX128; - uint256 feeGrowthInside1LastX128; - // how many uncollected tokens are owed to the position, as of the last computation - uint128 tokensOwed0; - uint128 tokensOwed1; - } - - error LockFailure(); -} diff --git a/src/interfaces/INonfungiblePositionManager.sol b/src/interfaces/INonfungiblePositionManager.sol index ad2a97ce..34145198 100644 --- a/src/interfaces/INonfungiblePositionManager.sol +++ b/src/interfaces/INonfungiblePositionManager.sol @@ -1,24 +1,20 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {LiquidityRange} from "../types/LiquidityRange.sol"; enum Actions { MINT, BURN, INCREASE, DECREASE, - CLOSE_CURRENCY // Any positive delta on a currency will be sent to specified address - + // Any positive delta on a currency will be sent to specified address + CLOSE_CURRENCY } interface INonfungiblePositionManager { error MismatchedLengths(); error NotApproved(address caller); - error MustBeUnlockedByThisContract(); error DeadlinePassed(); error UnsupportedAction(); error PositionMustBeEmpty(); @@ -34,11 +30,5 @@ interface INonfungiblePositionManager { /// @return returnData is the endocing of each actions return information function modifyLiquidities(bytes calldata payload, uint256 deadline) external returns (bytes[] memory); - /// TODO Can decide if we want burn to auto encode a decrease/collect. - //// @notice Burn a position and delete the tokenId - //// @dev It enforces that there is no open liquidity or tokens to be collected - //// @param tokenId The ID of the position - //// function burn(uint256 tokenId) external; - function nextTokenId() external view returns (uint256); } diff --git a/src/interfaces/ISelfPermit.sol b/src/interfaces/ISelfPermit.sol deleted file mode 100644 index cb2445f5..00000000 --- a/src/interfaces/ISelfPermit.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.0; - -/// @title Self Permit -/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route -interface ISelfPermit { - /// @notice Permits this contract to spend a given token from `msg.sender` - /// @dev The `owner` is always msg.sender and the `spender` is always address(this). - /// @param token The address of the token spent - /// @param value The amount that can be spent of token - /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable; - - /// @notice Permits this contract to spend a given token from `msg.sender` - /// @dev The `owner` is always msg.sender and the `spender` is always address(this). - /// Can be used instead of #selfPermit to prevent calls from failing due to a frontrun of a call to #selfPermit - /// @param token The address of the token spent - /// @param value The amount that can be spent of token - /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable; - - /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter - /// @dev The `owner` is always msg.sender and the `spender` is always address(this) - /// @param token The address of the token spent - /// @param nonce The current nonce of the owner - /// @param expiry The timestamp at which the permit is no longer valid - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermitAllowed(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - external - payable; - - /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter - /// @dev The `owner` is always msg.sender and the `spender` is always address(this) - /// Can be used instead of #selfPermitAllowed to prevent calls from failing due to a frontrun of a call to #selfPermitAllowed. - /// @param token The address of the token spent - /// @param nonce The current nonce of the owner - /// @param expiry The timestamp at which the permit is no longer valid - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermitAllowedIfNecessary(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - external - payable; -} diff --git a/src/libraries/BalanceDeltaExtensionLibrary.sol b/src/libraries/BalanceDeltaExtensionLibrary.sol deleted file mode 100644 index e8b3a7f0..00000000 --- a/src/libraries/BalanceDeltaExtensionLibrary.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - -library BalanceDeltaExtensionLibrary { - function setAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) { - assembly { - // set the upper 128 bits of a to amount0 - a := or(shl(128, amount0), and(sub(shl(128, 1), 1), a)) - } - return a; - } - - function setAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) { - assembly { - // set the lower 128 bits of a to amount1 - a := or(and(shl(128, sub(shl(128, 1), 1)), a), amount1) - } - return a; - } - - function addAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) { - assembly { - let a0 := sar(128, a) - let res0 := add(a0, amount0) - a := or(shl(128, res0), and(sub(shl(128, 1), 1), a)) - } - return a; - } - - function addAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) { - assembly { - let a1 := signextend(15, a) - let res1 := add(a1, amount1) - a := or(and(shl(128, sub(shl(128, 1), 1)), a), res1) - } - return a; - } - - function addAndAssign(BalanceDelta a, BalanceDelta b) internal pure returns (BalanceDelta) { - assembly { - let a0 := sar(128, a) - let a1 := signextend(15, a) - let b0 := sar(128, b) - let b1 := signextend(15, b) - let res0 := add(a0, b0) - let res1 := add(a1, b1) - a := or(shl(128, res0), and(sub(shl(128, 1), 1), res1)) - } - return a; - } -} diff --git a/src/libraries/CurrencyDeltas.sol b/src/libraries/CurrencyDeltas.sol deleted file mode 100644 index 55389e4f..00000000 --- a/src/libraries/CurrencyDeltas.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; - -library CurrencyDeltas { - using SafeCast for int256; - - /// @notice Get the current delta for a caller in the two given currencies - /// @param caller_ The address of the caller - /// @param currency0 The currency for which to lookup the delta - /// @param currency1 The other currency for which to lookup the delta - function currencyDeltas(IPoolManager manager, address caller_, Currency currency0, Currency currency1) - internal - view - returns (BalanceDelta) - { - bytes32 key0; - bytes32 key1; - assembly { - mstore(0, caller_) - mstore(32, currency0) - key0 := keccak256(0, 64) - - mstore(0, caller_) - mstore(32, currency1) - key1 := keccak256(0, 64) - } - bytes32[] memory slots = new bytes32[](2); - slots[0] = key0; - slots[1] = key1; - bytes32[] memory result = manager.exttload(slots); - return toBalanceDelta(int256(uint256(result[0])).toInt128(), int256(uint256(result[1])).toInt128()); - } -} diff --git a/src/libraries/LiquiditySaltLibrary.sol b/src/libraries/LiquiditySaltLibrary.sol deleted file mode 100644 index c0a4fda8..00000000 --- a/src/libraries/LiquiditySaltLibrary.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.24; - -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -/// @notice Library used to interact with PoolManager.sol to settle any open deltas. -/// To settle a positive delta (a credit to the user), a user may take or mint. -/// To settle a negative delta (a debt on the user), a user make transfer or burn to pay off a debt. -/// @dev Note that sync() is called before any erc-20 transfer in `settle`. -library LiquiditySaltLibrary { - /// @notice Calculates the salt parameters for IPoolManager.ModifyLiquidityParams - /// If the hook uses after*LiquidityReturnDelta, the salt is the address of the sender - /// otherwise, use 0 for warm-storage gas savings - function getLiquiditySalt(IHooks hooks, address sender) internal pure returns (bytes32 salt) { - salt = Hooks.hasPermission(hooks, Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG) - || Hooks.hasPermission(hooks, Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG) - ? bytes32(uint256(uint160(sender))) - : bytes32(0); - } -} diff --git a/src/libraries/Position.sol b/src/libraries/Position.sol deleted file mode 100644 index 11ef1771..00000000 --- a/src/libraries/Position.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.8.20; - -import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; -import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; - -// Updates Position storage -library PositionLibrary { - error InsufficientLiquidity(); - - // TODO ensure this is one sstore. - function addTokensOwed(IBaseLiquidityManagement.Position storage position, BalanceDelta tokensOwed) internal { - position.tokensOwed0 += uint128(tokensOwed.amount0()); - position.tokensOwed1 += uint128(tokensOwed.amount1()); - } - - function clearTokensOwed(IBaseLiquidityManagement.Position storage position) internal { - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - } - - function addLiquidity(IBaseLiquidityManagement.Position storage position, uint256 liquidity) internal { - unchecked { - position.liquidity += liquidity; - } - } - - function subtractLiquidity(IBaseLiquidityManagement.Position storage position, uint256 liquidity) internal { - if (position.liquidity < liquidity) revert InsufficientLiquidity(); - unchecked { - position.liquidity -= liquidity; - } - } - - // TODO ensure this is one sstore. - function updateFeeGrowthInside( - IBaseLiquidityManagement.Position storage position, - uint256 feeGrowthInside0X128, - uint256 feeGrowthInside1X128 - ) internal { - position.feeGrowthInside0LastX128 = feeGrowthInside0X128; - position.feeGrowthInside1LastX128 = feeGrowthInside1X128; - } -} diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 0a4f3981..6e75e284 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -157,19 +157,20 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { snapLastCall("increaseLiquidity_erc20"); } - function test_gas_increaseLiquidity_erc6909() public { - _mint(range, 10_000 ether, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; + // TODO: ERC6909 Support. + // function test_gas_increaseLiquidity_erc6909() public { + // _mint(range, 10_000 ether, address(this), ZERO_BYTES); + // uint256 tokenId = lpm.nextTokenId() - 1; - Planner.Plan memory planner = - Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); + // Planner.Plan memory planner = + // Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + // planner = planner.finalize(range.poolKey); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); - snapLastCall("increaseLiquidity_erc6909"); - } + // bytes memory actions = planner.zip(); + // lpm.modifyLiquidities(actions, _deadline); + // snapLastCall("increaseLiquidity_erc6909"); + // } function test_gas_autocompound_exactUnclaimedFees() public { // Alice and Bob provide liquidity on the range @@ -329,19 +330,20 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { snapLastCall("decreaseLiquidity_erc20"); } - function test_gas_decreaseLiquidity_erc6909() public { - _mint(range, 10_000 ether, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; + // TODO: ERC6909 Support + // function test_gas_decreaseLiquidity_erc6909() public { + // _mint(range, 10_000 ether, address(this), ZERO_BYTES); + // uint256 tokenId = lpm.nextTokenId() - 1; - Planner.Plan memory planner = - Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); + // Planner.Plan memory planner = + // Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + // planner = planner.finalize(range.poolKey); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); - snapLastCall("decreaseLiquidity_erc6909"); - } + // bytes memory actions = planner.zip(); + // lpm.modifyLiquidities(actions, _deadline); + // snapLastCall("decreaseLiquidity_erc6909"); + // } function test_gas_burn() public {} function test_gas_burnEmpty() public {} From 55978acafa58dfb4af49a4ce669e7cacdb372b17 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Fri, 19 Jul 2024 09:54:33 -0400 Subject: [PATCH 70/98] Update src/NonfungiblePositionManager.sol Co-authored-by: saucepoint <98790946+saucepoint@users.noreply.github.com> --- src/NonfungiblePositionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NonfungiblePositionManager.sol b/src/NonfungiblePositionManager.sol index 2fabebfe..fb1efe45 100644 --- a/src/NonfungiblePositionManager.sol +++ b/src/NonfungiblePositionManager.sol @@ -123,7 +123,7 @@ contract NonfungiblePositionManager is _modifyLiquidity(tokenRange[tokenId], -(liquidity.toInt256()), bytes32(tokenId), hookData); return abi.encode(delta); } - + /// @param params is an encoding of LiquidityRange memory range, uint256 liquidity, address recipient, bytes hookData where recipient is the receiver / owner of the ERC721 function _mint(bytes memory param) internal returns (bytes memory) { (LiquidityRange memory range, uint256 liquidity, address owner, bytes memory hookData) = abi.decode(param, (LiquidityRange, uint256, address, bytes)); From 4a3d64a533d172dbdcbe5c496f628cdb91f12f5c Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 10:04:25 -0400 Subject: [PATCH 71/98] pr comments --- src/NonfungiblePositionManager.sol | 4 +++- test/position-managers/FeeCollection.t.sol | 4 ++-- test/position-managers/NonfungiblePositionManager.t.sol | 6 +++--- .../{Multicall.t.sol => PositionManager.multicall.t.sol} | 0 test/shared/fuzz/LiquidityFuzzers.sol | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) rename test/position-managers/{Multicall.t.sol => PositionManager.multicall.t.sol} (100%) diff --git a/src/NonfungiblePositionManager.sol b/src/NonfungiblePositionManager.sol index fb1efe45..8d46c935 100644 --- a/src/NonfungiblePositionManager.sol +++ b/src/NonfungiblePositionManager.sol @@ -123,7 +123,9 @@ contract NonfungiblePositionManager is _modifyLiquidity(tokenRange[tokenId], -(liquidity.toInt256()), bytes32(tokenId), hookData); return abi.encode(delta); } - /// @param params is an encoding of LiquidityRange memory range, uint256 liquidity, address recipient, bytes hookData where recipient is the receiver / owner of the ERC721 + + /// @param param is an encoding of LiquidityRange memory range, uint256 liquidity, address recipient, bytes hookData where recipient is the receiver / owner of the ERC721 + /// @return returns an encoding of the BalanceDelta from the initial increase function _mint(bytes memory param) internal returns (bytes memory) { (LiquidityRange memory range, uint256 liquidity, address owner, bytes memory hookData) = abi.decode(param, (LiquidityRange, uint256, address, bytes)); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index f7a2b90e..bd73145e 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -72,7 +72,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); // uint256 tokenId; - // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // // swap to create fees @@ -92,7 +92,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 10b17b25..9e6c7aee 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -191,7 +191,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // create liquidity we can burn uint256 tokenId; - (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); @@ -235,7 +235,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi public { uint256 tokenId; - (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(0 < decreaseLiquidityDelta); vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); @@ -257,7 +257,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // uint256 decreaseLiquidityDelta // ) public { // uint256 tokenId; - // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // vm.assume(0 < decreaseLiquidityDelta); // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); diff --git a/test/position-managers/Multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol similarity index 100% rename from test/position-managers/Multicall.t.sol rename to test/position-managers/PositionManager.multicall.t.sol diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index c95c0af5..47524371 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -14,7 +14,7 @@ import {Planner} from "../../utils/Planner.sol"; contract LiquidityFuzzers is Fuzzers { using Planner for Planner.Plan; - function createFuzzyLiquidity( + function addFuzzyLiquidity( INonfungiblePositionManager lpm, address recipient, PoolKey memory key, From 8f1be9fb38c37c5d4ca93b7acd4ed01f07a21862 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 10:26:39 -0400 Subject: [PATCH 72/98] pr comments --- src/NonfungiblePositionManager.sol | 1 - src/interfaces/INonfungiblePositionManager.sol | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NonfungiblePositionManager.sol b/src/NonfungiblePositionManager.sol index 8d46c935..fb11473a 100644 --- a/src/NonfungiblePositionManager.sol +++ b/src/NonfungiblePositionManager.sol @@ -206,7 +206,6 @@ contract NonfungiblePositionManager is address owner = address(this); // positionId = keccak256(abi.encodePacked(owner, tickLower, tickUpper, salt)) - /// @solidity memory-safe-assembly assembly { mstore(0x26, salt) // [0x26, 0x46) mstore(0x06, tickUpper) // [0x23, 0x26) diff --git a/src/interfaces/INonfungiblePositionManager.sol b/src/interfaces/INonfungiblePositionManager.sol index 34145198..737a382b 100644 --- a/src/interfaces/INonfungiblePositionManager.sol +++ b/src/interfaces/INonfungiblePositionManager.sol @@ -19,6 +19,7 @@ interface INonfungiblePositionManager { error UnsupportedAction(); error PositionMustBeEmpty(); + // TODO: This will just return a positionId function tokenRange(uint256 tokenId) external view From a35b9ead16bd354dd15bcd35f63d169501a09ff7 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 19 Jul 2024 11:52:20 -0400 Subject: [PATCH 73/98] refactor test helpers per feedback --- .../multicall_initialize_mint.snap | 2 +- test/position-managers/Execute.t.sol | 16 +-- test/position-managers/FeeCollection.t.sol | 28 ++-- test/position-managers/Gas.t.sol | 127 ++++++++---------- .../position-managers/IncreaseLiquidity.t.sol | 33 +++-- .../NonfungiblePositionManager.t.sol | 14 +- test/position-managers/Permit.t.sol | 26 ++-- .../PositionManager.multicall.t.sol | 5 +- test/shared/LiquidityOperations.sol | 101 ++++---------- test/shared/fuzz/LiquidityFuzzers.sol | 5 +- test/utils/Planner.sol | 6 +- 11 files changed, 144 insertions(+), 219 deletions(-) diff --git a/.forge-snapshots/multicall_initialize_mint.snap b/.forge-snapshots/multicall_initialize_mint.snap index ba481d52..865faa50 100644 --- a/.forge-snapshots/multicall_initialize_mint.snap +++ b/.forge-snapshots/multicall_initialize_mint.snap @@ -1 +1 @@ -427828 \ No newline at end of file +427971 \ No newline at end of file diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 9ac5da7d..6c54d7ea 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -82,10 +82,10 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - _mint(range, initialLiquidity, address(this), ZERO_BYTES); + mint(range, initialLiquidity, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - _increaseLiquidity(tokenId, liquidityToAdd, ZERO_BYTES); + increaseLiquidity(tokenId, liquidityToAdd, ZERO_BYTES); bytes32 positionId = keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); @@ -102,7 +102,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit initialiLiquidity = bound(initialiLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); - _mint(range, initialiLiquidity, address(this), ZERO_BYTES); + mint(range, initialiLiquidity, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = Planner.init(); @@ -110,9 +110,8 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES)); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd2, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); - (bytes memory actions) = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); bytes32 positionId = keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); @@ -133,9 +132,8 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit planner = planner.add(Actions.MINT, abi.encode(range, initialLiquidity, address(this), ZERO_BYTES)); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); bytes32 positionId = keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index bd73145e..797ea8e2 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -80,7 +80,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // swap(key, false, -int256(swapAmount), ZERO_BYTES); // // collect fees - // BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES, true); + // BalanceDelta delta = collect(tokenId, address(this), ZERO_BYTES, true); // assertEq(delta.amount0(), 0); @@ -102,7 +102,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // collect fees uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES); + BalanceDelta delta = collect(tokenId, address(this), ZERO_BYTES); // express key.fee as wad (i.e. 3000 = 0.003e18) assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); @@ -124,11 +124,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // LiquidityRange memory range = // LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); // vm.prank(alice); - // _mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + // mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); // uint256 tokenIdAlice = lpm.nextTokenId() - 1; // vm.prank(bob); - // _mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + // mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); // uint256 tokenIdBob = lpm.nextTokenId() - 1; // // swap to create fees @@ -137,14 +137,14 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // // alice collects only her fees // vm.prank(alice); - // BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES, true); + // BalanceDelta delta = collect(tokenIdAlice, alice, ZERO_BYTES, true); // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); // assertTrue(delta.amount1() != 0); // // bob collects only his fees // vm.prank(bob); - // delta = _collect(tokenIdBob, bob, ZERO_BYTES, true); + // delta = collect(tokenIdBob, bob, ZERO_BYTES, true); // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); // assertTrue(delta.amount1() != 0); @@ -166,11 +166,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); - _mint(range, uint256(params.liquidityDelta), alice, ZERO_BYTES); + mint(range, uint256(params.liquidityDelta), alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(bob); - _mint(range, liquidityDeltaBob, bob, ZERO_BYTES); + mint(range, liquidityDeltaBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // confirm the positions are same range @@ -187,7 +187,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance0AliceBefore = currency0.balanceOf(alice); uint256 balance1AliceBefore = currency1.balanceOf(alice); vm.startPrank(alice); - BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES); + BalanceDelta delta = collect(tokenIdAlice, alice, ZERO_BYTES); vm.stopPrank(); uint256 balance0AliceAfter = currency0.balanceOf(alice); uint256 balance1AliceAfter = currency1.balanceOf(alice); @@ -200,7 +200,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance0BobBefore = currency0.balanceOf(bob); uint256 balance1BobBefore = currency1.balanceOf(bob); vm.startPrank(bob); - delta = _collect(tokenIdBob, bob, ZERO_BYTES); + delta = collect(tokenIdBob, bob, ZERO_BYTES); vm.stopPrank(); uint256 balance0BobAfter = currency0.balanceOf(bob); uint256 balance1BobAfter = currency1.balanceOf(bob); @@ -228,14 +228,14 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 liquidityBob = 1000e18; vm.prank(alice); - BalanceDelta lpDeltaAlice = _mint(range, liquidityAlice, alice, ZERO_BYTES); + BalanceDelta lpDeltaAlice = mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; uint256 aliceBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)); uint256 aliceBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)); vm.prank(bob); - BalanceDelta lpDeltaBob = _mint(range, liquidityBob, bob, ZERO_BYTES); + BalanceDelta lpDeltaBob = mint(range, liquidityBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; uint256 bobBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)); @@ -249,7 +249,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // alice decreases liquidity vm.startPrank(alice); lpm.approve(address(this), tokenIdAlice); - _decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES); + decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES); vm.stopPrank(); uint256 tolerance = 0.000000001 ether; @@ -270,7 +270,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // bob decreases half of his liquidity vm.startPrank(bob); lpm.approve(address(this), tokenIdBob); - _decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES); + decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES); vm.stopPrank(); // bob has accrued half his principle liquidity + any fees in token0 diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 6e75e284..334a1740 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -89,9 +89,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { function test_gas_mint() public { Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(this), ZERO_BYTES)); - planner = planner.finalize(range.poolKey); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("mint"); } @@ -99,15 +98,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Explicitly mint to a new range on the same pool. LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 0, tickUpper: 60}); vm.startPrank(bob); - _mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); + mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff range, diff user. Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("mint_differentRanges"); } @@ -115,15 +113,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Explicitly mint to range whos tickLower is the same. LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: -60}); vm.startPrank(bob); - _mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); + mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff range, diff user. Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("mint_same_tickLower"); } @@ -131,35 +128,32 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Explicitly mint to range whos tickUpperis the same. LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 60, tickUpper: 300}); vm.startPrank(bob); - _mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); + mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff range, diff user. Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("mint_same_tickUpper"); } function test_gas_increaseLiquidity_erc20() public { - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); - - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("increaseLiquidity_erc20"); } // TODO: ERC6909 Support. // function test_gas_increaseLiquidity_erc6909() public { - // _mint(range, 10_000 ether, address(this), ZERO_BYTES); + // mint(range, 10_000 ether, address(this), ZERO_BYTES); // uint256 tokenId = lpm.nextTokenId() - 1; // Planner.Plan memory planner = @@ -181,12 +175,12 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, bob, ZERO_BYTES); + mint(range, liquidityBob, bob, ZERO_BYTES); // donate to create fees uint256 amountDonate = 0.2e18; @@ -207,11 +201,9 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); - + bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("autocompound_exactUnclaimedFees"); } @@ -223,12 +215,12 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, bob, ZERO_BYTES); + mint(range, liquidityBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees @@ -240,11 +232,9 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenIdBob, 0, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); - + bytes memory calls = planner.finalize(range.poolKey); vm.prank(bob); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); // donate to create more fees donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); @@ -264,11 +254,10 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); - actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); } } @@ -282,12 +271,12 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, bob, ZERO_BYTES); + mint(range, liquidityBob, bob, ZERO_BYTES); // donate to create fees uint256 amountDonate = 20e18; @@ -308,31 +297,28 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("autocompound_excessFeesCredit"); } function test_gas_decreaseLiquidity_erc20() public { - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); - - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("decreaseLiquidity_erc20"); } // TODO: ERC6909 Support // function test_gas_decreaseLiquidity_erc6909() public { - // _mint(range, 10_000 ether, address(this), ZERO_BYTES); + // mint(range, 10_000 ether, address(this), ZERO_BYTES); // uint256 tokenId = lpm.nextTokenId() - 1; // Planner.Plan memory planner = @@ -366,9 +352,10 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory actions = planner.finalize(range.poolKey); - calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); + calls[1] = + abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, actions, _deadline); lpm.multicall(calls); snapLastCall("multicall_initialize_mint"); @@ -378,7 +365,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice permits for the first time uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -396,7 +383,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice permits for her two tokens, benchmark the 2nd permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -409,7 +396,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // alice creates another position vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -428,7 +415,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives operator permission to bob @@ -450,7 +437,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { } function test_gas_collect_erc20() public { - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -459,51 +446,47 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Collect by calling decrease with 0. Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 0, ZERO_BYTES, false)); - planner = planner.finalize(range.poolKey); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("collect_erc20"); } // same-range gas tests function test_gas_sameRange_mint() public { - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, 10_001 ether, address(alice), ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("sameRange_mint"); } function test_gas_sameRange_decrease() public { // two positions of the same range, one of them decreases the entirety of the liquidity vm.startPrank(alice); - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); vm.stopPrank(); - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); - planner = planner.finalize(range.poolKey); - - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("sameRange_decreaseAllLiquidity"); } function test_gas_sameRange_collect() public { // two positions of the same range, one of them collects all their fees vm.startPrank(alice); - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); vm.stopPrank(); - _mint(range, 10_000 ether, address(this), ZERO_BYTES); + mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -511,10 +494,8 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 0, ZERO_BYTES, false)); - planner = planner.finalize(range.poolKey); - - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, _deadline); snapLastCall("sameRange_collect"); } } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 39843f18..386a1348 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -85,12 +85,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, bob, ZERO_BYTES); + mint(range, liquidityBob, bob, ZERO_BYTES); // swap to create fees uint256 swapAmount = 0.001e18; @@ -118,10 +118,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // TODO: Can we make this easier to re-invest fees, so that you don't need to know the exact collect amount? Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory calls = planner.finalize(range.poolKey); vm.startPrank(alice); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, _deadline); + lpm.modifyLiquidities(calls, _deadline); vm.stopPrank(); // It is not exact because of the error in the fee calculation and error in the @@ -143,12 +142,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, bob, ZERO_BYTES); + mint(range, liquidityBob, bob, ZERO_BYTES); // donate to create fees uint256 amountDonate = 0.2e18; @@ -170,7 +169,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi uint256 balance1BeforeAlice = currency1.balanceOf(alice); vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); + increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); vm.stopPrank(); // It is not exact because of the error in the fee calculation and error in the @@ -191,12 +190,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // // alice provides liquidity // vm.prank(alice); - // _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + // mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); // uint256 tokenIdAlice = lpm.nextTokenId() - 1; // // bob provides liquidity // vm.prank(bob); - // _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + // mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // uint256 tokenIdBob = lpm.nextTokenId() - 1; // // swap to create fees @@ -217,7 +216,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // ); // vm.startPrank(alice); - // _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + // increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); // vm.stopPrank(); // } @@ -226,7 +225,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // uint256 balance0BeforeBob = currency0.balanceOf(bob); // uint256 balance1BeforeBob = currency1.balanceOf(bob); // vm.startPrank(bob); - // _collect(tokenIdBob, bob, ZERO_BYTES, false); + // collect(tokenIdBob, bob, ZERO_BYTES, false); // vm.stopPrank(); // uint256 balance0AfterBob = currency0.balanceOf(bob); // uint256 balance1AfterBob = currency1.balanceOf(bob); @@ -247,7 +246,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // uint256 balance0BeforeAlice = currency0.balanceOf(alice); // uint256 balance1BeforeAlice = currency1.balanceOf(alice); // vm.startPrank(alice); - // _collect(tokenIdAlice, alice, ZERO_BYTES, false); + // collect(tokenIdAlice, alice, ZERO_BYTES, false); // vm.stopPrank(); // uint256 balance0AfterAlice = currency0.balanceOf(alice); // uint256 balance1AfterAlice = currency1.balanceOf(alice); @@ -273,12 +272,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // alice provides liquidity vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - _mint(range, liquidityBob, bob, ZERO_BYTES); + mint(range, liquidityBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // swap to create fees @@ -302,7 +301,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); + increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); vm.stopPrank(); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); @@ -317,7 +316,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi uint256 balance0BeforeBob = currency0.balanceOf(bob); uint256 balance1BeforeBob = currency1.balanceOf(bob); vm.startPrank(bob); - _collect(tokenIdBob, bob, ZERO_BYTES); + collect(tokenIdBob, bob, ZERO_BYTES); vm.stopPrank(); uint256 balance0AfterBob = currency0.balanceOf(bob); uint256 balance1AfterBob = currency1.balanceOf(bob); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 9e6c7aee..8c89a7eb 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -77,7 +77,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance1Before = currency1.balanceOfSelf(); uint256 tokenId = lpm.nextTokenId(); - BalanceDelta delta = _mint(range, liquidityToAdd, address(this), ZERO_BYTES); + BalanceDelta delta = mint(range, liquidityToAdd, address(this), ZERO_BYTES); assertEq(tokenId, 1); assertEq(lpm.ownerOf(tokenId), address(this)); @@ -110,7 +110,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance1Before = currency1.balanceOfSelf(); uint256 tokenId = lpm.nextTokenId(); - BalanceDelta delta = _mint(range, liquidityToAdd, address(this), ZERO_BYTES); + BalanceDelta delta = mint(range, liquidityToAdd, address(this), ZERO_BYTES); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); @@ -133,7 +133,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 tokenId = lpm.nextTokenId(); - _mint(range, liquidityToAdd, address(alice), ZERO_BYTES); + mint(range, liquidityToAdd, address(alice), ZERO_BYTES); assertEq(tokenId, 1); assertEq(lpm.ownerOf(tokenId), alice); @@ -207,8 +207,8 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0BeforeBurn = currency0.balanceOfSelf(); uint256 balance1BeforeBurn = currency1.balanceOfSelf(); - BalanceDelta deltaDecrease = _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES); - _burn(tokenId); + BalanceDelta deltaDecrease = decreaseLiquidity(tokenId, liquidity, ZERO_BYTES); + burn(tokenId); (liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); @@ -243,7 +243,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES); + decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES); bytes32 positionId = keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); @@ -297,7 +297,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // add liquidity to verify pool initialized LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - _mint(range, 100e18, address(this), ZERO_BYTES); + mint(range, 100e18, address(this), ZERO_BYTES); assertEq(lpm.ownerOf(1), address(this)); } diff --git a/test/position-managers/Permit.t.sol b/test/position-managers/Permit.t.sol index 5532520c..2cef66e2 100644 --- a/test/position-managers/Permit.t.sol +++ b/test/position-managers/Permit.t.sol @@ -84,7 +84,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_increaseLiquidity() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives bob operator permissions @@ -95,7 +95,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation uint256 balance0BobBefore = currency0.balanceOf(bob); uint256 balance1BobBefore = currency1.balanceOf(bob); vm.startPrank(bob); - _increaseLiquidity(tokenIdAlice, newLiquidity, ZERO_BYTES); + increaseLiquidity(tokenIdAlice, newLiquidity, ZERO_BYTES); vm.stopPrank(); // alice's position has new liquidity @@ -112,7 +112,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_decreaseLiquidity() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives bob operator permissions @@ -121,7 +121,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation // bob can decrease liquidity on alice's token uint256 liquidityToRemove = 0.4444e18; vm.startPrank(bob); - _decreaseLiquidity(tokenIdAlice, liquidityToRemove, ZERO_BYTES); + decreaseLiquidity(tokenIdAlice, liquidityToRemove, ZERO_BYTES); vm.stopPrank(); // alice's position decreased liquidity @@ -135,7 +135,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_collect() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -153,7 +153,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation uint256 balance0BobBefore = currency0.balanceOf(bob); uint256 balance1BobBefore = currency1.balanceOf(bob); vm.startPrank(bob); - _collect(tokenIdAlice, recipient, ZERO_BYTES); + collect(tokenIdAlice, recipient, ZERO_BYTES); vm.stopPrank(); assertApproxEqAbs(currency0.balanceOf(recipient), balance0BobBefore + currency0Revenue, 1 wei); @@ -166,7 +166,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob cannot permit himself on alice's token @@ -184,7 +184,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation // increaseLiquidity fails if the owner did not permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob cannot increase liquidity on alice's token @@ -200,7 +200,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation // decreaseLiquidity fails if the owner did not permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob cannot decrease liquidity on alice's token @@ -216,7 +216,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation // collect fails if the owner did not permit uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // donate to create fee revenue @@ -236,7 +236,7 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_nonceAlreadyUsed() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // alice gives bob operator permissions @@ -257,13 +257,13 @@ contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperation function test_permit_nonceAlreadyUsed_twoPositions() public { uint256 liquidityAlice = 1e18; vm.prank(alice); - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; vm.prank(alice); range.tickLower = -600; range.tickUpper = 600; - _mint(range, liquidityAlice, alice, ZERO_BYTES); + mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice2 = lpm.nextTokenId() - 1; // alice gives bob operator permissions for first token diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 64a99289..45e4b6d7 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -68,9 +68,10 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); - planner = planner.finalize(range.poolKey); + bytes memory actions = planner.finalize(range.poolKey); - calls[1] = abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, planner.zip()); + calls[1] = + abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, actions, _deadline); lpm.multicall(calls); diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 16679a73..3e79f17b 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Vm} from "forge-std/Vm.sol"; +import {CommonBase} from "forge-std/Base.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -10,102 +10,55 @@ import {NonfungiblePositionManager, Actions} from "../../src/NonfungiblePosition import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {Planner} from "../utils/Planner.sol"; -contract LiquidityOperations { - Vm internal constant _vm1 = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - NonfungiblePositionManager lpm; - +abstract contract LiquidityOperations is CommonBase { using Planner for Planner.Plan; + NonfungiblePositionManager lpm; + uint256 _deadline = block.timestamp + 1; - function _mint(LiquidityRange memory _range, uint256 liquidity, address recipient, bytes memory hookData) + function mint(LiquidityRange memory _range, uint256 liquidity, address recipient, bytes memory hookData) internal returns (BalanceDelta) { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, recipient, hookData)); - planner = planner.finalize(_range.poolKey); // Close the currencies. - bytes memory actions = planner.zip(); - bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = planner.finalize(_range.poolKey); + bytes[] memory result = lpm.modifyLiquidities(calls, _deadline); return abi.decode(result[0], (BalanceDelta)); } - // we overloaded this function because vm.prank was hitting .tokenPositions() - // TODO: now that vm.prank is hitting Planner, we can probably consolidate to a single function - function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData) + function increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData) internal returns (BalanceDelta) { - (PoolKey memory key, int24 tickLower, int24 tickUpper) = lpm.tokenRange(tokenId); - return _increaseLiquidity(LiquidityRange(key, tickLower, tickUpper), tokenId, liquidityToAdd, hookData); - } - - function _increaseLiquidity( - LiquidityRange memory _range, - uint256 tokenId, - uint256 liquidityToAdd, - bytes memory hookData - ) internal returns (BalanceDelta) { - Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData)); - - planner = planner.finalize(_range.poolKey); // Close the currencies. - bytes memory actions = planner.zip(); - bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = getIncreaseEncoded(tokenId, liquidityToAdd, hookData); + bytes[] memory result = lpm.modifyLiquidities(calls, _deadline); return abi.decode(result[0], (BalanceDelta)); } - function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData) + // do not make external call before unlockAndExecute, allows us to test reverts + function decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData) internal returns (BalanceDelta) { - (PoolKey memory key, int24 tickLower, int24 tickUpper) = lpm.tokenRange(tokenId); - - return _decreaseLiquidity(LiquidityRange(key, tickLower, tickUpper), tokenId, liquidityToRemove, hookData); - } - - // do not make external call before unlockAndExecute, allows us to test reverts - function _decreaseLiquidity( - LiquidityRange memory _range, - uint256 tokenId, - uint256 liquidityToRemove, - bytes memory hookData - ) internal returns (BalanceDelta) { - Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData)); - - planner = planner.finalize(_range.poolKey); // Close the currencies. - bytes memory actions = planner.zip(); - bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); + bytes memory calls = getDecreaseEncoded(tokenId, liquidityToRemove, hookData); + bytes[] memory result = lpm.modifyLiquidities(calls, _deadline); return abi.decode(result[0], (BalanceDelta)); } - function _collect(uint256 tokenId, address recipient, bytes memory hookData) internal returns (BalanceDelta) { - (PoolKey memory key, int24 tickLower, int24 tickUpper) = lpm.tokenRange(tokenId); - return _collect(LiquidityRange(key, tickLower, tickUpper), tokenId, recipient, hookData); - } - - // do not make external call before unlockAndExecute, allows us to test reverts - function _collect(LiquidityRange memory _range, uint256 tokenId, address recipient, bytes memory hookData) - internal - returns (BalanceDelta) - { - Planner.Plan memory planner = Planner.init(); - planner = planner.add(Actions.DECREASE, abi.encode(tokenId, 0, hookData)); - - planner = planner.finalize(_range.poolKey); // Close the currencies. - - bytes memory actions = planner.zip(); - bytes[] memory result = lpm.modifyLiquidities(actions, _deadline); + function collect(uint256 tokenId, address recipient, bytes memory hookData) internal returns (BalanceDelta) { + bytes memory calls = getCollectEncoded(tokenId, recipient, hookData); + bytes[] memory result = lpm.modifyLiquidities(calls, _deadline); return abi.decode(result[0], (BalanceDelta)); } - function _burn(uint256 tokenId) internal { + function burn(uint256 tokenId) internal { Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.BURN, abi.encode(tokenId)); // No close needed on burn. - bytes memory actions = planner.zip(); + bytes memory actions = planner.encode(); lpm.modifyLiquidities(actions, _deadline); } @@ -113,9 +66,9 @@ contract LiquidityOperations { function _permit(address signer, uint256 privateKey, uint256 tokenId, address operator, uint256 nonce) internal { bytes32 digest = lpm.getDigest(operator, tokenId, 1, block.timestamp + 1); - (uint8 v, bytes32 r, bytes32 s) = _vm1.sign(privateKey, digest); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - _vm1.prank(signer); + vm.prank(signer); lpm.permit(operator, tokenId, block.timestamp + 1, nonce, v, r, s); } @@ -128,8 +81,7 @@ contract LiquidityOperations { (PoolKey memory key,,) = lpm.tokenRange(tokenId); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData)); - planner = planner.finalize(key); - return planner.zip(); + return planner.finalize(key); } function getDecreaseEncoded(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData) @@ -140,8 +92,7 @@ contract LiquidityOperations { (PoolKey memory key,,) = lpm.tokenRange(tokenId); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData)); - planner = planner.finalize(key); - return planner.zip(); + return planner.finalize(key); } function getCollectEncoded(uint256 tokenId, address recipient, bytes memory hookData) @@ -152,10 +103,6 @@ contract LiquidityOperations { (PoolKey memory poolKey,,) = lpm.tokenRange(tokenId); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.DECREASE, abi.encode(tokenId, 0, hookData)); - - // TODO: allow recipient when supported on CLOSE_CURRENCY? - planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency0)); - planner = planner.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency1)); - return planner.zip(); + return planner.finalize(poolKey); } } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 47524371..5d121dd3 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -29,9 +29,8 @@ contract LiquidityFuzzers is Fuzzers { Planner.Plan memory planner = Planner.init().add(Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), recipient, hookData)); - planner = planner.finalize(range.poolKey); - bytes memory actions = planner.zip(); - lpm.modifyLiquidities(actions, block.timestamp + 1); + bytes memory calls = planner.finalize(range.poolKey); + lpm.modifyLiquidities(calls, block.timestamp + 1); uint256 tokenId = lpm.nextTokenId() - 1; return (tokenId, params); diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol index 77329259..50ce71e6 100644 --- a/test/utils/Planner.sol +++ b/test/utils/Planner.sol @@ -34,13 +34,13 @@ library Planner { return Plan({actions: actions, params: params}); } - function finalize(Plan memory plan, PoolKey memory poolKey) internal pure returns (Plan memory) { + function finalize(Plan memory plan, PoolKey memory poolKey) internal pure returns (bytes memory) { plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency0)); plan = plan.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency1)); - return plan; + return plan.encode(); } - function zip(Plan memory plan) internal pure returns (bytes memory) { + function encode(Plan memory plan) internal pure returns (bytes memory) { return abi.encode(plan.actions, plan.params); } } From bc1ee23fd15ec29b24866133edca15fc0ac683c5 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 12:03:49 -0400 Subject: [PATCH 74/98] fix gas --- ...rc20.snap => PositionManager_collect.snap} | 0 ...=> PositionManager_collect_sameRange.snap} | 0 ...=> PositionManager_decreaseLiquidity.snap} | 0 ...ager_decrease_sameRange_allLiquidity.snap} | 0 ...itionManager_increaseLiquidity_erc20.snap} | 0 ...rease_autocompoundExactUnclaimedFees.snap} | 0 ...ncrease_autocompoundExcessFeesCredit.snap} | 0 .../{mint.snap => PositionManager_mint.snap} | 0 ...PositionManager_mint_onSameTickLower.snap} | 0 ...PositionManager_mint_onSameTickUpper.snap} | 0 ...ap => PositionManager_mint_sameRange.snap} | 0 ...nager_mint_warmedPool_differentRange.snap} | 0 ...ionManager_multicall_initialize_mint.snap} | 0 ...ermit.snap => PositionManager_permit.snap} | 0 ...ositionManager_permit_secondPosition.snap} | 0 ...snap => PositionManager_permit_twice.snap} | 0 ...exactUnclaimedFees_exactCustodiedFees.snap | 1 - test/position-managers/Gas.t.sol | 134 ++++-------------- 18 files changed, 26 insertions(+), 109 deletions(-) rename .forge-snapshots/{collect_erc20.snap => PositionManager_collect.snap} (100%) rename .forge-snapshots/{sameRange_collect.snap => PositionManager_collect_sameRange.snap} (100%) rename .forge-snapshots/{decreaseLiquidity_erc20.snap => PositionManager_decreaseLiquidity.snap} (100%) rename .forge-snapshots/{sameRange_decreaseAllLiquidity.snap => PositionManager_decrease_sameRange_allLiquidity.snap} (100%) rename .forge-snapshots/{increaseLiquidity_erc20.snap => PositionManager_increaseLiquidity_erc20.snap} (100%) rename .forge-snapshots/{autocompound_exactUnclaimedFees.snap => PositionManager_increase_autocompoundExactUnclaimedFees.snap} (100%) rename .forge-snapshots/{autocompound_excessFeesCredit.snap => PositionManager_increase_autocompoundExcessFeesCredit.snap} (100%) rename .forge-snapshots/{mint.snap => PositionManager_mint.snap} (100%) rename .forge-snapshots/{mint_same_tickLower.snap => PositionManager_mint_onSameTickLower.snap} (100%) rename .forge-snapshots/{mint_same_tickUpper.snap => PositionManager_mint_onSameTickUpper.snap} (100%) rename .forge-snapshots/{sameRange_mint.snap => PositionManager_mint_sameRange.snap} (100%) rename .forge-snapshots/{mint_differentRanges.snap => PositionManager_mint_warmedPool_differentRange.snap} (100%) rename .forge-snapshots/{multicall_initialize_mint.snap => PositionManager_multicall_initialize_mint.snap} (100%) rename .forge-snapshots/{permit.snap => PositionManager_permit.snap} (100%) rename .forge-snapshots/{permit_secondPosition.snap => PositionManager_permit_secondPosition.snap} (100%) rename .forge-snapshots/{permit_twice.snap => PositionManager_permit_twice.snap} (100%) delete mode 100644 .forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap diff --git a/.forge-snapshots/collect_erc20.snap b/.forge-snapshots/PositionManager_collect.snap similarity index 100% rename from .forge-snapshots/collect_erc20.snap rename to .forge-snapshots/PositionManager_collect.snap diff --git a/.forge-snapshots/sameRange_collect.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap similarity index 100% rename from .forge-snapshots/sameRange_collect.snap rename to .forge-snapshots/PositionManager_collect_sameRange.snap diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap similarity index 100% rename from .forge-snapshots/decreaseLiquidity_erc20.snap rename to .forge-snapshots/PositionManager_decreaseLiquidity.snap diff --git a/.forge-snapshots/sameRange_decreaseAllLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap similarity index 100% rename from .forge-snapshots/sameRange_decreaseAllLiquidity.snap rename to .forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap similarity index 100% rename from .forge-snapshots/increaseLiquidity_erc20.snap rename to .forge-snapshots/PositionManager_increaseLiquidity_erc20.snap diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap similarity index 100% rename from .forge-snapshots/autocompound_exactUnclaimedFees.snap rename to .forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap similarity index 100% rename from .forge-snapshots/autocompound_excessFeesCredit.snap rename to .forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/PositionManager_mint.snap similarity index 100% rename from .forge-snapshots/mint.snap rename to .forge-snapshots/PositionManager_mint.snap diff --git a/.forge-snapshots/mint_same_tickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap similarity index 100% rename from .forge-snapshots/mint_same_tickLower.snap rename to .forge-snapshots/PositionManager_mint_onSameTickLower.snap diff --git a/.forge-snapshots/mint_same_tickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap similarity index 100% rename from .forge-snapshots/mint_same_tickUpper.snap rename to .forge-snapshots/PositionManager_mint_onSameTickUpper.snap diff --git a/.forge-snapshots/sameRange_mint.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap similarity index 100% rename from .forge-snapshots/sameRange_mint.snap rename to .forge-snapshots/PositionManager_mint_sameRange.snap diff --git a/.forge-snapshots/mint_differentRanges.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap similarity index 100% rename from .forge-snapshots/mint_differentRanges.snap rename to .forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap diff --git a/.forge-snapshots/multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap similarity index 100% rename from .forge-snapshots/multicall_initialize_mint.snap rename to .forge-snapshots/PositionManager_multicall_initialize_mint.snap diff --git a/.forge-snapshots/permit.snap b/.forge-snapshots/PositionManager_permit.snap similarity index 100% rename from .forge-snapshots/permit.snap rename to .forge-snapshots/PositionManager_permit.snap diff --git a/.forge-snapshots/permit_secondPosition.snap b/.forge-snapshots/PositionManager_permit_secondPosition.snap similarity index 100% rename from .forge-snapshots/permit_secondPosition.snap rename to .forge-snapshots/PositionManager_permit_secondPosition.snap diff --git a/.forge-snapshots/permit_twice.snap b/.forge-snapshots/PositionManager_permit_twice.snap similarity index 100% rename from .forge-snapshots/permit_twice.snap rename to .forge-snapshots/PositionManager_permit_twice.snap diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap deleted file mode 100644 index 3b59a386..00000000 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ /dev/null @@ -1 +0,0 @@ -185267 \ No newline at end of file diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 334a1740..dfbaaba9 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -91,7 +91,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { Planner.init().add(Actions.MINT, abi.encode(range, 10_000 ether, address(this), ZERO_BYTES)); bytes memory calls = planner.finalize(range.poolKey); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("mint"); + snapLastCall("PositionManager_mint"); } function test_gas_mint_differentRanges() public { @@ -106,7 +106,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("mint_differentRanges"); + snapLastCall("PositionManager_mint_warmedPool_differentRange"); } function test_gas_mint_sameTickLower() public { @@ -121,7 +121,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("mint_same_tickLower"); + snapLastCall("PositionManager_mint_onSameTickLower"); } function test_gas_mint_sameTickUpper() public { @@ -136,7 +136,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("mint_same_tickUpper"); + snapLastCall("PositionManager_mint_onSameTickUpper"); } function test_gas_increaseLiquidity_erc20() public { @@ -148,24 +148,9 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("increaseLiquidity_erc20"); + snapLastCall("PositionManager_increaseLiquidity_erc20"); } - // TODO: ERC6909 Support. - // function test_gas_increaseLiquidity_erc6909() public { - // mint(range, 10_000 ether, address(this), ZERO_BYTES); - // uint256 tokenId = lpm.nextTokenId() - 1; - - // Planner.Plan memory planner = - // Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - - // planner = planner.finalize(range.poolKey); - - // bytes memory actions = planner.zip(); - // lpm.modifyLiquidities(actions, _deadline); - // snapLastCall("increaseLiquidity_erc6909"); - // } - function test_gas_autocompound_exactUnclaimedFees() public { // Alice and Bob provide liquidity on the range // Alice uses her exact fees to increase liquidity (compounding) @@ -204,65 +189,10 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("autocompound_exactUnclaimedFees"); + snapLastCall("PositionManager_increase_autocompoundExactUnclaimedFees"); } - function test_gas_autocompound_exactUnclaimedFees_exactCustodiedFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - mint(range, liquidityBob, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // donate to create fees - uint256 amountDonate = 20e18; - donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); - uint256 tokensOwedAlice = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; - - // bob collects fees so some of alice's fees are now cached - - Planner.Plan memory planner = Planner.init().add(Actions.DECREASE, abi.encode(tokenIdBob, 0, ZERO_BYTES)); - - bytes memory calls = planner.finalize(range.poolKey); - vm.prank(bob); - lpm.modifyLiquidities(calls, _deadline); - - // donate to create more fees - donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); - - tokensOwedAlice = tokensOwedAlice + amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; - - // alice will use ALL of her fees to increase liquidity - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - tokensOwedAlice, - tokensOwedAlice - ); - - planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES)); - - bytes memory calls = planner.finalize(range.poolKey); - - vm.prank(alice); - lpm.modifyLiquidities(calls, _deadline); - snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); - } - } - - // autocompounding but the excess fees are credited to tokensOwed + // Autocompounding but the excess fees are taken to the user function test_gas_autocompound_excessFeesCredit() public { // Alice and Bob provide liquidity on the range // Alice uses her fees to increase liquidity. Excess fees are accounted to alice @@ -301,10 +231,10 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("autocompound_excessFeesCredit"); + snapLastCall("PositionManager_increase_autocompoundExcessFeesCredit"); } - function test_gas_decreaseLiquidity_erc20() public { + function test_gas_decreaseLiquidity() public { mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; @@ -313,28 +243,9 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("decreaseLiquidity_erc20"); + snapLastCall("PositionManager_decreaseLiquidity"); } - // TODO: ERC6909 Support - // function test_gas_decreaseLiquidity_erc6909() public { - // mint(range, 10_000 ether, address(this), ZERO_BYTES); - // uint256 tokenId = lpm.nextTokenId() - 1; - - // Planner.Plan memory planner = - // Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES)); - - // planner = planner.finalize(range.poolKey); - - // bytes memory actions = planner.zip(); - // lpm.modifyLiquidities(actions, _deadline); - // snapLastCall("decreaseLiquidity_erc6909"); - // } - - function test_gas_burn() public {} - function test_gas_burnEmpty() public {} - function test_gas_collect() public {} - function test_gas_multicall_initialize_mint() public { key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); @@ -358,7 +269,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, actions, _deadline); lpm.multicall(calls); - snapLastCall("multicall_initialize_mint"); + snapLastCall("PositionManager_multicall_initialize_mint"); } function test_gas_permit() public { @@ -376,7 +287,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { vm.prank(alice); lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - snapLastCall("permit"); + snapLastCall("PositionManager_permit"); } function test_gas_permit_secondPosition() public { @@ -406,7 +317,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { vm.prank(alice); lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - snapLastCall("permit_secondPosition"); + snapLastCall("PositionManager_permit_secondPosition"); } function test_gas_permit_twice() public { @@ -433,10 +344,10 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { vm.prank(alice); lpm.permit(charlie, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - snapLastCall("permit_twice"); + snapLastCall("PositionManager_permit_twice"); } - function test_gas_collect_erc20() public { + function test_gas_collect() public { mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; @@ -448,7 +359,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("collect_erc20"); + snapLastCall("PositionManager_collect"); } // same-range gas tests @@ -460,7 +371,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("sameRange_mint"); + snapLastCall("PositionManager_mint_sameRange"); } function test_gas_sameRange_decrease() public { @@ -477,7 +388,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("sameRange_decreaseAllLiquidity"); + snapLastCall("PositionManager_decrease_sameRange_allLiquidity"); } function test_gas_sameRange_collect() public { @@ -496,6 +407,13 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { bytes memory calls = planner.finalize(range.poolKey); lpm.modifyLiquidities(calls, _deadline); - snapLastCall("sameRange_collect"); + snapLastCall("PositionManager_collect_sameRange"); } + + // TODO: ERC6909 Support. + function test_gas_increaseLiquidity_erc6909() public {} + function test_gas_decreaseLiquidity_erc6909() public {} + + function test_gas_burn() public {} + function test_gas_burnEmpty() public {} } From 026c1b09eb175cf00a52ec845cb015f1298b6a84 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 12:20:26 -0400 Subject: [PATCH 75/98] remove permit --- .forge-snapshots/PositionManager_collect.snap | 2 +- .../PositionManager_collect_sameRange.snap | 2 +- .../PositionManager_decreaseLiquidity.snap | 2 +- ...nager_decrease_sameRange_allLiquidity.snap | 2 +- ...sitionManager_increaseLiquidity_erc20.snap | 2 +- ...crease_autocompoundExactUnclaimedFees.snap | 2 +- ...increase_autocompoundExcessFeesCredit.snap | 2 +- .forge-snapshots/PositionManager_mint.snap | 2 +- .../PositionManager_mint_onSameTickLower.snap | 2 +- .../PositionManager_mint_onSameTickUpper.snap | 2 +- .../PositionManager_mint_sameRange.snap | 2 +- ...anager_mint_warmedPool_differentRange.snap | 2 +- ...tionManager_multicall_initialize_mint.snap | 2 +- .forge-snapshots/PositionManager_permit.snap | 1 - ...PositionManager_permit_secondPosition.snap | 1 - .../PositionManager_permit_twice.snap | 1 - src/base/ERC721Permit.sol | 108 +------ test/position-managers/Gas.t.sol | 75 ----- test/position-managers/Permit.t.sol | 283 ------------------ test/shared/LiquidityOperations.sol | 10 - 20 files changed, 20 insertions(+), 485 deletions(-) delete mode 100644 .forge-snapshots/PositionManager_permit.snap delete mode 100644 .forge-snapshots/PositionManager_permit_secondPosition.snap delete mode 100644 .forge-snapshots/PositionManager_permit_twice.snap delete mode 100644 test/position-managers/Permit.t.sol diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap index 91ea58d8..18ede557 100644 --- a/.forge-snapshots/PositionManager_collect.snap +++ b/.forge-snapshots/PositionManager_collect.snap @@ -1 +1 @@ -162398 \ No newline at end of file +162376 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index 91ea58d8..18ede557 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -162398 \ No newline at end of file +162376 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap index 9e97b505..7b04c2fe 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -1 +1 @@ -127776 \ No newline at end of file +127754 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap index 28446f0c..ca26bdc3 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -140657 \ No newline at end of file +140635 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap index d5f6bc6f..e2b8005b 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap @@ -1 +1 @@ -157242 \ No newline at end of file +157220 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index f35bf0f5..811326c5 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -170568 \ No newline at end of file +170546 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index 7b170ca9..26a05e3d 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -184968 \ No newline at end of file +184946 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap index 6521db2a..8816d2df 100644 --- a/.forge-snapshots/PositionManager_mint.snap +++ b/.forge-snapshots/PositionManager_mint.snap @@ -1 +1 @@ -384052 \ No newline at end of file +384030 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 3cc3aa84..0e907caf 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -343834 \ No newline at end of file +343812 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index 8c54f121..cadb957f 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -344476 \ No newline at end of file +344454 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index 3e347363..a443a79a 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -287158 \ No newline at end of file +287136 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index 1e5ce948..675e6672 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -349852 \ No newline at end of file +349830 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index 865faa50..0a085b82 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -427971 \ No newline at end of file +427905 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_permit.snap b/.forge-snapshots/PositionManager_permit.snap deleted file mode 100644 index 0bd11bf5..00000000 --- a/.forge-snapshots/PositionManager_permit.snap +++ /dev/null @@ -1 +0,0 @@ -76881 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_permit_secondPosition.snap b/.forge-snapshots/PositionManager_permit_secondPosition.snap deleted file mode 100644 index 78a29c3e..00000000 --- a/.forge-snapshots/PositionManager_permit_secondPosition.snap +++ /dev/null @@ -1 +0,0 @@ -59781 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_permit_twice.snap b/.forge-snapshots/PositionManager_permit_twice.snap deleted file mode 100644 index 78fdba19..00000000 --- a/.forge-snapshots/PositionManager_permit_twice.snap +++ /dev/null @@ -1 +0,0 @@ -42681 \ No newline at end of file diff --git a/src/base/ERC721Permit.sol b/src/base/ERC721Permit.sol index 27719643..f163bf2e 100644 --- a/src/base/ERC721Permit.sol +++ b/src/base/ERC721Permit.sol @@ -2,113 +2,19 @@ pragma solidity ^0.8.24; import {ERC721} from "solmate/tokens/ERC721.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import {ChainId} from "../libraries/ChainId.sol"; -import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; - -/// @title ERC721 with permit -/// @notice Nonfungible tokens that support an approve via signature, i.e. permit -abstract contract ERC721Permit is ERC721, IERC721Permit { - mapping(address owner => mapping(uint256 word => uint256 bitmap)) public nonces; - - /// @dev The hash of the name used in the permit signature verification - bytes32 private immutable nameHash; - - /// @dev The hash of the version string used in the permit signature verification - bytes32 private immutable versionHash; - - /// @notice Computes the nameHash and versionHash - constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) { - nameHash = keccak256(bytes(name_)); - versionHash = keccak256(bytes(version_)); - } - - // TODO: implement here, or in posm - function tokenURI(uint256) public pure override returns (string memory) { - return "https://example.com"; - } - - /// @inheritdoc IERC721Permit - function DOMAIN_SEPARATOR() public view override returns (bytes32) { - return keccak256( - abi.encode( - // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') - 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, - nameHash, - versionHash, - ChainId.get(), - address(this) - ) - ); - } - - /// @inheritdoc IERC721Permit - /// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); - bytes32 public constant override PERMIT_TYPEHASH = - 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad; - - /// @inheritdoc IERC721Permit - function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, uint8 v, bytes32 r, bytes32 s) - external - payable - override - { - require(block.timestamp <= deadline, "Permit expired"); - - address owner = ownerOf(tokenId); - require(spender != owner, "ERC721Permit: approval to current owner"); - - bytes32 digest = getDigest(spender, tokenId, nonce, deadline); - - if (Address.isContract(owner)) { - require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, "Unauthorized"); - } else { - address recoveredAddress = ecrecover(digest, v, r, s); - require(recoveredAddress != address(0), "Invalid signature"); - require(recoveredAddress == owner, "Unauthorized"); - } - - _useUnorderedNonce(owner, nonce); - approve(spender, tokenId); - } - - function getDigest(address spender, uint256 tokenId, uint256 _nonce, uint256 deadline) - public - view - returns (bytes32 digest) - { - digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _nonce, deadline)) - ) - ); - } +/// @notice An ERC721 contract that supports permit. +/// TODO: Support permit. +contract ERC721Permit is ERC721 { + constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) {} function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { return spender == ownerOf(tokenId) || getApproved[tokenId] == spender || isApprovedForAll[ownerOf(tokenId)][spender]; } - /// @notice Returns the index of the bitmap and the bit position within the bitmap. Used for unordered nonces - /// @param nonce The nonce to get the associated word and bit positions - /// @return wordPos The word position or index into the nonceBitmap - /// @return bitPos The bit position - /// @dev The first 248 bits of the nonce value is the index of the desired bitmap - /// @dev The last 8 bits of the nonce value is the position of the bit in the bitmap - function bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) { - wordPos = uint248(nonce >> 8); - bitPos = uint8(nonce); - } - - function _useUnorderedNonce(address from, uint256 nonce) internal { - (uint256 wordPos, uint256 bitPos) = bitmapPositions(nonce); - uint256 bit = 1 << bitPos; - uint256 flipped = nonces[from][wordPos] ^= bit; - - if (flipped & bit == 0) revert NonceAlreadyUsed(); + // TODO: Use PositionDescriptor. + function tokenURI(uint256 id) public view override returns (string memory) { + return ""; } } diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index dfbaaba9..f6599782 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -272,81 +272,6 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { snapLastCall("PositionManager_multicall_initialize_mint"); } - function test_gas_permit() public { - // alice permits for the first time - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // alice gives operator permission to bob - uint256 nonce = 1; - bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); - - vm.prank(alice); - lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - snapLastCall("PositionManager_permit"); - } - - function test_gas_permit_secondPosition() public { - // alice permits for her two tokens, benchmark the 2nd permit - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // alice gives operator permission to bob - uint256 nonce = 1; - bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); - - vm.prank(alice); - lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - - // alice creates another position - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - tokenIdAlice = lpm.nextTokenId() - 1; - - // alice gives operator permission to bob - nonce = 2; - digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); - (v, r, s) = vm.sign(alicePK, digest); - - vm.prank(alice); - lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - snapLastCall("PositionManager_permit_secondPosition"); - } - - function test_gas_permit_twice() public { - // alice permits the same token, twice - address charlie = makeAddr("CHARLIE"); - - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // alice gives operator permission to bob - uint256 nonce = 1; - bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); - - vm.prank(alice); - lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - - // alice gives operator permission to charlie - nonce = 2; - digest = lpm.getDigest(charlie, tokenIdAlice, nonce, block.timestamp + 1); - (v, r, s) = vm.sign(alicePK, digest); - - vm.prank(alice); - lpm.permit(charlie, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - snapLastCall("PositionManager_permit_twice"); - } - function test_gas_collect() public { mint(range, 10_000 ether, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; diff --git a/test/position-managers/Permit.t.sol b/test/position-managers/Permit.t.sol deleted file mode 100644 index 2cef66e2..00000000 --- a/test/position-managers/Permit.t.sol +++ /dev/null @@ -1,283 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {IERC721Permit} from "../../src/interfaces/IERC721Permit.sol"; - -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; -import {INonfungiblePositionManager} from "../../src/interfaces/INonfungiblePositionManager.sol"; - -import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; - -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; - -contract PermitTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { - using FixedPointMathLib for uint256; - using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - PoolId poolId; - address alice; - uint256 alicePK; - address bob; - uint256 bobPK; - - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) - uint256 FEE_WAD; - - LiquidityRange range; - - function setUp() public { - (alice, alicePK) = makeAddrAndKey("ALICE"); - (bob, bobPK) = makeAddrAndKey("BOB"); - - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); - - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - lpm = new NonfungiblePositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - - // define a reusable range - range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); - } - - function test_permit_increaseLiquidity() public { - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // alice gives bob operator permissions - _permit(alice, alicePK, tokenIdAlice, bob, 1); - - // bob can increase liquidity on alice's token - uint256 newLiquidity = 2e18; - uint256 balance0BobBefore = currency0.balanceOf(bob); - uint256 balance1BobBefore = currency1.balanceOf(bob); - vm.startPrank(bob); - increaseLiquidity(tokenIdAlice, newLiquidity, ZERO_BYTES); - vm.stopPrank(); - - // alice's position has new liquidity - bytes32 positionId = - keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenIdAlice))); - (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); - assertEq(liquidity, liquidityAlice + newLiquidity); - - // bob used his tokens to increase liquidity - assertGt(balance0BobBefore, currency0.balanceOf(bob)); - assertGt(balance1BobBefore, currency1.balanceOf(bob)); - } - - function test_permit_decreaseLiquidity() public { - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // alice gives bob operator permissions - _permit(alice, alicePK, tokenIdAlice, bob, 1); - - // bob can decrease liquidity on alice's token - uint256 liquidityToRemove = 0.4444e18; - vm.startPrank(bob); - decreaseLiquidity(tokenIdAlice, liquidityToRemove, ZERO_BYTES); - vm.stopPrank(); - - // alice's position decreased liquidity - bytes32 positionId = - keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenIdAlice))); - (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); - - assertEq(liquidity, liquidityAlice - liquidityToRemove); - } - - function test_permit_collect() public { - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // donate to create fee revenue - uint256 currency0Revenue = 0.4444e18; - uint256 currency1Revenue = 0.2222e18; - donateRouter.donate(key, currency0Revenue, currency1Revenue, ZERO_BYTES); - - // alice gives bob operator permissions - _permit(alice, alicePK, tokenIdAlice, bob, 1); - - // TODO: test collection to recipient with a permissioned operator - - // bob collects fees to himself - address recipient = bob; - uint256 balance0BobBefore = currency0.balanceOf(bob); - uint256 balance1BobBefore = currency1.balanceOf(bob); - vm.startPrank(bob); - collect(tokenIdAlice, recipient, ZERO_BYTES); - vm.stopPrank(); - - assertApproxEqAbs(currency0.balanceOf(recipient), balance0BobBefore + currency0Revenue, 1 wei); - assertApproxEqAbs(currency1.balanceOf(recipient), balance1BobBefore + currency1Revenue, 1 wei); - } - - // --- Fail Scenarios --- // - function test_permit_notOwnerRevert() public { - // calling permit on a token that is not owned will fail - - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob cannot permit himself on alice's token - bytes32 digest = lpm.getDigest(bob, tokenIdAlice, 0, block.timestamp + 1); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPK, digest); - - vm.startPrank(bob); - vm.expectRevert("Unauthorized"); - lpm.permit(bob, tokenIdAlice, block.timestamp + 1, 0, v, r, s); - vm.stopPrank(); - } - - function test_noPermit_increaseLiquidityRevert() public { - // increaseLiquidity fails if the owner did not permit - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob cannot increase liquidity on alice's token - uint256 newLiquidity = 2e18; - bytes memory increase = LiquidityOperations.getIncreaseEncoded(tokenIdAlice, newLiquidity, ZERO_BYTES); - vm.startPrank(bob); - vm.expectRevert(abi.encodeWithSelector(INonfungiblePositionManager.NotApproved.selector, address(bob))); - lpm.modifyLiquidities(increase, _deadline); - vm.stopPrank(); - } - - function test_noPermit_decreaseLiquidityRevert() public { - // decreaseLiquidity fails if the owner did not permit - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob cannot decrease liquidity on alice's token - uint256 liquidityToRemove = 0.4444e18; - bytes memory decrease = LiquidityOperations.getDecreaseEncoded(tokenIdAlice, liquidityToRemove, ZERO_BYTES); - vm.startPrank(bob); - vm.expectRevert(abi.encodeWithSelector(INonfungiblePositionManager.NotApproved.selector, address(bob))); - lpm.modifyLiquidities(decrease, _deadline); - vm.stopPrank(); - } - - function test_noPermit_collectRevert() public { - // collect fails if the owner did not permit - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // donate to create fee revenue - uint256 currency0Revenue = 0.4444e18; - uint256 currency1Revenue = 0.2222e18; - donateRouter.donate(key, currency0Revenue, currency1Revenue, ZERO_BYTES); - - // bob cannot collect fees to a recipient - address recipient = address(0x00444400); - bytes memory collect = LiquidityOperations.getCollectEncoded(tokenIdAlice, recipient, ZERO_BYTES); - vm.startPrank(bob); - vm.expectRevert(abi.encodeWithSelector(INonfungiblePositionManager.NotApproved.selector, address(bob))); - lpm.modifyLiquidities(collect, block.timestamp + 1); - vm.stopPrank(); - } - - function test_permit_nonceAlreadyUsed() public { - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // alice gives bob operator permissions - uint256 nonce = 1; - _permit(alice, alicePK, tokenIdAlice, bob, nonce); - - // alice cannot reuse the nonce - bytes32 digest = lpm.getDigest(bob, tokenIdAlice, nonce, block.timestamp + 1); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); - - vm.startPrank(alice); - vm.expectRevert(IERC721Permit.NonceAlreadyUsed.selector); - lpm.permit(bob, tokenIdAlice, block.timestamp + 1, nonce, v, r, s); - vm.stopPrank(); - } - - function test_permit_nonceAlreadyUsed_twoPositions() public { - uint256 liquidityAlice = 1e18; - vm.prank(alice); - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - vm.prank(alice); - range.tickLower = -600; - range.tickUpper = 600; - mint(range, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice2 = lpm.nextTokenId() - 1; - - // alice gives bob operator permissions for first token - uint256 nonce = 1; - _permit(alice, alicePK, tokenIdAlice, bob, nonce); - - // alice cannot reuse the nonce for the second token - bytes32 digest = lpm.getDigest(bob, tokenIdAlice2, nonce, block.timestamp + 1); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest); - - vm.startPrank(alice); - vm.expectRevert(IERC721Permit.NonceAlreadyUsed.selector); - lpm.permit(bob, tokenIdAlice2, block.timestamp + 1, nonce, v, r, s); - vm.stopPrank(); - } -} diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 3e79f17b..7d525b69 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -62,16 +62,6 @@ abstract contract LiquidityOperations is CommonBase { lpm.modifyLiquidities(actions, _deadline); } - // TODO: organize somewhere else, or rename this file to NFTLiquidityHelpers? - function _permit(address signer, uint256 privateKey, uint256 tokenId, address operator, uint256 nonce) internal { - bytes32 digest = lpm.getDigest(operator, tokenId, 1, block.timestamp + 1); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - - vm.prank(signer); - lpm.permit(operator, tokenId, block.timestamp + 1, nonce, v, r, s); - } - // Helper functions for getting encoded calldata for .modifyLiquidities function getIncreaseEncoded(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData) internal From 4470dba13a3854e621c3583c7163301683c2f3c0 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 12:27:52 -0400 Subject: [PATCH 76/98] fix compiler warnings --- src/NonfungiblePositionManager.sol | 2 +- src/base/ERC721Permit.sol | 4 ++-- test/position-managers/FeeCollection.t.sol | 6 +++--- test/position-managers/IncreaseLiquidity.t.sol | 2 +- test/shared/LiquidityOperations.sol | 10 +++------- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/NonfungiblePositionManager.sol b/src/NonfungiblePositionManager.sol index fb11473a..fd8b28be 100644 --- a/src/NonfungiblePositionManager.sol +++ b/src/NonfungiblePositionManager.sol @@ -191,7 +191,7 @@ contract NonfungiblePositionManager is ); } - function _validateBurn(uint256 tokenId) internal { + function _validateBurn(uint256 tokenId) internal view { bytes32 positionId = getPositionIdFromTokenId(tokenId); uint128 liquidity = manager.getPositionLiquidity(tokenRange[tokenId].poolKey.toId(), positionId); if (liquidity > 0) revert PositionMustBeEmpty(); diff --git a/src/base/ERC721Permit.sol b/src/base/ERC721Permit.sol index f163bf2e..197b69c6 100644 --- a/src/base/ERC721Permit.sol +++ b/src/base/ERC721Permit.sol @@ -14,7 +14,7 @@ contract ERC721Permit is ERC721 { } // TODO: Use PositionDescriptor. - function tokenURI(uint256 id) public view override returns (string memory) { - return ""; + function tokenURI(uint256 id) public pure override returns (string memory) { + return string(abi.encode(id)); } } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 797ea8e2..b091f2b8 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -102,7 +102,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // collect fees uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = collect(tokenId, address(this), ZERO_BYTES); + BalanceDelta delta = collect(tokenId, ZERO_BYTES); // express key.fee as wad (i.e. 3000 = 0.003e18) assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); @@ -187,7 +187,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance0AliceBefore = currency0.balanceOf(alice); uint256 balance1AliceBefore = currency1.balanceOf(alice); vm.startPrank(alice); - BalanceDelta delta = collect(tokenIdAlice, alice, ZERO_BYTES); + BalanceDelta delta = collect(tokenIdAlice, ZERO_BYTES); vm.stopPrank(); uint256 balance0AliceAfter = currency0.balanceOf(alice); uint256 balance1AliceAfter = currency1.balanceOf(alice); @@ -200,7 +200,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance0BobBefore = currency0.balanceOf(bob); uint256 balance1BobBefore = currency1.balanceOf(bob); vm.startPrank(bob); - delta = collect(tokenIdBob, bob, ZERO_BYTES); + delta = collect(tokenIdBob, ZERO_BYTES); vm.stopPrank(); uint256 balance0BobAfter = currency0.balanceOf(bob); uint256 balance1BobAfter = currency1.balanceOf(bob); diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 386a1348..4c6c9578 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -316,7 +316,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi uint256 balance0BeforeBob = currency0.balanceOf(bob); uint256 balance1BeforeBob = currency1.balanceOf(bob); vm.startPrank(bob); - collect(tokenIdBob, bob, ZERO_BYTES); + collect(tokenIdBob, ZERO_BYTES); vm.stopPrank(); uint256 balance0AfterBob = currency0.balanceOf(bob); uint256 balance1AfterBob = currency1.balanceOf(bob); diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 7d525b69..fc0cc60a 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -48,8 +48,8 @@ abstract contract LiquidityOperations is CommonBase { return abi.decode(result[0], (BalanceDelta)); } - function collect(uint256 tokenId, address recipient, bytes memory hookData) internal returns (BalanceDelta) { - bytes memory calls = getCollectEncoded(tokenId, recipient, hookData); + function collect(uint256 tokenId, bytes memory hookData) internal returns (BalanceDelta) { + bytes memory calls = getCollectEncoded(tokenId, hookData); bytes[] memory result = lpm.modifyLiquidities(calls, _deadline); return abi.decode(result[0], (BalanceDelta)); } @@ -85,11 +85,7 @@ abstract contract LiquidityOperations is CommonBase { return planner.finalize(key); } - function getCollectEncoded(uint256 tokenId, address recipient, bytes memory hookData) - internal - view - returns (bytes memory) - { + function getCollectEncoded(uint256 tokenId, bytes memory hookData) internal view returns (bytes memory) { (PoolKey memory poolKey,,) = lpm.tokenRange(tokenId); Planner.Plan memory planner = Planner.init(); planner = planner.add(Actions.DECREASE, abi.encode(tokenId, 0, hookData)); From af62834c87037c0ac54e4ff5a5f620549de5f137 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 12:35:12 -0400 Subject: [PATCH 77/98] rename to PositionManager --- ...blePositionManager.sol => PositionManager.sol} | 10 ++-------- ...lePositionManager.sol => IPositionManager.sol} | 2 +- test/position-managers/Execute.t.sol | 6 +++--- test/position-managers/FeeCollection.t.sol | 4 ++-- test/position-managers/Gas.t.sol | 15 ++++++--------- test/position-managers/IncreaseLiquidity.t.sol | 12 ++++++------ .../PositionManager.multicall.t.sol | 13 +++++-------- ...ositionManager.t.sol => PositionManager.t.sol} | 12 ++++++------ test/shared/FeeMath.sol | 6 +++--- test/shared/LiquidityOperations.sol | 4 ++-- test/shared/fuzz/LiquidityFuzzers.sol | 4 ++-- test/utils/Planner.sol | 2 +- 12 files changed, 39 insertions(+), 51 deletions(-) rename src/{NonfungiblePositionManager.sol => PositionManager.sol} (97%) rename src/interfaces/{INonfungiblePositionManager.sol => IPositionManager.sol} (96%) rename test/position-managers/{NonfungiblePositionManager.t.sol => PositionManager.t.sol} (96%) diff --git a/src/NonfungiblePositionManager.sol b/src/PositionManager.sol similarity index 97% rename from src/NonfungiblePositionManager.sol rename to src/PositionManager.sol index fd8b28be..3dcbc15e 100644 --- a/src/NonfungiblePositionManager.sol +++ b/src/PositionManager.sol @@ -11,7 +11,7 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {ERC721Permit} from "./base/ERC721Permit.sol"; -import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; +import {IPositionManager, Actions} from "./interfaces/IPositionManager.sol"; import {SafeCallback} from "./base/SafeCallback.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; import {Multicall} from "./base/Multicall.sol"; @@ -19,13 +19,7 @@ import {PoolInitializer} from "./base/PoolInitializer.sol"; import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; -contract NonfungiblePositionManager is - INonfungiblePositionManager, - ERC721Permit, - PoolInitializer, - Multicall, - SafeCallback -{ +contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Multicall, SafeCallback { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; diff --git a/src/interfaces/INonfungiblePositionManager.sol b/src/interfaces/IPositionManager.sol similarity index 96% rename from src/interfaces/INonfungiblePositionManager.sol rename to src/interfaces/IPositionManager.sol index 737a382b..6c55b974 100644 --- a/src/interfaces/INonfungiblePositionManager.sol +++ b/src/interfaces/IPositionManager.sol @@ -12,7 +12,7 @@ enum Actions { CLOSE_CURRENCY } -interface INonfungiblePositionManager { +interface IPositionManager { error MismatchedLengths(); error NotApproved(address caller); error DeadlinePassed(); diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 6c54d7ea..4bee15dd 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -21,8 +21,8 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -57,7 +57,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - lpm = new NonfungiblePositionManager(manager); + lpm = new PositionManager(manager); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index b091f2b8..8b8c255e 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -19,7 +19,7 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -49,7 +49,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - lpm = new NonfungiblePositionManager(manager); + lpm = new PositionManager(manager); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index f6599782..51c72c1e 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -21,8 +21,8 @@ import {FeeMath} from "../shared/FeeMath.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; @@ -34,7 +34,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using Planner for Planner.Plan; - using FeeMath for INonfungiblePositionManager; + using FeeMath for IPositionManager; PoolId poolId; address alice; @@ -59,7 +59,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - lpm = new NonfungiblePositionManager(manager); + lpm = new PositionManager(manager); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); @@ -251,9 +251,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Use multicall to initialize a pool and mint liquidity bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector( - NonfungiblePositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES - ); + calls[0] = abi.encodeWithSelector(PositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); range = LiquidityRange({ poolKey: key, @@ -265,8 +263,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); bytes memory actions = planner.finalize(range.poolKey); - calls[1] = - abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, actions, _deadline); + calls[1] = abi.encodeWithSelector(PositionManager(lpm).modifyLiquidities.selector, actions, _deadline); lpm.multicall(calls); snapLastCall("PositionManager_multicall_initialize_mint"); diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 4c6c9578..02d3bad0 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -21,9 +21,9 @@ import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; -import {Actions, INonfungiblePositionManager} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {Actions, IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; import {FeeMath} from "../shared/FeeMath.sol"; @@ -34,7 +34,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using Planner for Planner.Plan; - using FeeMath for INonfungiblePositionManager; + using FeeMath for IPositionManager; PoolId poolId; address alice = makeAddr("ALICE"); @@ -54,7 +54,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - lpm = new NonfungiblePositionManager(manager); + lpm = new PositionManager(manager); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); @@ -99,7 +99,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // alice uses her exact fees to increase liquidity // Slight error in this calculation vs. actual fees.. TODO: Fix this. - BalanceDelta feesOwedAlice = INonfungiblePositionManager(lpm).getFeesOwed(manager, tokenIdAlice); + BalanceDelta feesOwedAlice = IPositionManager(lpm).getFeesOwed(manager, tokenIdAlice); // Note: You can alternatively calculate Alice's fees owed from the swap amount, fee on the pool, and total liquidity in that range. // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob); @@ -286,7 +286,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back // alice will use all of her fees + additional capital to increase liquidity - BalanceDelta feesOwed = INonfungiblePositionManager(lpm).getFeesOwed(manager, tokenIdAlice); + BalanceDelta feesOwed = IPositionManager(lpm).getFeesOwed(manager, tokenIdAlice); { (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 45e4b6d7..ca0c0c37 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -21,8 +21,8 @@ import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -45,7 +45,7 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - lpm = new NonfungiblePositionManager(manager); + lpm = new PositionManager(manager); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); @@ -56,9 +56,7 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid // Use multicall to initialize a pool and mint liquidity bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector( - NonfungiblePositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES - ); + calls[0] = abi.encodeWithSelector(PositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); LiquidityRange memory range = LiquidityRange({ poolKey: key, @@ -70,8 +68,7 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); bytes memory actions = planner.finalize(range.poolKey); - calls[1] = - abi.encodeWithSelector(NonfungiblePositionManager(lpm).modifyLiquidities.selector, actions, _deadline); + calls[1] = abi.encodeWithSelector(PositionManager(lpm).modifyLiquidities.selector, actions, _deadline); lpm.multicall(calls); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/PositionManager.t.sol similarity index 96% rename from test/position-managers/NonfungiblePositionManager.t.sol rename to test/position-managers/PositionManager.t.sol index 8c89a7eb..54aa36e7 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -22,8 +22,8 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -31,7 +31,7 @@ import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; -contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { +contract PositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; @@ -48,7 +48,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - lpm = new NonfungiblePositionManager(manager); + lpm = new PositionManager(manager); IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); @@ -61,7 +61,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi bytes[] memory badParams = new bytes[](1); - vm.expectRevert(INonfungiblePositionManager.MismatchedLengths.selector); + vm.expectRevert(IPositionManager.MismatchedLengths.selector); lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1); } @@ -154,7 +154,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // uint256 amount1Min = amount1Desired - 1; // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // IPositionManager.MintParams memory params = IPositionManager.MintParams({ // range: range, // amount0Desired: amount0Desired, // amount1Desired: amount1Desired, diff --git a/test/shared/FeeMath.sol b/test/shared/FeeMath.sol index 8812efa2..e549d588 100644 --- a/test/shared/FeeMath.sol +++ b/test/shared/FeeMath.sol @@ -11,8 +11,8 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {INonfungiblePositionManager} from "../../src/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../src/NonfungiblePositionManager.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; library FeeMath { @@ -22,7 +22,7 @@ library FeeMath { using PoolIdLibrary for PoolKey; /// @notice Calculates the fees accrued to a position. Used for testing purposes. - function getFeesOwed(INonfungiblePositionManager posm, IPoolManager manager, uint256 tokenId) + function getFeesOwed(IPositionManager posm, IPoolManager manager, uint256 tokenId) internal view returns (BalanceDelta feesOwed) diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index fc0cc60a..caec186e 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -6,14 +6,14 @@ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {NonfungiblePositionManager, Actions} from "../../src/NonfungiblePositionManager.sol"; +import {PositionManager, Actions} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {Planner} from "../utils/Planner.sol"; abstract contract LiquidityOperations is CommonBase { using Planner for Planner.Plan; - NonfungiblePositionManager lpm; + PositionManager lpm; uint256 _deadline = block.timestamp + 1; diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 5d121dd3..c15c8cd9 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -7,7 +7,7 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -import {INonfungiblePositionManager, Actions} from "../../../src/interfaces/INonfungiblePositionManager.sol"; +import {IPositionManager, Actions} from "../../../src/interfaces/IPositionManager.sol"; import {LiquidityRange} from "../../../src/types/LiquidityRange.sol"; import {Planner} from "../../utils/Planner.sol"; @@ -15,7 +15,7 @@ contract LiquidityFuzzers is Fuzzers { using Planner for Planner.Plan; function addFuzzyLiquidity( - INonfungiblePositionManager lpm, + IPositionManager lpm, address recipient, PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params, diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol index 50ce71e6..7c372855 100644 --- a/test/utils/Planner.sol +++ b/test/utils/Planner.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {INonfungiblePositionManager, Actions} from "../../src/interfaces/INonfungiblePositionManager.sol"; +import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; library Planner { From 2761404c63eac15cf1d253113c8cb2b9b22c05db Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 15:45:25 -0400 Subject: [PATCH 78/98] cache length --- .forge-snapshots/PositionManager_collect.snap | 2 +- .../PositionManager_collect_sameRange.snap | 2 +- .../PositionManager_decreaseLiquidity.snap | 2 +- ...nager_decrease_sameRange_allLiquidity.snap | 2 +- ...sitionManager_increaseLiquidity_erc20.snap | 2 +- ...crease_autocompoundExactUnclaimedFees.snap | 2 +- ...increase_autocompoundExcessFeesCredit.snap | 2 +- .forge-snapshots/PositionManager_mint.snap | 2 +- .../PositionManager_mint_onSameTickLower.snap | 2 +- .../PositionManager_mint_onSameTickUpper.snap | 2 +- .../PositionManager_mint_sameRange.snap | 2 +- ...anager_mint_warmedPool_differentRange.snap | 2 +- ...tionManager_multicall_initialize_mint.snap | 2 +- src/PositionManager.sol | 20 ++++++++++--------- 14 files changed, 24 insertions(+), 22 deletions(-) diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap index 18ede557..0d60d127 100644 --- a/.forge-snapshots/PositionManager_collect.snap +++ b/.forge-snapshots/PositionManager_collect.snap @@ -1 +1 @@ -162376 \ No newline at end of file +162360 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index 18ede557..0d60d127 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -162376 \ No newline at end of file +162360 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap index 7b04c2fe..5d2f17b2 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -1 +1 @@ -127754 \ No newline at end of file +127738 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap index ca26bdc3..40e24830 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -140635 \ No newline at end of file +140619 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap index e2b8005b..0e6c2b08 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap @@ -1 +1 @@ -157220 \ No newline at end of file +157204 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index 811326c5..56255546 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -170546 \ No newline at end of file +170530 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index 26a05e3d..c3713db5 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -184946 \ No newline at end of file +184930 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap index 8816d2df..25a58897 100644 --- a/.forge-snapshots/PositionManager_mint.snap +++ b/.forge-snapshots/PositionManager_mint.snap @@ -1 +1 @@ -384030 \ No newline at end of file +384014 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 0e907caf..470829f3 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -343812 \ No newline at end of file +343796 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index cadb957f..23429153 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -344454 \ No newline at end of file +344438 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index a443a79a..bda37a64 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -287136 \ No newline at end of file +287120 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index 675e6672..d78daf91 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -349830 \ No newline at end of file +349814 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index 0a085b82..43874535 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -427905 \ No newline at end of file +427889 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 3dcbc15e..f21a666a 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -39,6 +39,11 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} + modifier checkDeadline(uint256 deadline) { + if (block.timestamp > deadline) revert DeadlinePassed(); + _; + } + /// @param unlockData is an encoding of actions, params, and currencies /// @return returnData is the endocing of each actions return information function modifyLiquidities(bytes calldata unlockData, uint256 deadline) @@ -65,9 +70,10 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul internal returns (bytes[] memory returnData) { - if (actions.length != params.length) revert MismatchedLengths(); - returnData = new bytes[](actions.length); - for (uint256 i; i < actions.length; i++) { + uint256 length = actions.length; + if (length != params.length) revert MismatchedLengths(); + returnData = new bytes[](length); + for (uint256 i; i < length; i++) { if (actions[i] == Actions.INCREASE) { returnData[i] = _increase(params[i], sender); } else if (actions[i] == Actions.DECREASE) { @@ -126,6 +132,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul // mint receipt token uint256 tokenId; + // tokenId is assigned to current nextTokenId before incrementing it unchecked { tokenId = nextTokenId++; } @@ -159,7 +166,6 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul function burn(uint256 tokenId, address sender) internal { _requireApprovedOrOwner(tokenId, sender); - // We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId. // Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. _validateBurn(tokenId); @@ -185,6 +191,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul ); } + // ensures liquidity of the position is empty before burning the token. function _validateBurn(uint256 tokenId) internal view { bytes32 positionId = getPositionIdFromTokenId(tokenId); uint128 liquidity = manager.getPositionLiquidity(tokenRange[tokenId].poolKey.toId(), positionId); @@ -213,9 +220,4 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul function _requireApprovedOrOwner(uint256 tokenId, address sender) internal view { if (!_isApprovedOrOwner(sender, tokenId)) revert NotApproved(sender); } - - modifier checkDeadline(uint256 deadline) { - if (block.timestamp > deadline) revert DeadlinePassed(); - _; - } } From bbacc75b38f18ad32f5eec6f8e85f62214620aae Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 15:49:40 -0400 Subject: [PATCH 79/98] skip take for 0 --- .forge-snapshots/PositionManager_collect.snap | 2 +- .forge-snapshots/PositionManager_collect_sameRange.snap | 2 +- .forge-snapshots/PositionManager_decreaseLiquidity.snap | 2 +- .../PositionManager_decrease_sameRange_allLiquidity.snap | 2 +- ...PositionManager_increase_autocompoundExactUnclaimedFees.snap | 2 +- .../PositionManager_increase_autocompoundExcessFeesCredit.snap | 2 +- src/PositionManager.sol | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap index 0d60d127..bcb84d63 100644 --- a/.forge-snapshots/PositionManager_collect.snap +++ b/.forge-snapshots/PositionManager_collect.snap @@ -1 +1 @@ -162360 \ No newline at end of file +162408 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index 0d60d127..bcb84d63 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -162360 \ No newline at end of file +162408 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap index 5d2f17b2..88b59abd 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -1 +1 @@ -127738 \ No newline at end of file +127786 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap index 40e24830..0063df7b 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -140619 \ No newline at end of file +140667 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index 56255546..17ba51c5 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -170530 \ No newline at end of file +148714 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index c3713db5..809cd270 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -184930 \ No newline at end of file +184978 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index f21a666a..730e5e4e 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -157,7 +157,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul // the sender is the payer or receiver if (currencyDelta < 0) { currency.settle(manager, sender, uint256(-int256(currencyDelta)), false); - } else { + } else if (currencyDelta > 0) { currency.take(manager, sender, uint256(int256(currencyDelta)), false); } From 2adafa99db01bf2311290904fc63bbad89e66735 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 19 Jul 2024 16:59:04 -0400 Subject: [PATCH 80/98] fix tests --- test/position-managers/FeeCollection.t.sol | 70 +------ test/position-managers/Gas.t.sol | 7 +- .../position-managers/IncreaseLiquidity.t.sol | 172 +++++++++--------- .../PositionManager.multicall.t.sol | 7 +- test/position-managers/PositionManager.t.sol | 107 ++++------- 5 files changed, 128 insertions(+), 235 deletions(-) diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 8b8c255e..db6b3d54 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -18,6 +18,7 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; @@ -68,27 +69,6 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li vm.stopPrank(); } - // TODO: we dont accept collecting fees as 6909 yet - // function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { - // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - // uint256 tokenId; - // (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - // // swap to create fees - // uint256 swapAmount = 0.01e18; - // swap(key, false, -int256(swapAmount), ZERO_BYTES); - - // // collect fees - // BalanceDelta delta = collect(tokenId, address(this), ZERO_BYTES, true); - - // assertEq(delta.amount0(), 0); - - // assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); - - // assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); - // } - function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; @@ -110,50 +90,6 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); } - // TODO: we dont accept collecting fees as 6909 yet - // two users with the same range; one user cannot collect the other's fees - // function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) - // public - // { - // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - // params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); - // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - // liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); - - // LiquidityRange memory range = - // LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - // vm.prank(alice); - // mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); - // uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // vm.prank(bob); - // mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); - // uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // // swap to create fees - // uint256 swapAmount = 0.01e18; - // swap(key, false, -int256(swapAmount), ZERO_BYTES); - - // // alice collects only her fees - // vm.prank(alice); - // BalanceDelta delta = collect(tokenIdAlice, alice, ZERO_BYTES, true); - // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); - // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); - // assertTrue(delta.amount1() != 0); - - // // bob collects only his fees - // vm.prank(bob); - // delta = collect(tokenIdBob, bob, ZERO_BYTES, true); - // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); - // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); - // assertTrue(delta.amount1() != 0); - - // // position manager holds no fees now - // assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); - // assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); - // } - function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) public { @@ -286,4 +222,8 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li tolerance ); } + + // TODO: ERC6909 Support. + function test_collect_6909() public {} + function test_collect_sameRange_6909() public {} } diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 51c72c1e..a49fda1e 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -24,6 +24,7 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {IMulticall} from "../../src/interfaces/IMulticall.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; @@ -251,7 +252,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // Use multicall to initialize a pool and mint liquidity bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(PositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); range = LiquidityRange({ poolKey: key, @@ -263,9 +264,9 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); bytes memory actions = planner.finalize(range.poolKey); - calls[1] = abi.encodeWithSelector(PositionManager(lpm).modifyLiquidities.selector, actions, _deadline); + calls[1] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); - lpm.multicall(calls); + IMulticall(lpm).multicall(calls); snapLastCall("PositionManager_multicall_initialize_mint"); } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 02d3bad0..78929423 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -28,6 +28,8 @@ import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; import {FeeMath} from "../shared/FeeMath.sol"; +import "forge-std/console2.sol"; + contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -47,6 +49,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi LiquidityRange range; + // Error tolerance. + uint256 tolerance = 0.00000000001 ether; + function setUp() public { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); @@ -76,7 +81,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } - function test_increaseLiquidity_withExactFees1() public { + function test_increaseLiquidity_withExactFees() public { // Alice and Bob provide liquidity on the range // Alice uses her exact fees to increase liquidity (compounding) @@ -123,9 +128,6 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi lpm.modifyLiquidities(calls, _deadline); vm.stopPrank(); - // It is not exact because of the error in the fee calculation and error in the - uint256 tolerance = 0.00000000001 ether; - // alice barely spent any tokens // TODO: This is a case for not caring about dust left in pool manager :/ assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance); @@ -172,96 +174,86 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); vm.stopPrank(); - // It is not exact because of the error in the fee calculation and error in the - uint256 tolerance = 0.00000000001 ether; - // alice barely spent any tokens // TODO: This is a case for not caring about dust left in pool manager :/ assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance); assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), tolerance); } - // function test_increaseLiquidity_withExcessFees() public { - // // Alice and Bob provide liquidity on the range - // // Alice uses her fees to increase liquidity. Excess fees are accounted to alice - // uint256 liquidityAlice = 3_000e18; - // uint256 liquidityBob = 1_000e18; - // uint256 totalLiquidity = liquidityAlice + liquidityBob; - - // // alice provides liquidity - // vm.prank(alice); - // mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - // uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // // bob provides liquidity - // vm.prank(bob); - // mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - // uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // // swap to create fees - // uint256 swapAmount = 0.001e18; - // swap(key, true, -int256(swapAmount), ZERO_BYTES); - // swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - // // alice will use half of her fees to increase liquidity - // (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - // { - // (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - // uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - // sqrtPriceX96, - // TickMath.getSqrtPriceAtTick(range.tickLower), - // TickMath.getSqrtPriceAtTick(range.tickUpper), - // token0Owed / 2, - // token1Owed / 2 - // ); - - // vm.startPrank(alice); - // increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - // vm.stopPrank(); - // } - - // { - // // bob collects his fees - // uint256 balance0BeforeBob = currency0.balanceOf(bob); - // uint256 balance1BeforeBob = currency1.balanceOf(bob); - // vm.startPrank(bob); - // collect(tokenIdBob, bob, ZERO_BYTES, false); - // vm.stopPrank(); - // uint256 balance0AfterBob = currency0.balanceOf(bob); - // uint256 balance1AfterBob = currency1.balanceOf(bob); - // assertApproxEqAbs( - // balance0AfterBob - balance0BeforeBob, - // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - // 1 wei - // ); - // assertApproxEqAbs( - // balance1AfterBob - balance1BeforeBob, - // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - // 1 wei - // ); - // } - - // { - // // alice collects her fees, which should be about half of the fees - // uint256 balance0BeforeAlice = currency0.balanceOf(alice); - // uint256 balance1BeforeAlice = currency1.balanceOf(alice); - // vm.startPrank(alice); - // collect(tokenIdAlice, alice, ZERO_BYTES, false); - // vm.stopPrank(); - // uint256 balance0AfterAlice = currency0.balanceOf(alice); - // uint256 balance1AfterAlice = currency1.balanceOf(alice); - // assertApproxEqAbs( - // balance0AfterAlice - balance0BeforeAlice, - // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, - // 9 wei - // ); - // assertApproxEqAbs( - // balance1AfterAlice - balance1BeforeAlice, - // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, - // 1 wei - // ); - // } - // } + function test_increaseLiquidity_sameRange_withExcessFees() public { + // Alice and Bob provide liquidity on the same range + // Alice uses half her fees to increase liquidity. The other half are collected to her wallet. + // Bob collects all fees. + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + mint(range, liquidityAlice, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob provides liquidity + vm.prank(bob); + mint(range, liquidityBob, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice will use half of her fees to increase liquidity + BalanceDelta aliceFeesOwed = IPositionManager(lpm).getFeesOwed(manager, tokenIdAlice); + + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + uint256(int256(aliceFeesOwed.amount0() / 2)), + uint256(int256(aliceFeesOwed.amount1() / 2)) + ); + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + vm.startPrank(alice); + increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES); + vm.stopPrank(); + + assertApproxEqAbs( + currency0.balanceOf(alice) - balance0BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + tolerance + ); + assertApproxEqAbs( + currency1.balanceOf(alice) - balance1BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + tolerance + ); + } + + { + // bob collects his fees + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.startPrank(bob); + collect(tokenIdBob, ZERO_BYTES); + vm.stopPrank(); + + assertApproxEqAbs( + currency0.balanceOf(bob) - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + tolerance + ); + assertApproxEqAbs( + currency1.balanceOf(bob) - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + tolerance + ); + } + } function test_increaseLiquidity_withInsufficientFees() public { // Alice and Bob provide liquidity on the range @@ -323,12 +315,12 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi assertApproxEqAbs( balance0AfterBob - balance0BeforeBob, swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei + tolerance ); assertApproxEqAbs( balance1AfterBob - balance1BeforeBob, swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei + tolerance ); } } diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index ca0c0c37..49e0882a 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -24,6 +24,7 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {IMulticall} from "../../src/interfaces/IMulticall.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -56,7 +57,7 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid // Use multicall to initialize a pool and mint liquidity bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(PositionManager(lpm).initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); LiquidityRange memory range = LiquidityRange({ poolKey: key, @@ -68,9 +69,9 @@ contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquid planner = planner.add(Actions.MINT, abi.encode(range, 100e18, address(this), ZERO_BYTES)); bytes memory actions = planner.finalize(range.poolKey); - calls[1] = abi.encodeWithSelector(PositionManager(lpm).modifyLiquidities.selector, actions, _deadline); + calls[1] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); - lpm.multicall(calls); + IMulticall(address(lpm)).multicall(calls); // test swap, doesn't revert swap(key, true, -1e18, ZERO_BYTES); diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 54aa36e7..1255559a 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -21,6 +21,7 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; @@ -139,52 +140,6 @@ contract PositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, assertEq(lpm.ownerOf(tokenId), alice); } - // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - // public - // { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // vm.assume(tickLower < 0 && 0 < tickUpper); - - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - // vm.assume(0.00001e18 < amount0Desired); - // vm.assume(0.00001e18 < amount1Desired); - - // uint256 amount0Min = amount0Desired - 1; - // uint256 amount1Min = amount1Desired - 1; - - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - // IPositionManager.MintParams memory params = IPositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: amount0Min, - // amount1Min: amount1Min, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - - // // seed some liquidity so we can move the price - // modifyLiquidityRouter.modifyLiquidity( - // key, - // IPoolManager.ModifyLiquidityParams({ - // tickLower: TickMath.minUsableTick(key.tickSpacing), - // tickUpper: TickMath.maxUsableTick(key.tickSpacing), - // liquidityDelta: 100_000e18, - // salt: 0 - // }), - // ZERO_BYTES - // ); - - // // swap to move the price - // swap(key, true, -1000e18, ZERO_BYTES); - - // // will revert because amount0Min and amount1Min are very strict - // vm.expectRevert(); - // lpm.mint(params); - // } - function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { uint256 balance0Start = currency0.balanceOfSelf(); uint256 balance1Start = currency1.balanceOfSelf(); @@ -252,40 +207,38 @@ contract PositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); } - // function test_decreaseLiquidity_collectFees( - // IPoolManager.ModifyLiquidityParams memory params, - // uint256 decreaseLiquidityDelta - // ) public { - // uint256 tokenId; - // (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - // vm.assume(0 < decreaseLiquidityDelta); - // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); - // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + function test_decreaseLiquidity_assertCollectedBalance( + IPoolManager.ModifyLiquidityParams memory params, + uint256 decreaseLiquidityDelta + ) public { + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + vm.assume(0 < decreaseLiquidityDelta); + vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - // // swap to create fees - // uint256 swapAmount = 0.01e18; - // swap(key, false, int256(swapAmount), ZERO_BYTES); + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, int256(swapAmount), ZERO_BYTES); - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - // (uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + BalanceDelta delta = decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES); - // // express key.fee as wad (i.e. 3000 = 0.003e18) - // uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + bytes32 positionId = + keccak256(abi.encodePacked(address(lpm), range.tickLower, range.tickUpper, bytes32(tokenId))); + (uint256 liquidity,,) = manager.getPositionInfo(range.poolKey.toId(), positionId); - // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); - // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); - // } + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - function test_mintTransferBurn() public {} - function test_mintTransferCollect() public {} - function test_mintTransferIncrease() public {} - function test_mintTransferDecrease() public {} + // The change in balance equals the delta returned. + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0())), "boo"); + assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1())), "guh"); + } function test_initialize(IPoolManager.ModifyLiquidityParams memory params) public { // initialize a new pool and add liquidity @@ -301,4 +254,10 @@ contract PositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, assertEq(lpm.ownerOf(1), address(this)); } + + function test_mintTransferBurn() public {} + function test_mintTransferCollect() public {} + function test_mintTransferIncrease() public {} + function test_mintTransferDecrease() public {} + function test_mint_slippageRevert() public {} } From f04a857fe7370613d7f9ed2bd1c1407e3a969dd0 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:39:42 -0400 Subject: [PATCH 81/98] Update src/interfaces/IPositionManager.sol Co-authored-by: Alice <34962750+hensha256@users.noreply.github.com> --- src/interfaces/IPositionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IPositionManager.sol b/src/interfaces/IPositionManager.sol index 6c55b974..c47929a4 100644 --- a/src/interfaces/IPositionManager.sol +++ b/src/interfaces/IPositionManager.sol @@ -26,7 +26,7 @@ interface IPositionManager { returns (PoolKey memory poolKey, int24 tickLower, int24 tickUpper); /// @notice Batches many liquidity modification calls to pool manager - /// @param payload is an encoding of actions, params, and currencies + /// @param payload is an encoding of actions, and parameters for those actions /// @param deadline is the deadline for the batched actions to be executed /// @return returnData is the endocing of each actions return information function modifyLiquidities(bytes calldata payload, uint256 deadline) external returns (bytes[] memory); From 983ffc8a33932175ea5a4103bf30f735422396c6 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 22 Jul 2024 09:58:44 -0400 Subject: [PATCH 82/98] update multicall tests per feedback --- test/base/Multicall.t.sol | 34 ++++++++++++++++++++++++++++------ test/mock/MockMulticall.sol | 19 ++++++++++++------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/test/base/Multicall.t.sol b/test/base/Multicall.t.sol index c7c6fa78..dc90be4b 100644 --- a/test/base/Multicall.t.sol +++ b/test/base/Multicall.t.sol @@ -47,13 +47,13 @@ contract MulticallTest is Test { multicall.multicall(calls); } - function test_multicall_pays() public { + function test_multicall_payableStoresMsgValue() public { assertEq(address(multicall).balance, 0); bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(MockMulticall(multicall).pays.selector); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); multicall.multicall{value: 100}(calls); assertEq(address(multicall).balance, 100); - assertEq(multicall.paid(), 100); + assertEq(multicall.msgValue(), 100); } function test_multicall_returnSender() public { @@ -77,11 +77,33 @@ contract MulticallTest is Test { function test_multicall_double_send() public { bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(MockMulticall(multicall).pays.selector); - calls[1] = abi.encodeWithSelector(MockMulticall(multicall).pays.selector); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); multicall.multicall{value: 100}(calls); assertEq(address(multicall).balance, 100); - assertEq(multicall.paid(), 100); + assertEq(multicall.msgValue(), 100); + } + + function test_multicall_unpayableRevert() public { + // first call is payable, second is not which causes a revert + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 10, 20); + + vm.expectRevert(); + multicall.multicall{value: 100}(calls); + } + + function test_multicall_bothPayable() public { + // msg.value is provided to both calls + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValueDouble.selector); + + multicall.multicall{value: 100}(calls); + assertEq(address(multicall).balance, 100); + assertEq(multicall.msgValue(), 100); + assertEq(multicall.msgValueDouble(), 200); } } diff --git a/test/mock/MockMulticall.sol b/test/mock/MockMulticall.sol index 351803cd..4b96d915 100644 --- a/test/mock/MockMulticall.sol +++ b/test/mock/MockMulticall.sol @@ -4,23 +4,28 @@ pragma solidity ^0.8.20; import "../../src/base/Multicall.sol"; contract MockMulticall is Multicall { - function functionThatRevertsWithError(string memory error) external pure { - revert(error); - } - struct Tuple { uint256 a; uint256 b; } + uint256 public msgValue; + uint256 public msgValueDouble; + + function functionThatRevertsWithError(string memory error) external pure { + revert(error); + } + function functionThatReturnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) { tuple = Tuple({a: a, b: b}); } - uint256 public paid; + function payableStoresMsgValue() external payable { + msgValue = msg.value; + } - function pays() external payable { - paid = msg.value; + function payableStoresMsgValueDouble() external payable { + msgValueDouble = 2 * msg.value; } function returnSender() external view returns (address) { From c1cab0c818f72f619292c962925b95968e841853 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 22 Jul 2024 10:13:19 -0400 Subject: [PATCH 83/98] remove unused imports --- test/position-managers/Execute.t.sol | 11 ++--------- test/position-managers/FeeCollection.t.sol | 14 +++----------- test/position-managers/Gas.t.sol | 8 +------- test/position-managers/IncreaseLiquidity.t.sol | 12 +++--------- .../PositionManager.multicall.t.sol | 15 ++++----------- test/position-managers/PositionManager.t.sol | 10 ++-------- 6 files changed, 15 insertions(+), 55 deletions(-) diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 4bee15dd..c3d01ee3 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; @@ -11,31 +10,25 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; -contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { +contract ExecuteTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; - using SafeCast for uint256; using Planner for Planner.Plan; using StateLibrary for IPoolManager; diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index db6b3d54..9327d3b6 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; @@ -10,29 +9,22 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; import {PositionManager} from "../../src/PositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { +contract FeeCollectionTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; PoolId poolId; address alice = makeAddr("ALICE"); diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index a49fda1e..1e75864c 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -10,20 +10,16 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {FeeMath} from "../shared/FeeMath.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {IMulticall} from "../../src/interfaces/IMulticall.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; @@ -32,10 +28,8 @@ import {Planner} from "../utils/Planner.sol"; contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using Planner for Planner.Plan; - using FeeMath for IPositionManager; PoolId poolId; address alice; diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 78929423..43ab5b46 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; @@ -10,8 +9,7 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; @@ -19,21 +17,17 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {PositionManager} from "../../src/PositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {Actions, IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; import {FeeMath} from "../shared/FeeMath.sol"; -import "forge-std/console2.sol"; - -contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { +contract IncreaseLiquidityTest is Test, Deployers, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using Planner for Planner.Plan; using FeeMath for IPositionManager; diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 49e0882a..979e73c2 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -2,28 +2,22 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {IMulticall} from "../../src/interfaces/IMulticall.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -31,10 +25,9 @@ import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; -contract MulticallTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { +contract PositionManagerMulticallTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; using Planner for Planner.Plan; PoolId poolId; diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 1255559a..5984808f 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; @@ -10,18 +9,13 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; @@ -32,7 +26,7 @@ import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; -contract PositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { +contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; From 75b3543694358098bb061482ff263acbe4380780 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 22 Jul 2024 10:20:53 -0400 Subject: [PATCH 84/98] more unused imports --- test/position-managers/Execute.t.sol | 1 - test/position-managers/PositionManager.t.sol | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index c3d01ee3..06412304 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -14,7 +14,6 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 5984808f..c6c13c94 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -19,7 +19,7 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../src/types/LiquidityRange.sol"; +import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -29,7 +29,6 @@ import {Planner} from "../utils/Planner.sol"; contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; using Planner for Planner.Plan; using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; From 71b1a4f15c23c1e0579bcbce183e40ec3667c5d0 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 22 Jul 2024 10:24:42 -0400 Subject: [PATCH 85/98] improve assertion --- test/position-managers/PositionManager.multicall.t.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 979e73c2..c0327983 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -66,7 +66,10 @@ contract PositionManagerMulticallTest is Test, Deployers, LiquidityFuzzers, Liqu IMulticall(address(lpm)).multicall(calls); - // test swap, doesn't revert - swap(key, true, -1e18, ZERO_BYTES); + // test swap, doesn't revert, showing the pool was initialized + int256 amountSpecified = -1e18; + BalanceDelta result = swap(key, true, amountSpecified, ZERO_BYTES); + assertEq(result.amount0(), amountSpecified); + assertGt(result.amount1(), 0); } } From 5c96bed95704c739a769701f98b8c7284222bcf8 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 22 Jul 2024 11:33:06 -0400 Subject: [PATCH 86/98] assert mint recipient is the payer and not the recipient --- test/position-managers/PositionManager.t.sol | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index c6c13c94..fafe1580 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -74,6 +74,7 @@ contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOper BalanceDelta delta = mint(range, liquidityToAdd, address(this), ZERO_BYTES); assertEq(tokenId, 1); + assertEq(lpm.nextTokenId(), 2); assertEq(lpm.ownerOf(tokenId), address(this)); bytes32 positionId = @@ -127,10 +128,20 @@ contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOper LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 tokenId = lpm.nextTokenId(); - mint(range, liquidityToAdd, address(alice), ZERO_BYTES); + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + BalanceDelta delta = mint(range, liquidityToAdd, alice, ZERO_BYTES); assertEq(tokenId, 1); assertEq(lpm.ownerOf(tokenId), alice); + + // alice was not the payer + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0()))); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1()))); + assertEq(currency0.balanceOf(alice), balance0BeforeAlice); + assertEq(currency1.balanceOf(alice), balance1BeforeAlice); } function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { From a769478a3851f4bcc6aac082500c1aeb650f5f5e Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 22 Jul 2024 12:20:14 -0400 Subject: [PATCH 87/98] pr feedback --- test/position-managers/FeeCollection.t.sol | 97 ++++++++++++---------- test/position-managers/Gas.t.sol | 4 +- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 9327d3b6..028d51d5 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -21,7 +21,6 @@ import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; - contract FeeCollectionTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -155,64 +154,70 @@ contract FeeCollectionTest is Test, Deployers, LiquidityFuzzers, LiquidityOperat uint256 liquidityAlice = 3000e18; uint256 liquidityBob = 1000e18; - vm.prank(alice); + vm.startPrank(alice); BalanceDelta lpDeltaAlice = mint(range, liquidityAlice, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); - uint256 aliceBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)); - uint256 aliceBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)); - - vm.prank(bob); + vm.startPrank(bob); BalanceDelta lpDeltaBob = mint(range, liquidityBob, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; - - uint256 bobBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)); - uint256 bobBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)); + vm.stopPrank(); // swap to create fees uint256 swapAmount = 0.001e18; swap(key, true, -int256(swapAmount), ZERO_BYTES); // zeroForOne is true, so zero is the input swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back, // zeroForOne is false, so one is the input - // alice decreases liquidity - vm.startPrank(alice); - lpm.approve(address(this), tokenIdAlice); - decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES); - vm.stopPrank(); - uint256 tolerance = 0.000000001 ether; - // alice has accrued her principle liquidity + any fees in token0 - assertApproxEqAbs( - IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)) - aliceBalance0Before, - uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD) * 3 / 4, - tolerance - ); - // alice has accrued her principle liquidity + any fees in token1 - assertApproxEqAbs( - IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)) - aliceBalance1Before, - uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD) * 3 / 4, - tolerance - ); - - // bob decreases half of his liquidity - vm.startPrank(bob); - lpm.approve(address(this), tokenIdBob); - decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES); - vm.stopPrank(); - - // bob has accrued half his principle liquidity + any fees in token0 - assertApproxEqAbs( - IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)) - bobBalance0Before, - uint256(int256(-lpDeltaBob.amount0()) / 2) + swapAmount.mulWadDown(FEE_WAD) * 1 / 4, - tolerance - ); - // bob has accrued half his principle liquidity + any fees in token0 - assertApproxEqAbs( - IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)) - bobBalance1Before, - uint256(int256(-lpDeltaBob.amount1()) / 2) + swapAmount.mulWadDown(FEE_WAD) * 1 / 4, - tolerance - ); + { + uint256 aliceBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)); + uint256 aliceBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)); + // alice decreases liquidity + vm.startPrank(alice); + decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES); + vm.stopPrank(); + + // alice has accrued her principle liquidity + any fees in token0 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)) - aliceBalance0Before, + uint256(int256(-lpDeltaAlice.amount0())) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + tolerance + ); + // alice has accrued her principle liquidity + any fees in token1 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)) - aliceBalance1Before, + uint256(int256(-lpDeltaAlice.amount1())) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + tolerance + ); + } + + { + uint256 bobBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)); + uint256 bobBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)); + // bob decreases half of his liquidity + vm.startPrank(bob); + decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES); + vm.stopPrank(); + + // bob has accrued half his principle liquidity + any fees in token0 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)) - bobBalance0Before, + uint256(int256(-lpDeltaBob.amount0()) / 2) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + tolerance + ); + // bob has accrued half his principle liquidity + any fees in token0 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)) - bobBalance1Before, + uint256(int256(-lpDeltaBob.amount1()) / 2) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + tolerance + ); + } } // TODO: ERC6909 Support. diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 1e75864c..d3ce2d36 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -106,7 +106,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { function test_gas_mint_sameTickLower() public { // Explicitly mint to range whos tickLower is the same. - LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: -60}); + LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: range.tickLower, tickUpper: -60}); vm.startPrank(bob); mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); @@ -121,7 +121,7 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { function test_gas_mint_sameTickUpper() public { // Explicitly mint to range whos tickUpperis the same. - LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 60, tickUpper: 300}); + LiquidityRange memory bob_mint = LiquidityRange({poolKey: key, tickLower: 60, tickUpper: range.tickUpper}); vm.startPrank(bob); mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); From 49a02ce3e9c67290aef9e7bebac16b2aa6ca2dab Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 22 Jul 2024 13:00:05 -0400 Subject: [PATCH 88/98] assert pool creation --- test/position-managers/PositionManager.t.sol | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index fafe1580..d8c3ff1a 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -249,14 +249,11 @@ contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOper key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); lpm.initializePool(key, SQRT_PRICE_1_1, ZERO_BYTES); - params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); - - // add liquidity to verify pool initialized - LiquidityRange memory range = - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - mint(range, 100e18, address(this), ZERO_BYTES); - - assertEq(lpm.ownerOf(1), address(this)); + (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); + assertEq(tick, 0); + assertEq(protocolFee, 0); + assertEq(lpFee, key.fee); } function test_mintTransferBurn() public {} From bbaf355f0246a0a71ce6840e414c4fee3dfa061d Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 14:36:55 -0400 Subject: [PATCH 89/98] use poolManager --- .forge-snapshots/PositionManager_collect.snap | 2 +- .../PositionManager_collect_sameRange.snap | 2 +- .../PositionManager_decreaseLiquidity.snap | 2 +- ...nager_decrease_sameRange_allLiquidity.snap | 2 +- ...sitionManager_increaseLiquidity_erc20.snap | 2 +- ...crease_autocompoundExactUnclaimedFees.snap | 2 +- ...increase_autocompoundExcessFeesCredit.snap | 2 +- .forge-snapshots/PositionManager_mint.snap | 2 +- .../PositionManager_mint_onSameTickLower.snap | 2 +- .../PositionManager_mint_onSameTickUpper.snap | 2 +- .../PositionManager_mint_sameRange.snap | 2 +- ...anager_mint_warmedPool_differentRange.snap | 2 +- ...tionManager_multicall_initialize_mint.snap | 2 +- src/PositionManager.sol | 12 ++--- src/base/ImmutableState.sol | 6 +-- src/base/PoolInitializer.sol | 2 +- src/base/SafeCallback.sol | 8 ++-- src/interfaces/IQuoter.sol | 1 - src/lens/Quoter.sol | 46 ++++++++----------- test/Quoter.t.sol | 2 +- test/position-managers/PositionManager.t.sol | 3 +- 21 files changed, 49 insertions(+), 57 deletions(-) diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap index bcb84d63..ad535d66 100644 --- a/.forge-snapshots/PositionManager_collect.snap +++ b/.forge-snapshots/PositionManager_collect.snap @@ -1 +1 @@ -162408 \ No newline at end of file +162386 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index bcb84d63..ad535d66 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -162408 \ No newline at end of file +162386 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap index 88b59abd..4c00c495 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -1 +1 @@ -127786 \ No newline at end of file +127764 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap index 0063df7b..829d911f 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -140667 \ No newline at end of file +140645 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap index 0e6c2b08..47d4166c 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap @@ -1 +1 @@ -157204 \ No newline at end of file +157182 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index 17ba51c5..41f6d2f0 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -148714 \ No newline at end of file +148692 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index 809cd270..8dd57a0a 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -184978 \ No newline at end of file +184956 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap index 25a58897..d2c092b7 100644 --- a/.forge-snapshots/PositionManager_mint.snap +++ b/.forge-snapshots/PositionManager_mint.snap @@ -1 +1 @@ -384014 \ No newline at end of file +383992 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 470829f3..363e0ea0 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -343796 \ No newline at end of file +343774 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index 23429153..aa2bb70c 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -344438 \ No newline at end of file +344416 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index bda37a64..6ba2ef01 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -287120 \ No newline at end of file +287098 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index d78daf91..b8dc0610 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -349814 \ No newline at end of file +349792 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index 43874535..219c80b0 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -427889 \ No newline at end of file +427911 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 730e5e4e..1a5a9ad6 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -52,7 +52,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul returns (bytes[] memory) { // TODO: Edit the encoding/decoding. - return abi.decode(manager.unlock(abi.encode(unlockData, msg.sender)), (bytes[])); + return abi.decode(poolManager.unlock(abi.encode(unlockData, msg.sender)), (bytes[])); } function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { @@ -152,13 +152,13 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul (Currency currency) = abi.decode(params, (Currency)); // this address has applied all deltas on behalf of the user/owner // it is safe to close this entire delta because of slippage checks throughout the batched calls. - int256 currencyDelta = manager.currencyDelta(address(this), currency); + int256 currencyDelta = poolManager.currencyDelta(address(this), currency); // the sender is the payer or receiver if (currencyDelta < 0) { - currency.settle(manager, sender, uint256(-int256(currencyDelta)), false); + currency.settle(poolManager, sender, uint256(-int256(currencyDelta)), false); } else if (currencyDelta > 0) { - currency.take(manager, sender, uint256(int256(currencyDelta)), false); + currency.take(poolManager, sender, uint256(int256(currencyDelta)), false); } return abi.encode(currencyDelta); @@ -179,7 +179,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul internal returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) { - (liquidityDelta, totalFeesAccrued) = manager.modifyLiquidity( + (liquidityDelta, totalFeesAccrued) = poolManager.modifyLiquidity( range.poolKey, IPoolManager.ModifyLiquidityParams({ tickLower: range.tickLower, @@ -194,7 +194,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul // ensures liquidity of the position is empty before burning the token. function _validateBurn(uint256 tokenId) internal view { bytes32 positionId = getPositionIdFromTokenId(tokenId); - uint128 liquidity = manager.getPositionLiquidity(tokenRange[tokenId].poolKey.toId(), positionId); + uint128 liquidity = poolManager.getPositionLiquidity(tokenRange[tokenId].poolKey.toId(), positionId); if (liquidity > 0) revert PositionMustBeEmpty(); } diff --git a/src/base/ImmutableState.sol b/src/base/ImmutableState.sol index cce37514..dab1563c 100644 --- a/src/base/ImmutableState.sol +++ b/src/base/ImmutableState.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.19; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; contract ImmutableState { - IPoolManager public immutable manager; + IPoolManager public immutable poolManager; - constructor(IPoolManager _manager) { - manager = _manager; + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; } } diff --git a/src/base/PoolInitializer.sol b/src/base/PoolInitializer.sol index caba2a51..bb75a3d9 100644 --- a/src/base/PoolInitializer.sol +++ b/src/base/PoolInitializer.sol @@ -10,6 +10,6 @@ abstract contract PoolInitializer is ImmutableState { external returns (int24) { - return manager.initialize(key, sqrtPriceX96, hookData); + return poolManager.initialize(key, sqrtPriceX96, hookData); } } diff --git a/src/base/SafeCallback.sol b/src/base/SafeCallback.sol index f985e67c..aab521a7 100644 --- a/src/base/SafeCallback.sol +++ b/src/base/SafeCallback.sol @@ -6,15 +6,15 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./ImmutableState.sol"; abstract contract SafeCallback is ImmutableState, IUnlockCallback { - error NotManager(); + error NotPoolManager(); - modifier onlyByManager() { - if (msg.sender != address(manager)) revert NotManager(); + modifier onlyByPoolManager() { + if (msg.sender != address(poolManager)) revert NotPoolManager(); _; } /// @dev We force the onlyByManager modifier by exposing a virtual function after the onlyByManager check. - function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { + function unlockCallback(bytes calldata data) external onlyByPoolManager returns (bytes memory) { return _unlockCallback(data); } diff --git a/src/interfaces/IQuoter.sol b/src/interfaces/IQuoter.sol index 8774e548..3aed2ac2 100644 --- a/src/interfaces/IQuoter.sol +++ b/src/interfaces/IQuoter.sol @@ -11,7 +11,6 @@ import {PathKey} from "../libraries/PathKey.sol"; /// @dev These functions are not marked view because they rely on calling non-view functions and reverting /// to compute the result. They are also not gas efficient and should not be called on-chain. interface IQuoter { - error InvalidUnlockCallbackSender(); error InvalidLockCaller(); error InvalidQuoteBatchParams(); error InsufficientAmountOut(); diff --git a/src/lens/Quoter.sol b/src/lens/Quoter.sol index a4c25e42..d7f96bbe 100644 --- a/src/lens/Quoter.sol +++ b/src/lens/Quoter.sol @@ -14,8 +14,10 @@ import {IQuoter} from "../interfaces/IQuoter.sol"; import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {ImmutableState} from "../base/ImmutableState.sol"; +import {SafeCallback} from "../base/SafeCallback.sol"; -contract Quoter is IQuoter, IUnlockCallback { +contract Quoter is IQuoter, SafeCallback { using Hooks for IHooks; using PoolIdLibrary for PoolKey; using PathKeyLib for PathKey; @@ -24,9 +26,6 @@ contract Quoter is IQuoter, IUnlockCallback { /// @dev cache used to check a safety condition in exact output swaps. uint128 private amountOutCached; - // v4 Singleton contract - IPoolManager public immutable manager; - /// @dev min valid reason is 3-words long /// @dev int128[2] + sqrtPriceX96After padded to 32bytes + intializeTicksLoaded padded to 32bytes uint256 internal constant MINIMUM_VALID_RESPONSE_LENGTH = 96; @@ -54,9 +53,7 @@ contract Quoter is IQuoter, IUnlockCallback { _; } - constructor(address _manager) { - manager = IPoolManager(_manager); - } + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} /// @inheritdoc IQuoter function quoteExactInputSingle(QuoteExactSingleParams memory params) @@ -64,7 +61,7 @@ contract Quoter is IQuoter, IUnlockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} catch (bytes memory reason) { return _handleRevertSingle(reason); } @@ -79,7 +76,7 @@ contract Quoter is IQuoter, IUnlockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } @@ -91,7 +88,7 @@ contract Quoter is IQuoter, IUnlockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} catch (bytes memory reason) { if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; return _handleRevertSingle(reason); @@ -108,18 +105,13 @@ contract Quoter is IQuoter, IUnlockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } } - /// @inheritdoc IUnlockCallback - function unlockCallback(bytes calldata data) external returns (bytes memory) { - if (msg.sender != address(manager)) { - revert InvalidUnlockCallbackSender(); - } - + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); @@ -177,7 +169,7 @@ contract Quoter is IQuoter, IUnlockCallback { for (uint256 i = 0; i < pathLength; i++) { (PoolKey memory poolKey, bool zeroForOne) = params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : cache.prevCurrency); - (, cache.tickBefore,,) = manager.getSlot0(poolKey.toId()); + (, cache.tickBefore,,) = poolManager.getSlot0(poolKey.toId()); (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap( poolKey, @@ -197,7 +189,7 @@ contract Quoter is IQuoter, IUnlockCallback { cache.prevCurrency = params.path[i].intermediateCurrency; result.sqrtPriceX96AfterList[i] = cache.sqrtPriceX96After; result.initializedTicksLoadedList[i] = - PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, poolKey, cache.tickBefore, cache.tickAfter); } bytes memory r = abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); @@ -208,7 +200,7 @@ contract Quoter is IQuoter, IUnlockCallback { /// @dev quote an ExactInput swap on a pool, then revert with the result function _quoteExactInputSingle(QuoteExactSingleParams memory params) public selfOnly returns (bytes memory) { - (, int24 tickBefore,,) = manager.getSlot0(params.poolKey.toId()); + (, int24 tickBefore,,) = poolManager.getSlot0(params.poolKey.toId()); (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( params.poolKey, @@ -224,7 +216,7 @@ contract Quoter is IQuoter, IUnlockCallback { deltaAmounts[1] = -deltas.amount1(); uint32 initializedTicksLoaded = - PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, params.poolKey, tickBefore, tickAfter); bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); assembly { revert(add(0x20, result), mload(result)) @@ -251,7 +243,7 @@ contract Quoter is IQuoter, IUnlockCallback { params.path[i - 1], i == pathLength ? params.exactCurrency : cache.prevCurrency ); - (, cache.tickBefore,,) = manager.getSlot0(poolKey.toId()); + (, cache.tickBefore,,) = poolManager.getSlot0(poolKey.toId()); (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap(poolKey, !oneForZero, int256(uint256(curAmountOut)), 0, params.path[i - 1].hookData); @@ -268,7 +260,7 @@ contract Quoter is IQuoter, IUnlockCallback { cache.prevCurrency = params.path[i - 1].intermediateCurrency; result.sqrtPriceX96AfterList[i - 1] = cache.sqrtPriceX96After; result.initializedTicksLoadedList[i - 1] = - PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, poolKey, cache.tickBefore, cache.tickAfter); } bytes memory r = abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); @@ -282,7 +274,7 @@ contract Quoter is IQuoter, IUnlockCallback { // if no price limit has been specified, cache the output amount for comparison in the swap callback if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.exactAmount; - (, int24 tickBefore,,) = manager.getSlot0(params.poolKey.toId()); + (, int24 tickBefore,,) = poolManager.getSlot0(params.poolKey.toId()); (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( params.poolKey, params.zeroForOne, @@ -298,7 +290,7 @@ contract Quoter is IQuoter, IUnlockCallback { deltaAmounts[1] = -deltas.amount1(); uint32 initializedTicksLoaded = - PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, params.poolKey, tickBefore, tickAfter); bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); assembly { revert(add(0x20, result), mload(result)) @@ -314,7 +306,7 @@ contract Quoter is IQuoter, IUnlockCallback { uint160 sqrtPriceLimitX96, bytes memory hookData ) private returns (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) { - deltas = manager.swap( + deltas = poolManager.swap( poolKey, IPoolManager.SwapParams({ zeroForOne: zeroForOne, @@ -327,7 +319,7 @@ contract Quoter is IQuoter, IUnlockCallback { if (amountOutCached != 0 && amountOutCached != uint128(zeroForOne ? deltas.amount1() : deltas.amount0())) { revert InsufficientAmountOut(); } - (sqrtPriceX96After, tickAfter,,) = manager.getSlot0(poolKey.toId()); + (sqrtPriceX96After, tickAfter,,) = poolManager.getSlot0(poolKey.toId()); } /// @dev return either the sqrtPriceLimit from user input, or the max/min value possible depending on trade direction diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index 5e27a1ce..b16f721c 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -54,7 +54,7 @@ contract QuoterTest is Test, Deployers { function setUp() public { deployFreshManagerAndRouters(); - quoter = new Quoter(address(manager)); + quoter = new Quoter(IPoolManager(manager)); positionManager = new PoolModifyLiquidityTest(manager); // salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2) diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index d8c3ff1a..5cc2266c 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -244,7 +244,7 @@ contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOper assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1())), "guh"); } - function test_initialize(IPoolManager.ModifyLiquidityParams memory params) public { + function test_initialize() public { // initialize a new pool and add liquidity key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); lpm.initializePool(key, SQRT_PRICE_1_1, ZERO_BYTES); @@ -256,6 +256,7 @@ contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOper assertEq(lpFee, key.fee); } + function test_initialize_fuzz() public {} function test_mintTransferBurn() public {} function test_mintTransferCollect() public {} function test_mintTransferIncrease() public {} From 538a0fbe5d60dbf65ea37a2ad6c4f7a5a706fa24 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 14:44:37 -0400 Subject: [PATCH 90/98] remove liquidityrange imports --- src/PositionManager.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 1a5a9ad6..fdb7f95d 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -17,13 +17,12 @@ import {ImmutableState} from "./base/ImmutableState.sol"; import {Multicall} from "./base/Multicall.sol"; import {PoolInitializer} from "./base/PoolInitializer.sol"; import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; +import {LiquidityRange} from "./types/LiquidityRange.sol"; contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Multicall, SafeCallback { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; - using LiquidityRangeIdLibrary for LiquidityRange; using StateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager; using SafeCast for uint256; From b215934455bf592a1d7943c4570333615b5b0d0c Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 14:45:47 -0400 Subject: [PATCH 91/98] remove version string --- src/PositionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PositionManager.sol b/src/PositionManager.sol index fdb7f95d..4cc68985 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -35,7 +35,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul constructor(IPoolManager _manager) ImmutableState(_manager) - ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") + ERC721Permit("Uniswap V4 Positions NFT", "UNI-V4-POSM", "1") {} modifier checkDeadline(uint256 deadline) { From 6093c4b90dc2680e8ccf0a07b5dd1337378d95bd Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 15:26:26 -0400 Subject: [PATCH 92/98] pr comments, use base test setup --- .forge-snapshots/PositionManager_mint.snap | 2 +- .../PositionManager_mint_onSameTickLower.snap | 2 +- .../PositionManager_mint_onSameTickUpper.snap | 2 +- ...anager_mint_warmedPool_differentRange.snap | 2 +- ...tionManager_multicall_initialize_mint.snap | 2 +- src/PositionManager.sol | 2 +- test/position-managers/Execute.t.sol | 46 ++++++------------- test/position-managers/FeeCollection.t.sol | 38 ++++++--------- test/position-managers/Gas.t.sol | 40 +++++----------- .../position-managers/IncreaseLiquidity.t.sol | 36 ++++++--------- .../PositionManager.multicall.t.sol | 17 +++---- test/position-managers/PositionManager.t.sol | 17 +++---- test/shared/PosmTestSetup.sol | 38 +++++++++++++++ 13 files changed, 109 insertions(+), 135 deletions(-) create mode 100644 test/shared/PosmTestSetup.sol diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap index d2c092b7..f0022990 100644 --- a/.forge-snapshots/PositionManager_mint.snap +++ b/.forge-snapshots/PositionManager_mint.snap @@ -1 +1 @@ -383992 \ No newline at end of file +418192 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 363e0ea0..6be7a189 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -343774 \ No newline at end of file +360874 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index aa2bb70c..c3f898bc 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -344416 \ No newline at end of file +361516 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index b8dc0610..86a7d8c2 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -349792 \ No newline at end of file +366892 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index 219c80b0..59036d4e 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -427911 \ No newline at end of file +462111 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 4cc68985..208bc7bf 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -146,7 +146,7 @@ contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Mul /// @param params is an encoding of the Currency to close /// @param sender is the msg.sender encoded by the `modifyLiquidities` function before the `unlockCallback`. - /// @return int256 the balance of the currency being settled by this call + /// @return an encoding of int256 the balance of the currency being settled by this call function _close(bytes memory params, address sender) internal returns (bytes memory) { (Currency currency) = abi.decode(params, (Currency)); // this address has applied all deltas on behalf of the user/owner diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 06412304..30a1c260 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -18,13 +17,11 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; - import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; -contract ExecuteTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { +contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; @@ -35,37 +32,24 @@ contract ExecuteTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) - uint256 FEE_WAD; - LiquidityRange range; function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - lpm = new PositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); + + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); + + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); + + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); // define a reusable range range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 028d51d5..6e2f1141 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -16,12 +15,10 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; - import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; - -contract FeeCollectionTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { +contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -29,35 +26,26 @@ contract FeeCollectionTest is Test, Deployers, LiquidityFuzzers, LiquidityOperat address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18) uint256 FEE_WAD; function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - lpm = new PositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); + + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); } function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index d3ce2d36..df267aec 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -6,7 +6,6 @@ import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -21,11 +20,10 @@ import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.s import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {IMulticall} from "../../src/interfaces/IMulticall.sol"; - -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; -contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { +contract GasTest is Test, PosmTestSetup, GasSnapshot { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; @@ -37,8 +35,6 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { address bob; uint256 bobPK; - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) uint256 FEE_WAD; @@ -48,34 +44,22 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { (alice, alicePK) = makeAddrAndKey("ALICE"); (bob, bobPK) = makeAddrAndKey("BOB"); - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - lpm = new PositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); - // mint some ERC6909 tokens - claimsRouter.deposit(currency0, address(this), 100_000_000 ether); - claimsRouter.deposit(currency1, address(this), 100_000_000 ether); - manager.setOperator(address(lpm), true); + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); // define a reusable range range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 43ab5b46..03c7740c 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -21,11 +20,11 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {Actions, IPositionManager} from "../../src/interfaces/IPositionManager.sol"; -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; import {FeeMath} from "../shared/FeeMath.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; -contract IncreaseLiquidityTest is Test, Deployers, Fuzzers, LiquidityOperations { +contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; @@ -36,8 +35,6 @@ contract IncreaseLiquidityTest is Test, Deployers, Fuzzers, LiquidityOperations address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) uint256 FEE_WAD; @@ -47,29 +44,22 @@ contract IncreaseLiquidityTest is Test, Deployers, Fuzzers, LiquidityOperations uint256 tolerance = 0.00000000001 ether; function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - lpm = new PositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); + + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); // define a reusable range range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index c0327983..0a7aa688 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -19,13 +18,11 @@ import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.s import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {IMulticall} from "../../src/interfaces/IMulticall.sol"; - import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; -contract PositionManagerMulticallTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { +contract PositionManagerMulticallTest is Test, PosmTestSetup, LiquidityFuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using Planner for Planner.Plan; @@ -34,15 +31,13 @@ contract PositionManagerMulticallTest is Test, Deployers, LiquidityFuzzers, Liqu address alice = makeAddr("ALICE"); function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - lpm = new PositionManager(manager); - - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); } function test_multicall_initializePool_mint() public { diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 5cc2266c..9007285c 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -20,13 +19,11 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {IPositionManager, Actions} from "../../src/interfaces/IPositionManager.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; - import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; import {Planner} from "../utils/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; -contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOperations { +contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using Planner for Planner.Plan; @@ -37,15 +34,13 @@ contract PositionManagerTest is Test, Deployers, LiquidityFuzzers, LiquidityOper address alice = makeAddr("ALICE"); function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - lpm = new PositionManager(manager); - - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); } function test_modifyLiquidities_reverts_mismatchedLengths() public { diff --git a/test/shared/PosmTestSetup.sol b/test/shared/PosmTestSetup.sol new file mode 100644 index 00000000..4185ea22 --- /dev/null +++ b/test/shared/PosmTestSetup.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {LiquidityOperations} from "./LiquidityOperations.sol"; + +/// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic liquidity operations on posm. +contract PosmTestSetup is Test, Deployers, LiquidityOperations { + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + function deployAndApprovePosm(IPoolManager poolManager) public { + lpm = new PositionManager(poolManager); + approvePosm(); + } + + function seedBalance(address to) public { + IERC20(Currency.unwrap(currency0)).transfer(to, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(to, STARTING_USER_BALANCE); + } + + function approvePosm() public { + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + } + + function approvePosmFor(address addr) public { + vm.startPrank(addr); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + } +} From 84f9629a15c203ff13b2e58ee12be73acfcebb43 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 15:28:10 -0400 Subject: [PATCH 93/98] fuzz sqrtPrice --- test/position-managers/PositionManager.t.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 9007285c..25ea5dc5 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -54,8 +54,11 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1); } - function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { - params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + function test_fuzz_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params, uint160 sqrtPriceX96) + public + { + bound(sqrtPriceX96, MIN_PRICE_LIMIT, MAX_PRICE_LIMIT); + params = createFuzzyLiquidityParams(key, params, sqrtPriceX96); // liquidity is a uint uint256 liquidityToAdd = params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); From 7dbc6b262e040d2151132ff56cf9e33bcd69a118 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 15:35:09 -0400 Subject: [PATCH 94/98] use fuzz, assert eq --- test/position-managers/PositionManager.t.sol | 23 +++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 25ea5dc5..e0e37336 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -117,7 +117,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); } - function test_mint_recipient(IPoolManager.ModifyLiquidityParams memory seedParams) public { + function test_fuzz_mint_recipient(IPoolManager.ModifyLiquidityParams memory seedParams) public { IPoolManager.ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); uint256 liquidityToAdd = params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); @@ -142,7 +142,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(currency1.balanceOf(alice), balance1BeforeAlice); } - function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { + function test_fuzz_burn(IPoolManager.ModifyLiquidityParams memory params) public { uint256 balance0Start = currency0.balanceOfSelf(); uint256 balance1Start = currency1.balanceOfSelf(); @@ -171,26 +171,23 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(liquidity, 0); - // TODO: slightly off by 1 bip (0.0001%) - assertApproxEqRel( - currency0.balanceOfSelf(), balance0BeforeBurn + uint256(uint128(deltaDecrease.amount0())), 0.0001e18 - ); - assertApproxEqRel( - currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1())), 0.0001e18 - ); + assertEq(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(deltaDecrease.amount0()))); + assertEq(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1()))); // OZ 721 will revert if the token does not exist vm.expectRevert(); lpm.ownerOf(1); // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + // Potentially because we round down in core. I believe this is known in V3. But let's check! assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) - public - { + function test_fuzz_decreaseLiquidity( + IPoolManager.ModifyLiquidityParams memory params, + uint256 decreaseLiquidityDelta + ) public { uint256 tokenId; (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(0 < decreaseLiquidityDelta); @@ -209,7 +206,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); } - function test_decreaseLiquidity_assertCollectedBalance( + function test_fuzz_decreaseLiquidity_assertCollectedBalance( IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta ) public { From decc74515f4978f36749446da268a1e224b228e2 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 15:57:22 -0400 Subject: [PATCH 95/98] other final rand pr comment fixes --- test/position-managers/Execute.t.sol | 6 +++--- test/position-managers/FeeCollection.t.sol | 20 +++++++++---------- .../position-managers/IncreaseLiquidity.t.sol | 8 ++++---- test/position-managers/PositionManager.t.sol | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 30a1c260..1b9dd36c 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -55,7 +55,7 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } - function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { + function test_fuzz_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); mint(range, initialLiquidity, address(this), ZERO_BYTES); @@ -70,7 +70,7 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(liquidity, initialLiquidity + liquidityToAdd); } - function test_execute_increaseLiquidity_twice( + function test_fuzz_execute_increaseLiquidity_twice( uint256 initialiLiquidity, uint256 liquidityToAdd, uint256 liquidityToAdd2 @@ -97,7 +97,7 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { } // this case doesnt make sense in real world usage, so it doesnt have a cool name. but its a good test case - function test_execute_mintAndIncrease(uint256 initialLiquidity, uint256 liquidityToAdd) public { + function test_fuzz_execute_mintAndIncrease(uint256 initialLiquidity, uint256 liquidityToAdd) public { initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 6e2f1141..6c3e1fec 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -48,7 +48,7 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { approvePosmFor(bob); } - function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { + function test_fuzz_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); @@ -69,9 +69,10 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); } - function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) - public - { + function test_fuzz_collect_sameRange_erc20( + IPoolManager.ModifyLiquidityParams memory params, + uint256 liquidityDeltaBob + ) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity @@ -124,14 +125,11 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(uint256(uint128(delta.amount1())), balance1BobAfter - balance1BobBefore); assertTrue(delta.amount1() != 0); - // position manager holds no fees now - assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); - assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); + // position manager should never hold fees + assertEq(manager.balanceOf(address(lpm), currency0.toId()), 0); + assertEq(manager.balanceOf(address(lpm), currency1.toId()), 0); } - function test_collect_donate() public {} - function test_collect_donate_sameRange() public {} - /// @dev Alice and Bob create liquidity on the same range, and decrease their liquidity // Even though their positions are the same range, they are unique positions in pool manager. function test_decreaseLiquidity_sameRange_exact() public { @@ -208,6 +206,8 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { } } + function test_collect_donate() public {} + function test_collect_donate_sameRange() public {} // TODO: ERC6909 Support. function test_collect_6909() public {} function test_collect_sameRange_6909() public {} diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 03c7740c..7d489e91 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -113,7 +113,7 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { vm.stopPrank(); // alice barely spent any tokens - // TODO: This is a case for not caring about dust left in pool manager :/ + // TODO: Use clear. assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance); assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), tolerance); } @@ -159,7 +159,7 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { vm.stopPrank(); // alice barely spent any tokens - // TODO: This is a case for not caring about dust left in pool manager :/ + // TODO: Use clear. assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance); assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), tolerance); } @@ -283,8 +283,8 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { uint256 balance1AfterAlice = currency1.balanceOf(alice); // Alice owed feesOwed amount in 0 and 1 because she places feesOwed * 2 back into the pool. - assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, uint256(int256(feesOwed.amount0())), 37 wei); - assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, uint256(int256(feesOwed.amount1())), 1 wei); + assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, uint256(int256(feesOwed.amount0())), tolerance); + assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, uint256(int256(feesOwed.amount1())), tolerance); } { diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index e0e37336..5b1bb630 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -235,8 +235,8 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); // The change in balance equals the delta returned. - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0())), "boo"); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1())), "guh"); + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); + assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); } function test_initialize() public { From 7dfdbeb97eb13a12ea5f214b87fdbf60ebc2a10e Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 16:09:37 -0400 Subject: [PATCH 96/98] fix off by 1 --- test/position-managers/FeeCollection.t.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 6c3e1fec..5a31c766 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -17,10 +17,13 @@ import {PositionManager} from "../../src/PositionManager.sol"; import {LiquidityRange} from "../../src/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; +import {FeeMath} from "../shared/FeeMath.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; + using FeeMath for IPositionManager; PoolId poolId; address alice = makeAddr("ALICE"); @@ -58,13 +61,16 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { uint256 swapAmount = 0.01e18; swap(key, false, -int256(swapAmount), ZERO_BYTES); + BalanceDelta expectedFees = IPositionManager(address(lpm)).getFeesOwed(manager, tokenId); + // collect fees uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); BalanceDelta delta = collect(tokenId, ZERO_BYTES); - // express key.fee as wad (i.e. 3000 = 0.003e18) - assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); + assertEq(uint256(int256(delta.amount1())), uint256(int256(expectedFees.amount1()))); + assertEq(uint256(int256(delta.amount0())), uint256(int256(expectedFees.amount0()))); + assertEq(uint256(int256(delta.amount0())), currency0.balanceOfSelf() - balance0Before); assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); } From d7c358c057eef3880d8ffdc4b0032cd19bdd372e Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 16:15:29 -0400 Subject: [PATCH 97/98] use bound --- test/position-managers/PositionManager.t.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 5b1bb630..30679d17 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -190,9 +190,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { ) public { uint256 tokenId; (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - vm.assume(0 < decreaseLiquidityDelta); - vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); - vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + decreaseLiquidityDelta = uint256(bound(int256(decreaseLiquidityDelta), 0, params.liquidityDelta)); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); From 04660de9b99e237f67385f5fa17184a3759ff364 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 22 Jul 2024 20:13:16 -0400 Subject: [PATCH 98/98] use nextTokenId --- test/position-managers/Execute.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 1b9dd36c..06f65cd8 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -101,7 +101,7 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - uint256 tokenId = 1; // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity + uint256 tokenId = lpm.nextTokenId(); // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity Planner.Plan memory planner = Planner.init();