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); }