From 6c63e1fc19d302a2169d76b2152648b2eb954aeb Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Tue, 5 Dec 2023 12:53:29 -0500 Subject: [PATCH 01/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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/61] 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 916cbaa45b4522e04d1b740ed4374258c830c7b9 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Wed, 10 Jul 2024 10:57:33 -0400 Subject: [PATCH 61/61] using internal calls, first pass --- .../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 | 114 ++-- contracts/base/BaseLiquidityManagement.sol | 24 +- contracts/base/SelfPermit.sol | 2 +- .../INonfungiblePositionManager.sol | 54 +- test/position-managers/Execute.t.sol | 362 +++++----- test/position-managers/FeeCollection.t.sol | 4 +- test/position-managers/Gas.t.sol | 626 +++++++++--------- .../position-managers/IncreaseLiquidity.t.sol | 80 +-- .../NonfungiblePositionManager.t.sol | 137 ++-- test/shared/LiquidityOperations.sol | 43 +- test/shared/fuzz/LiquidityFuzzers.sol | 18 +- test/utils/Planner.sol | 34 + 20 files changed, 781 insertions(+), 733 deletions(-) create mode 100644 test/utils/Planner.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 7c5efba1..25c6c9a1 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -293336 \ No newline at end of file +208172 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index aad1fd07..d2cec2fa 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -225695 \ No newline at end of file +129307 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index bfd89eca..2ca8e9b9 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -313875 \ No newline at end of file +228687 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 8335b197..539775ae 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -211756 \ No newline at end of file +150019 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 043cac00..539775ae 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -211766 \ No newline at end of file +150019 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 031afb54..227ab8f7 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -196952 \ No newline at end of file +96576 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 55c77716..227ab8f7 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -196964 \ No newline at end of file +96576 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 671b63ca..2848a86d 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -493415 \ No newline at end of file +487667 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index ac34f27e..b71d633b 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"; @@ -36,83 +36,104 @@ 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) + { + 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 +143,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 +178,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..40047a33 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -4,6 +4,15 @@ 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 { struct TokenPosition { @@ -13,51 +22,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..9c568936 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -1,180 +1,182 @@ -// 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"; - -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; - - 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); - _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); - - (,, 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); - _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 - ); - - 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); - } - - // 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 - ); - - 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); - } - - // 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 {} -} +// // 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"; + +// import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +// import {Planner} from "../utils/Planner.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; +// using Planner for Planner.Plan; + +// 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); +// _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); + +// (,, 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); +// _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 +// ); + +// 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); +// } + +// // 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 +// ); + +// 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); +// } + +// // 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 {} +// } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index fe85b408..a6e48e18 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 diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 81616e2e..63c6b48d 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -1,313 +1,313 @@ -// 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 {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 {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; - - 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(); - - // 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({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 - ); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); - snapLastCall("mintWithLiquidity"); - } - - 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); - 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 { - _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); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - lpm.modifyLiquidities(calls, currencies); - 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); - _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); - - // 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 - ); - - 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.modifyLiquidities(calls, currencies); - 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); - _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); - - // 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.modifyLiquidities(calls, currencies); - - // 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 - ); - - 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.modifyLiquidities(calls, currencies); - 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); - _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); - - // 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 - ); - - 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.modifyLiquidities(calls, currencies); - snapLastCall("autocompound_excessFeesCredit"); - } - - function test_gas_decreaseLiquidity_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.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 { - _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); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - lpm.modifyLiquidities(calls, currencies); - snapLastCall("decreaseLiquidity_erc6909"); - } - - function test_gas_burn() public {} - function test_gas_burnEmpty() public {} - function test_gas_collect() public {} -} +// // 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 {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 {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; + +// 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(); + +// // 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({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 +// ); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("mintWithLiquidity"); +// } + +// 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); +// 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 { +// _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); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// lpm.modifyLiquidities(calls, currencies); +// 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); +// _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); + +// // 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 +// ); + +// 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.modifyLiquidities(calls, currencies); +// 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); +// _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); + +// // 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.modifyLiquidities(calls, currencies); + +// // 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 +// ); + +// 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.modifyLiquidities(calls, currencies); +// 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); +// _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); + +// // 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 +// ); + +// 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.modifyLiquidities(calls, currencies); +// snapLastCall("autocompound_excessFeesCredit"); +// } + +// function test_gas_decreaseLiquidity_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.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 { +// _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); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("decreaseLiquidity_erc6909"); +// } + +// function test_gas_burn() public {} +// function test_gas_burnEmpty() public {} +// function test_gas_collect() public {} +// } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 39a6e329..8f91a055 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; @@ -73,7 +75,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) @@ -322,7 +324,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi } } - function test_increaseLiquidity_withExactFees_withExactCachedFees() public { + function test_increaseLiquidity_withExactFees_withExactCachedFees1() 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; @@ -347,46 +349,46 @@ 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 - - (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); + //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 diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index f652bc93..7892f4b3 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -14,22 +14,29 @@ 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"; + +import "forge-std/console2.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"); @@ -48,92 +55,75 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi 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}); - 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 + 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; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - BalanceDelta delta = toBalanceDelta(result[0], result[1]); - uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency1.balanceOfSelf(); + 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)); 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(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - currency1.balanceOfSelf(), 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); + 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}); + LiquidityRange memory range = LiquidityRange({poolKey: 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(); + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = 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()))); - // } + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; - // // 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), address(this), 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(); + 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(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()))); - // } + 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 @@ -210,7 +200,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 +214,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 +239,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); @@ -268,8 +258,9 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi (,, 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 +268,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..683e5919 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)); + bytes[] memory result = 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..302c0f83 --- /dev/null +++ b/test/utils/Planner.sol @@ -0,0 +1,34 @@ +// 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 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 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++) { + actions[i] = actions[i]; + params[i] = params[i]; + } + + actions[actions.length - 1] = action; + params[params.length - 1] = param; + + return Plan({actions: actions, params: params}); + } + + function zip(Plan memory plan) public returns (bytes memory) { + return abi.encode(plan.actions, plan.params); + } +}