Files
v4-core/test/ModifyLiquidity.t.sol

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");
}
}