diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 7c5efba1..021404a1 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -293336 \ No newline at end of file +291244 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index aad1fd07..10b683f2 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -225695 \ No newline at end of file +223603 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index bfd89eca..3aa28ddf 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -313875 \ No newline at end of file +311783 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 8335b197..c2b3a62d 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -211756 \ No newline at end of file +209314 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 043cac00..a164d000 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -211766 \ No newline at end of file +209326 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 031afb54..a2041485 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -196952 \ No newline at end of file +194862 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 55c77716..553b43e9 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -196964 \ No newline at end of file +194874 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index 5d250ba5..b0807c1b 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -422785 \ No newline at end of file +493163 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap deleted file mode 100644 index 671b63ca..00000000 --- a/.forge-snapshots/mintWithLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -493415 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index ac34f27e..fb12aca9 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {ERC721Permit} from "./base/ERC721Permit.sol"; -import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -20,6 +20,8 @@ import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientSta import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {TransientLiquidityDelta} from "./libraries/TransientLiquidityDelta.sol"; +import "forge-std/console2.sol"; + contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; @@ -36,83 +38,105 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range) mapping(uint256 tokenId => TokenPosition position) public tokenPositions; - // TODO: We won't need this once we move to internal calls. - address internal msgSender; - - function _msgSenderInternal() internal view override returns (address) { - return msgSender; - } - constructor(IPoolManager _manager) BaseLiquidityManagement(_manager) ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} - function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) - public - returns (int128[] memory returnData) - { - // TODO: This will be removed when we use internal calls. Otherwise we need to prevent calls to other code paths and prevent reentrancy or add a queue. - msgSender = msg.sender; - returnData = abi.decode(manager.unlock(abi.encode(data, currencies)), (int128[])); - msgSender = address(0); + /// @param unlockData is an encoding of actions, params, and currencies + /// @return returnData is the endocing of each actions return information + function modifyLiquidities(bytes calldata unlockData) public returns (bytes[] memory) { + // TODO: Edit the encoding/decoding. + return abi.decode(manager.unlock(abi.encode(unlockData, msg.sender)), (bytes[])); } function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { - (bytes[] memory data, Currency[] memory currencies) = abi.decode(payload, (bytes[], Currency[])); + // TODO: Fix double encode/decode + (bytes memory unlockData, address sender) = abi.decode(payload, (bytes, address)); - bool success; + (Actions[] memory actions, bytes[] memory params, Currency[] memory currencies) = + abi.decode(unlockData, (Actions[], bytes[], Currency[])); - for (uint256 i; i < data.length; i++) { - // TODO: Move to internal call and bubble up all call return data. - (success,) = address(this).call(data[i]); - if (!success) revert("EXECUTE_FAILED"); - } + bytes[] memory returnData = _dispatch(actions, params, sender); - // close the final deltas - int128[] memory returnData = new int128[](currencies.length); for (uint256 i; i < currencies.length; i++) { - returnData[i] = currencies[i].close(manager, _msgSenderInternal(), false); // TODO: support claims + currencies[i].close(manager, sender, false); // TODO: support claims currencies[i].close(manager, address(this), true); // position manager always takes 6909 } return abi.encode(returnData); } + function _dispatch(Actions[] memory actions, bytes[] memory params, address sender) + internal + returns (bytes[] memory returnData) + { + if (actions.length != params.length) revert MismatchedLengths(); + + returnData = new bytes[](actions.length); + for (uint256 i; i < actions.length; i++) { + if (actions[i] == Actions.INCREASE) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, uint256, bytes, bool)); + returnData[i] = abi.encode(increaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + } else if (actions[i] == Actions.DECREASE) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, uint256, bytes, bool)); + returnData[i] = abi.encode(decreaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + } else if (actions[i] == Actions.MINT) { + (LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, bytes memory hookData) + = abi.decode(params[i], (LiquidityRange, uint256, uint256, address, bytes)); + (BalanceDelta delta, uint256 tokenId) = mint(range, liquidity, deadline, owner, hookData, sender); + returnData[i] = abi.encode(delta, tokenId); + } else if (actions[i] == Actions.BURN) { + (uint256 tokenId) = abi.decode(params[i], (uint256)); + burn(tokenId, sender); + } else if (actions[i] == Actions.COLLECT) { + (uint256 tokenId, address recipient, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, address, bytes, bool)); + returnData[i] = abi.encode(collect(tokenId, recipient, hookData, claims, sender)); + } else { + revert UnsupportedAction(); + } + } + } + function mint( - LiquidityRange calldata range, + LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, - bytes calldata hookData - ) external payable checkDeadline(deadline) { - _increaseLiquidity(owner, range, liquidity, hookData); + bytes memory hookData, + address sender + ) internal checkDeadline(deadline) returns (BalanceDelta delta, uint256 tokenId) { + delta = _increaseLiquidity(owner, range, liquidity, hookData, sender); // mint receipt token - uint256 tokenId; _mint(owner, (tokenId = nextTokenId++)); tokenPositions[tokenId] = TokenPosition({owner: owner, range: range}); } - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - isAuthorizedForToken(tokenId) + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + delta = _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, sender); } - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - isAuthorizedForToken(tokenId) + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + delta = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } - function burn(uint256 tokenId) public isAuthorizedForToken(tokenId) { + function burn(uint256 tokenId, address sender) internal isAuthorizedForToken(tokenId, sender) { // We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId. TokenPosition memory tokenPos = tokenPositions[tokenId]; // Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. @@ -122,11 +146,14 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit _burn(tokenId); } - // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external { + function collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) + { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _collect(recipient, tokenPos.owner, tokenPos.range, hookData); + delta = _collect(recipient, tokenPos.owner, tokenPos.range, hookData, sender); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { @@ -154,8 +181,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit return uint256(positions[tokenPosition.owner][tokenPosition.range.toId()].nonce++); } - modifier isAuthorizedForToken(uint256 tokenId) { - require(_isApprovedOrOwner(_msgSenderInternal(), tokenId), "Not approved"); + modifier isAuthorizedForToken(uint256 tokenId, address sender) { + require(_isApprovedOrOwner(sender, tokenId), "Not approved"); _; } diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index a6bfaf0f..a72cbad3 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -49,8 +49,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb constructor(IPoolManager _manager) ImmutableState(_manager) {} - function _msgSenderInternal() internal virtual returns (address); - function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) internal returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) @@ -73,8 +71,9 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb address owner, LiquidityRange memory range, uint256 liquidityToAdd, - bytes memory hookData - ) internal { + bytes memory hookData, + address sender + ) internal returns (BalanceDelta) { // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData); @@ -103,11 +102,12 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb } // Accrue all deltas to the caller. - callerDelta.flush(_msgSenderInternal(), range.poolKey.currency0, range.poolKey.currency1); + callerDelta.flush(sender, range.poolKey.currency0, range.poolKey.currency1); thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.addTokensOwed(tokensOwed); position.addLiquidity(liquidityToAdd); + return liquidityDelta; } function _moveCallerDeltaToTokensOwed( @@ -136,7 +136,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb LiquidityRange memory range, uint256 liquidityToRemove, bytes memory hookData - ) internal { + ) internal returns (BalanceDelta) { (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); @@ -166,10 +166,17 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb position.addTokensOwed(tokensOwed); position.subtractLiquidity(liquidityToRemove); + return liquidityDelta; } // The recipient may not be the original owner. - function _collect(address recipient, address owner, LiquidityRange memory range, bytes memory hookData) internal { + function _collect( + address recipient, + address owner, + LiquidityRange memory range, + bytes memory hookData, + address sender + ) internal returns (BalanceDelta) { BalanceDelta callerDelta; BalanceDelta thisDelta; Position storage position = positions[owner][range.toId()]; @@ -196,7 +203,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb callerDelta = callerDelta + tokensOwed; thisDelta = thisDelta - tokensOwed; - if (recipient == _msgSenderInternal()) { + if (recipient == sender) { callerDelta.flush(recipient, range.poolKey.currency0, range.poolKey.currency1); } else { TransientLiquidityDelta.closeDelta( @@ -206,6 +213,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.clearTokensOwed(); + return callerDelta; } function _validateBurn(address owner, LiquidityRange memory range) internal { diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol index 60ae6762..2f626496 100644 --- a/contracts/base/SelfPermit.sol +++ b/contracts/base/SelfPermit.sol @@ -10,7 +10,7 @@ import {ISelfPermit} from "../interfaces/ISelfPermit.sol"; /// @title Self Permit /// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route /// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function -/// that requires an approval in a single transaction. +/// that requires an approval in a single transactions. abstract contract SelfPermit is ISelfPermit { /// @inheritdoc ISelfPermit function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 62acbfd9..92e0f35a 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -4,8 +4,19 @@ pragma solidity ^0.8.24; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; +// TODO: ADD/REMOVE ACTIONS + +enum Actions { + MINT, + BURN, + COLLECT, + INCREASE, + DECREASE +} interface INonfungiblePositionManager { + error MismatchedLengths(); + struct TokenPosition { address owner; LiquidityRange range; @@ -13,51 +24,18 @@ interface INonfungiblePositionManager { error MustBeUnlockedByThisContract(); error DeadlinePassed(); + error UnsupportedAction(); - // NOTE: more gas efficient as LiquidityAmounts is used offchain - function mint( - LiquidityRange calldata position, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes calldata hookData - ) external payable; - - // NOTE: more expensive since LiquidityAmounts is used onchain - // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); - - /// @notice Increase liquidity for an existing position - /// @param tokenId The ID of the position - /// @param liquidity The amount of liquidity to add - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the liquidity increase uses ERC-6909 claim tokens - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; - - /// @notice Decrease liquidity for an existing position - /// @param tokenId The ID of the position - /// @param liquidity The amount of liquidity to remove - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; + /// @notice Batches many liquidity modification calls to pool manager + /// @param payload is an encoding of actions, params, and currencies + /// @return returnData is the endocing of each actions return information + function modifyLiquidities(bytes calldata payload) external returns (bytes[] memory); // TODO Can decide if we want burn to auto encode a decrease/collect. /// @notice Burn a position and delete the tokenId /// @dev It enforces that there is no open liquidity or tokens to be collected /// @param tokenId The ID of the position - function burn(uint256 tokenId) external; - - // TODO: in v3, we can partially collect fees, but what was the usecase here? - /// @notice Collect fees for a position - /// @param tokenId The ID of the position - /// @param recipient The address to send the collected tokens to - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the collected fees are sent as ERC-6909 claim tokens - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external; - - /// @notice Execute a batch of external calls by unlocking the PoolManager - /// @param data an array of abi.encodeWithSelector(, ) for each call - /// @return delta The final delta changes of the caller - function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) external returns (int128[] memory); + // function burn(uint256 tokenId) external; /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees /// @param tokenId The ID of the position diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index b3f9f393..1d2ccc77 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -21,13 +21,16 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; + +import "forge-std/console2.sol"; contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; @@ -35,6 +38,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using SafeCast for uint256; + using Planner for Planner.Plan; PoolId poolId; address alice = makeAddr("ALICE"); @@ -82,15 +86,7 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit _mint(range, initialLiquidity, block.timestamp, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false - ); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); + _increaseLiquidity(tokenId, liquidityToAdd, ZERO_BYTES, false); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialLiquidity + liquidityToAdd); @@ -107,49 +103,45 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Liquidit _mint(range, initialiLiquidity, block.timestamp, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false - ); - data[1] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd2, ZERO_BYTES, false - ); + Planner.Plan memory planner = Planner.init(); + + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES, false)); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd2, ZERO_BYTES, false)); + + console2.log("action"); + console2.log(uint8(planner.actions[0])); + console2.log(uint8(planner.actions[1])); Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, initialiLiquidity + liquidityToAdd + liquidityToAdd2); } // this case doesnt make sense in real world usage, so it doesnt have a cool name. but its a good test case - function test_execute_mintAndIncrease(uint256 intialLiquidity, uint256 liquidityToAdd) public { - intialLiquidity = bound(intialLiquidity, 1e18, 1000e18); + function test_execute_mintAndIncrease(uint256 initialLiquidity, uint256 liquidityToAdd) public { + initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); uint256 tokenId = 1; // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity - bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.mint.selector, - range, - intialLiquidity, - block.timestamp + 1, - address(this), - ZERO_BYTES - ); - data[1] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false + + Planner.Plan memory planner = Planner.init(); + + planner = planner.add( + Actions.MINT, abi.encode(range, initialLiquidity, block.timestamp + 1, address(this), ZERO_BYTES) ); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, ZERO_BYTES, false)); Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, intialLiquidity + liquidityToAdd); + assertEq(liquidity, initialLiquidity + liquidityToAdd); } // rebalance: burn and mint diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index fe85b408..22d99a05 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -70,7 +70,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // // swap to create fees @@ -90,7 +90,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees @@ -102,11 +102,9 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li uint256 balance1Before = currency1.balanceOfSelf(); BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES, false); - assertEq(delta.amount0(), 0); - // express key.fee as wad (i.e. 3000 = 0.003e18) assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); - + assertEq(uint256(int256(delta.amount0())), currency0.balanceOfSelf() - balance0Before); assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); } diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 81616e2e..ce9989f0 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -20,16 +20,19 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; + using Planner for Planner.Plan; PoolId poolId; address alice = makeAddr("ALICE"); @@ -76,47 +79,29 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } - // function test_gas_mint() public { - // uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - // uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // snapStart("mint"); - // lpm.mint(params); - // snapLastCall(); - // } - - function test_gas_mintWithLiquidity() public { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES + function test_gas_mint() public { + Planner.Plan memory plan = Planner.init().add( + Actions.MINT, abi.encode(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES) ); Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); - snapLastCall("mintWithLiquidity"); + lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies, currencies)); + snapLastCall("mint"); } function test_gas_increaseLiquidity_erc20() public { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("increaseLiquidity_erc20"); } @@ -124,13 +109,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, true)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("increaseLiquidity_erc6909"); } @@ -165,15 +151,15 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { token1Owed ); - bytes[] memory calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("autocompound_exactUnclaimedFees"); } @@ -197,14 +183,16 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); // bob collects fees so some of alice's fees are now cached - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenIdBob, bob, ZERO_BYTES, false); + + Planner.Plan memory planner = + Planner.init().add(Actions.COLLECT, abi.encode(tokenIdBob, bob, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(bob); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); // donate to create more fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -222,15 +210,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { newToken1Owed ); - calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + planner = Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); } } @@ -250,7 +237,6 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { // bob provides liquidity vm.prank(bob); _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -267,15 +253,15 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { token1Owed / 2 ); - bytes[] memory calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.INCREASE, abi.encode(tokenIdAlice, liquidityDelta, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("autocompound_excessFeesCredit"); } @@ -283,13 +269,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Planner.Plan memory planner = + Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, false)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("decreaseLiquidity_erc20"); } @@ -297,13 +284,14 @@ contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Planner.Plan memory planner = + Planner.init().add(Actions.DECREASE, abi.encode(tokenId, 10_000 ether, ZERO_BYTES, true)); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); snapLastCall("decreaseLiquidity_erc6909"); } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 39a6e329..9f0bec24 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -27,6 +27,8 @@ import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import "forge-std/console2.sol"; + contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -327,7 +329,6 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity uint256 liquidityAlice = 3_000e18; uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; // alice provides liquidity vm.prank(alice); @@ -347,10 +348,10 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); // bob collects fees so some of alice's fees are now cached + vm.startPrank(bob); _collect(tokenIdBob, bob, ZERO_BYTES, false); vm.stopPrank(); - // swap to create more fees swap(key, true, -int256(swapAmount), ZERO_BYTES); swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back @@ -395,7 +396,6 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity uint256 liquidityAlice = 3_000e18; uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; // alice provides liquidity vm.prank(alice); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index f652bc93..959f5d4d 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -14,22 +14,27 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; + using Planner for Planner.Plan; PoolId poolId; address alice = makeAddr("ALICE"); @@ -46,117 +51,117 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); } + function test_modifyLiquidities_reverts_mismatchedLengths() public { + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.MINT, abi.encode("test")); + planner = planner.add(Actions.BURN, abi.encode("test")); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + bytes[] memory badParams = new bytes[](1); + + vm.expectRevert(INonfungiblePositionManager.MismatchedLengths.selector); + lpm.modifyLiquidities(abi.encode(planner.actions, badParams, currencies)); + } + function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + // liquidity is a uint + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) + ); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + + (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); + + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(tokenId), address(this)); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta)); + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())), "incorrect amount1"); + } + + function test_mint_exactTokenRatios() public { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0Desired, + amount1Desired ); + + LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - BalanceDelta delta = toBalanceDelta(result[0], result[1]); + + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) + ); + + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); + assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, uint256(params.liquidityDelta)); - assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0())), "incorrect amount0"); - assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); + assertEq(uint256(int256(-delta.amount0())), amount0Desired); + assertEq(uint256(int256(-delta.amount1())), amount1Desired); + assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); } - // function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + function test_mint_recipient(IPoolManager.ModifyLiquidityParams memory seedParams) public { + IPoolManager.ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // uint256 balance0After = currency0.balanceOfSelf(); - // uint256 balance1After = currency1.balanceOfSelf(); - - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - // // minting with perfect token ratios will use all of the tokens - // function test_mint_perfect() public { - // int24 tickLower = -int24(key.tickSpacing); - // int24 tickUpper = int24(key.tickSpacing); - // uint256 amount0Desired = 100e18; - // uint256 amount1Desired = 100e18; - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), alice, ZERO_BYTES) + ); - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: amount0Desired, - // amount1Min: amount1Desired, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // uint256 balance0After = currency0.balanceOfSelf(); - // uint256 balance1After = currency1.balanceOfSelf(); - - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(uint256(int256(-delta.amount0())), amount0Desired); - // assertEq(uint256(int256(-delta.amount1())), amount1Desired); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; - // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - // public - // { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + bytes[] memory results = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: alice, - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId,) = lpm.mint(params); - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(tokenId), alice); - // } + (, uint256 tokenId) = abi.decode(results[0], (BalanceDelta, uint256)); + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(tokenId), alice); + } // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) // public @@ -210,7 +215,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // create liquidity we can burn uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); @@ -224,7 +229,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // TODO, encode this under one call BalanceDelta deltaDecrease = _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); BalanceDelta deltaCollect = _collect(tokenId, address(this), ZERO_BYTES, false); - lpm.burn(tokenId); + _burn(tokenId); (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, 0); @@ -249,11 +254,11 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + function test_decreaseLiquidity1(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) public { uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(0 < decreaseLiquidityDelta); vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); @@ -263,13 +268,14 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + _decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); + // On decrease, balance doesn't change (currenct functionality). + assertEq(currency0.balanceOfSelf() - balance0Before, 0); + assertEq(currency1.balanceOfSelf() - balance1Before, 0); } // function test_decreaseLiquidity_collectFees( @@ -277,7 +283,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // uint256 decreaseLiquidityDelta // ) public { // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // vm.assume(0 < decreaseLiquidityDelta); // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 38867ea9..c122ea9b 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -4,12 +4,15 @@ pragma solidity ^0.8.24; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {NonfungiblePositionManager, Actions} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; +import {Planner} from "../utils/Planner.sol"; contract LiquidityOperations { NonfungiblePositionManager lpm; + using Planner for Planner.Plan; + function _mint( LiquidityRange memory _range, uint256 liquidity, @@ -17,56 +20,64 @@ contract LiquidityOperations { address recipient, bytes memory hookData ) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, deadline, recipient, hookData)); + Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); } function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); } function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); } function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.COLLECT, abi.encode(tokenId, recipient, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); + } + + function _burn(uint256 tokenId) internal { + Currency[] memory currencies = new Currency[](0); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.BURN, abi.encode(tokenId)); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); } } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index fd22c3b2..5def37bc 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -7,10 +7,13 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; +import {Planner} from "../../utils/Planner.sol"; contract LiquidityFuzzers is Fuzzers { + using Planner for Planner.Plan; + function createFuzzyLiquidity( INonfungiblePositionManager lpm, address recipient, @@ -18,25 +21,22 @@ contract LiquidityFuzzers is Fuzzers { IPoolManager.ModifyLiquidityParams memory params, uint160 sqrtPriceX96, bytes memory hookData - ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { + ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory) { params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); - LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData + Planner.Plan memory plan = Planner.init().add( + Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData) ); Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - BalanceDelta delta = toBalanceDelta(result[0], result[1]); + lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies)); uint256 tokenId = lpm.nextTokenId() - 1; - return (tokenId, params, delta); + return (tokenId, params); } } diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol new file mode 100644 index 00000000..788622c5 --- /dev/null +++ b/test/utils/Planner.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; + +library Planner { + struct Plan { + Actions[] actions; + bytes[] params; + } + + function init() public pure returns (Plan memory plan) { + return Plan({actions: new Actions[](0), params: new bytes[](0)}); + } + + function add(Plan memory plan, Actions action, bytes memory param) public pure returns (Plan memory) { + Actions[] memory actions = new Actions[](plan.actions.length + 1); + bytes[] memory params = new bytes[](plan.params.length + 1); + + for (uint256 i; i < actions.length - 1; i++) { + // Copy from plan. + actions[i] = plan.actions[i]; + params[i] = plan.params[i]; + } + + actions[actions.length - 1] = action; + params[params.length - 1] = param; + + return Plan({actions: actions, params: params}); + } +}