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