Files

979 lines
45 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 {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol";
import {StateLibrary} from "@luxamm/v4-core/src/libraries/StateLibrary.sol";
import {LPFeeLibrary} from "@luxamm/v4-core/src/libraries/LPFeeLibrary.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.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 {IERC721} from "forge-std/interfaces/IERC721.sol";
import {IPositionManager} from "../../src/interfaces/IPositionManager.sol";
import {Actions} from "../../src/libraries/Actions.sol";
import {DeltaResolver} from "../../src/base/DeltaResolver.sol";
import {PositionConfig} from "../shared/PositionConfig.sol";
import {SlippageCheck} from "../../src/libraries/SlippageCheck.sol";
import {BaseActionsRouter} from "../../src/base/BaseActionsRouter.sol";
import {ActionConstants} from "../../src/libraries/ActionConstants.sol";
import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol";
import {Planner, Plan} from "../shared/Planner.sol";
import {PosmTestSetup} from "../shared/PosmTestSetup.sol";
import {ReentrantToken} from "../mocks/ReentrantToken.sol";
import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol";
import {ModifyLiquidityParams} from "@luxamm/v4-core/src/types/PoolOperation.sol";
contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers {
using FixedPointMathLib for uint256;
using StateLibrary for IPoolManager;
PoolId poolId;
address alice = makeAddr("ALICE");
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);
// Requires currency0 and currency1 to be set in base Deployers contract.
deployAndApprovePosm(manager);
seedBalance(alice);
approvePosmFor(alice);
}
function test_modifyLiquidities_reverts_deadlinePassed() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: 0, tickUpper: 60});
bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, "");
uint256 deadline = vm.getBlockTimestamp() - 1;
vm.expectRevert(abi.encodeWithSelector(IPositionManager.DeadlinePassed.selector, deadline));
lpm.modifyLiquidities(calls, deadline);
}
function test_modifyLiquidities_reverts_mismatchedLengths() public {
Plan memory planner = Planner.init();
planner.add(Actions.MINT_POSITION, abi.encode("test"));
planner.add(Actions.BURN_POSITION, abi.encode("test"));
bytes[] memory badParams = new bytes[](1);
vm.expectRevert(BaseActionsRouter.InputLengthMismatch.selector);
lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1);
}
function test_modifyLiquidities_reverts_reentrancy() public {
// Create a reentrant token and initialize the pool
Currency reentrantToken = Currency.wrap(address(new ReentrantToken(lpm)));
(currency0, currency1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(currency1))
? (reentrantToken, currency1)
: (currency1, reentrantToken);
// Set up approvals for the reentrant token
approvePosmCurrency(reentrantToken);
(key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1);
// Try to add liquidity at that range, but the token reenters posm
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: -int24(key.tickSpacing), tickUpper: int24(key.tickSpacing)});
bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, "");
// Permit2.transferFrom does not bubble the ContractLocked error and instead reverts with its own error
vm.expectRevert("TRANSFER_FROM_FAILED");
lpm.modifyLiquidities(calls, block.timestamp + 1);
}
function test_fuzz_mint_withLiquidityDelta(ModifyLiquidityParams memory params, uint160 sqrtPriceX96) public {
bound(sqrtPriceX96, MIN_PRICE_LIMIT, MAX_PRICE_LIMIT);
params = createFuzzyLiquidityParams(key, params, sqrtPriceX96);
// liquidity is a uint
uint256 liquidityToAdd =
params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta);
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
uint256 tokenId = lpm.nextTokenId();
mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
assertEq(tokenId, 1);
assertEq(lpm.nextTokenId(), 2);
assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this));
uint256 liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, uint256(params.liquidityDelta));
assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0");
assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())), "incorrect amount1");
}
function test_mint_exactTokenRatios() public {
int24 tickLower = -int24(key.tickSpacing);
int24 tickUpper = int24(key.tickSpacing);
uint256 amount0Desired = 100e18;
uint256 amount1Desired = 100e18;
uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
amount0Desired,
amount1Desired
);
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper});
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
uint256 tokenId = lpm.nextTokenId();
mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint256 balance0After = currency0.balanceOfSelf();
uint256 balance1After = currency1.balanceOfSelf();
assertEq(tokenId, 1);
assertEq(IERC721(address(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_toRecipient() public {
int24 tickLower = -int24(key.tickSpacing);
int24 tickUpper = int24(key.tickSpacing);
uint256 amount0Desired = 100e18;
uint256 amount1Desired = 100e18;
uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
amount0Desired,
amount1Desired
);
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper});
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
uint256 tokenId = lpm.nextTokenId();
// mint to specific recipient, not using the recipient constants
mint(config, liquidityToAdd, alice, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint256 balance0After = currency0.balanceOfSelf();
uint256 balance1After = currency1.balanceOfSelf();
assertEq(tokenId, 1);
assertEq(IERC721(address(lpm)).ownerOf(1), alice);
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_fuzz_mint_recipient(ModifyLiquidityParams memory seedParams) public {
ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1);
uint256 liquidityToAdd =
params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta);
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
uint256 tokenId = lpm.nextTokenId();
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
mint(config, liquidityToAdd, alice, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
assertEq(tokenId, 1);
assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice);
// alice was not the payer
assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())));
assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())));
assertEq(currency0.balanceOf(alice), balance0BeforeAlice);
assertEq(currency1.balanceOf(alice), balance1BeforeAlice);
}
/// @dev clear cannot be used on mint (negative delta)
function test_fuzz_mint_clear_revert(ModifyLiquidityParams memory seedParams) public {
ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1);
uint256 liquidityToAdd =
params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta);
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
Plan memory planner = Planner.init();
planner.add(
Actions.MINT_POSITION,
abi.encode(
config.poolKey,
config.tickLower,
config.tickUpper,
liquidityToAdd,
MAX_SLIPPAGE_INCREASE,
MAX_SLIPPAGE_INCREASE,
address(this),
ZERO_BYTES
)
);
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, type(uint256).max));
bytes memory calls = planner.encode();
Currency negativeDeltaCurrency = currency0;
// because we're fuzzing the range, single-sided mint with currency1 means currency0Delta = 0 and currency1Delta < 0
if (config.tickUpper <= 0) {
negativeDeltaCurrency = currency1;
}
vm.expectRevert(abi.encodeWithSelector(DeltaResolver.DeltaNotPositive.selector, negativeDeltaCurrency));
lpm.modifyLiquidities(calls, _deadline);
}
function test_mint_slippage_revertAmount0() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 liquidity = 1e18;
(uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(liquidity)
);
bytes memory calls =
getMintEncoded(config, liquidity, 1 wei, MAX_SLIPPAGE_INCREASE, ActionConstants.MSG_SENDER, ZERO_BYTES);
vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount0 + 1));
lpm.modifyLiquidities(calls, _deadline);
}
function test_mint_slippage_revertAmount1() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 liquidity = 1e18;
(, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(liquidity)
);
bytes memory calls =
getMintEncoded(config, liquidity, MAX_SLIPPAGE_INCREASE, 1 wei, ActionConstants.MSG_SENDER, ZERO_BYTES);
vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount1 + 1));
lpm.modifyLiquidities(calls, _deadline);
}
function test_mint_slippage_exactDoesNotRevert() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 liquidity = 1e18;
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(liquidity)
);
assertEq(amount0, amount1); // symmetric liquidity
uint128 slippage = uint128(amount0) + 1;
bytes memory calls =
getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
assertEq(uint256(int256(-delta.amount0())), slippage);
assertEq(uint256(int256(-delta.amount1())), slippage);
}
function test_mint_slippage_revert_swap() public {
// swapping will cause a slippage revert
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 liquidity = 100e18;
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(liquidity)
);
assertEq(amount0, amount1); // symmetric liquidity
uint128 slippage = uint128(amount0) + 1;
bytes memory calls =
getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES);
// swap to move the price and cause a slippage revert
swap(key, true, -1e18, ZERO_BYTES);
vm.expectRevert(
abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, slippage, 1199947202932782783)
);
lpm.modifyLiquidities(calls, _deadline);
}
function test_fuzz_burn_emptyPosition(ModifyLiquidityParams memory params) public {
uint256 balance0Start = currency0.balanceOfSelf();
uint256 balance1Start = currency1.balanceOfSelf();
// create liquidity we can burn
uint256 tokenId;
(tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, key, params, SQRT_PRICE_1_1, ZERO_BYTES);
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
assertEq(tokenId, 1);
assertEq(IERC721(address(lpm)).ownerOf(1), address(this));
uint256 liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, uint256(params.liquidityDelta));
// burn liquidity
uint256 balance0BeforeBurn = currency0.balanceOfSelf();
uint256 balance1BeforeBurn = currency1.balanceOfSelf();
decreaseLiquidity(tokenId, config, liquidity, ZERO_BYTES);
BalanceDelta deltaDecrease = getLastDelta();
uint256 numDeltas = hook.numberDeltasReturned();
// No decrease/modifyLiq call will actually happen on the call to burn so the deltas array will be the same length.
burn(tokenId, config, ZERO_BYTES);
assertEq(numDeltas, hook.numberDeltasReturned());
liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, 0);
assertEq(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(deltaDecrease.amount0())));
assertEq(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1())));
// 721 will revert if the token does not exist
vm.expectRevert();
IERC721(address(lpm)).ownerOf(1);
// no tokens were lost, TODO: fuzzer showing off by 1 sometimes
// Potentially because we round down in core. I believe this is known in V3. But let's check!
assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei);
assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei);
}
function test_fuzz_burn_nonEmptyPosition(ModifyLiquidityParams memory params) public {
uint256 balance0Start = currency0.balanceOfSelf();
uint256 balance1Start = currency1.balanceOfSelf();
// create liquidity we can burn
uint256 tokenId;
(tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, key, params, SQRT_PRICE_1_1, ZERO_BYTES);
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
assertEq(tokenId, 1);
assertEq(IERC721(address(lpm)).ownerOf(1), address(this));
uint256 liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, uint256(params.liquidityDelta));
(uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId());
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(params.tickLower),
TickMath.getSqrtPriceAtTick(params.tickUpper),
uint128(int128(params.liquidityDelta))
);
// burn liquidity
uint256 balance0BeforeBurn = currency0.balanceOfSelf();
uint256 balance1BeforeBurn = currency1.balanceOfSelf();
burn(tokenId, config, ZERO_BYTES);
BalanceDelta deltaBurn = getLastDelta();
assertEq(uint256(int256(deltaBurn.amount0())), amount0);
assertEq(uint256(int256(deltaBurn.amount1())), amount1);
liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, 0);
assertEq(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(deltaBurn.amount0())));
assertEq(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaBurn.amount1())));
// OZ 721 will revert if the token does not exist
vm.expectRevert();
IERC721(address(lpm)).ownerOf(1);
// no tokens were lost, TODO: fuzzer showing off by 1 sometimes
// Potentially because we round down in core. I believe this is known in V3. But let's check!
assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei);
assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei);
}
function test_burn_slippage_revertAmount0() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint128 amount0 = uint128(-delta.amount0());
bytes memory calls = getBurnEncoded(tokenId, config, amount0 + 1 wei, MIN_SLIPPAGE_DECREASE, ZERO_BYTES);
vm.expectRevert(
abi.encodeWithSelector(SlippageCheck.MinimumAmountInsufficient.selector, amount0 + 1, amount0 - 1)
);
lpm.modifyLiquidities(calls, _deadline);
}
function test_burn_slippage_revertAmount1() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint128 amount1 = uint128(-delta.amount1());
bytes memory calls = getBurnEncoded(tokenId, config, MIN_SLIPPAGE_DECREASE, amount1 + 1 wei, ZERO_BYTES);
// reverts on amount1, because the swap sent token0 into the pool and took token1
vm.expectRevert(
abi.encodeWithSelector(SlippageCheck.MinimumAmountInsufficient.selector, amount1 + 1, amount1 - 1)
);
lpm.modifyLiquidities(calls, _deadline);
}
function test_burn_slippage_exactDoesNotRevert() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
// TODO: why does burning a newly minted position return original delta - 1 wei?
bytes memory calls = getBurnEncoded(
tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES
);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta burnDelta = getLastDelta();
assertApproxEqAbs(-delta.amount0(), burnDelta.amount0(), 1 wei);
assertApproxEqAbs(-delta.amount1(), burnDelta.amount1(), 1 wei);
}
function test_burn_slippage_revert_swap() public {
// swapping will cause a slippage revert
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint128 amount1 = uint128(-delta.amount1());
bytes memory calls =
getBurnEncoded(tokenId, config, uint128(-delta.amount0()) - 1 wei, amount1 - 1 wei, ZERO_BYTES);
// swap to move the price and cause a slippage revert
swap(key, true, -1e18, ZERO_BYTES);
vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MinimumAmountInsufficient.selector, amount1 - 1, 0));
lpm.modifyLiquidities(calls, _deadline);
}
function test_fuzz_decreaseLiquidity(ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) public {
uint256 tokenId;
(tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, key, params, SQRT_PRICE_1_1, ZERO_BYTES);
decreaseLiquidityDelta = uint256(bound(int256(decreaseLiquidityDelta), 0, params.liquidityDelta));
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
decreaseLiquidity(tokenId, config, decreaseLiquidityDelta, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint256 liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta);
assertEq(currency0.balanceOfSelf(), balance0Before + uint256(uint128(delta.amount0())));
assertEq(currency1.balanceOfSelf(), balance1Before + uint256(uint128(delta.amount1())));
}
/// @dev Clearing on decrease liquidity is allowed
function test_fuzz_decreaseLiquidity_clear(ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta)
public
{
uint256 tokenId;
(tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES);
decreaseLiquidityDelta = uint256(bound(int256(decreaseLiquidityDelta), 0, params.liquidityDelta));
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
// Clearing is allowed on decrease liquidity
Plan memory planner = Planner.init();
planner.add(
Actions.DECREASE_LIQUIDITY,
abi.encode(tokenId, decreaseLiquidityDelta, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, type(uint256).max));
bytes memory calls = planner.encode();
lpm.modifyLiquidities(calls, _deadline);
uint256 liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta);
// did not recieve tokens, as they were forfeited with CLEAR
assertEq(currency0.balanceOfSelf(), balance0Before);
assertEq(currency1.balanceOfSelf(), balance1Before);
}
/// @dev Clearing on decrease will take tokens if the amount exceeds the clear limit
function test_fuzz_decreaseLiquidity_clearExceedsThenTake(ModifyLiquidityParams memory params) public {
// use fuzzer for tick range
params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1);
vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
uint256 liquidityToAdd = 1e18;
uint256 liquidityToRemove = bound(liquidityToAdd, liquidityToAdd / 1000, liquidityToAdd);
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, address(this), ZERO_BYTES);
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(liquidityToRemove)
);
Plan memory planner = Planner.init();
planner.add(
Actions.DECREASE_LIQUIDITY,
abi.encode(tokenId, liquidityToRemove, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, amount0 - 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, amount1 - 1 wei));
bytes memory calls = planner.encode();
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
// expect to take the tokens
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
// amount exceeded clear limit, so we should have the tokens
assertEq(uint128(delta.amount0()), amount0);
assertEq(uint128(delta.amount1()), amount1);
assertEq(currency0.balanceOfSelf(), balance0Before + amount0);
assertEq(currency1.balanceOfSelf(), balance1Before + amount1);
}
function test_decreaseLiquidity_collectFees(ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta)
public
{
uint256 tokenId;
(tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, key, params, SQRT_PRICE_1_1, ZERO_BYTES);
vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity
decreaseLiquidityDelta = bound(decreaseLiquidityDelta, 1, uint256(params.liquidityDelta));
PositionConfig memory config =
PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
// donate to generate fee revenue
uint256 feeRevenue0 = 1e18;
uint256 feeRevenue1 = 0.1e18;
donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES);
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();
decreaseLiquidity(tokenId, config, decreaseLiquidityDelta, ZERO_BYTES);
uint256 liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta);
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(decreaseLiquidityDelta)
);
// claimed both principal liquidity and fee revenue
assertApproxEqAbs(currency0.balanceOfSelf() - balance0Before, amount0 + feeRevenue0, 1 wei);
assertApproxEqAbs(currency1.balanceOfSelf() - balance1Before, amount1 + feeRevenue1, 1 wei);
}
function test_decreaseLiquidity_slippage_revertAmount0() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint128 amount0Delta = uint128(-delta.amount0());
bytes memory calls =
getDecreaseEncoded(tokenId, config, 1e18, amount0Delta + 1, MIN_SLIPPAGE_DECREASE, ZERO_BYTES);
vm.expectRevert(
abi.encodeWithSelector(SlippageCheck.MinimumAmountInsufficient.selector, amount0Delta + 1, amount0Delta - 1)
);
lpm.modifyLiquidities(calls, _deadline);
}
function test_decreaseLiquidity_slippage_revertAmount1() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint128 amount1Delta = uint128(-delta.amount0());
bytes memory calls =
getDecreaseEncoded(tokenId, config, 1e18, MIN_SLIPPAGE_DECREASE, amount1Delta + 1 wei, ZERO_BYTES);
vm.expectRevert(
abi.encodeWithSelector(SlippageCheck.MinimumAmountInsufficient.selector, amount1Delta + 1, amount1Delta - 1)
);
lpm.modifyLiquidities(calls, _deadline);
}
function test_decreaseLiquidity_slippage_exactDoesNotRevert() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
// TODO: why does decreasing a newly minted position return original delta - 1 wei?
bytes memory calls = getDecreaseEncoded(
tokenId, config, 1e18, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES
);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta decreaseDelta = getLastDelta();
// TODO: why does decreasing a newly minted position return original delta - 1 wei?
assertApproxEqAbs(-delta.amount0(), decreaseDelta.amount0(), 1 wei);
assertApproxEqAbs(-delta.amount1(), decreaseDelta.amount1(), 1 wei);
}
function test_decreaseLiquidity_slippage_revert_swap() public {
// swapping will cause a slippage revert
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint128 amount1 = uint128(-delta.amount1());
bytes memory calls =
getDecreaseEncoded(tokenId, config, 1e18, uint128(-delta.amount0()) - 1 wei, amount1 - 1 wei, ZERO_BYTES);
// swap to move the price and cause a slippage revert
swap(key, true, -1e18, ZERO_BYTES);
// reverts on amount1, because the swap sent token0 into the pool and took token1
vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MinimumAmountInsufficient.selector, amount1 - 1, 0));
lpm.modifyLiquidities(calls, _deadline);
}
function test_fuzz_decreaseLiquidity_assertCollectedBalance(
ModifyLiquidityParams memory params,
uint256 decreaseLiquidityDelta
) public {
uint256 tokenId;
(tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, 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);
PositionConfig memory config =
PositionConfig({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();
decreaseLiquidity(tokenId, config, decreaseLiquidityDelta, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
uint256 liquidity = lpm.getPositionLiquidity(tokenId);
assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta);
// The change in balance equals the delta returned.
assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0())));
assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1())));
}
function test_mintTransferBurn() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600});
uint256 liquidity = 100e18;
uint256 tokenId = lpm.nextTokenId();
mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES);
BalanceDelta mintDelta = getLastDelta();
// transfer to alice
IERC721(address(lpm)).transferFrom(address(this), alice, tokenId);
// alice can burn the position
bytes memory calls = getBurnEncoded(tokenId, config, ZERO_BYTES);
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency0.balanceOf(alice);
vm.prank(alice);
lpm.modifyLiquidities(calls, _deadline);
// token was burned and does not exist anymore
vm.expectRevert();
IERC721(address(lpm)).ownerOf(tokenId);
// alice received the principal liquidity
assertApproxEqAbs(currency0.balanceOf(alice) - balance0BeforeAlice, uint128(-mintDelta.amount0()), 1 wei);
assertApproxEqAbs(currency1.balanceOf(alice) - balance1BeforeAlice, uint128(-mintDelta.amount1()), 1 wei);
}
function test_mintTransferCollect() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600});
uint256 liquidity = 100e18;
uint256 tokenId = lpm.nextTokenId();
mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES);
// donate to generate fee revenue
uint256 feeRevenue0 = 1e18;
uint256 feeRevenue1 = 0.1e18;
donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES);
// transfer to alice
IERC721(address(lpm)).transferFrom(address(this), alice, tokenId);
// alice can collect the fees
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
vm.startPrank(alice);
collect(tokenId, config, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
vm.stopPrank();
// alice received the fee revenue
assertApproxEqAbs(currency0.balanceOf(alice) - balance0BeforeAlice, feeRevenue0, 1 wei);
assertApproxEqAbs(currency1.balanceOf(alice) - balance1BeforeAlice, feeRevenue1, 1 wei);
assertApproxEqAbs(uint128(delta.amount0()), feeRevenue0, 1 wei);
assertApproxEqAbs(uint128(delta.amount1()), feeRevenue1, 1 wei);
}
function test_mintTransferIncrease() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600});
uint256 liquidity = 100e18;
uint256 tokenId = lpm.nextTokenId();
mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES);
// transfer to alice
IERC721(address(lpm)).transferFrom(address(this), alice, tokenId);
// alice increases liquidity and is the payer
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
vm.startPrank(alice);
uint256 liquidityToAdd = 10e18;
increaseLiquidity(tokenId, config, liquidityToAdd, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
vm.stopPrank();
// position liquidity increased
uint256 newLiq = lpm.getPositionLiquidity(tokenId);
assertEq(newLiq, liquidity + liquidityToAdd);
// alice paid the tokens
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(liquidityToAdd)
);
assertApproxEqAbs(balance0BeforeAlice - currency0.balanceOf(alice), amount0, 1 wei);
assertApproxEqAbs(balance1BeforeAlice - currency1.balanceOf(alice), amount1, 1 wei);
assertApproxEqAbs(uint128(-delta.amount0()), amount0, 1 wei);
assertApproxEqAbs(uint128(-delta.amount1()), amount1, 1 wei);
}
function test_mintTransferDecrease() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600});
uint256 liquidity = 100e18;
uint256 tokenId = lpm.nextTokenId();
mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES);
// donate to generate fee revenue
uint256 feeRevenue0 = 1e18;
uint256 feeRevenue1 = 0.1e18;
donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES);
// transfer to alice
IERC721(address(lpm)).transferFrom(address(this), alice, tokenId);
{
// alice decreases liquidity and is the recipient
uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);
vm.startPrank(alice);
uint256 liquidityToRemove = 10e18;
decreaseLiquidity(tokenId, config, liquidityToRemove, ZERO_BYTES);
BalanceDelta delta = getLastDelta();
vm.stopPrank();
{
// position liquidity decreased
uint256 newLiq = lpm.getPositionLiquidity(tokenId);
assertEq(newLiq, liquidity - liquidityToRemove);
}
// alice received the principal + fees
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
uint128(liquidityToRemove)
);
assertApproxEqAbs(currency0.balanceOf(alice) - balance0BeforeAlice, amount0 + feeRevenue0, 1 wei);
assertApproxEqAbs(currency1.balanceOf(alice) - balance1BeforeAlice, amount1 + feeRevenue1, 1 wei);
assertApproxEqAbs(uint128(delta.amount0()), amount0 + feeRevenue0, 1 wei);
assertApproxEqAbs(uint128(delta.amount1()), amount1 + feeRevenue1, 1 wei);
}
}
function test_initialize() public {
// initialize a new pool and add liquidity
key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))});
lpm.initializePool(key, SQRT_PRICE_1_1);
(uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId());
assertEq(sqrtPriceX96, SQRT_PRICE_1_1);
assertEq(tick, 0);
assertEq(protocolFee, 0);
assertEq(lpFee, key.fee);
}
function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public {
sqrtPrice =
uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE));
fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE));
key =
PoolKey({currency0: currency0, currency1: currency1, fee: fee, tickSpacing: 10, hooks: IHooks(address(0))});
lpm.initializePool(key, sqrtPrice);
(uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId());
assertEq(sqrtPriceX96, sqrtPrice);
assertEq(tick, TickMath.getTickAtSqrtPrice(sqrtPrice));
assertEq(protocolFee, 0);
assertEq(lpFee, fee);
}
// tests a decrease and take in both currencies
// does not use take pair, so its less optimal
function test_decrease_take() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
hook.clearDeltas();
uint256 balanceBefore0 = currency0.balanceOfSelf();
uint256 balanceBefore1 = currency1.balanceOfSelf();
Plan memory plan = Planner.init();
plan.add(
Actions.DECREASE_LIQUIDITY,
abi.encode(tokenId, 1e18, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES)
);
bytes memory calls = plan.finalizeModifyLiquidityWithTake(config.poolKey, ActionConstants.MSG_SENDER);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();
assertEq(currency0.balanceOfSelf(), balanceBefore0 + uint256(int256(delta.amount0())));
assertEq(currency1.balanceOfSelf(), balanceBefore1 + uint256(int256(delta.amount1())));
}
// decrease full range position
// mint new one sided position in currency1
// expect to TAKE currency0 and SETTLE currency1
function test_decrease_increaseCurrency1_take_settle() public {
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120});
uint256 tokenId = lpm.nextTokenId();
mint(config, 1e18, ActionConstants.MSG_SENDER, ZERO_BYTES);
hook.clearDeltas();
uint256 balanceBefore0 = currency0.balanceOfSelf();
uint256 balanceBefore1 = currency1.balanceOfSelf();
uint256 tokenIdMint = lpm.nextTokenId();
// one-sided liq in currency1
PositionConfig memory configMint = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 0});
Plan memory plan = Planner.init();
plan.add(
Actions.DECREASE_LIQUIDITY,
abi.encode(tokenId, 1e18, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES)
);
plan.add(
Actions.MINT_POSITION,
abi.encode(
configMint.poolKey,
configMint.tickLower,
configMint.tickUpper,
1e18,
MAX_SLIPPAGE_INCREASE,
MAX_SLIPPAGE_INCREASE,
ActionConstants.MSG_SENDER,
ZERO_BYTES
)
);
plan.add(Actions.TAKE, abi.encode(key.currency0, ActionConstants.MSG_SENDER, ActionConstants.OPEN_DELTA));
plan.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.OPEN_DELTA, true));
bytes memory calls = plan.finalizeModifyLiquidityWithTake(config.poolKey, ActionConstants.MSG_SENDER);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta deltaDecrease = hook.deltas(0);
BalanceDelta deltaMint = hook.deltas(1);
assertEq(deltaMint.amount0(), 0); // there is no currency0 in the new position
assertEq(currency0.balanceOfSelf(), balanceBefore0 + uint256(int256(deltaDecrease.amount0())));
assertEq(
currency1.balanceOfSelf(), balanceBefore1 - uint256(-int256(deltaDecrease.amount1() + deltaMint.amount1()))
);
assertEq(IERC721(address(lpm)).ownerOf(tokenIdMint), address(this));
assertLt(currency1.balanceOfSelf(), balanceBefore1); // currency1 was owed
assertLt(uint256(int256(deltaDecrease.amount1())), uint256(int256(-deltaMint.amount1()))); // amount1 in the second position was greater than amount1 in the first position
}
function test_mint_slippageRevert() public {}
}