From cb13e346eeebe53d75c92adb66bd6bdb985f800c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 30 Jun 2024 21:31:34 -0400 Subject: [PATCH 1/7] rip out vanilla and benchmark --- .../autocompound_exactUnclaimedFees.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 | 102 +- .../INonfungiblePositionManager.sol | 19 +- .../libraries/TransientLiquidityDelta.sol | 7 +- test/position-managers/Execute.t.sol | 360 +++---- test/position-managers/FeeCollection.t.sol | 554 +++++------ test/position-managers/Gas.t.sol | 106 +- .../position-managers/IncreaseLiquidity.t.sol | 914 +++++++++--------- .../NonfungiblePositionManager.t.sol | 463 ++++----- test/shared/fuzz/LiquidityFuzzers.sol | 23 +- 16 files changed, 1295 insertions(+), 1267 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 8dd96b22..6632f988 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -300126 \ No newline at end of file +299380 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index eed2bed5..ef8374e7 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -320665 \ No newline at end of file +319919 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 5e4cd19e..4850a5b4 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -215187 \ No newline at end of file +214439 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index e21ae2c4..24d8e85d 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -215199 \ No newline at end of file +214451 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 37a69b54..18129560 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -195029 \ No newline at end of file +194596 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 23ce9120..3d4e376c 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -195041 \ No newline at end of file +194608 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 39d02c26..cfba5ed8 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -512049 \ No newline at end of file +513548 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 594e4997..bc385874 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -51,35 +51,33 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") {} - function unlockAndExecute(bytes[] memory data, Currency[] memory currencies) public returns (bytes memory) { + function unlockAndExecute(bytes[] memory data, Currency[] memory currencies) public returns (int128[] memory) { msgSender = msg.sender; - return manager.unlock(abi.encode(data, currencies)); + return abi.decode(manager.unlock(abi.encode(data, currencies)), (int128[])); } function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { (bytes[] memory data, Currency[] memory currencies) = abi.decode(payload, (bytes[], Currency[])); bool success; - bytes memory returnData; + for (uint256 i; i < data.length; i++) { // TODO: bubble up the return - (success, returnData) = address(this).call(data[i]); + (success,) = address(this).call(data[i]); if (!success) revert("EXECUTE_FAILED"); } // close the deltas + int128[] memory returnData = new int128[](currencies.length); for (uint256 i; i < currencies.length; i++) { - currencies[i].close(manager, msgSender); + returnData[i] = currencies[i].close(manager, msgSender); currencies[i].close(manager, address(this)); } // Should just be returning the netted amount that was settled on behalf of the caller (msgSender) - // And any recipient deltas settled earlier. - - // TODO: @sara handle the return - // vanilla: return int128[2] - // batch: return int128[data.length] - return returnData; + // TODO: any recipient deltas settled earlier. + // @comment sauce: i dont think we can return recipient deltas since we cant parse the payload + return abi.encode(returnData); } // NOTE: more gas efficient as LiquidityAmounts is used offchain @@ -90,26 +88,14 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit uint256 deadline, address owner, bytes calldata hookData - ) public payable returns (BalanceDelta delta) { - // TODO: optimization, read/write manager.isUnlocked to avoid repeated external calls for batched execution - if (manager.isUnlocked()) { - _increaseLiquidity(owner, range, liquidity, hookData); - - // mint receipt token - uint256 tokenId; - _mint(owner, (tokenId = nextTokenId++)); - tokenPositions[tokenId] = TokenPosition({owner: owner, range: range}); - } else { - msgSender = msg.sender; - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.mint.selector, range, liquidity, deadline, owner, hookData); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = range.poolKey.currency0; - currencies[1] = range.poolKey.currency1; - bytes memory result = unlockAndExecute(data, currencies); - delta = abi.decode(result, (BalanceDelta)); - } + ) external payable { + // TODO: security -- what happens if PoolManager is already unlocked by UR??? + _increaseLiquidity(owner, range, liquidity, hookData); + + // mint receipt token + uint256 tokenId; + _mint(owner, (tokenId = nextTokenId++)); + tokenPositions[tokenId] = TokenPosition({owner: owner, range: range}); } // NOTE: more expensive since LiquidityAmounts is used onchain @@ -135,43 +121,21 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external isAuthorizedForToken(tokenId) - returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - if (manager.isUnlocked()) { - _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); - } else { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.increaseLiquidity.selector, tokenId, liquidity, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = tokenPos.range.poolKey.currency0; - currencies[1] = tokenPos.range.poolKey.currency1; - bytes memory result = unlockAndExecute(data, currencies); - delta = abi.decode(result, (BalanceDelta)); - } + // TODO: security -- what happens if PoolManager is already unlocked by UR??? + _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - public + external isAuthorizedForToken(tokenId) - returns (BalanceDelta delta) { TokenPosition memory tokenPos = tokenPositions[tokenId]; - if (manager.isUnlocked()) { - _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); - } else { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.decreaseLiquidity.selector, tokenId, liquidity, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = tokenPos.range.poolKey.currency0; - currencies[1] = tokenPos.range.poolKey.currency1; - bytes memory result = unlockAndExecute(data, currencies); - delta = abi.decode(result, (BalanceDelta)); - } + // TODO: security -- what happens if PoolManager is already unlocked by UR??? + _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } // TODO return type? @@ -187,22 +151,14 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit } // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) - public - returns (BalanceDelta delta) - { + function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external { TokenPosition memory tokenPos = tokenPositions[tokenId]; - if (manager.isUnlocked()) { - _collect(recipient, tokenPos.range, hookData); - } else { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(this.collect.selector, tokenId, recipient, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = tokenPos.range.poolKey.currency0; - currencies[1] = tokenPos.range.poolKey.currency1; - bytes memory result = unlockAndExecute(data, currencies); - delta = abi.decode(result, (BalanceDelta)); + + // TODO: security -- what happens if PoolManager is already unlocked by UR??? + _collect(recipient, tokenPos.range, hookData); + if (recipient != _msgSenderInternal()) { + tokenPos.range.poolKey.currency0.close(manager, recipient); + tokenPos.range.poolKey.currency1.close(manager, recipient); } } diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index dc7fdb71..03c83334 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -18,7 +18,7 @@ interface INonfungiblePositionManager { uint256 deadline, address recipient, bytes calldata hookData - ) external payable returns (BalanceDelta delta); + ) external payable; // NOTE: more expensive since LiquidityAmounts is used onchain // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); @@ -28,20 +28,14 @@ interface INonfungiblePositionManager { /// @param liquidity The amount of liquidity to add /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the liquidity increase uses ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of increasing liquidity - function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - returns (BalanceDelta delta); + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; /// @notice Decrease liquidity for an existing position /// @param tokenId The ID of the position /// @param liquidity The amount of liquidity to remove /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the removed liquidity is sent as ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of decreasing liquidity applied to user (number of tokens credited to tokensOwed) - function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) - external - returns (BalanceDelta delta); + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external; // TODO Can decide if we want burn to auto encode a decrease/collect. /// @notice Burn a position and delete the tokenId @@ -56,15 +50,12 @@ interface INonfungiblePositionManager { /// @param recipient The address to send the collected tokens to /// @param hookData Arbitrary data passed to the hook /// @param claims Whether the collected fees are sent as ERC-6909 claim tokens - /// @return delta Corresponding balance changes as a result of collecting fees - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) - external - returns (BalanceDelta delta); + function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external; /// @notice Execute a batch of external calls by unlocking the PoolManager /// @param data an array of abi.encodeWithSelector(, ) for each call /// @return delta The final delta changes of the caller - function unlockAndExecute(bytes[] memory data, Currency[] memory currencies) external returns (bytes memory); + function unlockAndExecute(bytes[] memory data, Currency[] memory currencies) external returns (int128[] memory); /// @notice Returns the fees owed for a position. Includes unclaimed fees + custodied fees + claimable fees /// @param tokenId The ID of the position diff --git a/contracts/libraries/TransientLiquidityDelta.sol b/contracts/libraries/TransientLiquidityDelta.sol index 9dca43a5..83ba3139 100644 --- a/contracts/libraries/TransientLiquidityDelta.sol +++ b/contracts/libraries/TransientLiquidityDelta.sol @@ -54,19 +54,18 @@ library TransientLiquidityDelta { } } - function close(Currency currency, IPoolManager manager, address holder) internal { + function close(Currency currency, IPoolManager manager, address holder) internal returns (int128 delta) { // getDelta(currency, holder); bytes32 hashSlot = _computeSlot(holder, currency); - int256 delta; assembly { delta := tload(hashSlot) } // TODO support claims field if (delta < 0) { - currency.settle(manager, holder, uint256(-delta), false); + currency.settle(manager, holder, uint256(-int256(delta)), false); } else { - currency.take(manager, holder, uint256(delta), false); + currency.take(manager, holder, uint256(int256(delta)), false); } // setDelta(0); diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 5d6b7d39..1cdde778 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -1,180 +1,180 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; - -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; - -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; - -import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { - using FixedPointMathLib for uint256; - using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; - using PoolIdLibrary for PoolKey; - using SafeCast for uint256; - - NonfungiblePositionManager lpm; - - PoolId poolId; - address alice = makeAddr("ALICE"); - address bob = makeAddr("BOB"); - - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) - uint256 FEE_WAD; - - LiquidityRange range; - - function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); - - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - lpm = new NonfungiblePositionManager(manager); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - - // Give tokens to Alice and Bob, with approvals - IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); - IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); - vm.startPrank(alice); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - vm.startPrank(bob); - IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); - IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - vm.stopPrank(); - - // define a reusable range - range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); - } - - function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { - initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); - liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); - lpm.mint(range, initialLiquidity, 0, 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.unlockAndExecute(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); - lpm.mint(range, initialiLiquidity, 0, 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.unlockAndExecute(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.unlockAndExecute(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"; + +// contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +// using FixedPointMathLib for uint256; +// using CurrencyLibrary for Currency; +// using LiquidityRangeIdLibrary for LiquidityRange; +// using PoolIdLibrary for PoolKey; +// using SafeCast for uint256; + +// NonfungiblePositionManager lpm; + +// PoolId poolId; +// address alice = makeAddr("ALICE"); +// address bob = makeAddr("BOB"); + +// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + +// // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) +// uint256 FEE_WAD; + +// LiquidityRange range; + +// function setUp() public { +// Deployers.deployFreshManagerAndRouters(); +// Deployers.deployMintAndApprove2Currencies(); + +// (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); +// FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + +// lpm = new NonfungiblePositionManager(manager); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + +// // Give tokens to Alice and Bob, with approvals +// IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); +// IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); +// vm.startPrank(alice); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); +// vm.stopPrank(); +// vm.startPrank(bob); +// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); +// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); +// vm.stopPrank(); + +// // define a reusable range +// range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); +// } + +// function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { +// initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); +// liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); +// lpm.mint(range, initialLiquidity, 0, 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.unlockAndExecute(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); +// lpm.mint(range, initialiLiquidity, 0, 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.unlockAndExecute(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.unlockAndExecute(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 1a8071a6..bf7fc256 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -1,277 +1,277 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; - -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; - -import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; - -import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { - using FixedPointMathLib for uint256; - using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; - - NonfungiblePositionManager lpm; - - PoolId poolId; - address alice = makeAddr("ALICE"); - address bob = makeAddr("BOB"); - - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - - // expresses the fee as a wad (i.e. 3000 = 0.003e18) - uint256 FEE_WAD; - - 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(); - } - - function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { - params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, -int256(swapAmount), ZERO_BYTES); - - // collect fees - BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, true); - - assertEq(delta.amount0(), 0); - - assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); - - assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); - } - - function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { - params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, -int256(swapAmount), ZERO_BYTES); - - // collect fees - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, 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.amount1())), currency1.balanceOfSelf() - balance1Before); - } - - // two users with the same range; one user cannot collect the other's fees - function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) - public - { - params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); - vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); - - LiquidityRange memory range = - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - vm.prank(alice); - lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - vm.prank(bob); - lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, -int256(swapAmount), ZERO_BYTES); - - // alice collects only her fees - vm.prank(alice); - BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, true); - assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); - assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); - assertTrue(delta.amount1() != 0); - - // bob collects only his fees - vm.prank(bob); - delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, true); - assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); - assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); - assertTrue(delta.amount1() != 0); - - // position manager holds no fees now - assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); - assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); - } - - function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) - public - { - params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); - params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); - vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - - liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); - - LiquidityRange memory range = - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - vm.prank(alice); - lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - vm.prank(bob); - lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // confirm the positions are same range - (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); - (, LiquidityRange memory rangeBob) = lpm.tokenPositions(tokenIdBob); - assertEq(rangeAlice.tickLower, rangeBob.tickLower); - assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); - - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, -int256(swapAmount), ZERO_BYTES); - - // alice collects only her fees - uint256 balance0AliceBefore = currency0.balanceOf(alice); - uint256 balance1AliceBefore = currency1.balanceOf(alice); - vm.prank(alice); - BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); - uint256 balance0AliceAfter = currency0.balanceOf(alice); - uint256 balance1AliceAfter = currency1.balanceOf(alice); - - assertEq(balance0AliceBefore, balance0AliceAfter); - assertEq(uint256(uint128(delta.amount1())), balance1AliceAfter - balance1AliceBefore); - assertTrue(delta.amount1() != 0); - - // bob collects only his fees - uint256 balance0BobBefore = currency0.balanceOf(bob); - uint256 balance1BobBefore = currency1.balanceOf(bob); - vm.prank(bob); - delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - uint256 balance0BobAfter = currency0.balanceOf(bob); - uint256 balance1BobAfter = currency1.balanceOf(bob); - - assertEq(balance0BobBefore, balance0BobAfter); - assertEq(uint256(uint128(delta.amount1())), balance1BobAfter - balance1BobBefore); - assertTrue(delta.amount1() != 0); - - // position manager holds no fees now - assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); - assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); - } - - function test_collect_donate() public {} - function test_collect_donate_sameRange() public {} - - /// @dev Alice and bob create liquidity on the same range - /// when alice decreases liquidity, she should only collect her fees - /// TODO Add back fuzz test on liquidityDeltaBob - /// TODO Assert state changes for lpm balance, position state, and return values - function test_decreaseLiquidity_sameRange_exact() public { - // alice and bob create liquidity on the same range [-120, 120] - LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120}); - - // alice provisions 3x the amount of liquidity as bob - uint256 liquidityAlice = 3000e18; - uint256 liquidityBob = 1000e18; - vm.prank(alice); - BalanceDelta lpDeltaAlice = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - vm.prank(bob); - BalanceDelta lpDeltaBob = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - // alice decreases liquidity - vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); - - uint256 tolerance = 0.000000001 ether; - - uint256 lpmBalance0 = manager.balanceOf(address(lpm), currency0.toId()); - uint256 lpmBalance1 = manager.balanceOf(address(lpm), currency1.toId()); - - // lpm collects alice's principal + all fees accrued on the range - assertApproxEqAbs( - lpmBalance0, uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD), tolerance - ); - assertApproxEqAbs( - lpmBalance1, uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD), tolerance - ); - - // bob decreases half of his liquidity - vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); - - // lpm collects half of bobs principal - // the fee amount has already been collected with alice's calls - assertApproxEqAbs( - manager.balanceOf(address(lpm), currency0.toId()) - lpmBalance0, - uint256(int256(-lpDeltaBob.amount0()) / 2), - tolerance - ); - assertApproxEqAbs( - manager.balanceOf(address(lpm), currency1.toId()) - lpmBalance1, - uint256(int256(-lpDeltaBob.amount1()) / 2), - tolerance - ); - } -} +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.24; + +// import "forge-std/Test.sol"; +// import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +// import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +// import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +// import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +// import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +// import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +// import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +// import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +// import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +// import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +// import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +// import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +// import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +// import {IERC20} from "forge-std/interfaces/IERC20.sol"; +// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +// import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +// import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +// contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +// using FixedPointMathLib for uint256; +// using CurrencyLibrary for Currency; +// using LiquidityRangeIdLibrary for LiquidityRange; + +// NonfungiblePositionManager lpm; + +// PoolId poolId; +// address alice = makeAddr("ALICE"); +// address bob = makeAddr("BOB"); + +// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + +// // unused value for the fuzz helper functions +// uint128 constant DEAD_VALUE = 6969.6969 ether; + +// // expresses the fee as a wad (i.e. 3000 = 0.003e18) +// uint256 FEE_WAD; + +// 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(); +// } + +// function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { +// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); +// uint256 tokenId; +// (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); +// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + +// // swap to create fees +// uint256 swapAmount = 0.01e18; +// swap(key, false, -int256(swapAmount), ZERO_BYTES); + +// // collect fees +// BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, true); + +// assertEq(delta.amount0(), 0); + +// assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); + +// assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); +// } + +// function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { +// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); +// uint256 tokenId; +// (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); +// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + +// // swap to create fees +// uint256 swapAmount = 0.01e18; +// swap(key, false, -int256(swapAmount), ZERO_BYTES); + +// // collect fees +// uint256 balance0Before = currency0.balanceOfSelf(); +// uint256 balance1Before = currency1.balanceOfSelf(); +// BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, 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.amount1())), currency1.balanceOfSelf() - balance1Before); +// } + +// // two users with the same range; one user cannot collect the other's fees +// function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) +// public +// { +// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); +// params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); +// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + +// liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + +// LiquidityRange memory range = +// LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); +// vm.prank(alice); +// lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// vm.prank(bob); +// lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // swap to create fees +// uint256 swapAmount = 0.01e18; +// swap(key, false, -int256(swapAmount), ZERO_BYTES); + +// // alice collects only her fees +// vm.prank(alice); +// BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, true); +// assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); +// assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); +// assertTrue(delta.amount1() != 0); + +// // bob collects only his fees +// vm.prank(bob); +// delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, true); +// assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); +// assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); +// assertTrue(delta.amount1() != 0); + +// // position manager holds no fees now +// assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); +// assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); +// } + +// function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) +// public +// { +// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); +// params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); +// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + +// liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + +// LiquidityRange memory range = +// LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); +// vm.prank(alice); +// lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// vm.prank(bob); +// lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // confirm the positions are same range +// (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); +// (, LiquidityRange memory rangeBob) = lpm.tokenPositions(tokenIdBob); +// assertEq(rangeAlice.tickLower, rangeBob.tickLower); +// assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); + +// // swap to create fees +// uint256 swapAmount = 0.01e18; +// swap(key, false, -int256(swapAmount), ZERO_BYTES); + +// // alice collects only her fees +// uint256 balance0AliceBefore = currency0.balanceOf(alice); +// uint256 balance1AliceBefore = currency1.balanceOf(alice); +// vm.prank(alice); +// BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); +// uint256 balance0AliceAfter = currency0.balanceOf(alice); +// uint256 balance1AliceAfter = currency1.balanceOf(alice); + +// assertEq(balance0AliceBefore, balance0AliceAfter); +// assertEq(uint256(uint128(delta.amount1())), balance1AliceAfter - balance1AliceBefore); +// assertTrue(delta.amount1() != 0); + +// // bob collects only his fees +// uint256 balance0BobBefore = currency0.balanceOf(bob); +// uint256 balance1BobBefore = currency1.balanceOf(bob); +// vm.prank(bob); +// delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); +// uint256 balance0BobAfter = currency0.balanceOf(bob); +// uint256 balance1BobAfter = currency1.balanceOf(bob); + +// assertEq(balance0BobBefore, balance0BobAfter); +// assertEq(uint256(uint128(delta.amount1())), balance1BobAfter - balance1BobBefore); +// assertTrue(delta.amount1() != 0); + +// // position manager holds no fees now +// assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); +// assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); +// } + +// function test_collect_donate() public {} +// function test_collect_donate_sameRange() public {} + +// /// @dev Alice and bob create liquidity on the same range +// /// when alice decreases liquidity, she should only collect her fees +// /// TODO Add back fuzz test on liquidityDeltaBob +// /// TODO Assert state changes for lpm balance, position state, and return values +// function test_decreaseLiquidity_sameRange_exact() public { +// // alice and bob create liquidity on the same range [-120, 120] +// LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120}); + +// // alice provisions 3x the amount of liquidity as bob +// uint256 liquidityAlice = 3000e18; +// uint256 liquidityBob = 1000e18; +// vm.prank(alice); +// BalanceDelta lpDeltaAlice = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// vm.prank(bob); +// BalanceDelta lpDeltaBob = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // swap to create fees +// uint256 swapAmount = 0.001e18; +// swap(key, true, -int256(swapAmount), ZERO_BYTES); +// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + +// // alice decreases liquidity +// vm.prank(alice); +// BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); + +// uint256 tolerance = 0.000000001 ether; + +// uint256 lpmBalance0 = manager.balanceOf(address(lpm), currency0.toId()); +// uint256 lpmBalance1 = manager.balanceOf(address(lpm), currency1.toId()); + +// // lpm collects alice's principal + all fees accrued on the range +// assertApproxEqAbs( +// lpmBalance0, uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD), tolerance +// ); +// assertApproxEqAbs( +// lpmBalance1, uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD), tolerance +// ); + +// // bob decreases half of his liquidity +// vm.prank(bob); +// BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); + +// // lpm collects half of bobs principal +// // the fee amount has already been collected with alice's calls +// assertApproxEqAbs( +// manager.balanceOf(address(lpm), currency0.toId()) - lpmBalance0, +// uint256(int256(-lpDeltaBob.amount0()) / 2), +// tolerance +// ); +// assertApproxEqAbs( +// manager.balanceOf(address(lpm), currency1.toId()) - lpmBalance1, +// uint256(int256(-lpDeltaBob.amount1()) / 2), +// tolerance +// ); +// } +// } diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index e25d85f7..de0f3fb4 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -37,9 +37,6 @@ contract GasTest is Test, Deployers, GasSnapshot { uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) uint256 FEE_WAD; @@ -98,23 +95,42 @@ contract GasTest is Test, Deployers, GasSnapshot { // } function test_gas_mintWithLiquidity() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector( + lpm.mint.selector, range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES + ); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.unlockAndExecute(calls, currencies); snapLastCall("mintWithLiquidity"); } function test_gas_increaseLiquidity_erc20() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, false); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.unlockAndExecute(calls, currencies); snapLastCall("increaseLiquidity_erc20"); } function test_gas_increaseLiquidity_erc6909() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, true); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.unlockAndExecute(calls, currencies); snapLastCall("increaseLiquidity_erc6909"); } @@ -127,12 +143,12 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); // donate to create fees donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); @@ -149,8 +165,15 @@ contract GasTest is Test, Deployers, GasSnapshot { token1Owed ); + bytes[] memory calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + lpm.unlockAndExecute(calls, currencies); snapLastCall("autocompound_exactUnclaimedFees"); } @@ -162,12 +185,12 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees @@ -193,8 +216,15 @@ contract GasTest is Test, Deployers, GasSnapshot { newToken1Owed ); + bytes[] memory calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + lpm.unlockAndExecute(calls, currencies); snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees"); } } @@ -208,12 +238,12 @@ contract GasTest is Test, Deployers, GasSnapshot { // alice provides liquidity vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); uint256 tokenIdAlice = lpm.nextTokenId() - 1; // bob provides liquidity vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); uint256 tokenIdBob = lpm.nextTokenId() - 1; // donate to create fees @@ -231,28 +261,62 @@ contract GasTest is Test, Deployers, GasSnapshot { token1Owed / 2 ); + bytes[] memory calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + lpm.unlockAndExecute(calls, currencies); snapLastCall("autocompound_excessFeesCredit"); } function test_gas_decreaseLiquidity_erc20() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, false); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.unlockAndExecute(calls, currencies); snapLastCall("decreaseLiquidity_erc20"); } function test_gas_decreaseLiquidity_erc6909() public { - lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); + _mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); uint256 tokenId = lpm.nextTokenId() - 1; - lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, true); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, 10_000 ether, ZERO_BYTES, true); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + + lpm.unlockAndExecute(calls, currencies); snapLastCall("decreaseLiquidity_erc6909"); } function test_gas_burn() public {} function test_gas_burnEmpty() public {} function test_gas_collect() public {} + + function _mint( + LiquidityRange memory _range, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes memory hookData + ) internal { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.unlockAndExecute(calls, currencies); + } } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 2f6a8a7b..70dfba36 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -1,457 +1,457 @@ -// 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 {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; - -contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { - using FixedPointMathLib for uint256; - using CurrencyLibrary for Currency; - using LiquidityRangeIdLibrary for LiquidityRange; - using PoolIdLibrary for PoolKey; - - NonfungiblePositionManager lpm; - - PoolId poolId; - address alice = makeAddr("ALICE"); - address bob = makeAddr("BOB"); - - uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) - uint256 FEE_WAD; - - LiquidityRange range; - - function setUp() public { - Deployers.deployFreshManagerAndRouters(); - Deployers.deployMintAndApprove2Currencies(); - - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_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_increaseLiquidity_withExactFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her exact fees to increase liquidity (compounding) - - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - // alice uses her exact fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed, - token1Owed - ); - - uint256 balance0BeforeAlice = currency0.balanceOf(alice); - uint256 balance1BeforeAlice = currency1.balanceOf(alice); - - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - - // alice did not spend any tokens - assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); - assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); - - // alice spent all of the fees, approximately - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertApproxEqAbs(token0Owed, 0, 20 wei); - assertApproxEqAbs(token1Owed, 0, 20 wei); - } - - // uses donate to simulate fee revenue - function test_increaseLiquidity_withExactFees_donate() public { - // Alice and Bob provide liquidity on the range - // Alice uses her exact fees to increase liquidity (compounding) - - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - - // alice provides liquidity - vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - - // donate to create fees - donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); - - // alice uses her exact fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed, - token1Owed - ); - - uint256 balance0BeforeAlice = currency0.balanceOf(alice); - uint256 balance1BeforeAlice = currency1.balanceOf(alice); - - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - - // alice did not spend any tokens - assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); - assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); - - // alice spent all of the fees - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertEq(token0Owed, 0); - assertEq(token1Owed, 0); - } - - function test_increaseLiquidity_withExcessFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Excess fees are accounted to alice - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; - - // alice provides liquidity - vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - // alice will use half of her fees to increase liquidity - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - token0Owed / 2, - token1Owed / 2 - ); - - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - } - - { - // bob collects his fees - uint256 balance0BeforeBob = currency0.balanceOf(bob); - uint256 balance1BeforeBob = currency1.balanceOf(bob); - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - uint256 balance0AfterBob = currency0.balanceOf(bob); - uint256 balance1AfterBob = currency1.balanceOf(bob); - assertApproxEqAbs( - balance0AfterBob - balance0BeforeBob, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei - ); - assertApproxEqAbs( - balance1AfterBob - balance1BeforeBob, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei - ); - } - - { - // alice collects her fees, which should be about half of the fees - uint256 balance0BeforeAlice = currency0.balanceOf(alice); - uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); - lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); - uint256 balance0AfterAlice = currency0.balanceOf(alice); - uint256 balance1AfterAlice = currency1.balanceOf(alice); - assertApproxEqAbs( - balance0AfterAlice - balance0BeforeAlice, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, - 9 wei - ); - assertApproxEqAbs( - balance1AfterAlice - balance1BeforeAlice, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, - 1 wei - ); - } - } - - function test_increaseLiquidity_withInsufficientFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Additional funds are used by alice to increase liquidity - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; - - // alice provides liquidity - vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - // alice will use all of her fees + additional capital 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 - ); - - uint256 balance0BeforeAlice = currency0.balanceOf(alice); - uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - uint256 balance0AfterAlice = currency0.balanceOf(alice); - uint256 balance1AfterAlice = currency1.balanceOf(alice); - - assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, token0Owed, 37 wei); - assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, token1Owed, 1 wei); - } - - { - // bob collects his fees - uint256 balance0BeforeBob = currency0.balanceOf(bob); - uint256 balance1BeforeBob = currency1.balanceOf(bob); - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - uint256 balance0AfterBob = currency0.balanceOf(bob); - uint256 balance1AfterBob = currency1.balanceOf(bob); - assertApproxEqAbs( - balance0AfterBob - balance0BeforeBob, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei - ); - assertApproxEqAbs( - balance1AfterBob - balance1BeforeBob, - swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), - 1 wei - ); - } - } - - function test_increaseLiquidity_withExactFees_withExactCachedFees() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; - - // alice provides liquidity - vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // swap to create fees - uint256 swapAmount = 0.001e18; - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - // bob collects fees so some of alice's fees are now cached - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - - // swap to create more fees - swap(key, true, -int256(swapAmount), ZERO_BYTES); - swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - - (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); - // alice's fees should be doubled - assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); - assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); - - uint256 balance0AliceBefore = currency0.balanceOf(alice); - uint256 balance1AliceBefore = currency1.balanceOf(alice); - - // alice will use ALL of her fees to increase liquidity - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - newToken0Owed, - newToken1Owed - ); - - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - } - - // alice did not spend any tokens - assertEq(balance0AliceBefore, currency0.balanceOf(alice)); - assertEq(balance1AliceBefore, currency1.balanceOf(alice)); - - // some dust was credited to alice's tokensOwed - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertApproxEqAbs(token0Owed, 0, 80 wei); - assertApproxEqAbs(token1Owed, 0, 80 wei); - } - - // uses donate to simulate fee revenue - function test_increaseLiquidity_withExactFees_withExactCachedFees_donate() public { - // Alice and Bob provide liquidity on the range - // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity - uint256 liquidityAlice = 3_000e18; - uint256 liquidityBob = 1_000e18; - uint256 totalLiquidity = liquidityAlice + liquidityBob; - - // alice provides liquidity - vm.prank(alice); - lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; - - // bob provides liquidity - vm.prank(bob); - lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; - - // donate to create fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - - (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - - // bob collects fees so some of alice's fees are now cached - vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - - // donate to create more fees - donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - - (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); - // alice's fees should be doubled - assertApproxEqAbs(newToken0Owed, token0Owed * 2, 1 wei); - assertApproxEqAbs(newToken1Owed, token1Owed * 2, 1 wei); - - uint256 balance0AliceBefore = currency0.balanceOf(alice); - uint256 balance1AliceBefore = currency1.balanceOf(alice); - - // alice will use ALL of her fees to increase liquidity - { - (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); - uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(range.tickLower), - TickMath.getSqrtPriceAtTick(range.tickUpper), - newToken0Owed, - newToken1Owed - ); - - vm.prank(alice); - lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - } - - // alice did not spend any tokens - assertEq(balance0AliceBefore, currency0.balanceOf(alice), "alice spent token0"); - assertEq(balance1AliceBefore, currency1.balanceOf(alice), "alice spent token1"); - - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); - assertEq(token0Owed, 0); - assertEq(token1Owed, 0); - - // bob still collects 5 - (token0Owed, token1Owed) = lpm.feesOwed(tokenIdBob); - assertApproxEqAbs(token0Owed, 5e18, 1 wei); - assertApproxEqAbs(token1Owed, 5e18, 1 wei); - - vm.prank(bob); - BalanceDelta result = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - assertApproxEqAbs(result.amount0(), 5e18, 1 wei); - assertApproxEqAbs(result.amount1(), 5e18, 1 wei); - } -} +// // 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 {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; + +// contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { +// using FixedPointMathLib for uint256; +// using CurrencyLibrary for Currency; +// using LiquidityRangeIdLibrary for LiquidityRange; +// using PoolIdLibrary for PoolKey; + +// NonfungiblePositionManager lpm; + +// PoolId poolId; +// address alice = makeAddr("ALICE"); +// address bob = makeAddr("BOB"); + +// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + +// // unused value for the fuzz helper functions +// uint128 constant DEAD_VALUE = 6969.6969 ether; + +// // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) +// uint256 FEE_WAD; + +// LiquidityRange range; + +// function setUp() public { +// Deployers.deployFreshManagerAndRouters(); +// Deployers.deployMintAndApprove2Currencies(); + +// (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_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_increaseLiquidity_withExactFees() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her exact fees to increase liquidity (compounding) + +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; + +// // alice provides liquidity +// vm.prank(alice); +// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + +// // swap to create fees +// uint256 swapAmount = 0.001e18; +// swap(key, true, -int256(swapAmount), ZERO_BYTES); +// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + +// // alice uses her exact fees to increase liquidity +// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// token0Owed, +// token1Owed +// ); + +// uint256 balance0BeforeAlice = currency0.balanceOf(alice); +// uint256 balance1BeforeAlice = currency1.balanceOf(alice); + +// vm.prank(alice); +// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + +// // alice did not spend any tokens +// assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); +// assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + +// // alice spent all of the fees, approximately +// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); +// assertApproxEqAbs(token0Owed, 0, 20 wei); +// assertApproxEqAbs(token1Owed, 0, 20 wei); +// } + +// // uses donate to simulate fee revenue +// function test_increaseLiquidity_withExactFees_donate() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her exact fees to increase liquidity (compounding) + +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; + +// // alice provides liquidity +// vm.prank(alice); +// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + +// // donate to create fees +// donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); + +// // alice uses her exact fees to increase liquidity +// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// token0Owed, +// token1Owed +// ); + +// uint256 balance0BeforeAlice = currency0.balanceOf(alice); +// uint256 balance1BeforeAlice = currency1.balanceOf(alice); + +// vm.prank(alice); +// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + +// // alice did not spend any tokens +// assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); +// assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + +// // alice spent all of the fees +// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); +// assertEq(token0Owed, 0); +// assertEq(token1Owed, 0); +// } + +// function test_increaseLiquidity_withExcessFees() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her fees to increase liquidity. Excess fees are accounted to alice +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; +// uint256 totalLiquidity = liquidityAlice + liquidityBob; + +// // alice provides liquidity +// vm.prank(alice); +// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // swap to create fees +// uint256 swapAmount = 0.001e18; +// swap(key, true, -int256(swapAmount), ZERO_BYTES); +// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + +// // alice will use half of her fees to increase liquidity +// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); +// { +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// token0Owed / 2, +// token1Owed / 2 +// ); + +// vm.prank(alice); +// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); +// } + +// { +// // bob collects his fees +// uint256 balance0BeforeBob = currency0.balanceOf(bob); +// uint256 balance1BeforeBob = currency1.balanceOf(bob); +// vm.prank(bob); +// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); +// uint256 balance0AfterBob = currency0.balanceOf(bob); +// uint256 balance1AfterBob = currency1.balanceOf(bob); +// assertApproxEqAbs( +// balance0AfterBob - balance0BeforeBob, +// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), +// 1 wei +// ); +// assertApproxEqAbs( +// balance1AfterBob - balance1BeforeBob, +// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), +// 1 wei +// ); +// } + +// { +// // alice collects her fees, which should be about half of the fees +// uint256 balance0BeforeAlice = currency0.balanceOf(alice); +// uint256 balance1BeforeAlice = currency1.balanceOf(alice); +// vm.prank(alice); +// lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); +// uint256 balance0AfterAlice = currency0.balanceOf(alice); +// uint256 balance1AfterAlice = currency1.balanceOf(alice); +// assertApproxEqAbs( +// balance0AfterAlice - balance0BeforeAlice, +// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, +// 9 wei +// ); +// assertApproxEqAbs( +// balance1AfterAlice - balance1BeforeAlice, +// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, +// 1 wei +// ); +// } +// } + +// function test_increaseLiquidity_withInsufficientFees() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her fees to increase liquidity. Additional funds are used by alice to increase liquidity +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; +// uint256 totalLiquidity = liquidityAlice + liquidityBob; + +// // alice provides liquidity +// vm.prank(alice); +// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // swap to create fees +// uint256 swapAmount = 0.001e18; +// swap(key, true, -int256(swapAmount), ZERO_BYTES); +// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + +// // alice will use all of her fees + additional capital 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 +// ); + +// uint256 balance0BeforeAlice = currency0.balanceOf(alice); +// uint256 balance1BeforeAlice = currency1.balanceOf(alice); +// vm.prank(alice); +// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); +// uint256 balance0AfterAlice = currency0.balanceOf(alice); +// uint256 balance1AfterAlice = currency1.balanceOf(alice); + +// assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, token0Owed, 37 wei); +// assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, token1Owed, 1 wei); +// } + +// { +// // bob collects his fees +// uint256 balance0BeforeBob = currency0.balanceOf(bob); +// uint256 balance1BeforeBob = currency1.balanceOf(bob); +// vm.prank(bob); +// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); +// uint256 balance0AfterBob = currency0.balanceOf(bob); +// uint256 balance1AfterBob = currency1.balanceOf(bob); +// assertApproxEqAbs( +// balance0AfterBob - balance0BeforeBob, +// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), +// 1 wei +// ); +// assertApproxEqAbs( +// balance1AfterBob - balance1BeforeBob, +// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), +// 1 wei +// ); +// } +// } + +// function test_increaseLiquidity_withExactFees_withExactCachedFees() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; +// uint256 totalLiquidity = liquidityAlice + liquidityBob; + +// // alice provides liquidity +// vm.prank(alice); +// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // swap to create fees +// uint256 swapAmount = 0.001e18; +// swap(key, true, -int256(swapAmount), ZERO_BYTES); +// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + +// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + +// // bob collects fees so some of alice's fees are now cached +// vm.prank(bob); +// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + +// // swap to create more fees +// swap(key, true, -int256(swapAmount), ZERO_BYTES); +// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + +// (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); +// // alice's fees should be doubled +// assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); +// assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); + +// uint256 balance0AliceBefore = currency0.balanceOf(alice); +// uint256 balance1AliceBefore = currency1.balanceOf(alice); + +// // alice will use ALL of her fees to increase liquidity +// { +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// newToken0Owed, +// newToken1Owed +// ); + +// vm.prank(alice); +// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); +// } + +// // alice did not spend any tokens +// assertEq(balance0AliceBefore, currency0.balanceOf(alice)); +// assertEq(balance1AliceBefore, currency1.balanceOf(alice)); + +// // some dust was credited to alice's tokensOwed +// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); +// assertApproxEqAbs(token0Owed, 0, 80 wei); +// assertApproxEqAbs(token1Owed, 0, 80 wei); +// } + +// // uses donate to simulate fee revenue +// function test_increaseLiquidity_withExactFees_withExactCachedFees_donate() public { +// // Alice and Bob provide liquidity on the range +// // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity +// uint256 liquidityAlice = 3_000e18; +// uint256 liquidityBob = 1_000e18; +// uint256 totalLiquidity = liquidityAlice + liquidityBob; + +// // alice provides liquidity +// vm.prank(alice); +// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); +// uint256 tokenIdAlice = lpm.nextTokenId() - 1; + +// // bob provides liquidity +// vm.prank(bob); +// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); +// uint256 tokenIdBob = lpm.nextTokenId() - 1; + +// // donate to create fees +// donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + +// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + +// // bob collects fees so some of alice's fees are now cached +// vm.prank(bob); +// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + +// // donate to create more fees +// donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + +// (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); +// // alice's fees should be doubled +// assertApproxEqAbs(newToken0Owed, token0Owed * 2, 1 wei); +// assertApproxEqAbs(newToken1Owed, token1Owed * 2, 1 wei); + +// uint256 balance0AliceBefore = currency0.balanceOf(alice); +// uint256 balance1AliceBefore = currency1.balanceOf(alice); + +// // alice will use ALL of her fees to increase liquidity +// { +// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); +// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(range.tickLower), +// TickMath.getSqrtPriceAtTick(range.tickUpper), +// newToken0Owed, +// newToken1Owed +// ); + +// vm.prank(alice); +// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); +// } + +// // alice did not spend any tokens +// assertEq(balance0AliceBefore, currency0.balanceOf(alice), "alice spent token0"); +// assertEq(balance1AliceBefore, currency1.balanceOf(alice), "alice spent token1"); + +// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); +// assertEq(token0Owed, 0); +// assertEq(token1Owed, 0); + +// // bob still collects 5 +// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdBob); +// assertApproxEqAbs(token0Owed, 5e18, 1 wei); +// assertApproxEqAbs(token1Owed, 5e18, 1 wei); + +// vm.prank(bob); +// BalanceDelta result = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); +// assertApproxEqAbs(result.amount0(), 5e18, 1 wei); +// assertApproxEqAbs(result.amount1(), 5e18, 1 wei); +// } +// } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 4b8a7791..22f3ae81 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -10,7 +10,7 @@ import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; @@ -56,8 +56,17 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = - lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES); + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector( + lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES + ); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + BalanceDelta delta = toBalanceDelta(result[0], result[1]); + uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); @@ -68,228 +77,228 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); } - // function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - // LiquidityRange memory range = LiquidityRange({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(); - - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } - - // // minting with perfect token ratios will use all of the tokens - // function test_mint_perfect() public { - // int24 tickLower = -int24(key.tickSpacing); - // int24 tickUpper = int24(key.tickSpacing); - // uint256 amount0Desired = 100e18; - // uint256 amount1Desired = 100e18; - // LiquidityRange memory range = LiquidityRange({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: amount0Desired, - // amount1Min: amount1Desired, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // uint256 balance0After = currency0.balanceOfSelf(); - // uint256 balance1After = currency1.balanceOfSelf(); - - // assertEq(tokenId, 1); - // assertEq(lpm.ownerOf(1), address(this)); - // assertEq(uint256(int256(-delta.amount0())), amount0Desired); - // assertEq(uint256(int256(-delta.amount1())), amount1Desired); - // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // } - - // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - // public - // { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - // LiquidityRange memory range = LiquidityRange({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); - // } - - // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - // public - // { - // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // vm.assume(tickLower < 0 && 0 < tickUpper); - - // (amount0Desired, amount1Desired) = - // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - // vm.assume(0.00001e18 < amount0Desired); - // vm.assume(0.00001e18 < amount1Desired); - - // uint256 amount0Min = amount0Desired - 1; - // uint256 amount1Min = amount1Desired - 1; - - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // range: range, - // amount0Desired: amount0Desired, - // amount1Desired: amount1Desired, - // amount0Min: amount0Min, - // amount1Min: amount1Min, - // deadline: block.timestamp + 1, - // recipient: address(this), - // hookData: ZERO_BYTES - // }); - - // // seed some liquidity so we can move the price - // modifyLiquidityRouter.modifyLiquidity( - // key, - // IPoolManager.ModifyLiquidityParams({ - // tickLower: TickMath.minUsableTick(key.tickSpacing), - // tickUpper: TickMath.maxUsableTick(key.tickSpacing), - // liquidityDelta: 100_000e18, - // salt: 0 - // }), - // ZERO_BYTES - // ); - - // // swap to move the price - // swap(key, true, -1000e18, ZERO_BYTES); - - // // will revert because amount0Min and amount1Min are very strict - // vm.expectRevert(); - // lpm.mint(params); - // } - - function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { - uint256 balance0Start = currency0.balanceOfSelf(); - uint256 balance1Start = currency1.balanceOfSelf(); - - // create liquidity we can burn - uint256 tokenId; - (tokenId, 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); - assertEq(lpm.ownerOf(1), address(this)); - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, uint256(params.liquidityDelta)); - - // burn liquidity - uint256 balance0BeforeBurn = currency0.balanceOfSelf(); - uint256 balance1BeforeBurn = currency1.balanceOfSelf(); - // TODO, encode this under one call - lpm.decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); - lpm.collect(tokenId, address(this), ZERO_BYTES, false); - BalanceDelta delta = lpm.burn(tokenId); - (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, 0); - - // TODO: slightly off by 1 bip (0.0001%) - assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(delta.amount0())), 0.0001e18); - assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(delta.amount1())), 0.0001e18); - - // OZ 721 will revert if the token does not exist - vm.expectRevert(); - lpm.ownerOf(1); - - // no tokens were lost, TODO: fuzzer showing off by 1 sometimes - assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); - assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); - } - - function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) - public - { - uint256 tokenId; - (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - vm.assume(0 < decreaseLiquidityDelta); - vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); - vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - - LiquidityRange memory range = - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - - (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); - } - - // function test_decreaseLiquidity_collectFees( - // IPoolManager.ModifyLiquidityParams memory params, - // uint256 decreaseLiquidityDelta - // ) public { - // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - // vm.assume(0 < decreaseLiquidityDelta); - // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); - // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - - // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - - // // swap to create fees - // uint256 swapAmount = 0.01e18; - // swap(key, false, int256(swapAmount), ZERO_BYTES); - - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - - // // express key.fee as wad (i.e. 3000 = 0.003e18) - // uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); - // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); - // } - - function test_mintTransferBurn() public {} - function test_mintTransferCollect() public {} - function test_mintTransferIncrease() public {} - function test_mintTransferDecrease() public {} + // // 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}); + + // // uint256 balance0Before = currency0.balanceOfSelf(); + // // uint256 balance1Before = currency1.balanceOfSelf(); + // // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // // range: range, + // // amount0Desired: amount0Desired, + // // amount1Desired: amount1Desired, + // // amount0Min: 0, + // // amount1Min: 0, + // // deadline: block.timestamp + 1, + // // recipient: address(this), + // // hookData: ZERO_BYTES + // // }); + // // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // // uint256 balance0After = currency0.balanceOfSelf(); + // // uint256 balance1After = currency1.balanceOfSelf(); + + // // assertEq(tokenId, 1); + // // assertEq(lpm.ownerOf(1), address(this)); + // // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // // } + + // // // minting with perfect token ratios will use all of the tokens + // // function test_mint_perfect() public { + // // int24 tickLower = -int24(key.tickSpacing); + // // int24 tickUpper = int24(key.tickSpacing); + // // uint256 amount0Desired = 100e18; + // // uint256 amount1Desired = 100e18; + // // LiquidityRange memory range = LiquidityRange({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: amount0Desired, + // // amount1Min: amount1Desired, + // // deadline: block.timestamp + 1, + // // recipient: address(this), + // // hookData: ZERO_BYTES + // // }); + // // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // // uint256 balance0After = currency0.balanceOfSelf(); + // // uint256 balance1After = currency1.balanceOfSelf(); + + // // assertEq(tokenId, 1); + // // assertEq(lpm.ownerOf(1), address(this)); + // // assertEq(uint256(int256(-delta.amount0())), amount0Desired); + // // assertEq(uint256(int256(-delta.amount1())), amount1Desired); + // // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // // } + + // // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // // public + // // { + // // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // // (amount0Desired, amount1Desired) = + // // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + // // LiquidityRange memory range = LiquidityRange({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); + // // } + + // // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // // public + // // { + // // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // // vm.assume(tickLower < 0 && 0 < tickUpper); + + // // (amount0Desired, amount1Desired) = + // // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + // // vm.assume(0.00001e18 < amount0Desired); + // // vm.assume(0.00001e18 < amount1Desired); + + // // uint256 amount0Min = amount0Desired - 1; + // // uint256 amount1Min = amount1Desired - 1; + + // // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + // // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // // range: range, + // // amount0Desired: amount0Desired, + // // amount1Desired: amount1Desired, + // // amount0Min: amount0Min, + // // amount1Min: amount1Min, + // // deadline: block.timestamp + 1, + // // recipient: address(this), + // // hookData: ZERO_BYTES + // // }); + + // // // seed some liquidity so we can move the price + // // modifyLiquidityRouter.modifyLiquidity( + // // key, + // // IPoolManager.ModifyLiquidityParams({ + // // tickLower: TickMath.minUsableTick(key.tickSpacing), + // // tickUpper: TickMath.maxUsableTick(key.tickSpacing), + // // liquidityDelta: 100_000e18, + // // salt: 0 + // // }), + // // ZERO_BYTES + // // ); + + // // // swap to move the price + // // swap(key, true, -1000e18, ZERO_BYTES); + + // // // will revert because amount0Min and amount1Min are very strict + // // vm.expectRevert(); + // // lpm.mint(params); + // // } + + // function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { + // uint256 balance0Start = currency0.balanceOfSelf(); + // uint256 balance1Start = currency1.balanceOfSelf(); + + // // create liquidity we can burn + // uint256 tokenId; + // (tokenId, 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); + // assertEq(lpm.ownerOf(1), address(this)); + // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + // assertEq(liquidity, uint256(params.liquidityDelta)); + + // // burn liquidity + // uint256 balance0BeforeBurn = currency0.balanceOfSelf(); + // uint256 balance1BeforeBurn = currency1.balanceOfSelf(); + // // TODO, encode this under one call + // lpm.decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); + // lpm.collect(tokenId, address(this), ZERO_BYTES, false); + // BalanceDelta delta = lpm.burn(tokenId); + // (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); + // assertEq(liquidity, 0); + + // // TODO: slightly off by 1 bip (0.0001%) + // assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(delta.amount0())), 0.0001e18); + // assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(delta.amount1())), 0.0001e18); + + // // OZ 721 will revert if the token does not exist + // vm.expectRevert(); + // lpm.ownerOf(1); + + // // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + // assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); + // assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); + // } + + // function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + // public + // { + // uint256 tokenId; + // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // vm.assume(0 < decreaseLiquidityDelta); + // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + + // LiquidityRange memory range = + // LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + + // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); + // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); + // } + + // // function test_decreaseLiquidity_collectFees( + // // IPoolManager.ModifyLiquidityParams memory params, + // // uint256 decreaseLiquidityDelta + // // ) public { + // // uint256 tokenId; + // // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + // // vm.assume(0 < decreaseLiquidityDelta); + // // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + // // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + + // // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // // // swap to create fees + // // uint256 swapAmount = 0.01e18; + // // swap(key, false, int256(swapAmount), ZERO_BYTES); + + // // uint256 balance0Before = currency0.balanceOfSelf(); + // // uint256 balance1Before = currency1.balanceOfSelf(); + // // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + // // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + // // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // // // express key.fee as wad (i.e. 3000 = 0.003e18) + // // uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + // // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); + // // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); + // // } + + // function test_mintTransferBurn() public {} + // function test_mintTransferCollect() public {} + // function test_mintTransferIncrease() public {} + // function test_mintTransferDecrease() public {} } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index e118e062..de4fe12f 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.24; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; @@ -20,13 +21,21 @@ contract LiquidityFuzzers is Fuzzers { ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); - BalanceDelta delta = lpm.mint( - LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}), - uint256(params.liquidityDelta), - block.timestamp, - recipient, - hookData + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector( + lpm.mint.selector, range, uint256(params.liquidityDelta), block.timestamp, recipient, hookData ); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = key.currency0; + currencies[1] = key.currency1; + + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + BalanceDelta delta = toBalanceDelta(result[0], result[1]); + uint256 tokenId = lpm.nextTokenId() - 1; return (tokenId, params, delta); } From 6db1674f95c17388e8de7d014421cd422db8cc7a Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 30 Jun 2024 21:38:14 -0400 Subject: [PATCH 2/7] fix gas benchmark --- ...mpound_exactUnclaimedFees_exactCustodiedFees.snap | 2 +- test/position-managers/Gas.t.sol | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index f9df8100..6e4bf338 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -239588 \ No newline at end of file +238844 \ No newline at end of file diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index de0f3fb4..ee47a857 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -197,8 +197,14 @@ contract GasTest is Test, Deployers, GasSnapshot { donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); // bob collects fees so some of alice's fees are now cached + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenIdBob, bob, ZERO_BYTES, false); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + vm.prank(bob); - lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); + lpm.unlockAndExecute(calls, currencies); // donate to create more fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -216,10 +222,10 @@ contract GasTest is Test, Deployers, GasSnapshot { newToken1Owed ); - bytes[] memory calls = new bytes[](1); + calls = new bytes[](1); calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - Currency[] memory currencies = new Currency[](2); + currencies = new Currency[](2); currencies[0] = currency0; currencies[1] = currency1; From 4224c5b7c97723c7b5a8bf7e6ab36750ddddbede Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 30 Jun 2024 22:19:58 -0400 Subject: [PATCH 3/7] check posm is the locker before allowing access to external functions --- .../autocompound_exactUnclaimedFees.snap | 2 +- ...exactUnclaimedFees_exactCustodiedFees.snap | 2 +- .../autocompound_excessFeesCredit.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 28 +++++++++++-------- contracts/base/BaseLiquidityManagement.sol | 8 ++++-- .../INonfungiblePositionManager.sol | 2 ++ .../libraries/TransientLiquidityDelta.sol | 11 ++++++++ 12 files changed, 43 insertions(+), 22 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 6632f988..8cace02e 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -299380 \ No newline at end of file +299532 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 6e4bf338..68ec6575 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -238844 \ No newline at end of file +238996 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index ef8374e7..c2107d3b 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -319919 \ No newline at end of file +320071 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 4850a5b4..ca417da9 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -214439 \ No newline at end of file +214591 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 24d8e85d..597e3839 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -214451 \ No newline at end of file +214603 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 18129560..3904f832 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -194596 \ No newline at end of file +194748 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 3d4e376c..a9e8eac5 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -194608 \ No newline at end of file +194760 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index cfba5ed8..c4d1bd5c 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -513548 \ No newline at end of file +513700 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index bc385874..b6318f31 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -38,8 +38,9 @@ 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: TSTORE this jawn + // TODO: TSTORE these jawns address internal msgSender; + bool internal unlockedByThis; // TODO: Context is inherited through ERC721 and will be not useful to use _msgSender() which will be address(this) with our current mutlicall. function _msgSenderInternal() internal override returns (address) { @@ -53,6 +54,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit function unlockAndExecute(bytes[] memory data, Currency[] memory currencies) public returns (int128[] memory) { msgSender = msg.sender; + unlockedByThis = true; return abi.decode(manager.unlock(abi.encode(data, currencies)), (int128[])); } @@ -88,8 +90,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit uint256 deadline, address owner, bytes calldata hookData - ) external payable { - // TODO: security -- what happens if PoolManager is already unlocked by UR??? + ) external payable onlyIfUnlocked { _increaseLiquidity(owner, range, liquidity, hookData); // mint receipt token @@ -121,20 +122,20 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external isAuthorizedForToken(tokenId) + onlyIfUnlocked { TokenPosition memory tokenPos = tokenPositions[tokenId]; - // TODO: security -- what happens if PoolManager is already unlocked by UR??? _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external isAuthorizedForToken(tokenId) + onlyIfUnlocked { TokenPosition memory tokenPos = tokenPositions[tokenId]; - // TODO: security -- what happens if PoolManager is already unlocked by UR??? _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData); } @@ -151,15 +152,13 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit } // TODO: in v3, we can partially collect fees, but what was the usecase here? - function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) external { + function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) + external + onlyIfUnlocked + { TokenPosition memory tokenPos = tokenPositions[tokenId]; - // TODO: security -- what happens if PoolManager is already unlocked by UR??? - _collect(recipient, tokenPos.range, hookData); - if (recipient != _msgSenderInternal()) { - tokenPos.range.poolKey.currency0.close(manager, recipient); - tokenPos.range.poolKey.currency1.close(manager, recipient); - } + _collect(recipient, tokenPos.owner, tokenPos.range, hookData); } function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { @@ -190,4 +189,9 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit require(msg.sender == address(this) || _isApprovedOrOwner(msg.sender, tokenId), "Not approved"); _; } + + modifier onlyIfUnlocked() { + if (!unlockedByThis) revert MustBeUnlockedByThisContract(); + _; + } } diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index c1c1308b..b6f5d0e2 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -206,7 +206,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb } // The recipient may not be the original owner. - function _collect(address owner, LiquidityRange memory range, bytes memory hookData) internal { + function _collect(address recipient, address owner, LiquidityRange memory range, bytes memory hookData) internal { BalanceDelta callerDelta; BalanceDelta thisDelta; Position storage position = positions[owner][range.toId()]; @@ -233,7 +233,11 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb callerDelta = callerDelta + tokensOwed; thisDelta = thisDelta - tokensOwed; - callerDelta.flush(owner, range.poolKey.currency0, range.poolKey.currency1); + if (recipient == _msgSenderInternal()) { + callerDelta.flush(recipient, range.poolKey.currency0, range.poolKey.currency1); + } else { + callerDelta.closeDelta(manager, recipient, range.poolKey.currency0, range.poolKey.currency1); + } thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); position.clearTokensOwed(); diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index 03c83334..94cf9206 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -11,6 +11,8 @@ interface INonfungiblePositionManager { LiquidityRange range; } + error MustBeUnlockedByThisContract(); + // NOTE: more gas efficient as LiquidityAmounts is used offchain function mint( LiquidityRange calldata position, diff --git a/contracts/libraries/TransientLiquidityDelta.sol b/contracts/libraries/TransientLiquidityDelta.sol index 83ba3139..ccf520d4 100644 --- a/contracts/libraries/TransientLiquidityDelta.sol +++ b/contracts/libraries/TransientLiquidityDelta.sol @@ -74,6 +74,17 @@ library TransientLiquidityDelta { } } + function closeDelta( + BalanceDelta delta, + IPoolManager manager, + address holder, + Currency currency0, + Currency currency1 + ) internal { + close(currency0, manager, holder); + close(currency1, manager, holder); + } + function getBalanceDelta(address holder, Currency currency0, Currency currency1) internal view From 2637b6eccf65f816eb5f64dbd1c4962c30c7433c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Sun, 30 Jun 2024 23:38:37 -0400 Subject: [PATCH 4/7] restore execute tests --- test/position-managers/Execute.t.sol | 375 ++++++++++++++------------- 1 file changed, 195 insertions(+), 180 deletions(-) diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 1cdde778..5409216d 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -1,180 +1,195 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.24; - -// import "forge-std/Test.sol"; -// import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -// import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -// import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -// import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -// import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -// import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -// import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -// import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -// import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -// import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -// import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; -// import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -// import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -// import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -// import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; - -// import {IERC20} from "forge-std/interfaces/IERC20.sol"; -// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; - -// import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; -// import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -// import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; - -// import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -// contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { -// using FixedPointMathLib for uint256; -// using CurrencyLibrary for Currency; -// using LiquidityRangeIdLibrary for LiquidityRange; -// using PoolIdLibrary for PoolKey; -// using SafeCast for uint256; - -// NonfungiblePositionManager lpm; - -// PoolId poolId; -// address alice = makeAddr("ALICE"); -// address bob = makeAddr("BOB"); - -// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - -// // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) -// uint256 FEE_WAD; - -// LiquidityRange range; - -// function setUp() public { -// Deployers.deployFreshManagerAndRouters(); -// Deployers.deployMintAndApprove2Currencies(); - -// (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); -// FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - -// lpm = new NonfungiblePositionManager(manager); -// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); -// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); - -// // Give tokens to Alice and Bob, with approvals -// IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); -// IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); -// IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); -// IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); -// vm.startPrank(alice); -// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); -// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); -// vm.stopPrank(); -// vm.startPrank(bob); -// IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); -// IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); -// vm.stopPrank(); - -// // define a reusable range -// range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); -// } - -// function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { -// initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); -// liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); -// lpm.mint(range, initialLiquidity, 0, 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.unlockAndExecute(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); -// lpm.mint(range, initialiLiquidity, 0, 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.unlockAndExecute(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.unlockAndExecute(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"; + +contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + using PoolIdLibrary for PoolKey; + using SafeCast for uint256; + + NonfungiblePositionManager lpm; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + LiquidityRange range; + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + lpm = new NonfungiblePositionManager(manager); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + + // Give tokens to Alice and Bob, with approvals + IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE); + vm.startPrank(alice); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + vm.startPrank(bob); + IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); + vm.stopPrank(); + + // define a reusable range + range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300}); + } + + function test_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { + initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); + liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + _mint(range, initialLiquidity, 0, 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.unlockAndExecute(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, 0, 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.unlockAndExecute(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.unlockAndExecute(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 {} + + function _mint( + LiquidityRange memory _range, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes memory hookData + ) internal { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.unlockAndExecute(calls, currencies); + } +} From c87b0ad1fcec6e1e44f3e5e9ac749cfebe82eaef Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 1 Jul 2024 00:03:25 -0400 Subject: [PATCH 5/7] posm takes as 6909; remove legacy deadcode --- contracts/NonfungiblePositionManager.sol | 4 +- contracts/base/BaseLiquidityManagement.sol | 39 +------------------ .../libraries/TransientLiquidityDelta.sol | 22 +++++------ 3 files changed, 13 insertions(+), 52 deletions(-) diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index b6318f31..e6be1e49 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -72,8 +72,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit // close the deltas int128[] memory returnData = new int128[](currencies.length); for (uint256 i; i < currencies.length; i++) { - returnData[i] = currencies[i].close(manager, msgSender); - currencies[i].close(manager, address(this)); + returnData[i] = currencies[i].close(manager, msgSender, false); // TODO: support claims + currencies[i].close(manager, address(this), true); // position manager always takes 6909 } // Should just be returning the netted amount that was settled on behalf of the caller (msgSender) diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index b6f5d0e2..79862ded 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -51,27 +51,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb function _msgSenderInternal() internal virtual returns (address); - function _closeCallerDeltas( - BalanceDelta callerDeltas, - Currency currency0, - Currency currency1, - address owner, - bool claims - ) internal { - int128 callerDelta0 = callerDeltas.amount0(); - int128 callerDelta1 = callerDeltas.amount1(); - // On liquidity increase, the deltas should never be > 0. - // We always 0 out a caller positive delta because it is instead accounted for in position.tokensOwed. - - if (callerDelta0 < 0) currency0.settle(manager, owner, uint256(int256(-callerDelta0)), claims); - else if (callerDelta0 > 0) currency0.take(manager, owner, uint128(callerDelta0), claims); - - if (callerDelta1 < 0) currency1.settle(manager, owner, uint256(int256(-callerDelta1)), claims); - else if (callerDelta1 > 0) currency1.take(manager, owner, uint128(callerDelta1), claims); - - owner.close(currency0, currency1); - } - function _modifyLiquidity(address owner, LiquidityRange memory range, int256 liquidityChange, bytes memory hookData) internal returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) @@ -131,22 +110,6 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb position.addLiquidity(liquidityToAdd); } - // When chaining many actions, this should be called at the very end to close out any open deltas owed to or by this contract for other users on the same range. - // This is safe because any amounts the caller should not pay or take have already been accounted for in closeCallerDeltas. - function _closeThisDeltas(BalanceDelta delta, Currency currency0, Currency currency1) internal { - int128 delta0 = delta.amount0(); - int128 delta1 = delta.amount1(); - - // Mint a receipt for the tokens owed to this address. - if (delta0 > 0) currency0.take(manager, address(this), uint128(delta0), true); - if (delta1 > 0) currency1.take(manager, address(this), uint128(delta1), true); - // Burn the receipt for tokens owed to this address. - if (delta0 < 0) currency0.settle(manager, address(this), uint256(int256(-delta0)), true); - if (delta1 < 0) currency1.settle(manager, address(this), uint256(int256(-delta1)), true); - - address(this).close(currency0, currency1); - } - function _moveCallerDeltaToTokensOwed( bool useAmount0, BalanceDelta tokensOwed, @@ -236,7 +199,7 @@ abstract contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallb if (recipient == _msgSenderInternal()) { callerDelta.flush(recipient, range.poolKey.currency0, range.poolKey.currency1); } else { - callerDelta.closeDelta(manager, recipient, range.poolKey.currency0, range.poolKey.currency1); + callerDelta.closeDelta(manager, recipient, range.poolKey.currency0, range.poolKey.currency1, false); // TODO: allow recipient to receive claims, and add test! } thisDelta.flush(address(this), range.poolKey.currency0, range.poolKey.currency1); diff --git a/contracts/libraries/TransientLiquidityDelta.sol b/contracts/libraries/TransientLiquidityDelta.sol index ccf520d4..fdc1e614 100644 --- a/contracts/libraries/TransientLiquidityDelta.sol +++ b/contracts/libraries/TransientLiquidityDelta.sol @@ -25,11 +25,6 @@ library TransientLiquidityDelta { } } - function close(address holder, Currency currency0, Currency currency1) internal { - setDelta(currency0, holder, 0); - setDelta(currency1, holder, 0); - } - /// @notice Flush a BalanceDelta into transient storage for a given holder function flush(BalanceDelta delta, address holder, Currency currency0, Currency currency1) internal { addDelta(currency0, holder, delta.amount0()); @@ -54,18 +49,20 @@ library TransientLiquidityDelta { } } - function close(Currency currency, IPoolManager manager, address holder) internal returns (int128 delta) { + function close(Currency currency, IPoolManager manager, address holder, bool claims) + internal + returns (int128 delta) + { // getDelta(currency, holder); bytes32 hashSlot = _computeSlot(holder, currency); assembly { delta := tload(hashSlot) } - // TODO support claims field if (delta < 0) { - currency.settle(manager, holder, uint256(-int256(delta)), false); + currency.settle(manager, holder, uint256(-int256(delta)), claims); } else { - currency.take(manager, holder, uint256(int256(delta)), false); + currency.take(manager, holder, uint256(int256(delta)), claims); } // setDelta(0); @@ -79,10 +76,11 @@ library TransientLiquidityDelta { IPoolManager manager, address holder, Currency currency0, - Currency currency1 + Currency currency1, + bool claims ) internal { - close(currency0, manager, holder); - close(currency1, manager, holder); + close(currency0, manager, holder, claims); + close(currency1, manager, holder, claims); } function getBalanceDelta(address holder, Currency currency0, Currency currency1) From 9f472a2f0dc2faec7e3cecc61f2e1528ae19d5a6 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 1 Jul 2024 00:03:49 -0400 Subject: [PATCH 6/7] restore tests --- .../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 +- test/position-managers/FeeCollection.t.sol | 610 ++++++----- .../position-managers/IncreaseLiquidity.t.sol | 953 +++++++++--------- .../NonfungiblePositionManager.t.sol | 502 +++++---- 11 files changed, 1115 insertions(+), 966 deletions(-) diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees.snap index 8cace02e..30ef564b 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees.snap @@ -1 +1 @@ -299532 \ No newline at end of file +293482 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap index 68ec6575..8715f5c7 100644 --- a/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap +++ b/.forge-snapshots/autocompound_exactUnclaimedFees_exactCustodiedFees.snap @@ -1 +1 @@ -238996 \ No newline at end of file +225841 \ No newline at end of file diff --git a/.forge-snapshots/autocompound_excessFeesCredit.snap b/.forge-snapshots/autocompound_excessFeesCredit.snap index c2107d3b..87b78db1 100644 --- a/.forge-snapshots/autocompound_excessFeesCredit.snap +++ b/.forge-snapshots/autocompound_excessFeesCredit.snap @@ -1 +1 @@ -320071 \ No newline at end of file +314021 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index ca417da9..42c0f57b 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -214591 \ No newline at end of file +208541 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 597e3839..786e11f0 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -214603 \ No newline at end of file +208553 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 3904f832..f872a578 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -194748 \ No newline at end of file +194298 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index a9e8eac5..d9d4f89f 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -194760 \ No newline at end of file +194310 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index c4d1bd5c..39b5a098 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -513700 \ No newline at end of file +513250 \ No newline at end of file diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index bf7fc256..136d8a38 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -1,277 +1,333 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.24; - -// import "forge-std/Test.sol"; -// import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -// import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -// import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -// import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -// import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -// import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -// import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -// import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -// import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -// import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -// import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; -// import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -// import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; - -// import {IERC20} from "forge-std/interfaces/IERC20.sol"; -// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; - -// import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; -// import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; - -// import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; - -// contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { -// using FixedPointMathLib for uint256; -// using CurrencyLibrary for Currency; -// using LiquidityRangeIdLibrary for LiquidityRange; - -// NonfungiblePositionManager lpm; - -// PoolId poolId; -// address alice = makeAddr("ALICE"); -// address bob = makeAddr("BOB"); - -// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - -// // unused value for the fuzz helper functions -// uint128 constant DEAD_VALUE = 6969.6969 ether; - -// // expresses the fee as a wad (i.e. 3000 = 0.003e18) -// uint256 FEE_WAD; - -// 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(); -// } - -// function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { -// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); -// uint256 tokenId; -// (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); -// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - -// // swap to create fees -// uint256 swapAmount = 0.01e18; -// swap(key, false, -int256(swapAmount), ZERO_BYTES); - -// // collect fees -// BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, true); - -// assertEq(delta.amount0(), 0); - -// assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); - -// assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); -// } - -// function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { -// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); -// uint256 tokenId; -// (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); -// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - -// // swap to create fees -// uint256 swapAmount = 0.01e18; -// swap(key, false, -int256(swapAmount), ZERO_BYTES); - -// // collect fees -// uint256 balance0Before = currency0.balanceOfSelf(); -// uint256 balance1Before = currency1.balanceOfSelf(); -// BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, 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.amount1())), currency1.balanceOfSelf() - balance1Before); -// } - -// // two users with the same range; one user cannot collect the other's fees -// function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) -// public -// { -// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); -// params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); -// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - -// liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); - -// LiquidityRange memory range = -// LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); -// vm.prank(alice); -// lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// vm.prank(bob); -// lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); -// uint256 tokenIdBob = lpm.nextTokenId() - 1; - -// // swap to create fees -// uint256 swapAmount = 0.01e18; -// swap(key, false, -int256(swapAmount), ZERO_BYTES); - -// // alice collects only her fees -// vm.prank(alice); -// BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, true); -// assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); -// assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); -// assertTrue(delta.amount1() != 0); - -// // bob collects only his fees -// vm.prank(bob); -// delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, true); -// assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); -// assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); -// assertTrue(delta.amount1() != 0); - -// // position manager holds no fees now -// assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); -// assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); -// } - -// function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) -// public -// { -// params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); -// params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); -// vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - -// liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); - -// LiquidityRange memory range = -// LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); -// vm.prank(alice); -// lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// vm.prank(bob); -// lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); -// uint256 tokenIdBob = lpm.nextTokenId() - 1; - -// // confirm the positions are same range -// (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); -// (, LiquidityRange memory rangeBob) = lpm.tokenPositions(tokenIdBob); -// assertEq(rangeAlice.tickLower, rangeBob.tickLower); -// assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); - -// // swap to create fees -// uint256 swapAmount = 0.01e18; -// swap(key, false, -int256(swapAmount), ZERO_BYTES); - -// // alice collects only her fees -// uint256 balance0AliceBefore = currency0.balanceOf(alice); -// uint256 balance1AliceBefore = currency1.balanceOf(alice); -// vm.prank(alice); -// BalanceDelta delta = lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); -// uint256 balance0AliceAfter = currency0.balanceOf(alice); -// uint256 balance1AliceAfter = currency1.balanceOf(alice); - -// assertEq(balance0AliceBefore, balance0AliceAfter); -// assertEq(uint256(uint128(delta.amount1())), balance1AliceAfter - balance1AliceBefore); -// assertTrue(delta.amount1() != 0); - -// // bob collects only his fees -// uint256 balance0BobBefore = currency0.balanceOf(bob); -// uint256 balance1BobBefore = currency1.balanceOf(bob); -// vm.prank(bob); -// delta = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); -// uint256 balance0BobAfter = currency0.balanceOf(bob); -// uint256 balance1BobAfter = currency1.balanceOf(bob); - -// assertEq(balance0BobBefore, balance0BobAfter); -// assertEq(uint256(uint128(delta.amount1())), balance1BobAfter - balance1BobBefore); -// assertTrue(delta.amount1() != 0); - -// // position manager holds no fees now -// assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); -// assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); -// } - -// function test_collect_donate() public {} -// function test_collect_donate_sameRange() public {} - -// /// @dev Alice and bob create liquidity on the same range -// /// when alice decreases liquidity, she should only collect her fees -// /// TODO Add back fuzz test on liquidityDeltaBob -// /// TODO Assert state changes for lpm balance, position state, and return values -// function test_decreaseLiquidity_sameRange_exact() public { -// // alice and bob create liquidity on the same range [-120, 120] -// LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120}); - -// // alice provisions 3x the amount of liquidity as bob -// uint256 liquidityAlice = 3000e18; -// uint256 liquidityBob = 1000e18; -// vm.prank(alice); -// BalanceDelta lpDeltaAlice = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// vm.prank(bob); -// BalanceDelta lpDeltaBob = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); -// uint256 tokenIdBob = lpm.nextTokenId() - 1; - -// // swap to create fees -// uint256 swapAmount = 0.001e18; -// swap(key, true, -int256(swapAmount), ZERO_BYTES); -// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - -// // alice decreases liquidity -// vm.prank(alice); -// BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); - -// uint256 tolerance = 0.000000001 ether; - -// uint256 lpmBalance0 = manager.balanceOf(address(lpm), currency0.toId()); -// uint256 lpmBalance1 = manager.balanceOf(address(lpm), currency1.toId()); - -// // lpm collects alice's principal + all fees accrued on the range -// assertApproxEqAbs( -// lpmBalance0, uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD), tolerance -// ); -// assertApproxEqAbs( -// lpmBalance1, uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD), tolerance -// ); - -// // bob decreases half of his liquidity -// vm.prank(bob); -// BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); - -// // lpm collects half of bobs principal -// // the fee amount has already been collected with alice's calls -// assertApproxEqAbs( -// manager.balanceOf(address(lpm), currency0.toId()) - lpmBalance0, -// uint256(int256(-lpDeltaBob.amount0()) / 2), -// tolerance -// ); -// assertApproxEqAbs( -// manager.balanceOf(address(lpm), currency1.toId()) - lpmBalance1, -// uint256(int256(-lpDeltaBob.amount1()) / 2), -// tolerance -// ); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +import {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 {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; + +contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + + NonfungiblePositionManager lpm; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + // unused value for the fuzz helper functions + uint128 constant DEAD_VALUE = 6969.6969 ether; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18) + uint256 FEE_WAD; + + 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(); + } + + // TODO: we dont accept collecting fees as 6909 yet + // function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { + // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + // uint256 tokenId; + // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + // // swap to create fees + // uint256 swapAmount = 0.01e18; + // swap(key, false, -int256(swapAmount), ZERO_BYTES); + + // // collect fees + // BalanceDelta delta = _collect(tokenId, address(this), ZERO_BYTES, true); + + // assertEq(delta.amount0(), 0); + + // assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); + + // assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); + // } + + function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + uint256 tokenId; + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, -int256(swapAmount), ZERO_BYTES); + + // collect fees + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + BalanceDelta delta = _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.amount1())), currency1.balanceOfSelf() - balance1Before); + } + + // TODO: we dont accept collecting fees as 6909 yet + // two users with the same range; one user cannot collect the other's fees + // function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) + // public + // { + // params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + // params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + // liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + // LiquidityRange memory range = + // LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + // vm.prank(alice); + // _mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + // uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // vm.prank(bob); + // _mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + // uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // // swap to create fees + // uint256 swapAmount = 0.01e18; + // swap(key, false, -int256(swapAmount), ZERO_BYTES); + + // // alice collects only her fees + // vm.prank(alice); + // BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES, true); + // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(alice, currency0.toId())); + // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(alice, currency1.toId())); + // assertTrue(delta.amount1() != 0); + + // // bob collects only his fees + // vm.prank(bob); + // delta = _collect(tokenIdBob, bob, ZERO_BYTES, true); + // assertEq(uint256(uint128(delta.amount0())), manager.balanceOf(bob, currency0.toId())); + // assertEq(uint256(uint128(delta.amount1())), manager.balanceOf(bob, currency1.toId())); + // assertTrue(delta.amount1() != 0); + + // // position manager holds no fees now + // assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); + // assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); + // } + + function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) + public + { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + vm.prank(alice); + _mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + vm.prank(bob); + _mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // confirm the positions are same range + (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); + (, LiquidityRange memory rangeBob) = lpm.tokenPositions(tokenIdBob); + assertEq(rangeAlice.tickLower, rangeBob.tickLower); + assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, -int256(swapAmount), ZERO_BYTES); + + // alice collects only her fees + uint256 balance0AliceBefore = currency0.balanceOf(alice); + uint256 balance1AliceBefore = currency1.balanceOf(alice); + vm.prank(alice); + BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES, false); + uint256 balance0AliceAfter = currency0.balanceOf(alice); + uint256 balance1AliceAfter = currency1.balanceOf(alice); + + assertEq(balance0AliceBefore, balance0AliceAfter); + assertEq(uint256(uint128(delta.amount1())), balance1AliceAfter - balance1AliceBefore); + assertTrue(delta.amount1() != 0); + + // bob collects only his fees + uint256 balance0BobBefore = currency0.balanceOf(bob); + uint256 balance1BobBefore = currency1.balanceOf(bob); + vm.prank(bob); + delta = _collect(tokenIdBob, bob, ZERO_BYTES, false); + uint256 balance0BobAfter = currency0.balanceOf(bob); + uint256 balance1BobAfter = currency1.balanceOf(bob); + + assertEq(balance0BobBefore, balance0BobAfter); + assertEq(uint256(uint128(delta.amount1())), balance1BobAfter - balance1BobBefore); + assertTrue(delta.amount1() != 0); + + // position manager holds no fees now + assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); + assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); + } + + function test_collect_donate() public {} + function test_collect_donate_sameRange() public {} + + /// @dev Alice and bob create liquidity on the same range + /// when alice decreases liquidity, she should only collect her fees + /// TODO Add back fuzz test on liquidityDeltaBob + /// TODO Assert state changes for lpm balance, position state, and return values + function test_decreaseLiquidity_sameRange_exact() public { + // alice and bob create liquidity on the same range [-120, 120] + LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120}); + + // alice provisions 3x the amount of liquidity as bob + uint256 liquidityAlice = 3000e18; + uint256 liquidityBob = 1000e18; + vm.prank(alice); + BalanceDelta lpDeltaAlice = _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + vm.prank(bob); + BalanceDelta lpDeltaBob = _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice decreases liquidity + vm.prank(alice); + _decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); + + uint256 tolerance = 0.000000001 ether; + + uint256 lpmBalance0 = manager.balanceOf(address(lpm), currency0.toId()); + uint256 lpmBalance1 = manager.balanceOf(address(lpm), currency1.toId()); + + // lpm collects alice's principal + all fees accrued on the range + assertApproxEqAbs( + lpmBalance0, uint256(int256(-lpDeltaAlice.amount0())) + swapAmount.mulWadDown(FEE_WAD), tolerance + ); + assertApproxEqAbs( + lpmBalance1, uint256(int256(-lpDeltaAlice.amount1())) + swapAmount.mulWadDown(FEE_WAD), tolerance + ); + + // bob decreases half of his liquidity + vm.prank(bob); + _decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); + + // lpm collects half of bobs principal + // the fee amount has already been collected with alice's calls + assertApproxEqAbs( + manager.balanceOf(address(lpm), currency0.toId()) - lpmBalance0, + uint256(int256(-lpDeltaBob.amount0()) / 2), + tolerance + ); + assertApproxEqAbs( + manager.balanceOf(address(lpm), currency1.toId()) - lpmBalance1, + uint256(int256(-lpDeltaBob.amount1()) / 2), + tolerance + ); + } + + function _mint( + LiquidityRange memory _range, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes memory hookData + ) internal returns (BalanceDelta) { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.unlockAndExecute(calls, currencies); + } + + function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } +} diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 70dfba36..f1d8b30a 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -1,457 +1,496 @@ -// // 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 {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; - -// contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { -// using FixedPointMathLib for uint256; -// using CurrencyLibrary for Currency; -// using LiquidityRangeIdLibrary for LiquidityRange; -// using PoolIdLibrary for PoolKey; - -// NonfungiblePositionManager lpm; - -// PoolId poolId; -// address alice = makeAddr("ALICE"); -// address bob = makeAddr("BOB"); - -// uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - -// // unused value for the fuzz helper functions -// uint128 constant DEAD_VALUE = 6969.6969 ether; - -// // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) -// uint256 FEE_WAD; - -// LiquidityRange range; - -// function setUp() public { -// Deployers.deployFreshManagerAndRouters(); -// Deployers.deployMintAndApprove2Currencies(); - -// (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_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_increaseLiquidity_withExactFees() public { -// // Alice and Bob provide liquidity on the range -// // Alice uses her exact fees to increase liquidity (compounding) - -// uint256 liquidityAlice = 3_000e18; -// uint256 liquidityBob = 1_000e18; - -// // alice provides liquidity -// vm.prank(alice); -// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// // bob provides liquidity -// vm.prank(bob); -// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - -// // swap to create fees -// uint256 swapAmount = 0.001e18; -// swap(key, true, -int256(swapAmount), ZERO_BYTES); -// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - -// // alice uses her exact fees to increase liquidity -// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - -// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); -// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( -// sqrtPriceX96, -// TickMath.getSqrtPriceAtTick(range.tickLower), -// TickMath.getSqrtPriceAtTick(range.tickUpper), -// token0Owed, -// token1Owed -// ); - -// uint256 balance0BeforeAlice = currency0.balanceOf(alice); -// uint256 balance1BeforeAlice = currency1.balanceOf(alice); - -// vm.prank(alice); -// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - -// // alice did not spend any tokens -// assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); -// assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); - -// // alice spent all of the fees, approximately -// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); -// assertApproxEqAbs(token0Owed, 0, 20 wei); -// assertApproxEqAbs(token1Owed, 0, 20 wei); -// } - -// // uses donate to simulate fee revenue -// function test_increaseLiquidity_withExactFees_donate() public { -// // Alice and Bob provide liquidity on the range -// // Alice uses her exact fees to increase liquidity (compounding) - -// uint256 liquidityAlice = 3_000e18; -// uint256 liquidityBob = 1_000e18; - -// // alice provides liquidity -// vm.prank(alice); -// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// // bob provides liquidity -// vm.prank(bob); -// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); - -// // donate to create fees -// donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); - -// // alice uses her exact fees to increase liquidity -// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - -// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); -// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( -// sqrtPriceX96, -// TickMath.getSqrtPriceAtTick(range.tickLower), -// TickMath.getSqrtPriceAtTick(range.tickUpper), -// token0Owed, -// token1Owed -// ); - -// uint256 balance0BeforeAlice = currency0.balanceOf(alice); -// uint256 balance1BeforeAlice = currency1.balanceOf(alice); - -// vm.prank(alice); -// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); - -// // alice did not spend any tokens -// assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); -// assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); - -// // alice spent all of the fees -// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); -// assertEq(token0Owed, 0); -// assertEq(token1Owed, 0); -// } - -// function test_increaseLiquidity_withExcessFees() public { -// // Alice and Bob provide liquidity on the range -// // Alice uses her fees to increase liquidity. Excess fees are accounted to alice -// uint256 liquidityAlice = 3_000e18; -// uint256 liquidityBob = 1_000e18; -// uint256 totalLiquidity = liquidityAlice + liquidityBob; - -// // alice provides liquidity -// vm.prank(alice); -// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// // bob provides liquidity -// vm.prank(bob); -// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); -// uint256 tokenIdBob = lpm.nextTokenId() - 1; - -// // swap to create fees -// uint256 swapAmount = 0.001e18; -// swap(key, true, -int256(swapAmount), ZERO_BYTES); -// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - -// // alice will use half of her fees to increase liquidity -// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); -// { -// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); -// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( -// sqrtPriceX96, -// TickMath.getSqrtPriceAtTick(range.tickLower), -// TickMath.getSqrtPriceAtTick(range.tickUpper), -// token0Owed / 2, -// token1Owed / 2 -// ); - -// vm.prank(alice); -// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); -// } - -// { -// // bob collects his fees -// uint256 balance0BeforeBob = currency0.balanceOf(bob); -// uint256 balance1BeforeBob = currency1.balanceOf(bob); -// vm.prank(bob); -// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); -// uint256 balance0AfterBob = currency0.balanceOf(bob); -// uint256 balance1AfterBob = currency1.balanceOf(bob); -// assertApproxEqAbs( -// balance0AfterBob - balance0BeforeBob, -// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), -// 1 wei -// ); -// assertApproxEqAbs( -// balance1AfterBob - balance1BeforeBob, -// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), -// 1 wei -// ); -// } - -// { -// // alice collects her fees, which should be about half of the fees -// uint256 balance0BeforeAlice = currency0.balanceOf(alice); -// uint256 balance1BeforeAlice = currency1.balanceOf(alice); -// vm.prank(alice); -// lpm.collect(tokenIdAlice, alice, ZERO_BYTES, false); -// uint256 balance0AfterAlice = currency0.balanceOf(alice); -// uint256 balance1AfterAlice = currency1.balanceOf(alice); -// assertApproxEqAbs( -// balance0AfterAlice - balance0BeforeAlice, -// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, -// 9 wei -// ); -// assertApproxEqAbs( -// balance1AfterAlice - balance1BeforeAlice, -// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, -// 1 wei -// ); -// } -// } - -// function test_increaseLiquidity_withInsufficientFees() public { -// // Alice and Bob provide liquidity on the range -// // Alice uses her fees to increase liquidity. Additional funds are used by alice to increase liquidity -// uint256 liquidityAlice = 3_000e18; -// uint256 liquidityBob = 1_000e18; -// uint256 totalLiquidity = liquidityAlice + liquidityBob; - -// // alice provides liquidity -// vm.prank(alice); -// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// // bob provides liquidity -// vm.prank(bob); -// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); -// uint256 tokenIdBob = lpm.nextTokenId() - 1; - -// // swap to create fees -// uint256 swapAmount = 0.001e18; -// swap(key, true, -int256(swapAmount), ZERO_BYTES); -// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - -// // alice will use all of her fees + additional capital 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 -// ); - -// uint256 balance0BeforeAlice = currency0.balanceOf(alice); -// uint256 balance1BeforeAlice = currency1.balanceOf(alice); -// vm.prank(alice); -// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); -// uint256 balance0AfterAlice = currency0.balanceOf(alice); -// uint256 balance1AfterAlice = currency1.balanceOf(alice); - -// assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, token0Owed, 37 wei); -// assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, token1Owed, 1 wei); -// } - -// { -// // bob collects his fees -// uint256 balance0BeforeBob = currency0.balanceOf(bob); -// uint256 balance1BeforeBob = currency1.balanceOf(bob); -// vm.prank(bob); -// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); -// uint256 balance0AfterBob = currency0.balanceOf(bob); -// uint256 balance1AfterBob = currency1.balanceOf(bob); -// assertApproxEqAbs( -// balance0AfterBob - balance0BeforeBob, -// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), -// 1 wei -// ); -// assertApproxEqAbs( -// balance1AfterBob - balance1BeforeBob, -// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), -// 1 wei -// ); -// } -// } - -// function test_increaseLiquidity_withExactFees_withExactCachedFees() public { -// // Alice and Bob provide liquidity on the range -// // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity -// uint256 liquidityAlice = 3_000e18; -// uint256 liquidityBob = 1_000e18; -// uint256 totalLiquidity = liquidityAlice + liquidityBob; - -// // alice provides liquidity -// vm.prank(alice); -// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// // bob provides liquidity -// vm.prank(bob); -// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); -// uint256 tokenIdBob = lpm.nextTokenId() - 1; - -// // swap to create fees -// uint256 swapAmount = 0.001e18; -// swap(key, true, -int256(swapAmount), ZERO_BYTES); -// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - -// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - -// // bob collects fees so some of alice's fees are now cached -// vm.prank(bob); -// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - -// // swap to create more fees -// swap(key, true, -int256(swapAmount), ZERO_BYTES); -// swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - -// (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); -// // alice's fees should be doubled -// assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); -// assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); - -// uint256 balance0AliceBefore = currency0.balanceOf(alice); -// uint256 balance1AliceBefore = currency1.balanceOf(alice); - -// // alice will use ALL of her fees to increase liquidity -// { -// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); -// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( -// sqrtPriceX96, -// TickMath.getSqrtPriceAtTick(range.tickLower), -// TickMath.getSqrtPriceAtTick(range.tickUpper), -// newToken0Owed, -// newToken1Owed -// ); - -// vm.prank(alice); -// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); -// } - -// // alice did not spend any tokens -// assertEq(balance0AliceBefore, currency0.balanceOf(alice)); -// assertEq(balance1AliceBefore, currency1.balanceOf(alice)); - -// // some dust was credited to alice's tokensOwed -// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); -// assertApproxEqAbs(token0Owed, 0, 80 wei); -// assertApproxEqAbs(token1Owed, 0, 80 wei); -// } - -// // uses donate to simulate fee revenue -// function test_increaseLiquidity_withExactFees_withExactCachedFees_donate() public { -// // Alice and Bob provide liquidity on the range -// // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity -// uint256 liquidityAlice = 3_000e18; -// uint256 liquidityBob = 1_000e18; -// uint256 totalLiquidity = liquidityAlice + liquidityBob; - -// // alice provides liquidity -// vm.prank(alice); -// lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); -// uint256 tokenIdAlice = lpm.nextTokenId() - 1; - -// // bob provides liquidity -// vm.prank(bob); -// lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); -// uint256 tokenIdBob = lpm.nextTokenId() - 1; - -// // donate to create fees -// donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - -// (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - -// // bob collects fees so some of alice's fees are now cached -// vm.prank(bob); -// lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); - -// // donate to create more fees -// donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); - -// (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); -// // alice's fees should be doubled -// assertApproxEqAbs(newToken0Owed, token0Owed * 2, 1 wei); -// assertApproxEqAbs(newToken1Owed, token1Owed * 2, 1 wei); - -// uint256 balance0AliceBefore = currency0.balanceOf(alice); -// uint256 balance1AliceBefore = currency1.balanceOf(alice); - -// // alice will use ALL of her fees to increase liquidity -// { -// (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); -// uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( -// sqrtPriceX96, -// TickMath.getSqrtPriceAtTick(range.tickLower), -// TickMath.getSqrtPriceAtTick(range.tickUpper), -// newToken0Owed, -// newToken1Owed -// ); - -// vm.prank(alice); -// lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); -// } - -// // alice did not spend any tokens -// assertEq(balance0AliceBefore, currency0.balanceOf(alice), "alice spent token0"); -// assertEq(balance1AliceBefore, currency1.balanceOf(alice), "alice spent token1"); - -// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); -// assertEq(token0Owed, 0); -// assertEq(token1Owed, 0); - -// // bob still collects 5 -// (token0Owed, token1Owed) = lpm.feesOwed(tokenIdBob); -// assertApproxEqAbs(token0Owed, 5e18, 1 wei); -// assertApproxEqAbs(token1Owed, 5e18, 1 wei); - -// vm.prank(bob); -// BalanceDelta result = lpm.collect(tokenIdBob, bob, ZERO_BYTES, false); -// assertApproxEqAbs(result.amount0(), 5e18, 1 wei); -// assertApproxEqAbs(result.amount1(), 5e18, 1 wei); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; + +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; + +contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using LiquidityRangeIdLibrary for LiquidityRange; + using PoolIdLibrary for PoolKey; + + NonfungiblePositionManager lpm; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + + // unused value for the fuzz helper functions + uint128 constant DEAD_VALUE = 6969.6969 ether; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + LiquidityRange range; + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_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_increaseLiquidity_withExactFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.prank(alice); + _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); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice uses her exact fees to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + token0Owed, + token1Owed + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + vm.prank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + + // alice did not spend any tokens + assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); + assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + + // alice spent all of the fees, approximately + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertApproxEqAbs(token0Owed, 0, 20 wei); + assertApproxEqAbs(token1Owed, 0, 20 wei); + } + + // uses donate to simulate fee revenue + function test_increaseLiquidity_withExactFees_donate() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.prank(alice); + _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 + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + vm.prank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + + // alice did not spend any tokens + assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); + assertEq(balance1BeforeAlice, currency1.balanceOf(alice)); + + // alice spent all of the fees + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertEq(token0Owed, 0); + assertEq(token1Owed, 0); + } + + function test_increaseLiquidity_withExcessFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Excess fees are accounted to alice + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob provides liquidity + vm.prank(bob); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice will use half of her fees to increase liquidity + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + token0Owed / 2, + token1Owed / 2 + ); + + vm.prank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + } + + { + // bob collects his fees + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.prank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + uint256 balance0AfterBob = currency0.balanceOf(bob); + uint256 balance1AfterBob = currency1.balanceOf(bob); + assertApproxEqAbs( + balance0AfterBob - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + assertApproxEqAbs( + balance1AfterBob - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + } + + { + // alice collects her fees, which should be about half of the fees + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.prank(alice); + _collect(tokenIdAlice, alice, ZERO_BYTES, false); + uint256 balance0AfterAlice = currency0.balanceOf(alice); + uint256 balance1AfterAlice = currency1.balanceOf(alice); + assertApproxEqAbs( + balance0AfterAlice - balance0BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + 9 wei + ); + assertApproxEqAbs( + balance1AfterAlice - balance1BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + 1 wei + ); + } + } + + function test_increaseLiquidity_withInsufficientFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Additional funds are used by alice to increase liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob provides liquidity + vm.prank(bob); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice will use all of her fees + additional capital 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 + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.prank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + uint256 balance0AfterAlice = currency0.balanceOf(alice); + uint256 balance1AfterAlice = currency1.balanceOf(alice); + + assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, token0Owed, 37 wei); + assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, token1Owed, 1 wei); + } + + { + // bob collects his fees + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.prank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + uint256 balance0AfterBob = currency0.balanceOf(bob); + uint256 balance1AfterBob = currency1.balanceOf(bob); + assertApproxEqAbs( + balance0AfterBob - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + assertApproxEqAbs( + balance1AfterBob - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + } + } + + function test_increaseLiquidity_withExactFees_withExactCachedFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob provides liquidity + vm.prank(bob); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + // bob collects fees so some of alice's fees are now cached + vm.prank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + + // swap to create more fees + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + // alice's fees should be doubled + assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei); + assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei); + + uint256 balance0AliceBefore = currency0.balanceOf(alice); + uint256 balance1AliceBefore = currency1.balanceOf(alice); + + // alice will use ALL of her fees to increase liquidity + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + newToken0Owed, + newToken1Owed + ); + + vm.prank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + } + + // alice did not spend any tokens + assertEq(balance0AliceBefore, currency0.balanceOf(alice)); + assertEq(balance1AliceBefore, currency1.balanceOf(alice)); + + // some dust was credited to alice's tokensOwed + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertApproxEqAbs(token0Owed, 0, 80 wei); + assertApproxEqAbs(token1Owed, 0, 80 wei); + } + + // uses donate to simulate fee revenue + function test_increaseLiquidity_withExactFees_withExactCachedFees_donate() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.prank(alice); + _mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + // bob provides liquidity + vm.prank(bob); + _mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES); + uint256 tokenIdBob = lpm.nextTokenId() - 1; + + // donate to create fees + donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + + (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); + + // bob collects fees so some of alice's fees are now cached + vm.prank(bob); + _collect(tokenIdBob, bob, ZERO_BYTES, false); + + // donate to create more fees + donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); + + (uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice); + // alice's fees should be doubled + assertApproxEqAbs(newToken0Owed, token0Owed * 2, 1 wei); + assertApproxEqAbs(newToken1Owed, token1Owed * 2, 1 wei); + + uint256 balance0AliceBefore = currency0.balanceOf(alice); + uint256 balance1AliceBefore = currency1.balanceOf(alice); + + // alice will use ALL of her fees to increase liquidity + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), + newToken0Owed, + newToken1Owed + ); + + vm.prank(alice); + _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + } + + // alice did not spend any tokens + assertEq(balance0AliceBefore, currency0.balanceOf(alice), "alice spent token0"); + assertEq(balance1AliceBefore, currency1.balanceOf(alice), "alice spent token1"); + + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice); + assertEq(token0Owed, 0); + assertEq(token1Owed, 0); + + // bob still collects 5 + (token0Owed, token1Owed) = lpm.feesOwed(tokenIdBob); + assertApproxEqAbs(token0Owed, 5e18, 1 wei); + assertApproxEqAbs(token1Owed, 5e18, 1 wei); + + vm.prank(bob); + BalanceDelta result = _collect(tokenIdBob, bob, ZERO_BYTES, false); + assertApproxEqAbs(result.amount0(), 5e18, 1 wei); + assertApproxEqAbs(result.amount1(), 5e18, 1 wei); + } + + function _mint( + LiquidityRange memory _range, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes memory hookData + ) internal { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.unlockAndExecute(calls, currencies); + } + + 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); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.unlockAndExecute(calls, currencies); + } + + 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); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } +} diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 22f3ae81..17b35832 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -77,228 +77,282 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); } - // // function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - // // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // // (amount0Desired, amount1Desired) = - // // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - // // LiquidityRange memory range = LiquidityRange({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(); - - // // assertEq(tokenId, 1); - // // assertEq(lpm.ownerOf(1), address(this)); - // // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // // } - - // // // minting with perfect token ratios will use all of the tokens - // // function test_mint_perfect() public { - // // int24 tickLower = -int24(key.tickSpacing); - // // int24 tickUpper = int24(key.tickSpacing); - // // uint256 amount0Desired = 100e18; - // // uint256 amount1Desired = 100e18; - // // LiquidityRange memory range = LiquidityRange({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: amount0Desired, - // // amount1Min: amount1Desired, - // // deadline: block.timestamp + 1, - // // recipient: address(this), - // // hookData: ZERO_BYTES - // // }); - // // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - // // uint256 balance0After = currency0.balanceOfSelf(); - // // uint256 balance1After = currency1.balanceOfSelf(); - - // // assertEq(tokenId, 1); - // // assertEq(lpm.ownerOf(1), address(this)); - // // assertEq(uint256(int256(-delta.amount0())), amount0Desired); - // // assertEq(uint256(int256(-delta.amount1())), amount1Desired); - // // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - // // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - // // } - - // // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - // // public - // // { - // // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // // (amount0Desired, amount1Desired) = - // // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - // // LiquidityRange memory range = LiquidityRange({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); - // // } - - // // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - // // public - // // { - // // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - // // vm.assume(tickLower < 0 && 0 < tickUpper); - - // // (amount0Desired, amount1Desired) = - // // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - // // vm.assume(0.00001e18 < amount0Desired); - // // vm.assume(0.00001e18 < amount1Desired); - - // // uint256 amount0Min = amount0Desired - 1; - // // uint256 amount1Min = amount1Desired - 1; - - // // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - // // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - // // range: range, - // // amount0Desired: amount0Desired, - // // amount1Desired: amount1Desired, - // // amount0Min: amount0Min, - // // amount1Min: amount1Min, - // // deadline: block.timestamp + 1, - // // recipient: address(this), - // // hookData: ZERO_BYTES - // // }); - - // // // seed some liquidity so we can move the price - // // modifyLiquidityRouter.modifyLiquidity( - // // key, - // // IPoolManager.ModifyLiquidityParams({ - // // tickLower: TickMath.minUsableTick(key.tickSpacing), - // // tickUpper: TickMath.maxUsableTick(key.tickSpacing), - // // liquidityDelta: 100_000e18, - // // salt: 0 - // // }), - // // ZERO_BYTES - // // ); - - // // // swap to move the price - // // swap(key, true, -1000e18, ZERO_BYTES); - - // // // will revert because amount0Min and amount1Min are very strict - // // vm.expectRevert(); - // // lpm.mint(params); - // // } - - // function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { - // uint256 balance0Start = currency0.balanceOfSelf(); - // uint256 balance1Start = currency1.balanceOfSelf(); - - // // create liquidity we can burn - // uint256 tokenId; - // (tokenId, 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); - // assertEq(lpm.ownerOf(1), address(this)); - // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - // assertEq(liquidity, uint256(params.liquidityDelta)); - - // // burn liquidity - // uint256 balance0BeforeBurn = currency0.balanceOfSelf(); - // uint256 balance1BeforeBurn = currency1.balanceOfSelf(); - // // TODO, encode this under one call - // lpm.decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); - // lpm.collect(tokenId, address(this), ZERO_BYTES, false); - // BalanceDelta delta = lpm.burn(tokenId); - // (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); - // assertEq(liquidity, 0); - - // // TODO: slightly off by 1 bip (0.0001%) - // assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(delta.amount0())), 0.0001e18); - // assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(delta.amount1())), 0.0001e18); - - // // OZ 721 will revert if the token does not exist - // vm.expectRevert(); - // lpm.ownerOf(1); - - // // no tokens were lost, TODO: fuzzer showing off by 1 sometimes - // assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); - // assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); - // } - - // function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) - // public - // { - // uint256 tokenId; - // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - // vm.assume(0 < decreaseLiquidityDelta); - // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); - // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - - // LiquidityRange memory range = - // LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - - // uint256 balance0Before = currency0.balanceOfSelf(); - // uint256 balance1Before = currency1.balanceOfSelf(); - // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - - // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - - // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); - // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); - // } - - // // function test_decreaseLiquidity_collectFees( - // // IPoolManager.ModifyLiquidityParams memory params, - // // uint256 decreaseLiquidityDelta - // // ) public { - // // uint256 tokenId; - // // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); - // // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - // // vm.assume(0 < decreaseLiquidityDelta); - // // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); - // // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - - // // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - - // // // swap to create fees - // // uint256 swapAmount = 0.01e18; - // // swap(key, false, int256(swapAmount), ZERO_BYTES); - - // // uint256 balance0Before = currency0.balanceOfSelf(); - // // uint256 balance1Before = currency1.balanceOfSelf(); - // // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - // // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); - // // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - - // // // express key.fee as wad (i.e. 3000 = 0.003e18) - // // uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - // // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); - // // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); - // // } - - // function test_mintTransferBurn() public {} - // function test_mintTransferCollect() public {} - // function test_mintTransferIncrease() public {} - // function test_mintTransferDecrease() public {} + // 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}); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: 0, + // amount1Min: 0, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // uint256 balance0After = currency0.balanceOfSelf(); + // uint256 balance1After = currency1.balanceOfSelf(); + + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(1), address(this)); + // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // } + + // // minting with perfect token ratios will use all of the tokens + // function test_mint_perfect() public { + // int24 tickLower = -int24(key.tickSpacing); + // int24 tickUpper = int24(key.tickSpacing); + // uint256 amount0Desired = 100e18; + // uint256 amount1Desired = 100e18; + // LiquidityRange memory range = LiquidityRange({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: amount0Desired, + // amount1Min: amount1Desired, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // uint256 balance0After = currency0.balanceOfSelf(); + // uint256 balance1After = currency1.balanceOfSelf(); + + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(1), address(this)); + // assertEq(uint256(int256(-delta.amount0())), amount0Desired); + // assertEq(uint256(int256(-delta.amount1())), amount1Desired); + // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // } + + // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // public + // { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + // LiquidityRange memory range = LiquidityRange({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); + // } + + // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // public + // { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // vm.assume(tickLower < 0 && 0 < tickUpper); + + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + // vm.assume(0.00001e18 < amount0Desired); + // vm.assume(0.00001e18 < amount1Desired); + + // uint256 amount0Min = amount0Desired - 1; + // uint256 amount1Min = amount1Desired - 1; + + // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: amount0Min, + // amount1Min: amount1Min, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + + // // seed some liquidity so we can move the price + // modifyLiquidityRouter.modifyLiquidity( + // key, + // IPoolManager.ModifyLiquidityParams({ + // tickLower: TickMath.minUsableTick(key.tickSpacing), + // tickUpper: TickMath.maxUsableTick(key.tickSpacing), + // liquidityDelta: 100_000e18, + // salt: 0 + // }), + // ZERO_BYTES + // ); + + // // swap to move the price + // swap(key, true, -1000e18, ZERO_BYTES); + + // // will revert because amount0Min and amount1Min are very strict + // vm.expectRevert(); + // lpm.mint(params); + // } + + function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { + uint256 balance0Start = currency0.balanceOfSelf(); + uint256 balance1Start = currency1.balanceOfSelf(); + + // create liquidity we can burn + uint256 tokenId; + (tokenId, 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); + assertEq(lpm.ownerOf(1), address(this)); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta)); + + // burn liquidity + uint256 balance0BeforeBurn = currency0.balanceOfSelf(); + uint256 balance1BeforeBurn = currency1.balanceOfSelf(); + // TODO, encode this under one call + _decreaseLiquidity(tokenId, liquidity, ZERO_BYTES, false); + _collect(tokenId, address(this), ZERO_BYTES, false); + BalanceDelta delta = lpm.burn(tokenId); + (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, 0); + + // TODO: slightly off by 1 bip (0.0001%) + assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(delta.amount0())), 0.0001e18); + assertApproxEqRel(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(int256(delta.amount1())), 0.0001e18); + + // OZ 721 will revert if the token does not exist + vm.expectRevert(); + lpm.ownerOf(1); + + // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); + assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); + } + + function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + public + { + uint256 tokenId; + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(0 < decreaseLiquidityDelta); + vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + + LiquidityRange memory range = + LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + BalanceDelta delta = _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()))); + } + + // function test_decreaseLiquidity_collectFees( + // IPoolManager.ModifyLiquidityParams memory params, + // uint256 decreaseLiquidityDelta + // ) public { + // uint256 tokenId; + // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + // vm.assume(0 < decreaseLiquidityDelta); + // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + + // LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // // swap to create fees + // uint256 swapAmount = 0.01e18; + // swap(key, false, int256(swapAmount), ZERO_BYTES); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // // express key.fee as wad (i.e. 3000 = 0.003e18) + // uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); + // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); + // } + + function test_mintTransferBurn() public {} + function test_mintTransferCollect() public {} + function test_mintTransferIncrease() public {} + function test_mintTransferDecrease() public {} + + function _mint( + LiquidityRange memory _range, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes memory hookData + ) internal returns (BalanceDelta) { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + lpm.unlockAndExecute(calls, currencies); + } + + function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = currency0; + currencies[1] = currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } } From b92abcc02d5cefc09b37efedafc9d42cd620c4d1 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 1 Jul 2024 00:26:44 -0400 Subject: [PATCH 7/7] move helpers to the same file --- test/position-managers/Execute.t.sol | 21 +---- test/position-managers/FeeCollection.t.sol | 69 ++------------- test/position-managers/Gas.t.sol | 21 +---- .../position-managers/IncreaseLiquidity.t.sol | 84 ++++++------------- .../NonfungiblePositionManager.t.sol | 63 +------------- test/shared/LiquidityOperations.sol | 72 ++++++++++++++++ 6 files changed, 115 insertions(+), 215 deletions(-) create mode 100644 test/shared/LiquidityOperations.sol diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 5409216d..5cc9eb8e 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -27,15 +27,15 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; -contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; using SafeCast for uint256; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); @@ -177,19 +177,4 @@ contract ExecuteTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_execute_crossShard() public {} // cross-feed: collect and increase on different keys function test_execute_crossFeed() public {} - - function _mint( - LiquidityRange memory _range, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes memory hookData - ) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.unlockAndExecute(calls, currencies); - } } diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 136d8a38..7d3d4716 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -24,22 +24,19 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; -contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18) uint256 FEE_WAD; @@ -189,8 +186,9 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice collects only her fees uint256 balance0AliceBefore = currency0.balanceOf(alice); uint256 balance1AliceBefore = currency1.balanceOf(alice); - vm.prank(alice); + vm.startPrank(alice); BalanceDelta delta = _collect(tokenIdAlice, alice, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AliceAfter = currency0.balanceOf(alice); uint256 balance1AliceAfter = currency1.balanceOf(alice); @@ -201,8 +199,9 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // bob collects only his fees uint256 balance0BobBefore = currency0.balanceOf(bob); uint256 balance1BobBefore = currency1.balanceOf(bob); - vm.prank(bob); + vm.startPrank(bob); delta = _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0BobAfter = currency0.balanceOf(bob); uint256 balance1BobAfter = currency1.balanceOf(bob); @@ -276,58 +275,4 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { tolerance ); } - - function _mint( - LiquidityRange memory _range, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes memory hookData - ) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - int128[] memory result = lpm.unlockAndExecute(calls, currencies); - return toBalanceDelta(result[0], result[1]); - } - - function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.unlockAndExecute(calls, currencies); - } - - function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) - internal - returns (BalanceDelta) - { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - int128[] memory result = lpm.unlockAndExecute(calls, currencies); - return toBalanceDelta(result[0], result[1]); - } - - function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) - internal - returns (BalanceDelta) - { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - int128[] memory result = lpm.unlockAndExecute(calls, currencies); - return toBalanceDelta(result[0], result[1]); - } } diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index ee47a857..3d87fd77 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -23,14 +23,14 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; -contract GasTest is Test, Deployers, GasSnapshot { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract GasTest is Test, Deployers, GasSnapshot, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); @@ -310,19 +310,4 @@ contract GasTest is Test, Deployers, GasSnapshot { function test_gas_burn() public {} function test_gas_burnEmpty() public {} function test_gas_collect() public {} - - function _mint( - LiquidityRange memory _range, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes memory hookData - ) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.unlockAndExecute(calls, currencies); - } } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index f1d8b30a..39a6e329 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -25,23 +25,20 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; using PoolIdLibrary for PoolKey; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) uint256 FEE_WAD; @@ -112,8 +109,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); + vm.startPrank(alice); _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); // alice did not spend any tokens assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); @@ -160,8 +158,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); + vm.startPrank(alice); _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); // alice did not spend any tokens assertEq(balance0BeforeAlice, currency0.balanceOf(alice)); @@ -207,16 +206,18 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { token1Owed / 2 ); - vm.prank(alice); + vm.startPrank(alice); _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); } { // bob collects his fees uint256 balance0BeforeBob = currency0.balanceOf(bob); uint256 balance1BeforeBob = currency1.balanceOf(bob); - vm.prank(bob); + vm.startPrank(bob); _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterBob = currency0.balanceOf(bob); uint256 balance1AfterBob = currency1.balanceOf(bob); assertApproxEqAbs( @@ -235,8 +236,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // alice collects her fees, which should be about half of the fees uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); + vm.startPrank(alice); _collect(tokenIdAlice, alice, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); assertApproxEqAbs( @@ -288,8 +290,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.prank(alice); + vm.startPrank(alice); _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); @@ -301,8 +304,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { // bob collects his fees uint256 balance0BeforeBob = currency0.balanceOf(bob); uint256 balance1BeforeBob = currency1.balanceOf(bob); - vm.prank(bob); + vm.startPrank(bob); _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); uint256 balance0AfterBob = currency0.balanceOf(bob); uint256 balance1AfterBob = currency1.balanceOf(bob); assertApproxEqAbs( @@ -343,8 +347,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); // bob collects fees so some of alice's fees are now cached - vm.prank(bob); + vm.startPrank(bob); _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); // swap to create more fees swap(key, true, -int256(swapAmount), ZERO_BYTES); @@ -369,8 +374,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { newToken1Owed ); - vm.prank(alice); + vm.startPrank(alice); _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); } // alice did not spend any tokens @@ -407,8 +413,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); // bob collects fees so some of alice's fees are now cached - vm.prank(bob); + vm.startPrank(bob); _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); // donate to create more fees donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES); @@ -432,8 +439,9 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { newToken1Owed ); - vm.prank(alice); + vm.startPrank(alice); _increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); + vm.stopPrank(); } // alice did not spend any tokens @@ -449,48 +457,10 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { assertApproxEqAbs(token0Owed, 5e18, 1 wei); assertApproxEqAbs(token1Owed, 5e18, 1 wei); - vm.prank(bob); + vm.startPrank(bob); BalanceDelta result = _collect(tokenIdBob, bob, ZERO_BYTES, false); + vm.stopPrank(); assertApproxEqAbs(result.amount0(), 5e18, 1 wei); assertApproxEqAbs(result.amount1(), 5e18, 1 wei); } - - function _mint( - LiquidityRange memory _range, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes memory hookData - ) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.unlockAndExecute(calls, currencies); - } - - 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); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.unlockAndExecute(calls, currencies); - } - - 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); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - int128[] memory result = lpm.unlockAndExecute(calls, currencies); - return toBalanceDelta(result[0], result[1]); - } } diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index 17b35832..0c4f52bc 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -24,19 +24,16 @@ import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../c import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; -contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +import {LiquidityOperations} from "../shared/LiquidityOperations.sol"; + +contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, LiquidityFuzzers, LiquidityOperations { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; - NonfungiblePositionManager lpm; - PoolId poolId; address alice = makeAddr("ALICE"); - // unused value for the fuzz helper functions - uint128 constant DEAD_VALUE = 6969.6969 ether; - function setUp() public { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); @@ -301,58 +298,4 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi function test_mintTransferCollect() public {} function test_mintTransferIncrease() public {} function test_mintTransferDecrease() public {} - - function _mint( - LiquidityRange memory _range, - uint256 liquidity, - uint256 deadline, - address recipient, - bytes memory hookData - ) internal returns (BalanceDelta) { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - int128[] memory result = lpm.unlockAndExecute(calls, currencies); - return toBalanceDelta(result[0], result[1]); - } - - function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - lpm.unlockAndExecute(calls, currencies); - } - - function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) - internal - returns (BalanceDelta) - { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - int128[] memory result = lpm.unlockAndExecute(calls, currencies); - return toBalanceDelta(result[0], result[1]); - } - - function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) - internal - returns (BalanceDelta) - { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); - - Currency[] memory currencies = new Currency[](2); - currencies[0] = currency0; - currencies[1] = currency1; - int128[] memory result = lpm.unlockAndExecute(calls, currencies); - return toBalanceDelta(result[0], result[1]); - } } diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol new file mode 100644 index 00000000..a3d556cd --- /dev/null +++ b/test/shared/LiquidityOperations.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; +import {LiquidityRange} from "../../contracts/types/LiquidityRange.sol"; + +contract LiquidityOperations { + NonfungiblePositionManager lpm; + + function _mint( + LiquidityRange memory _range, + uint256 liquidity, + uint256 deadline, + address recipient, + bytes memory hookData + ) internal returns (BalanceDelta) { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.mint.selector, _range, liquidity, deadline, recipient, hookData); + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _increaseLiquidity(uint256 tokenId, uint256 liquidityToAdd, bytes memory hookData, bool claims) internal { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.increaseLiquidity.selector, tokenId, liquidityToAdd, hookData, claims); + + (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + lpm.unlockAndExecute(calls, currencies); + } + + function _decreaseLiquidity(uint256 tokenId, uint256 liquidityToRemove, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.decreaseLiquidity.selector, tokenId, liquidityToRemove, hookData, claims); + + (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } + + function _collect(uint256 tokenId, address recipient, bytes memory hookData, bool claims) + internal + returns (BalanceDelta) + { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(lpm.collect.selector, tokenId, recipient, hookData, claims); + + (, LiquidityRange memory _range) = lpm.tokenPositions(tokenId); + + Currency[] memory currencies = new Currency[](2); + currencies[0] = _range.poolKey.currency0; + currencies[1] = _range.poolKey.currency1; + int128[] memory result = lpm.unlockAndExecute(calls, currencies); + return toBalanceDelta(result[0], result[1]); + } +}