mirror of
https://github.com/lux-amm/v4-core.git
synced 2026-10-30 20:54:53 +00:00
313 lines
15 KiB
Solidity
313 lines
15 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {Deployers} from "./utils/Deployers.sol";
|
|
import {PoolKey} from "src/types/PoolKey.sol";
|
|
import {ModifyLiquidityParams} from "src/types/PoolOperation.sol";
|
|
import {IPoolManager} from "src/interfaces/IPoolManager.sol";
|
|
import {IHooks} from "src/interfaces/IHooks.sol";
|
|
import {Position} from "src/libraries/Position.sol";
|
|
import {PoolId} from "src/types/PoolId.sol";
|
|
import {PoolModifyLiquidityTest} from "../src/test/PoolModifyLiquidityTest.sol";
|
|
import {Constants} from "./utils/Constants.sol";
|
|
import {Currency} from "src/types/Currency.sol";
|
|
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
|
|
import {StateLibrary} from "src/libraries/StateLibrary.sol";
|
|
import {JavascriptFfi} from "./utils/JavascriptFfi.sol";
|
|
import {BalanceDelta} from "../src/types/BalanceDelta.sol";
|
|
import {Fuzzers} from "../src/test/Fuzzers.sol";
|
|
import {TickMath} from "src/libraries/TickMath.sol";
|
|
import {toBalanceDelta} from "src/types/BalanceDelta.sol";
|
|
import {Logger} from "./utils/Logger.sol";
|
|
|
|
contract ModifyLiquidityTest is Test, Logger, Deployers, JavascriptFfi, Fuzzers {
|
|
using StateLibrary for IPoolManager;
|
|
|
|
PoolKey simpleKey; // vanilla pool key
|
|
PoolId simplePoolId; // id for vanilla pool key
|
|
|
|
bytes32 SALT = hex"CAFF";
|
|
|
|
int128 constant ONE_PIP = 1e6;
|
|
|
|
ModifyLiquidityParams public LIQ_PARAM_NO_SALT =
|
|
ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1e18, salt: 0});
|
|
|
|
ModifyLiquidityParams public LIQ_PARAM_SALT =
|
|
ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1e18, salt: SALT});
|
|
|
|
function setUp() public {
|
|
deployFreshManagerAndRouters();
|
|
deployMintAndApprove2Currencies();
|
|
(simpleKey, simplePoolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1);
|
|
}
|
|
|
|
/*//////////////////////////////////////////////////////////////
|
|
Fuzz Add Liquidity
|
|
//////////////////////////////////////////////////////////////*/
|
|
|
|
/// forge-config: default.fuzz.runs = 10
|
|
/// forge-config: pr.fuzz.runs = 10
|
|
/// forge-config: ci.fuzz.runs = 500
|
|
/// forge-config: debug.fuzz.runs = 10
|
|
function test_ffi_fuzz_addLiquidity_defaultPool_ReturnsCorrectLiquidityDelta(ModifyLiquidityParams memory paramSeed)
|
|
public
|
|
{
|
|
// Sanitize the fuzzed params to get valid tickLower, tickUpper, and liquidityDelta.
|
|
// We use SQRT_PRICE_1_1 because the simpleKey pool has initial sqrtPrice of SQRT_PRICE_1_1.
|
|
ModifyLiquidityParams memory params = createFuzzyLiquidityParams(simpleKey, paramSeed, SQRT_PRICE_1_1);
|
|
|
|
logParams(params);
|
|
|
|
(BalanceDelta delta) = modifyLiquidityRouter.modifyLiquidity(simpleKey, params, ZERO_BYTES);
|
|
|
|
(int128 jsDelta0, int128 jsDelta1) = _modifyLiquidityJS(simplePoolId, params);
|
|
|
|
_checkError(delta.amount0(), jsDelta0, "amount0 is off by more than one pip");
|
|
_checkError(delta.amount1(), jsDelta1, "amount1 is off by more than one pip");
|
|
}
|
|
|
|
// Static edge case, no fuzz test, to make sure we test max tickspacing.
|
|
function test_ffi_addLiqudity_weirdPool_0_returnsCorrectLiquidityDelta() public {
|
|
// Use a pool with TickSpacing of MAX_TICK_SPACING
|
|
(PoolKey memory wp0, PoolId wpId0) =
|
|
initPool(currency0, currency1, IHooks(address(0)), 500, TickMath.MAX_TICK_SPACING, SQRT_PRICE_1_1);
|
|
|
|
// Set the params to add random amount of liquidity to random tick boundary.
|
|
int24 tickUpper = TickMath.MAX_TICK_SPACING * 4;
|
|
int24 tickLower = TickMath.MAX_TICK_SPACING * -9;
|
|
ModifyLiquidityParams memory params = ModifyLiquidityParams({
|
|
tickLower: tickLower,
|
|
tickUpper: tickUpper,
|
|
liquidityDelta: 16787899214600939458,
|
|
salt: 0
|
|
});
|
|
|
|
(BalanceDelta delta) = modifyLiquidityRouter.modifyLiquidity(wp0, params, ZERO_BYTES);
|
|
|
|
(int128 jsDelta0, int128 jsDelta1) = _modifyLiquidityJS(wpId0, params);
|
|
|
|
_checkError(delta.amount0(), jsDelta0, "amount0 is off by more than one pip");
|
|
_checkError(delta.amount1(), jsDelta1, "amount1 is off by more than one pip");
|
|
}
|
|
|
|
// Static edge case, no fuzz test, to make sure we test min tick spacing.
|
|
function test_ffi_addLiqudity_weirdPool_1_returnsCorrectLiquidityDelta() public {
|
|
// Use a pool with TickSpacing of MIN_TICK_SPACING
|
|
(PoolKey memory wp0, PoolId wpId0) =
|
|
initPool(currency0, currency1, IHooks(address(0)), 551, TickMath.MIN_TICK_SPACING, SQRT_PRICE_1_1);
|
|
|
|
// Set the params to add random amount of liquidity to random tick boundary.
|
|
int24 tickUpper = TickMath.MIN_TICK_SPACING * 17;
|
|
int24 tickLower = TickMath.MIN_TICK_SPACING * 9;
|
|
ModifyLiquidityParams memory params = ModifyLiquidityParams({
|
|
tickLower: tickLower,
|
|
tickUpper: tickUpper,
|
|
liquidityDelta: 922871614499955267459963,
|
|
salt: 0
|
|
});
|
|
|
|
params.tickLower = 10;
|
|
|
|
(BalanceDelta delta) = modifyLiquidityRouter.modifyLiquidity(wp0, params, ZERO_BYTES);
|
|
|
|
(int128 jsDelta0, int128 jsDelta1) = _modifyLiquidityJS(wpId0, params);
|
|
|
|
_checkError(delta.amount0(), jsDelta0, "amount0 is off by more than one pip");
|
|
_checkError(delta.amount1(), jsDelta1, "amount1 is off by more than one pip");
|
|
}
|
|
|
|
function _modifyLiquidityJS(PoolId poolId, ModifyLiquidityParams memory params) public returns (int128, int128) {
|
|
(uint256 price, int24 tick,,) = manager.getSlot0(poolId);
|
|
|
|
string memory jsParameters = string(
|
|
abi.encodePacked(
|
|
vm.toString(params.tickLower),
|
|
",",
|
|
vm.toString(params.tickUpper),
|
|
",",
|
|
vm.toString(params.liquidityDelta),
|
|
",",
|
|
vm.toString(tick),
|
|
",",
|
|
vm.toString(price)
|
|
)
|
|
);
|
|
|
|
string memory scriptName = "forge-test-getModifyLiquidityResult";
|
|
bytes memory jsResult = runScript(scriptName, jsParameters);
|
|
|
|
int128[] memory result = abi.decode(jsResult, (int128[]));
|
|
int128 jsDelta0 = result[0];
|
|
int128 jsDelta1 = result[1];
|
|
return (jsDelta0, jsDelta1);
|
|
}
|
|
|
|
// assert solc/js result is at most off by 1/100th of a bip (aka one pip)
|
|
function _checkError(int128 solc, int128 js, string memory errMsg) public pure {
|
|
if (solc != js) {
|
|
// Ensures no div by 0 in the case of one-sided liquidity adds.
|
|
(int128 gtResult, int128 ltResult) = js > solc ? (js, solc) : (solc, js);
|
|
int128 resultsDiff = gtResult - ltResult;
|
|
assertEq(resultsDiff * ONE_PIP / js, 0, errMsg);
|
|
}
|
|
}
|
|
|
|
/*//////////////////////////////////////////////////////////////
|
|
Salt
|
|
//////////////////////////////////////////////////////////////*/
|
|
|
|
function test_modifyLiquidity_samePosition_zeroSalt_isUpdated() public {
|
|
(uint128 liquidity,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_NO_SALT.tickLower, LIQ_PARAM_NO_SALT.tickUpper, 0
|
|
);
|
|
assertEq(liquidity, 0);
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_NO_SALT, ZERO_BYTES);
|
|
(liquidity,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_NO_SALT.tickLower, LIQ_PARAM_NO_SALT.tickUpper, 0
|
|
);
|
|
|
|
assertEq(liquidity, uint128(uint256(LIQ_PARAM_NO_SALT.liquidityDelta)));
|
|
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_NO_SALT, ZERO_BYTES);
|
|
(uint128 liquidityUpdated,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_NO_SALT.tickLower, LIQ_PARAM_NO_SALT.tickUpper, 0
|
|
);
|
|
assertEq(liquidityUpdated, liquidity + uint128(uint256(LIQ_PARAM_NO_SALT.liquidityDelta)));
|
|
}
|
|
|
|
function test_modifyLiquidity_samePosition_withSalt_isUpdated() public {
|
|
(uint128 liquidity,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
assertEq(liquidity, 0);
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
(liquidity,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
assertEq(liquidity, uint128(uint256(LIQ_PARAM_SALT.liquidityDelta)));
|
|
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
(uint128 liquidityUpdated,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
assertEq(liquidityUpdated, liquidity + uint128(uint256(LIQ_PARAM_SALT.liquidityDelta)));
|
|
}
|
|
|
|
function test_modifyLiquidity_sameTicks_withDifferentSalt_isNotUpdated() public {
|
|
(uint128 liquidityNoSalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_NO_SALT.tickLower, LIQ_PARAM_NO_SALT.tickUpper, 0
|
|
);
|
|
|
|
(uint128 liquiditySalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
assertEq(liquidityNoSalt, 0);
|
|
assertEq(liquiditySalt, 0);
|
|
|
|
// Modify the liquidity with the salt.
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
|
|
(liquidityNoSalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_NO_SALT.tickLower, LIQ_PARAM_NO_SALT.tickUpper, 0
|
|
);
|
|
|
|
(liquiditySalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
|
|
assertEq(liquidityNoSalt, 0); // This position does not have liquidity.
|
|
assertEq(liquiditySalt, uint128(uint256(LIQ_PARAM_SALT.liquidityDelta))); // This position does.
|
|
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_NO_SALT, ZERO_BYTES); // Now the positions should have the same liquidity.
|
|
|
|
(liquidityNoSalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_NO_SALT.tickLower, LIQ_PARAM_NO_SALT.tickUpper, 0
|
|
);
|
|
|
|
(liquiditySalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
|
|
// positionSalt should still only have the original liquidity deposited to it
|
|
assertEq(liquiditySalt, uint128(uint256(LIQ_PARAM_SALT.liquidityDelta)));
|
|
assertEq(liquidityNoSalt, liquiditySalt);
|
|
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
(uint128 updatedLiquidityWithSalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
(uint128 updatedLiquidityNoSalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_NO_SALT.tickLower, LIQ_PARAM_NO_SALT.tickUpper, 0
|
|
);
|
|
|
|
assertEq(updatedLiquidityWithSalt, liquiditySalt + uint128(uint256(LIQ_PARAM_SALT.liquidityDelta)));
|
|
assertGt(updatedLiquidityWithSalt, updatedLiquidityNoSalt);
|
|
assertEq(updatedLiquidityNoSalt, liquidityNoSalt);
|
|
}
|
|
|
|
function test_modifyLiquidity_sameSalt_differentLiquidityRouters_doNotEditSamePosition() public {
|
|
// Set up new router.
|
|
PoolModifyLiquidityTest modifyLiquidityRouter2 = new PoolModifyLiquidityTest(manager);
|
|
|
|
MockERC20(Currency.unwrap(currency0)).approve(address(modifyLiquidityRouter2), Constants.MAX_UINT256);
|
|
MockERC20(Currency.unwrap(currency1)).approve(address(modifyLiquidityRouter2), Constants.MAX_UINT256);
|
|
|
|
ModifyLiquidityParams memory LIQ_PARAM_SALT_2 =
|
|
ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 2e18, salt: SALT});
|
|
|
|
// Get the uninitialized positions and assert they have no liquidity.
|
|
(uint128 liquiditySalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
|
|
(uint128 liquiditySalt2,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter2), LIQ_PARAM_SALT_2.tickLower, LIQ_PARAM_SALT_2.tickUpper, SALT
|
|
);
|
|
|
|
assertEq(liquiditySalt, 0);
|
|
assertEq(liquiditySalt2, 0);
|
|
|
|
// Modify the liquidity with the salt with the first router.
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
|
|
(uint128 updatedLiquiditySalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
|
|
(uint128 updatedLiquiditySalt2,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter2), LIQ_PARAM_SALT_2.tickLower, LIQ_PARAM_SALT_2.tickUpper, SALT
|
|
);
|
|
|
|
// Assert only the liquidity from the first router is updated.
|
|
assertEq(updatedLiquiditySalt, uint128(uint256(LIQ_PARAM_SALT.liquidityDelta)));
|
|
assertEq(updatedLiquiditySalt2, 0);
|
|
|
|
// Modify the liquidity with the second router.
|
|
modifyLiquidityRouter2.modifyLiquidity(simpleKey, LIQ_PARAM_SALT_2, ZERO_BYTES);
|
|
|
|
(updatedLiquiditySalt2,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter2), LIQ_PARAM_SALT_2.tickLower, LIQ_PARAM_SALT_2.tickUpper, SALT
|
|
);
|
|
(updatedLiquiditySalt,,) = manager.getPositionInfo(
|
|
simplePoolId, address(modifyLiquidityRouter), LIQ_PARAM_SALT.tickLower, LIQ_PARAM_SALT.tickUpper, SALT
|
|
);
|
|
|
|
// Assert only the liquidity from the second router is updated.
|
|
assertEq(updatedLiquiditySalt2, uint128(uint256(LIQ_PARAM_SALT_2.liquidityDelta)));
|
|
assertEq(updatedLiquiditySalt, uint128(uint256(LIQ_PARAM_SALT.liquidityDelta)));
|
|
}
|
|
|
|
function test_gas_modifyLiquidity_newPosition() public {
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("create new liquidity to a position with salt");
|
|
}
|
|
|
|
function test_gas_modifyLiquidity_updateSamePosition_withSalt() public {
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("add liquidity to already existing position with salt");
|
|
}
|
|
}
|