Files

829 lines
35 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import {PoolManager} from "@luxamm/v4-core/src/PoolManager.sol";
import {IPoolManager} from "@luxamm/v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "@luxamm/v4-core/src/interfaces/IHooks.sol";
import {Currency} from "@luxamm/v4-core/src/types/Currency.sol";
import {PoolId} from "@luxamm/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@luxamm/v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "@luxamm/v4-core/src/types/BalanceDelta.sol";
import {LiquidityAmounts} from "@luxamm/v4-core/test/utils/LiquidityAmounts.sol";
import {TickMath} from "@luxamm/v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "@luxamm/v4-core/src/libraries/StateLibrary.sol";
import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol";
import {Fuzzers} from "@luxamm/v4-core/src/test/Fuzzers.sol";
import {Position} from "@luxamm/v4-core/src/libraries/Position.sol";
import {SafeCast} from "@luxamm/v4-core/src/libraries/SafeCast.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {DeltaResolver} from "../../src/base/DeltaResolver.sol";
import {PositionConfig} from "../shared/PositionConfig.sol";
import {SlippageCheck} from "../../src/libraries/SlippageCheck.sol";
import {IPositionManager} from "../../src/interfaces/IPositionManager.sol";
import {Actions} from "../../src/libraries/Actions.sol";
import {Planner, Plan} from "../shared/Planner.sol";
import {FeeMath} from "../shared/FeeMath.sol";
import {PosmTestSetup} from "../shared/PosmTestSetup.sol";
import {ActionConstants} from "../../src/libraries/ActionConstants.sol";
contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
using FixedPointMathLib for uint256;
using FeeMath for IPositionManager;
using StateLibrary for IPoolManager;
PoolId poolId;
address alice = makeAddr("ALICE");
address bob = makeAddr("BOB");
// expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%)
uint256 FEE_WAD;
PositionConfig config;
// Error tolerance.
uint256 tolerance = 0.00000000001 ether;
function setUp() public {
deployFreshManagerAndRouters();
deployMintAndApprove2Currencies();
// This is needed to receive return deltas from modifyLiquidity calls.
deployPosmHookSavesDelta();
(key, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1);
FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000);
// Requires currency0 and currency1 to be set in base Deployers contract.
deployAndApprovePosm(manager);
// Give tokens to Alice and Bob.
seedBalance(alice);
seedBalance(bob);
// Approve posm for Alice and bob.
approvePosmFor(alice);
approvePosmFor(bob);
// define a reusable range
config = PositionConfig({poolKey: key, tickLower: -300, tickUpper: 300});
}
/// @notice Increase liquidity by less than the amount of liquidity the position has earned, requiring a take
function test_increaseLiquidity_withCollection_takePair() 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.startPrank(alice);
uint256 tokenIdAlice = lpm.nextTokenId();
mint(config, liquidityAlice, alice, ZERO_BYTES);
vm.stopPrank();
// bob provides liquidity
vm.startPrank(bob);
mint(config, liquidityBob, bob, ZERO_BYTES);
vm.stopPrank();
// donate to create fees
uint256 amountDonate = 0.1e18;
donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES);
// alice uses her half her fees to increase liquidity
// Slight error in this calculation vs. actual fees.. TODO: Fix this.
BalanceDelta feesOwedAlice = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice);
// Note: You can alternatively calculate Alice's fees owed from the swap amount, fee on the pool, and total liquidity in that range.
// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob);
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint256(int256(feesOwedAlice.amount0() / 2)),
uint256(int256(feesOwedAlice.amount1() / 2))
);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
// Set the slippage amounts to be exactly half the fees that alice is reinvesting.
Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(
tokenIdAlice, liquidityDelta, feesOwedAlice.amount0() / 2, feesOwedAlice.amount1() / 2, ZERO_BYTES
)
);
bytes memory calls = planner.finalizeModifyLiquidityWithTakePair(config.poolKey, address(alice));
vm.startPrank(alice);
lpm.modifyLiquidities(calls, _deadline);
vm.stopPrank();
// alices current balance is the balanceBefore plus half of her fees owed
assertApproxEqAbs(
currency0.balanceOf(alice), balance0BeforeAlice + uint256(int256(feesOwedAlice.amount0() / 2)), tolerance
);
assertApproxEqAbs(
currency1.balanceOf(alice), balance1BeforeAlice + uint256(int256(feesOwedAlice.amount1() / 2)), tolerance
);
}
/// @notice Increase liquidity with exact fees, taking dust
function test_increaseLiquidity_withExactFees_take() 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.startPrank(alice);
uint256 tokenIdAlice = lpm.nextTokenId();
mint(config, liquidityAlice, alice, ZERO_BYTES);
vm.stopPrank();
// bob provides liquidity
vm.startPrank(bob);
mint(config, liquidityBob, bob, ZERO_BYTES);
vm.stopPrank();
// 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
// Slight error in this calculation vs. actual fees.. TODO: Fix this.
BalanceDelta feesOwedAlice = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice);
// Note: You can alternatively calculate Alice's fees owed from the swap amount, fee on the pool, and total liquidity in that range.
// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob);
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint256(int256(feesOwedAlice.amount0())),
uint256(int256(feesOwedAlice.amount1()))
);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, liquidityDelta, feesOwedAlice.amount0(), feesOwedAlice.amount1(), ZERO_BYTES)
);
bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey);
vm.startPrank(alice);
lpm.modifyLiquidities(calls, _deadline);
vm.stopPrank();
// alice barely spent any tokens
assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance);
assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), tolerance);
}
/// @dev Increase liquidity with exact fees, clearing dust
function test_increaseLiquidity_withExactFees_clear() 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.startPrank(alice);
uint256 tokenIdAlice = lpm.nextTokenId();
mint(config, liquidityAlice, alice, ZERO_BYTES);
vm.stopPrank();
// bob provides liquidity
vm.startPrank(bob);
mint(config, liquidityBob, bob, ZERO_BYTES);
vm.stopPrank();
// 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
// Slight error in this calculation vs. actual fees.. TODO: Fix this.
BalanceDelta feesOwedAlice = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice);
// Note: You can alternatively calculate Alice's fees owed from the swap amount, fee on the pool, and total liquidity in that range.
// swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob);
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint256(int256(feesOwedAlice.amount0())),
uint256(int256(feesOwedAlice.amount1()))
);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, 18 wei)); // alice is willing to forfeit 18 wei
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, 18 wei));
bytes memory calls = planner.encode();
vm.prank(alice);
lpm.modifyLiquidities(calls, _deadline);
// alice did not spend or receive tokens
// (alice forfeited a small amount of tokens to the pool with CLEAR)
assertEq(currency0.balanceOf(alice), balance0BeforeAlice);
assertEq(currency1.balanceOf(alice), balance1BeforeAlice);
}
// uses donate to simulate fee revenue, taking dust
function test_increaseLiquidity_withExactFees_take_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.startPrank(alice);
uint256 tokenIdAlice = lpm.nextTokenId();
mint(config, liquidityAlice, alice, ZERO_BYTES);
vm.stopPrank();
// bob provides liquidity
vm.startPrank(bob);
mint(config, liquidityBob, bob, ZERO_BYTES);
vm.stopPrank();
// donate to create fees
uint256 amountDonate = 0.2e18;
donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES);
// subtract 1 cause we'd rather take than pay
uint256 feesAmount = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1;
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
feesAmount,
feesAmount
);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
vm.startPrank(alice);
increaseLiquidity(tokenIdAlice, config, liquidityDelta, ZERO_BYTES);
vm.stopPrank();
// alice barely spent any tokens
assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), 1 wei);
assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), 1 wei);
}
// uses donate to simulate fee revenue, clearing dust
function test_increaseLiquidity_withExactFees_clear_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.startPrank(alice);
uint256 tokenIdAlice = lpm.nextTokenId();
mint(config, liquidityAlice, alice, ZERO_BYTES);
vm.stopPrank();
// bob provides liquidity
vm.startPrank(bob);
mint(config, liquidityBob, bob, ZERO_BYTES);
vm.stopPrank();
// donate to create fees
uint256 amountDonate = 0.2e18;
donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES);
// subtract 1 cause we'd rather take than pay
uint256 feesAmount = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1;
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
feesAmount,
feesAmount
);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, 1 wei)); // alice is willing to forfeit 1 wei
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, 1 wei));
bytes memory calls = planner.encode();
vm.prank(alice);
lpm.modifyLiquidities(calls, _deadline);
// alice did not spend or receive tokens
// (alice forfeited a small amount of tokens to the pool with CLEAR)
assertEq(currency0.balanceOf(alice), balance0BeforeAlice);
assertEq(currency1.balanceOf(alice), balance1BeforeAlice);
}
function test_increaseLiquidity_sameRange_withExcessFees() public {
// Alice and Bob provide liquidity on the same range
// Alice uses half her fees to increase liquidity. The other half are collected to her wallet.
// Bob collects all fees.
uint256 liquidityAlice = 3_000e18;
uint256 liquidityBob = 1_000e18;
uint256 totalLiquidity = liquidityAlice + liquidityBob;
// alice provides liquidity
vm.startPrank(alice);
uint256 tokenIdAlice = lpm.nextTokenId();
mint(config, liquidityAlice, alice, ZERO_BYTES);
vm.stopPrank();
// bob provides liquidity
vm.prank(bob);
uint256 tokenIdBob = lpm.nextTokenId();
mint(config, liquidityBob, bob, ZERO_BYTES);
vm.stopPrank();
// 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
BalanceDelta aliceFeesOwed = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice);
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint256(int256(aliceFeesOwed.amount0() / 2)),
uint256(int256(aliceFeesOwed.amount1() / 2))
);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
vm.startPrank(alice);
increaseLiquidity(tokenIdAlice, config, liquidityDelta, ZERO_BYTES);
vm.stopPrank();
assertApproxEqAbs(
currency0.balanceOf(alice) - balance0BeforeAlice,
swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2,
tolerance
);
assertApproxEqAbs(
currency1.balanceOf(alice) - balance1BeforeAlice,
swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2,
tolerance
);
assertApproxEqAbs(
currency0.balanceOf(alice) - balance0BeforeAlice, uint128(aliceFeesOwed.amount0()) / 2, tolerance
);
assertApproxEqAbs(
currency1.balanceOf(alice) - balance1BeforeAlice, uint128(aliceFeesOwed.amount1()) / 2, tolerance
);
}
{
// bob collects his fees
uint256 balance0BeforeBob = currency0.balanceOf(bob);
uint256 balance1BeforeBob = currency1.balanceOf(bob);
vm.startPrank(bob);
collect(tokenIdBob, config, ZERO_BYTES);
vm.stopPrank();
assertApproxEqAbs(
currency0.balanceOf(bob) - balance0BeforeBob,
swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity),
tolerance
);
assertApproxEqAbs(
currency1.balanceOf(bob) - balance1BeforeBob,
swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity),
tolerance
);
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_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.startPrank(alice);
uint256 tokenIdAlice = lpm.nextTokenId();
mint(config, liquidityAlice, alice, ZERO_BYTES);
vm.stopPrank();
// bob provides liquidity
vm.startPrank(bob);
uint256 tokenIdBob = lpm.nextTokenId();
mint(config, liquidityBob, bob, ZERO_BYTES);
vm.stopPrank();
// 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
BalanceDelta feesOwed = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice);
{
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint256(int256(feesOwed.amount0())) * 2,
uint256(int256(feesOwed.amount1())) * 2
);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
vm.startPrank(alice);
increaseLiquidity(tokenIdAlice, config, liquidityDelta, ZERO_BYTES);
vm.stopPrank();
uint256 balance0AfterAlice = currency0.balanceOf(alice);
uint256 balance1AfterAlice = currency1.balanceOf(alice);
// Alice owed feesOwed amount in 0 and 1 because she places feesOwed * 2 back into the pool.
assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, uint256(int256(feesOwed.amount0())), tolerance);
assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, uint256(int256(feesOwed.amount1())), tolerance);
}
{
// bob collects his fees
uint256 balance0BeforeBob = currency0.balanceOf(bob);
uint256 balance1BeforeBob = currency1.balanceOf(bob);
vm.startPrank(bob);
collect(tokenIdBob, config, ZERO_BYTES);
vm.stopPrank();
uint256 balance0AfterBob = currency0.balanceOf(bob);
uint256 balance1AfterBob = currency1.balanceOf(bob);
assertApproxEqAbs(
balance0AfterBob - balance0BeforeBob,
swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity),
tolerance
);
assertApproxEqAbs(
balance1AfterBob - balance1BeforeBob,
swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity),
tolerance
);
}
}
function test_increaseLiquidity_slippage_revertAmount0() public {
// increasing liquidity with strict slippage parameters (amount0) will revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
uint128 newLiquidity = 100e18;
(uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
newLiquidity
);
// revert since amount0Max is too low
bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, 1 wei, type(uint128).max, ZERO_BYTES);
vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount0 + 1));
lpm.modifyLiquidities(calls, _deadline);
}
function test_increaseLiquidity_slippage_revertAmount1() public {
// increasing liquidity with strict slippage parameters (amount1) will revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
uint128 newLiquidity = 100e18;
(, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
newLiquidity
);
// revert since amount1Max is too low
bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, type(uint128).max, 1 wei, ZERO_BYTES);
vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount1 + 1));
lpm.modifyLiquidities(calls, _deadline);
}
function test_increaseLiquidity_slippage_exactDoesNotRevert() public {
// increasing liquidity with perfect slippage parameters does not revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
uint128 newLiquidity = 10e18;
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
newLiquidity
);
assertEq(amount0, amount1); // symmetric liquidity addition
uint128 slippage = uint128(amount0) + 1;
bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
// confirm that delta == slippage tolerance
assertEq(-delta.amount0(), int128(slippage));
assertEq(-delta.amount1(), int128(slippage));
}
/// price movement from swaps will cause slippage reverts
function test_increaseLiquidity_slippage_revert_swap() public {
// increasing liquidity with perfect slippage parameters does not revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
uint128 newLiquidity = 10e18;
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
newLiquidity
);
assertEq(amount0, amount1); // symmetric liquidity addition
uint128 slippage = uint128(amount0) + 1;
// swap to create slippage
swap(key, true, -10e18, ZERO_BYTES);
bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES);
vm.expectRevert(
abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, slippage, 299996249439153403)
);
lpm.modifyLiquidities(calls, _deadline);
}
function test_mint_settleWithBalance_andSweepToOtherAddress() public {
uint256 liquidityAlice = 3_000e18;
Plan memory planner = Planner.init();
planner.add(
Actions.MINT_POSITION,
abi.encode(
config.poolKey,
config.tickLower,
config.tickUpper,
liquidityAlice,
MAX_SLIPPAGE_INCREASE,
MAX_SLIPPAGE_INCREASE,
alice,
ZERO_BYTES
)
);
planner.add(Actions.SETTLE, abi.encode(currency0, ActionConstants.OPEN_DELTA, false));
planner.add(Actions.SETTLE, abi.encode(currency1, ActionConstants.OPEN_DELTA, false));
// this test sweeps to the test contract, even though Alice is the caller of the transaction
planner.add(Actions.SWEEP, abi.encode(currency0, address(this)));
planner.add(Actions.SWEEP, abi.encode(currency1, address(this)));
uint256 balanceBefore0 = currency0.balanceOf(address(this));
uint256 balanceBefore1 = currency1.balanceOf(address(this));
assertEq(currency0.balanceOf(address(lpm)), 0);
assertEq(currency0.balanceOf(address(lpm)), 0);
currency0.transfer(address(lpm), 100e18);
currency1.transfer(address(lpm), 100e18);
assertEq(currency0.balanceOf(address(lpm)), 100e18);
assertEq(currency0.balanceOf(address(lpm)), 100e18);
bytes memory calls = planner.encode();
vm.prank(alice);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
uint256 amount0 = uint128(-delta.amount0());
uint256 amount1 = uint128(-delta.amount1());
// The balances were swept back to this address.
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(lpm)), 0);
assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(lpm)), 0);
assertEq(currency0.balanceOf(address(this)), balanceBefore0 - amount0);
assertEq(currency1.balanceOf(address(this)), balanceBefore1 - amount1);
}
function test_mint_settleWithBalance_andSweepToMsgSender() public {
uint256 liquidityAlice = 3_000e18;
Plan memory planner = Planner.init();
planner.add(
Actions.MINT_POSITION,
abi.encode(
config.poolKey,
config.tickLower,
config.tickUpper,
liquidityAlice,
MAX_SLIPPAGE_INCREASE,
MAX_SLIPPAGE_INCREASE,
alice,
ZERO_BYTES
)
);
planner.add(Actions.SETTLE, abi.encode(currency0, ActionConstants.OPEN_DELTA, false));
planner.add(Actions.SETTLE, abi.encode(currency1, ActionConstants.OPEN_DELTA, false));
planner.add(Actions.SWEEP, abi.encode(currency0, ActionConstants.MSG_SENDER));
planner.add(Actions.SWEEP, abi.encode(currency1, ActionConstants.MSG_SENDER));
uint256 balanceBefore0 = currency0.balanceOf(alice);
uint256 balanceBefore1 = currency1.balanceOf(alice);
uint256 seedAmount = 100e18;
currency0.transfer(address(lpm), seedAmount);
currency1.transfer(address(lpm), seedAmount);
assertEq(currency0.balanceOf(address(lpm)), seedAmount);
assertEq(currency0.balanceOf(address(lpm)), seedAmount);
bytes memory calls = planner.encode();
vm.prank(alice);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
uint256 amount0 = uint128(-delta.amount0());
uint256 amount1 = uint128(-delta.amount1());
// alice's balance has increased by the seeded funds that werent used to pay for the mint
assertEq(currency0.balanceOf(alice), balanceBefore0 + (seedAmount - amount0));
assertEq(currency1.balanceOf(alice), balanceBefore1 + (seedAmount - amount1));
}
function test_increaseLiquidity_settleWithBalance() public {
uint256 liquidityAlice = 3_000e18;
// alice provides liquidity
vm.prank(alice);
mint(config, liquidityAlice, alice, ZERO_BYTES);
uint256 tokenIdAlice = lpm.nextTokenId() - 1;
uint256 liquidity = lpm.getPositionLiquidity(tokenIdAlice);
assertEq(liquidity, liquidityAlice);
// alice increases with the balance in the position manager
Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.SETTLE, abi.encode(currency0, ActionConstants.OPEN_DELTA, false));
planner.add(Actions.SETTLE, abi.encode(currency1, ActionConstants.OPEN_DELTA, false));
// this test sweeps to the test contract, even though Alice is the caller of the transaction
planner.add(Actions.SWEEP, abi.encode(currency0, address(this)));
planner.add(Actions.SWEEP, abi.encode(currency1, address(this)));
uint256 balanceBefore0 = currency0.balanceOf(address(this));
uint256 balanceBefore1 = currency1.balanceOf(address(this));
assertEq(currency0.balanceOf(address(lpm)), 0);
assertEq(currency0.balanceOf(address(lpm)), 0);
currency0.transfer(address(lpm), 100e18);
currency1.transfer(address(lpm), 100e18);
assertEq(currency0.balanceOf(address(lpm)), 100e18);
assertEq(currency0.balanceOf(address(lpm)), 100e18);
bytes memory calls = planner.encode();
vm.prank(alice);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
uint256 amount0 = uint128(-delta.amount0());
uint256 amount1 = uint128(-delta.amount1());
liquidity = lpm.getPositionLiquidity(tokenIdAlice);
assertEq(liquidity, 2 * liquidityAlice);
// The balances were swept back to this address.
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(lpm)), 0);
assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(lpm)), 0);
assertEq(currency0.balanceOf(address(this)), balanceBefore0 - amount0);
assertEq(currency1.balanceOf(address(this)), balanceBefore1 - amount1);
}
/// @dev if clearing exceeds the max amount, the amount is taken instead
function test_increaseLiquidity_clearExceedsThenTake() public {
uint256 tokenId = lpm.nextTokenId();
mint(config, 1000e18, address(this), ZERO_BYTES);
// donate to create fee revenue
uint256 amountToDonate = 0.2e18;
donateRouter.donate(key, amountToDonate, amountToDonate, ZERO_BYTES);
// calculate the amount of liquidity to add, using half of the proceeds
uint256 amountToReinvest = amountToDonate / 2;
uint256 amountToReclaim = amountToDonate / 2; // expect to reclaim the other half of the fee revenue
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
amountToReinvest,
amountToReinvest
);
// set the max-forfeit to less than the amount we expect to claim
uint256 maxClear = amountToReclaim - 2 wei;
Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, maxClear));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, maxClear));
bytes memory calls = planner.encode();
uint256 balance0Before = currency0.balanceOf(address(this));
uint256 balance1Before = currency1.balanceOf(address(this));
// expect to take the excess, as it exceeds the amount to clear
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
assertEq(uint128(delta.amount0()), amountToReclaim - 1 wei); // imprecision
assertEq(uint128(delta.amount1()), amountToReclaim - 1 wei);
assertEq(currency0.balanceOf(address(this)), balance0Before + amountToReclaim - 1 wei);
assertEq(currency1.balanceOf(address(this)), balance1Before + amountToReclaim - 1 wei);
}
/// @dev clearing a negative delta reverts
function test_increaseLiquidity_clearNegative_revert() public {
uint256 tokenId = lpm.nextTokenId();
mint(config, 1000e18, address(this), ZERO_BYTES);
// increase liquidity with new tokens but try clearing the negative delta
Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, type(uint256).max));
bytes memory calls = planner.encode();
// revert since we're trying to clear a negative delta
vm.expectRevert(abi.encodeWithSelector(DeltaResolver.DeltaNotPositive.selector, currency0));
lpm.modifyLiquidities(calls, _deadline);
}
}