Files

587 lines
22 KiB
Solidity

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {PathKey} from "../src/libraries/PathKey.sol";
import {Deploy, IV4Quoter} from "../test/shared/Deploy.sol";
import {BaseV4Quoter} from "../src/base/BaseV4Quoter.sol";
import {MockMsgSenderHook} from "./mocks/MockMsgSenderHook.sol";
// v4-core
import {LiquidityAmounts} from "@luxamm/v4-core/test/utils/LiquidityAmounts.sol";
import {SafeCast} from "@luxamm/v4-core/src/libraries/SafeCast.sol";
import {Deployers} from "@luxamm/v4-core/test/utils/Deployers.sol";
import {IHooks} from "@luxamm/v4-core/src/interfaces/IHooks.sol";
import {PoolModifyLiquidityTest} from "@luxamm/v4-core/src/test/PoolModifyLiquidityTest.sol";
import {PoolId} from "@luxamm/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@luxamm/v4-core/src/types/PoolKey.sol";
import {IPoolManager} from "@luxamm/v4-core/src/interfaces/IPoolManager.sol";
import {Currency} from "@luxamm/v4-core/src/types/Currency.sol";
import {TickMath} from "@luxamm/v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "@luxamm/v4-core/src/libraries/StateLibrary.sol";
import {Hooks} from "@luxamm/v4-core/src/libraries/Hooks.sol";
import {ModifyLiquidityParams} from "@luxamm/v4-core/src/types/PoolOperation.sol";
// solmate
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
contract QuoterTest is Test, Deployers {
using SafeCast for *;
using StateLibrary for IPoolManager;
// Min tick for full range with tick spacing of 60
int24 internal constant MIN_TICK = -887220;
// Max tick for full range with tick spacing of 60
int24 internal constant MAX_TICK = -MIN_TICK;
uint160 internal constant SQRT_PRICE_100_102 = 78447570448055484695608110440;
uint160 internal constant SQRT_PRICE_102_100 = 80016521857016594389520272648;
uint256 internal constant CONTROLLER_GAS_LIMIT = 500000;
IV4Quoter quoter;
PoolModifyLiquidityTest positionManager;
MockERC20 token0;
MockERC20 token1;
MockERC20 token2;
PoolKey key01;
PoolKey key02;
PoolKey key12;
PoolKey key01Hook;
MockERC20[] tokenPath;
function setUp() public {
deployFreshManagerAndRouters();
quoter = Deploy.v4Quoter(address(manager), hex"00");
positionManager = new PoolModifyLiquidityTest(manager);
// salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2)
token0 = new MockERC20("Test0", "0", 18);
vm.etch(address(0x1111), address(token0).code);
token0 = MockERC20(address(0x1111));
token0.mint(address(this), 2 ** 128);
vm.etch(address(0x2222), address(token0).code);
token1 = MockERC20(address(0x2222));
token1.mint(address(this), 2 ** 128);
vm.etch(address(0x3333), address(token0).code);
token2 = MockERC20(address(0x3333));
token2.mint(address(this), 2 ** 128);
// deploy a hook that reads msgSender
MockMsgSenderHook msgSenderHook = new MockMsgSenderHook();
address mockMsgSenderHookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG));
vm.etch(mockMsgSenderHookAddr, address(msgSenderHook).code);
key01 = createPoolKey(token0, token1, address(0));
key02 = createPoolKey(token0, token2, address(0));
key12 = createPoolKey(token1, token2, address(0));
key01Hook = createPoolKey(token0, token1, mockMsgSenderHookAddr);
setupPool(key01);
setupPool(key12);
setupPool(key01Hook);
setupPoolMultiplePositions(key02);
}
function testQuoter_quoteExactInputSingle_ZeroForOne_MultiplePositions() public {
uint256 amountIn = 10000;
uint256 expectedAmountOut = 9871;
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInputSingle(
IV4Quoter.QuoteExactSingleParams({
poolKey: key02, zeroForOne: true, exactAmount: uint128(amountIn), hookData: ZERO_BYTES
})
);
vm.snapshotGasLastCall("Quoter_exactInputSingle_zeroForOne_multiplePositions");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, expectedAmountOut);
}
function testQuoter_quoteExactInputSingle_OneForZero_MultiplePositions() public {
uint256 amountIn = 10000;
uint256 expectedAmountOut = 9871;
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInputSingle(
IV4Quoter.QuoteExactSingleParams({
poolKey: key02, zeroForOne: false, exactAmount: uint128(amountIn), hookData: ZERO_BYTES
})
);
vm.snapshotGasLastCall("Quoter_exactInputSingle_oneForZero_multiplePositions");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, expectedAmountOut);
}
function testQuoter_quoteExactInput_0to2_2TicksLoaded() public {
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 9871);
}
function testQuoter_quoteExactInput_0to2_2TicksLoaded_initializedAfter() public {
tokenPath.push(token0);
tokenPath.push(token2);
// The swap amount is set such that the active tick after the swap is -120.
// -120 is an initialized tick for this pool. We check that we don't count it.
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6200);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 6143);
}
function testQuoter_quoteExactInput_0to2_1TickLoaded() public {
tokenPath.push(token0);
tokenPath.push(token2);
// The swap amount is set such that the active tick after the swap is -60.
// -60 is an initialized tick for this pool. We check that we don't count it.
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 4000);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
vm.snapshotGasLastCall("Quoter_quoteExactInput_oneHop_1TickLoaded");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 3971);
}
function testQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public {
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 8);
}
function testQuoter_quoteExactInput_0to2_0TickLoaded_startingInitialized() public {
setupPoolWithZeroTickInitialized(key02);
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 8);
}
function testQuoter_quoteExactInput_2to0_2TicksLoaded() public {
tokenPath.push(token2);
tokenPath.push(token0);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 9871);
}
function testQuoter_quoteExactInput_2to0_2TicksLoaded_initializedAfter() public {
tokenPath.push(token2);
tokenPath.push(token0);
// The swap amount is set such that the active tick after the swap is 120.
// 120 is an initialized tick for this pool. We check that we don't count it.
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6250);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
vm.snapshotGasLastCall("Quoter_quoteExactInput_oneHop_initializedAfter");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 6190);
}
function testQuoter_quoteExactInput_2to0_0TickLoaded_startingInitialized() public {
setupPoolWithZeroTickInitialized(key02);
tokenPath.push(token2);
tokenPath.push(token0);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 200);
// Tick 0 initialized. Tick after = 1
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
vm.snapshotGasLastCall("Quoter_quoteExactInput_oneHop_startingInitialized");
assertGt(gasEstimate, 40000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 198);
}
// 2->0 starting not initialized
function testQuoter_quoteExactInput_2to0_0TickLoaded_startingNotInitialized() public {
tokenPath.push(token2);
tokenPath.push(token0);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 101);
}
function testQuoter_quoteExactInput_2to1() public {
tokenPath.push(token2);
tokenPath.push(token1);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 9871);
}
function testQuoter_quoteExactInput_0to2to1() public {
tokenPath.push(token0);
tokenPath.push(token2);
tokenPath.push(token1);
IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000);
(uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params);
vm.snapshotGasLastCall("Quoter_quoteExactInput_twoHops");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 9745);
}
function testQuoter_quoteExactOutputSingle_0to1() public {
uint256 amountOut = 10000;
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutputSingle(
IV4Quoter.QuoteExactSingleParams({
poolKey: key01, zeroForOne: true, exactAmount: uint128(amountOut), hookData: ZERO_BYTES
})
);
vm.snapshotGasLastCall("Quoter_exactOutputSingle_zeroForOne");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 10133);
}
function testQuoter_quoteExactOutputSingle_1to0() public {
uint256 amountOut = 10000;
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutputSingle(
IV4Quoter.QuoteExactSingleParams({
poolKey: key01, zeroForOne: false, exactAmount: uint128(amountOut), hookData: ZERO_BYTES
})
);
vm.snapshotGasLastCall("Quoter_exactOutputSingle_oneForZero");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 10133);
}
function testQuoter_quoteExactOutput_0to2_2TicksLoaded() public {
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
vm.snapshotGasLastCall("Quoter_quoteExactOutput_oneHop_2TicksLoaded");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 15273);
}
function testQuoter_quoteExactOutput_0to2_1TickLoaded_initializedAfter() public {
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
vm.snapshotGasLastCall("Quoter_quoteExactOutput_oneHop_initializedAfter");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 6200);
}
function testQuoter_quoteExactOutput_0to2_1TickLoaded() public {
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
vm.snapshotGasLastCall("Quoter_quoteExactOutput_oneHop_1TickLoaded");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 4029);
}
function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingInitialized() public {
setupPoolWithZeroTickInitialized(key02);
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 100);
// Tick 0 initialized. Tick after = 1
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
vm.snapshotGasLastCall("Quoter_quoteExactOutput_oneHop_startingInitialized");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 102);
}
function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingNotInitialized() public {
tokenPath.push(token0);
tokenPath.push(token2);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 12);
}
function testQuoter_quoteExactOutput_2to0_2TicksLoaded() public {
tokenPath.push(token2);
tokenPath.push(token0);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 15273);
}
function testQuoter_quoteExactOutput_2to0_2TicksLoaded_initializedAfter() public {
tokenPath.push(token2);
tokenPath.push(token0);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 6283);
}
function testQuoter_quoteExactOutput_2to0_1TickLoaded() public {
tokenPath.push(token2);
tokenPath.push(token0);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 6055);
}
function testQuoter_quoteExactOutput_2to1() public {
tokenPath.push(token2);
tokenPath.push(token1);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 10000);
}
function testQuoter_quoteExactOutput_0to2to1() public {
tokenPath.push(token0);
tokenPath.push(token2);
tokenPath.push(token1);
IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745);
(uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params);
vm.snapshotGasLastCall("Quoter_quoteExactOutput_twoHops");
assertGt(gasEstimate, 50000);
assertLt(gasEstimate, 400000);
assertEq(amountIn, 10000);
}
function test_fuzz_quoter_msgSender(address pranker, bool zeroForOne, bool exactInput) public {
IV4Quoter.QuoteExactSingleParams memory params = IV4Quoter.QuoteExactSingleParams({
poolKey: key01Hook, zeroForOne: zeroForOne, exactAmount: 10000, hookData: ZERO_BYTES
});
vm.expectEmit(true, true, true, true);
emit MockMsgSenderHook.BeforeSwapMsgSender(pranker);
vm.expectEmit(true, true, true, true);
emit MockMsgSenderHook.AfterSwapMsgSender(pranker);
vm.prank(pranker);
exactInput ? quoter.quoteExactInputSingle(params) : quoter.quoteExactOutputSingle(params);
}
function createPoolKey(MockERC20 tokenA, MockERC20 tokenB, address hookAddr)
internal
pure
returns (PoolKey memory)
{
if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA);
return PoolKey(Currency.wrap(address(tokenA)), Currency.wrap(address(tokenB)), 3000, 60, IHooks(hookAddr));
}
function setupPool(PoolKey memory poolKey) internal {
manager.initialize(poolKey, SQRT_PRICE_1_1);
MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max);
MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max);
positionManager.modifyLiquidity(
poolKey,
ModifyLiquidityParams(
MIN_TICK,
MAX_TICK,
calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(),
0
),
ZERO_BYTES
);
}
function setupPoolMultiplePositions(PoolKey memory poolKey) internal {
manager.initialize(poolKey, SQRT_PRICE_1_1);
MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max);
MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max);
positionManager.modifyLiquidity(
poolKey,
ModifyLiquidityParams(
MIN_TICK,
MAX_TICK,
calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(),
0
),
ZERO_BYTES
);
positionManager.modifyLiquidity(
poolKey,
ModifyLiquidityParams(
-60, 60, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -60, 60, 100, 100).toInt256(), 0
),
ZERO_BYTES
);
positionManager.modifyLiquidity(
poolKey,
ModifyLiquidityParams(
-120, 120, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -120, 120, 100, 100).toInt256(), 0
),
ZERO_BYTES
);
}
function setupPoolWithZeroTickInitialized(PoolKey memory poolKey) internal {
PoolId poolId = poolKey.toId();
(uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId);
if (sqrtPriceX96 == 0) {
manager.initialize(poolKey, SQRT_PRICE_1_1);
}
MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max);
MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max);
positionManager.modifyLiquidity(
poolKey,
ModifyLiquidityParams(
MIN_TICK,
MAX_TICK,
calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(),
0
),
ZERO_BYTES
);
positionManager.modifyLiquidity(
poolKey,
ModifyLiquidityParams(0, 60, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, 0, 60, 100, 100).toInt256(), 0),
ZERO_BYTES
);
positionManager.modifyLiquidity(
poolKey,
ModifyLiquidityParams(
-120, 0, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -120, 0, 100, 100).toInt256(), 0
),
ZERO_BYTES
);
}
function calculateLiquidityFromAmounts(
uint160 sqrtRatioX96,
int24 tickLower,
int24 tickUpper,
uint256 amount0,
uint256 amount1
) internal pure returns (uint128 liquidity) {
uint160 sqrtRatioAX96 = TickMath.getSqrtPriceAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtPriceAtTick(tickUpper);
liquidity =
LiquidityAmounts.getLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1);
}
function getExactInputParams(MockERC20[] memory _tokenPath, uint256 amountIn)
internal
pure
returns (IV4Quoter.QuoteExactParams memory params)
{
PathKey[] memory path = new PathKey[](_tokenPath.length - 1);
for (uint256 i = 0; i < _tokenPath.length - 1; i++) {
path[i] = PathKey(Currency.wrap(address(_tokenPath[i + 1])), 3000, 60, IHooks(address(0)), bytes(""));
}
params.exactCurrency = Currency.wrap(address(_tokenPath[0]));
params.path = path;
params.exactAmount = uint128(amountIn);
}
function getExactOutputParams(MockERC20[] memory _tokenPath, uint256 amountOut)
internal
pure
returns (IV4Quoter.QuoteExactParams memory params)
{
PathKey[] memory path = new PathKey[](_tokenPath.length - 1);
for (uint256 i = _tokenPath.length - 1; i > 0; i--) {
path[i - 1] = PathKey(Currency.wrap(address(_tokenPath[i - 1])), 3000, 60, IHooks(address(0)), bytes(""));
}
params.exactCurrency = Currency.wrap(address(_tokenPath[_tokenPath.length - 1]));
params.path = path;
params.exactAmount = uint128(amountOut);
}
}