mirror of
https://github.com/lux-amm/v4-periphery.git
synced 2026-10-30 20:54:39 +00:00
829 lines
35 KiB
Solidity
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);
|
|
}
|
|
}
|