From 916cbaa45b4522e04d1b740ed4374258c830c7b9 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Wed, 10 Jul 2024 10:57:33 -0400 Subject: [PATCH] using internal calls, first pass --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 114 ++-- contracts/base/BaseLiquidityManagement.sol | 24 +- contracts/base/SelfPermit.sol | 2 +- .../INonfungiblePositionManager.sol | 54 +- test/position-managers/Execute.t.sol | 362 +++++----- test/position-managers/FeeCollection.t.sol | 4 +- test/position-managers/Gas.t.sol | 626 +++++++++--------- .../position-managers/IncreaseLiquidity.t.sol | 80 +-- .../NonfungiblePositionManager.t.sol | 137 ++-- test/shared/LiquidityOperations.sol | 43 +- test/shared/fuzz/LiquidityFuzzers.sol | 18 +- test/utils/Planner.sol | 34 + 20 files changed, 781 insertions(+), 733 deletions(-) create mode 100644 test/utils/Planner.sol diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 7c5efba1..25c6c9a1 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -293336 \ No newline at end of file +208172 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index aad1fd07..d2cec2fa 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -225695 \ No newline at end of file +129307 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index bfd89eca..2ca8e9b9 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -313875 \ No newline at end of file +228687 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 8335b197..539775ae 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -211756 \ No newline at end of file +150019 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 043cac00..539775ae 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -211766 \ No newline at end of file +150019 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 031afb54..227ab8f7 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -196952 \ No newline at end of file +96576 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 55c77716..227ab8f7 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -196964 \ No newline at end of file +96576 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 671b63ca..2848a86d 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -493415 \ No newline at end of file +487667 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index ac34f27e..b71d633b 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {ERC721Permit} from "./base/ERC721Permit.sol"; -import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "./interfaces/INonfungiblePositionManager.sol"; import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -36,83 +36,104 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range) mapping(uint256 tokenId => TokenPosition position) public tokenPositions; - // TODO: We won't need this once we move to internal calls. - address internal msgSender; - - function _msgSenderInternal() internal view override returns (address) { - return msgSender; - } - constructor(IPoolManager _manager) BaseLiquidityManagement(_manager) ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} - function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) - public - returns (int128[] memory returnData) - { - // TODO: This will be removed when we use internal calls. Otherwise we need to prevent calls to other code paths and prevent reentrancy or add a queue. - msgSender = msg.sender; - returnData = abi.decode(manager.unlock(abi.encode(data, currencies)), (int128[])); - msgSender = address(0); + /// @param unlockData is an encoding of actions, params, and currencies + /// @return returnData is the endocing of each actions return information + function modifyLiquidities(bytes calldata unlockData) public returns (bytes[] memory) { + // TODO: Edit the encoding/decoding. + return abi.decode(manager.unlock(abi.encode(unlockData, msg.sender)), (bytes[])); } function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { - (bytes[] memory data, Currency[] memory currencies) = abi.decode(payload, (bytes[], Currency[])); + // TODO: Fix double encode/decode + (bytes memory unlockData, address sender) = abi.decode(payload, (bytes, address)); - bool success; + (Actions[] memory actions, bytes[] memory params, Currency[] memory currencies) = + abi.decode(unlockData, (Actions[], bytes[], Currency[])); - for (uint256 i; i < data.length; i++) { - // TODO: Move to internal call and bubble up all call return data. - (success,) = address(this).call(data[i]); - if (!success) revert("EXECUTE_FAILED"); - } + bytes[] memory returnData = _dispatch(actions, params, sender); - // close the final deltas - int128[] memory returnData = new int128[](currencies.length); for (uint256 i; i < currencies.length; i++) { - returnData[i] = currencies[i].close(manager, _msgSenderInternal(), false); // TODO: support claims + currencies[i].close(manager, sender, false); // TODO: support claims currencies[i].close(manager, address(this), true); // position manager always takes 6909 } return abi.encode(returnData); } + function _dispatch(Actions[] memory actions, bytes[] memory params, address sender) + internal + returns (bytes[] memory returnData) + { + returnData = new bytes[](actions.length); + + for (uint256 i; i < actions.length; i++) { + if (actions[i] == Actions.INCREASE) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, uint256, bytes, bool)); + returnData[i] = abi.encode(increaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + } else if (actions[i] == Actions.DECREASE) { + (uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, uint256, bytes, bool)); + returnData[i] = abi.encode(decreaseLiquidity(tokenId, liquidity, hookData, claims, sender)); + } else if (actions[i] == Actions.MINT) { + (LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, bytes memory hookData) + = abi.decode(params[i], (LiquidityRange, uint256, uint256, address, bytes)); + (BalanceDelta delta, uint256 tokenId) = mint(range, liquidity, deadline, owner, hookData, sender); + returnData[i] = abi.encode(delta, tokenId); + } else if (actions[i] == Actions.BURN) { + (uint256 tokenId) = abi.decode(params[i], (uint256)); + burn(tokenId, sender); + } else if (actions[i] == Actions.COLLECT) { + (uint256 tokenId, address recipient, bytes memory hookData, bool claims) = + abi.decode(params[i], (uint256, address, bytes, bool)); + returnData[i] = abi.encode(collect(tokenId, recipient, hookData, claims, sender)); + } else { + revert UnsupportedAction(); + } + } + } + function mint( - LiquidityRange calldata range, + LiquidityRange memory range, uint256 liquidity, uint256 deadline, address owner, - bytes calldata hookData - ) external payable checkDeadline(deadline) { - _increaseLiquidity(owner, range, liquidity, hookData); + bytes memory hookData, + address sender + ) internal checkDeadline(deadline) returns (BalanceDelta delta, uint256 tokenId) { + delta = _increaseLiquidity(owner, range, liquidity, hookData, sender); // mint receipt token - uint256 tokenId; _mint(owner, (tokenId = nextTokenId++)); tokenPositions[tokenId] = TokenPosition({owner: owner, range: range}); } - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - isAuthorizedForToken(tokenId) + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + delta = _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, sender); } - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - isAuthorizedForToken(tokenId) + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); + delta = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } - function burn(uint256 tokenId) public isAuthorizedForToken(tokenId) { + function burn(uint256 tokenId, address sender) internal isAuthorizedForToken(tokenId, sender) { // We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId. TokenPosition memory tokenPos = tokenPositions[tokenId]; // Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. @@ -122,11 +143,14 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit _burn(tokenId); } - // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external { + function collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims, address sender) + internal + isAuthorizedForToken(tokenId, sender) + returns (BalanceDelta delta) + { TokenPosition memory tokenPos = tokenPositions[tokenId]; - _collect(recipient, tokenPos.owner, tokenPos.range, hookData); + delta = _collect(recipient, tokenPos.owner, tokenPos.range, hookData, sender); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { @@ -154,8 +178,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit return uint256(positions[tokenPosition.owner][tokenPosition.range.toId()].nonce++); } - modifier isAuthorizedForToken(uint256 tokenId) { - require(_isApprovedOrOwner(_msgSenderInternal(), tokenId), "Not approved"); + modifier isAuthorizedForToken(uint256 tokenId, address sender) { + require(_isApprovedOrOwner(sender, tokenId), "Not approved"); _; } diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index a6bfaf0f..a72cbad3 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -49,8 +49,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb constructor(IPoolManager _manager) ImmutableState(_manager) {} - function _msgSenderInternal() internal virtual returns (address); - function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) internal returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) @@ -73,8 +71,9 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb address owner, LiquidityRange memory range, uint256 liquidityToAdd, - bytes memory hookData - ) internal { + bytes memory hookData, + address sender + ) internal returns (BalanceDelta) { // Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes. (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData); @@ -103,11 +102,12 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb } // Accrue all deltas to the caller. - callerDelta.flush(_msgSenderInternal(), range.poolKey.currency0, range.poolKey.currency1); + callerDelta.flush(sender, range.poolKey.currency0, range.poolKey.currency1); thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.addTokensOwed(tokensOwed); position.addLiquidity(liquidityToAdd); + return liquidityDelta; } function _moveCallerDeltaToTokensOwed( @@ -136,7 +136,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb LiquidityRange memory range, uint256 liquidityToRemove, bytes memory hookData - ) internal { + ) internal returns (BalanceDelta) { (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) = _modifyLiquidity(owner, range, -(liquidityToRemove.toInt256()), hookData); @@ -166,10 +166,17 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb position.addTokensOwed(tokensOwed); position.subtractLiquidity(liquidityToRemove); + return liquidityDelta; } // The recipient may not be the original owner. - function _collect(address recipient, address owner, LiquidityRange memory range, bytes memory hookData) internal { + function _collect( + address recipient, + address owner, + LiquidityRange memory range, + bytes memory hookData, + address sender + ) internal returns (BalanceDelta) { BalanceDelta callerDelta; BalanceDelta thisDelta; Position storage position = positions[owner][range.toId()]; @@ -196,7 +203,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb callerDelta = callerDelta + tokensOwed; thisDelta = thisDelta - tokensOwed; - if (recipient == _msgSenderInternal()) { + if (recipient == sender) { callerDelta.flush(recipient, range.poolKey.currency0, range.poolKey.currency1); } else { TransientLiquidityDelta.closeDelta( @@ -206,6 +213,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.clearTokensOwed(); + return callerDelta; } function _validateBurn(address owner, LiquidityRange memory range) internal { diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol index 60ae6762..2f626496 100644 --- a/contracts/base/SelfPermit.sol +++ b/contracts/base/SelfPermit.sol @@ -10,7 +10,7 @@ import {ISelfPermit} from "../interfaces/ISelfPermit.sol"; /// @title Self Permit /// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route /// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function -/// that requires an approval in a single transaction. +/// that requires an approval in a single transactions. abstract contract SelfPermit is ISelfPermit { /// @inheritdoc ISelfPermit function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 62acbfd9..40047a33 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -4,6 +4,15 @@ pragma solidity ^0.8.24; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; +// TODO: ADD/REMOVE ACTIONS + +enum Actions { + MINT, + BURN, + COLLECT, + INCREASE, + DECREASE +} interface INonfungiblePositionManager { struct TokenPosition { @@ -13,51 +22,18 @@ interface INonfungiblePositionManager { error MustBeUnlockedByThisContract(); error DeadlinePassed(); + error UnsupportedAction(); - // NOTE: more gas efficient as LiquidityAmounts is used offchain - function mint( - LiquidityRange calldata position, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes calldata hookData - ) external payable; - - // NOTE: more expensive since LiquidityAmounts is used onchain - // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); - - /// @notice Increase liquidity for an existing position - /// @param tokenId The ID of the position - /// @param liquidity The amount of liquidity to add - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the liquidity increase uses ERC-6909 claim tokens - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; - - /// @notice Decrease liquidity for an existing position - /// @param tokenId The ID of the position - /// @param liquidity The amount of liquidity to remove - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; + /// @notice Batches many liquidity modification calls to pool manager + /// @param payload is an encoding of actions, params, and currencies + /// @return returnData is the endocing of each actions return information + function modifyLiquidities(bytes calldata payload) external returns (bytes[] memory); // TODO Can decide if we want burn to auto encode a decrease/collect. /// @notice Burn a position and delete the tokenId /// @dev It enforces that there is no open liquidity or tokens to be collected /// @param tokenId The ID of the position - function burn(uint256 tokenId) external; - - // TODO: in v3, we can partially collect fees, but what was the usecase here? - /// @notice Collect fees for a position - /// @param tokenId The ID of the position - /// @param recipient The address to send the collected tokens to - /// @param hookData Arbitrary data passed to the hook - /// @param claims Whether the collected fees are sent as ERC-6909 claim tokens - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external; - - /// @notice Execute a batch of external calls by unlocking the PoolManager - /// @param data an array of abi.encodeWithSelector(, ) for each call - /// @return delta The final delta changes of the caller - function modifyLiquidities(bytes[] memory data, Currency[] memory currencies) external returns (int128[] memory); + // function burn(uint256 tokenId) external; /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees /// @param tokenId The ID of the position diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index b3f9f393..9c568936 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -1,180 +1,182 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; - -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; - -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; - -import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; - -contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { - using FixedPointMathLib for uint256; - using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; - using PoolIdLibrary for PoolKey; - using SafeCast for uint256; - - PoolId poolId; - address alice = makeAddr("ALICE"); - address bob = makeAddr("BOB"); - - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) - uint256 FEE_WAD; - - LiquidityRange range; - - function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); - - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - lpm = new NonfungiblePositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - - // define a reusable range - range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); - } - - function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { - initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); - liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - _mint(range, initialLiquidity, block.timestamp, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; - - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false - ); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); - - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, initialLiquidity + liquidityToAdd); - } - - function test_execute_increaseLiquidity_twice( - uint256 initialiLiquidity, - uint256 liquidityToAdd, - uint256 liquidityToAdd2 - ) public { - initialiLiquidity = bound(initialiLiquidity, 1e18, 1000e18); - liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); - _mint(range, initialiLiquidity, block.timestamp, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; - - bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false - ); - data[1] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd2, ZERO_BYTES, false - ); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); - - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, initialiLiquidity + liquidityToAdd + liquidityToAdd2); - } - - // this case doesnt make sense in real world usage, so it doesnt have a cool name. but its a good test case - function test_execute_mintAndIncrease(uint256 intialLiquidity, uint256 liquidityToAdd) public { - intialLiquidity = bound(intialLiquidity, 1e18, 1000e18); - liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - - uint256 tokenId = 1; // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity - bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector( - INonfungiblePositionManager.mint.selector, - range, - intialLiquidity, - block.timestamp + 1, - address(this), - ZERO_BYTES - ); - data[1] = abi.encodeWithSelector( - INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false - ); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(data, currencies); - - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, intialLiquidity + liquidityToAdd); - } - - // rebalance: burn and mint - function test_execute_rebalance() public {} - // coalesce: burn and increase - function test_execute_coalesce() public {} - // split: decrease and mint - function test_execute_split() public {} - // shift: decrease and increase - function test_execute_shift() public {} - // shard: collect and mint - function test_execute_shard() public {} - // feed: collect and increase - function test_execute_feed() public {} - - // transplant: burn and mint on different keys - function test_execute_transplant() public {} - // cross-coalesce: burn and increase on different keys - function test_execute_crossCoalesce() public {} - // cross-split: decrease and mint on different keys - function test_execute_crossSplit() public {} - // cross-shift: decrease and increase on different keys - function test_execute_crossShift() public {} - // cross-shard: collect and mint on different keys - function test_execute_crossShard() public {} - // cross-feed: collect and increase on different keys - function test_execute_crossFeed() public {} -} +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.24; + +// import "forge-std/Test.sol"; +// import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +// import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +// import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +// import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +// import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +// import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +// import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +// import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +// import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +// import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +// import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +// import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +// import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +// import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +// import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +// import {IERC20} from "forge-std/interfaces/IERC20.sol"; +// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; +// import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +// import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +// import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +// import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +// import {Planner} from "../utils/Planner.sol"; + +// contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { +// using FixedPointMathLib for uint256; +// using CurrencyLibrary for Currency; +// using LiquidityRangeIdLibrary for LiquidityRange; +// using PoolIdLibrary for PoolKey; +// using SafeCast for uint256; +// using Planner for Planner.Plan; + +// PoolId poolId; +// address alice = makeAddr("ALICE"); +// address bob = makeAddr("BOB"); + +// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + +// // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) +// uint256 FEE_WAD; + +// LiquidityRange range; + +// function setUp() public { +// Deployers.deployFreshManagerAndRouters(); +// Deployers.deployMintAndApprove2Currencies(); + +// (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); +// FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + +// lpm = new NonfungiblePositionManager(manager); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + +// // Give tokens to Alice and Bob, with approvals +// IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); +// vm.startPrank(alice); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); +// vm.stopPrank(); +// vm.startPrank(bob); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); +// vm.stopPrank(); + +// // define a reusable range +// range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); +// } + +// function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { +// initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); +// liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); +// _mint(range, initialLiquidity, block.timestamp, address(this), ZERO_BYTES); +// uint256 tokenId = lpm.nextTokenId() - 1; + +// bytes[] memory data = new bytes[](1); +// data[0] = abi.encodeWithSelector( +// INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false +// ); + +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; +// lpm.modifyLiquidities(data, currencies); + +// (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); +// assertEq(liquidity, initialLiquidity + liquidityToAdd); +// } + +// function test_execute_increaseLiquidity_twice( +// uint256 initialiLiquidity, +// uint256 liquidityToAdd, +// uint256 liquidityToAdd2 +// ) public { +// initialiLiquidity = bound(initialiLiquidity, 1e18, 1000e18); +// liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); +// liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); +// _mint(range, initialiLiquidity, block.timestamp, address(this), ZERO_BYTES); +// uint256 tokenId = lpm.nextTokenId() - 1; + +// bytes[] memory data = new bytes[](2); +// data[0] = abi.encodeWithSelector( +// INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false +// ); +// data[1] = abi.encodeWithSelector( +// INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd2, ZERO_BYTES, false +// ); + +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; +// lpm.modifyLiquidities(data, currencies); + +// (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); +// assertEq(liquidity, initialiLiquidity + liquidityToAdd + liquidityToAdd2); +// } + +// // this case doesnt make sense in real world usage, so it doesnt have a cool name. but its a good test case +// function test_execute_mintAndIncrease(uint256 intialLiquidity, uint256 liquidityToAdd) public { +// intialLiquidity = bound(intialLiquidity, 1e18, 1000e18); +// liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + +// uint256 tokenId = 1; // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity +// bytes[] memory data = new bytes[](2); +// data[0] = abi.encodeWithSelector( +// INonfungiblePositionManager.mint.selector, +// range, +// intialLiquidity, +// block.timestamp + 1, +// address(this), +// ZERO_BYTES +// ); +// data[1] = abi.encodeWithSelector( +// INonfungiblePositionManager.increaseLiquidity.selector, tokenId, liquidityToAdd, ZERO_BYTES, false +// ); + +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; +// lpm.modifyLiquidities(data, currencies); + +// (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); +// assertEq(liquidity, intialLiquidity + liquidityToAdd); +// } + +// // rebalance: burn and mint +// function test_execute_rebalance() public {} +// // coalesce: burn and increase +// function test_execute_coalesce() public {} +// // split: decrease and mint +// function test_execute_split() public {} +// // shift: decrease and increase +// function test_execute_shift() public {} +// // shard: collect and mint +// function test_execute_shard() public {} +// // feed: collect and increase +// function test_execute_feed() public {} + +// // transplant: burn and mint on different keys +// function test_execute_transplant() public {} +// // cross-coalesce: burn and increase on different keys +// function test_execute_crossCoalesce() public {} +// // cross-split: decrease and mint on different keys +// function test_execute_crossSplit() public {} +// // cross-shift: decrease and increase on different keys +// function test_execute_crossShift() public {} +// // cross-shard: collect and mint on different keys +// function test_execute_crossShard() public {} +// // cross-feed: collect and increase on different keys +// function test_execute_crossFeed() public {} +// } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index fe85b408..a6e48e18 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -70,7 +70,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li // function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // // swap to create fees @@ -90,7 +90,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, Li function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 81616e2e..63c6b48d 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -1,313 +1,313 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; - -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; - -import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; - -contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { - using FixedPointMathLib for uint256; - using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; - using PoolIdLibrary for PoolKey; - - PoolId poolId; - address alice = makeAddr("ALICE"); - address bob = makeAddr("BOB"); - - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) - uint256 FEE_WAD; - - LiquidityRange range; - - function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); - - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - lpm = new NonfungiblePositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - - // mint some ERC6909 tokens - claimsRouter.deposit(currency0, address(this), 100_000_000 ether); - claimsRouter.deposit(currency1, address(this), 100_000_000 ether); - manager.setOperator(address(lpm), true); - - // define a reusable range - range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); - } - - // function test_gas_mint() public { - // uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - // uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // snapStart("mint"); - // lpm.mint(params); - // snapLastCall(); - // } - - function test_gas_mintWithLiquidity() public { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES - ); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.modifyLiquidities(calls, currencies); - snapLastCall("mintWithLiquidity"); - } - - function test_gas_increaseLiquidity_erc20() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; - - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - lpm.modifyLiquidities(calls, currencies); - snapLastCall("increaseLiquidity_erc20"); - } - - function test_gas_increaseLiquidity_erc6909() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; - - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - lpm.modifyLiquidities(calls, currencies); - snapLastCall("increaseLiquidity_erc6909"); - } - - function test_gas_autocompound_exactUnclaimedFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her exact fees to increase liquidity (compounding) - - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - - // donate to create fees - donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); - - // alice uses her exact fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed, - token1Owed - ); - - bytes[] memory calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); - snapLastCall("autocompound_exactUnclaimedFees"); - } - - function test_gas_autocompound_exactUnclaimedFees_exactCustodiedFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // donate to create fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - - // bob collects fees so some of alice's fees are now cached - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenIdBob, bob, ZERO_BYTES, false); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - vm.prank(bob); - lpm.modifyLiquidities(calls, currencies); - - // donate to create more fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - - (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); - - // alice will use ALL of her fees to increase liquidity - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - newToken0Owed, - newToken1Owed - ); - - calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); - snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); - } - } - - // autocompounding but the excess fees are credited to tokensOwed - function test_gas_autocompound_excessFeesCredit() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Excess fees are accounted to alice - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // donate to create fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - - // alice will use half of her fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed / 2, - token1Owed / 2 - ); - - bytes[] memory calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - vm.prank(alice); - lpm.modifyLiquidities(calls, currencies); - snapLastCall("autocompound_excessFeesCredit"); - } - - function test_gas_decreaseLiquidity_erc20() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; - - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - lpm.modifyLiquidities(calls, currencies); - snapLastCall("decreaseLiquidity_erc20"); - } - - function test_gas_decreaseLiquidity_erc6909() public { - _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; - - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - - lpm.modifyLiquidities(calls, currencies); - snapLastCall("decreaseLiquidity_erc6909"); - } - - function test_gas_burn() public {} - function test_gas_burnEmpty() public {} - function test_gas_collect() public {} -} +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.24; + +// import "forge-std/Test.sol"; +// import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +// import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +// import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +// import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +// import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +// import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +// import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +// import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +// import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +// import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +// import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +// import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +// import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +// import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; + +// import {IERC20} from "forge-std/interfaces/IERC20.sol"; +// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +// import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +// import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +// contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { +// using FixedPointMathLib for uint256; +// using CurrencyLibrary for Currency; +// using LiquidityRangeIdLibrary for LiquidityRange; +// using PoolIdLibrary for PoolKey; + +// PoolId poolId; +// address alice = makeAddr("ALICE"); +// address bob = makeAddr("BOB"); + +// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + +// // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) +// uint256 FEE_WAD; + +// LiquidityRange range; + +// function setUp() public { +// Deployers.deployFreshManagerAndRouters(); +// Deployers.deployMintAndApprove2Currencies(); + +// (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); +// FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + +// lpm = new NonfungiblePositionManager(manager); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + +// // Give tokens to Alice and Bob, with approvals +// IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); +// vm.startPrank(alice); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); +// vm.stopPrank(); +// vm.startPrank(bob); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); +// vm.stopPrank(); + +// // mint some ERC6909 tokens +// claimsRouter.deposit(currency0, address(this), 100_000_000 ether); +// claimsRouter.deposit(currency1, address(this), 100_000_000 ether); +// manager.setOperator(address(lpm), true); + +// // define a reusable range +// range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); +// } + +// // function test_gas_mint() public { +// // uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity +// // uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity +// // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ +// // range: range, +// // amount0Desired: amount0Desired, +// // amount1Desired: amount1Desired, +// // amount0Min: 0, +// // amount1Min: 0, +// // deadline: block.timestamp + 1, +// // recipient: address(this), +// // hookData: ZERO_BYTES +// // }); +// // snapStart("mint"); +// // lpm.mint(params); +// // snapLastCall(); +// // } + +// function test_gas_mintWithLiquidity() public { +// bytes[] memory calls = new bytes[](1); +// calls[0] = abi.encodeWithSelector( +// lpm.mint.selector, range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES +// ); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("mintWithLiquidity"); +// } + +// function test_gas_increaseLiquidity_erc20() public { +// _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); +// uint256 tokenId = lpm.nextTokenId() - 1; + +// bytes[] memory calls = new bytes[](1); +// calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("increaseLiquidity_erc20"); +// } + +// function test_gas_increaseLiquidity_erc6909() public { +// _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); +// uint256 tokenId = lpm.nextTokenId() - 1; + +// bytes[] memory calls = new bytes[](1); +// calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("increaseLiquidity_erc6909"); +// } + +// function test_gas_autocompound_exactUnclaimedFees() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her exact fees to increase liquidity (compounding) + +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; + +// // alice provides liquidity +// vm.prank(alice); +// _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + +// // donate to create fees +// donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); + +// // alice uses her exact fees to increase liquidity +// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// token0Owed, +// token1Owed +// ); + +// bytes[] memory calls = new bytes[](1); +// calls[0] = +// abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// vm.prank(alice); +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("autocompound_exactUnclaimedFees"); +// } + +// function test_gas_autocompound_exactUnclaimedFees_exactCustodiedFees() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; + +// // alice provides liquidity +// vm.prank(alice); +// _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // donate to create fees +// donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + +// // bob collects fees so some of alice's fees are now cached +// bytes[] memory calls = new bytes[](1); +// calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenIdBob, bob, ZERO_BYTES, false); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// vm.prank(bob); +// lpm.modifyLiquidities(calls, currencies); + +// // donate to create more fees +// donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + +// (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + +// // alice will use ALL of her fees to increase liquidity +// { +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// newToken0Owed, +// newToken1Owed +// ); + +// calls = new bytes[](1); +// calls[0] = +// abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); +// currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// vm.prank(alice); +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); +// } +// } + +// // autocompounding but the excess fees are credited to tokensOwed +// function test_gas_autocompound_excessFeesCredit() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her fees to increase liquidity. Excess fees are accounted to alice +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; + +// // alice provides liquidity +// vm.prank(alice); +// _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // donate to create fees +// donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + +// // alice will use half of her fees to increase liquidity +// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// token0Owed / 2, +// token1Owed / 2 +// ); + +// bytes[] memory calls = new bytes[](1); +// calls[0] = +// abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// vm.prank(alice); +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("autocompound_excessFeesCredit"); +// } + +// function test_gas_decreaseLiquidity_erc20() public { +// _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); +// uint256 tokenId = lpm.nextTokenId() - 1; + +// bytes[] memory calls = new bytes[](1); +// calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("decreaseLiquidity_erc20"); +// } + +// function test_gas_decreaseLiquidity_erc6909() public { +// _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); +// uint256 tokenId = lpm.nextTokenId() - 1; + +// bytes[] memory calls = new bytes[](1); +// calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); +// Currency[] memory currencies = new Currency[](2); +// currencies[0] = currency0; +// currencies[1] = currency1; + +// lpm.modifyLiquidities(calls, currencies); +// snapLastCall("decreaseLiquidity_erc6909"); +// } + +// function test_gas_burn() public {} +// function test_gas_burnEmpty() public {} +// function test_gas_collect() public {} +// } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 39a6e329..8f91a055 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -27,6 +27,8 @@ import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import "forge-std/console2.sol"; + contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; @@ -73,7 +75,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); } - function test_increaseLiquidity_withExactFees() public { + function test_increaseLiquidity_withExactFees1() public { // Alice and Bob provide liquidity on the range // Alice uses her exact fees to increase liquidity (compounding) @@ -322,7 +324,7 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi } } - function test_increaseLiquidity_withExactFees_withExactCachedFees() public { + function test_increaseLiquidity_withExactFees_withExactCachedFees1() public { // Alice and Bob provide liquidity on the range // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity uint256 liquidityAlice = 3_000e18; @@ -347,46 +349,46 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, Liquidi (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); // bob collects fees so some of alice's fees are now cached + vm.startPrank(bob); _collect(tokenIdBob, bob, ZERO_BYTES, false); vm.stopPrank(); - - // swap to create more fees - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); - // alice's fees should be doubled - assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); - assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); - - uint256 balance0AliceBefore = currency0.balanceOf(alice); - uint256 balance1AliceBefore = currency1.balanceOf(alice); - - // alice will use ALL of her fees to increase liquidity - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - newToken0Owed, - newToken1Owed - ); - - vm.startPrank(alice); - _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - vm.stopPrank(); - } - - // alice did not spend any tokens - assertEq(balance0AliceBefore, currency0.balanceOf(alice)); - assertEq(balance1AliceBefore, currency1.balanceOf(alice)); - - // some dust was credited to alice's tokensOwed - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertApproxEqAbs(token0Owed, 0, 80 wei); - assertApproxEqAbs(token1Owed, 0, 80 wei); + //swap to create more fees + // swap(key, true, -int256(swapAmount), ZERO_BYTES); + // swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + // // alice's fees should be doubled + // assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); + // assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); + + // uint256 balance0AliceBefore = currency0.balanceOf(alice); + // uint256 balance1AliceBefore = currency1.balanceOf(alice); + + // // alice will use ALL of her fees to increase liquidity + // { + // (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + // uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + // sqrtPriceX96, + // TickMath.getSqrtPriceAtTick(range.tickLower), + // TickMath.getSqrtPriceAtTick(range.tickUpper), + // newToken0Owed, + // newToken1Owed + // ); + + // vm.startPrank(alice); + // _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + // vm.stopPrank(); + // } + + // // alice did not spend any tokens + // assertEq(balance0AliceBefore, currency0.balanceOf(alice)); + // assertEq(balance1AliceBefore, currency1.balanceOf(alice)); + + // // some dust was credited to alice's tokensOwed + // (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + // assertApproxEqAbs(token0Owed, 0, 80 wei); + // assertApproxEqAbs(token1Owed, 0, 80 wei); } // uses donate to simulate fee revenue diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index f652bc93..7892f4b3 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -14,22 +14,29 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; +import {Planner} from "../utils/Planner.sol"; + +import "forge-std/console2.sol"; contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; + using Planner for Planner.Plan; PoolId poolId; address alice = makeAddr("ALICE"); @@ -48,92 +55,75 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + // liquidity is a uint + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) ); + Currency[] memory currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - BalanceDelta delta = toBalanceDelta(result[0], result[1]); - uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency1.balanceOfSelf(); + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + + (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); assertEq(lpm.ownerOf(1), address(this)); (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta)); - assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0())), "incorrect amount0"); - assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())), "incorrect amount1"); } - // function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + function test_mint_exactTokenRatios() public { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0Desired, + amount1Desired + ); - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: 0, - // amount1Min: 0, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // uint256 balance0After = currency0.balanceOfSelf(); - // uint256 balance1After = currency1.balanceOfSelf(); + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; - // // minting with perfect token ratios will use all of the tokens - // function test_mint_perfect() public { - // int24 tickLower = -int24(key.tickSpacing); - // int24 tickUpper = int24(key.tickSpacing); - // uint256 amount0Desired = 100e18; - // uint256 amount1Desired = 100e18; - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + Planner.Plan memory planner = Planner.init(); + planner = planner.add( + Actions.MINT, abi.encode(range, liquidityToAdd, uint256(block.timestamp + 1), address(this), ZERO_BYTES) + ); - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: amount0Desired, - // amount1Min: amount1Desired, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // uint256 balance0After = currency0.balanceOfSelf(); - // uint256 balance1After = currency1.balanceOfSelf(); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + (BalanceDelta delta, uint256 tokenId) = abi.decode(result[0], (BalanceDelta, uint256)); - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(uint256(int256(-delta.amount0())), amount0Desired); - // assertEq(uint256(int256(-delta.amount1())), amount1Desired); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } + uint256 balance0After = currency0.balanceOfSelf(); + uint256 balance1After = currency1.balanceOfSelf(); + + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + assertEq(uint256(int256(-delta.amount0())), amount0Desired); + assertEq(uint256(int256(-delta.amount1())), amount1Desired); + assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + } // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) // public @@ -210,7 +200,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // create liquidity we can burn uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); @@ -224,7 +214,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // TODO, encode this under one call BalanceDelta deltaDecrease = _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); BalanceDelta deltaCollect = _collect(tokenId, address(this), ZERO_BYTES, false); - lpm.burn(tokenId); + _burn(tokenId); (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, 0); @@ -249,11 +239,11 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + function test_decreaseLiquidity1(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) public { uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(0 < decreaseLiquidityDelta); vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); @@ -268,8 +258,9 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); + // On decrease, balance doesn't change (currenct functionality). + assertEq(currency0.balanceOfSelf() - balance0Before, 0); + assertEq(currency1.balanceOfSelf() - balance1Before, 0); } // function test_decreaseLiquidity_collectFees( @@ -277,7 +268,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi // uint256 decreaseLiquidityDelta // ) public { // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // (tokenId, params) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // vm.assume(0 < decreaseLiquidityDelta); // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 38867ea9..683e5919 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -4,12 +4,15 @@ pragma solidity ^0.8.24; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {NonfungiblePositionManager, Actions} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; +import {Planner} from "../utils/Planner.sol"; contract LiquidityOperations { NonfungiblePositionManager lpm; + using Planner for Planner.Plan; + function _mint( LiquidityRange memory _range, uint256 liquidity, @@ -17,56 +20,64 @@ contract LiquidityOperations { address recipient, bytes memory hookData ) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.MINT, abi.encode(_range, liquidity, deadline, recipient, hookData)); + Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); } function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.INCREASE, abi.encode(tokenId, liquidityToAdd, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - lpm.modifyLiquidities(calls, currencies); + lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); } function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.DECREASE, abi.encode(tokenId, liquidityToRemove, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); } function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.COLLECT, abi.encode(tokenId, recipient, hookData, claims)); (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); Currency[] memory currencies = new Currency[](2); currencies[0] = _range.poolKey.currency0; currencies[1] = _range.poolKey.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - return toBalanceDelta(result[0], result[1]); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); + return abi.decode(result[0], (BalanceDelta)); + } + + function _burn(uint256 tokenId) internal { + Currency[] memory currencies = new Currency[](0); + Planner.Plan memory planner = Planner.init(); + planner = planner.add(Actions.BURN, abi.encode(tokenId)); + bytes[] memory result = lpm.modifyLiquidities(abi.encode(planner.actions, planner.params, currencies)); } } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index fd22c3b2..5def37bc 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -7,10 +7,13 @@ import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDe import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; +import {INonfungiblePositionManager, Actions} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; +import {Planner} from "../../utils/Planner.sol"; contract LiquidityFuzzers is Fuzzers { + using Planner for Planner.Plan; + function createFuzzyLiquidity( INonfungiblePositionManager lpm, address recipient, @@ -18,25 +21,22 @@ contract LiquidityFuzzers is Fuzzers { IPoolManager.ModifyLiquidityParams memory params, uint160 sqrtPriceX96, bytes memory hookData - ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { + ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory) { params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); - LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector( - lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData + Planner.Plan memory plan = Planner.init().add( + Actions.MINT, abi.encode(range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData) ); Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - int128[] memory result = lpm.modifyLiquidities(calls, currencies); - BalanceDelta delta = toBalanceDelta(result[0], result[1]); + lpm.modifyLiquidities(abi.encode(plan.actions, plan.params, currencies)); uint256 tokenId = lpm.nextTokenId() - 1; - return (tokenId, params, delta); + return (tokenId, params); } } diff --git a/test/utils/Planner.sol b/test/utils/Planner.sol new file mode 100644 index 00000000..302c0f83 --- /dev/null +++ b/test/utils/Planner.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {INonfungiblePositionManager, Actions} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; + +library Planner { + struct Plan { + Actions[] actions; + bytes[] params; + } + + function init() public returns (Plan memory plan) { + return Plan({actions: new Actions[](0), params: new bytes[](0)}); + } + + function add(Plan memory plan, Actions action, bytes memory param) public returns (Plan memory) { + Actions[] memory actions = new Actions[](plan.actions.length + 1); + bytes[] memory params = new bytes[](plan.params.length + 1); + + for (uint256 i; i < actions.length - 1; i++) { + actions[i] = actions[i]; + params[i] = params[i]; + } + + actions[actions.length - 1] = action; + params[params.length - 1] = param; + + return Plan({actions: actions, params: params}); + } + + function zip(Plan memory plan) public returns (bytes memory) { + return abi.encode(plan.actions, plan.params); + } +}