mirror of
https://github.com/lux-amm/v4-core.git
synced 2026-10-30 20:54:53 +00:00
290 lines
12 KiB
Solidity
290 lines
12 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {Vm} from "forge-std/Vm.sol";
|
|
import {SqrtPriceMath} from "../../src/libraries/SqrtPriceMath.sol";
|
|
import {SwapMath} from "../../src/libraries/SwapMath.sol";
|
|
import {Constants} from "../utils/Constants.sol";
|
|
|
|
contract SwapMathTest is Test {
|
|
function test_fuzz_getSqrtPriceTarget(bool zeroForOne, uint160 sqrtPriceNextX96, uint160 sqrtPriceLimitX96)
|
|
public
|
|
pure
|
|
{
|
|
assertEq(
|
|
SwapMath.getSqrtPriceTarget(zeroForOne, sqrtPriceNextX96, sqrtPriceLimitX96),
|
|
(zeroForOne ? sqrtPriceNextX96 < sqrtPriceLimitX96 : sqrtPriceNextX96 > sqrtPriceLimitX96)
|
|
? sqrtPriceLimitX96
|
|
: sqrtPriceNextX96
|
|
);
|
|
}
|
|
|
|
function test_computeSwapStep_exactAmountIn_oneForZero_thatGetsCappedAtPriceTargetIn() public pure {
|
|
uint160 priceTarget = Constants.SQRT_PRICE_101_100;
|
|
uint160 price = Constants.SQRT_PRICE_1_1;
|
|
uint128 liquidity = 2 ether;
|
|
int256 amount = (1 ether) * -1;
|
|
uint24 lpFee = 600;
|
|
bool zeroForOne = false;
|
|
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee);
|
|
|
|
assertEq(amountIn, 9975124224178055);
|
|
assertEq(amountOut, 9925619580021728);
|
|
assertEq(feeAmount, 5988667735148);
|
|
assert(amountIn + feeAmount < uint256(amount * -1));
|
|
|
|
uint256 priceAfterWholeInputAmount =
|
|
SqrtPriceMath.getNextSqrtPriceFromInput(price, liquidity, uint256(amount * -1), zeroForOne);
|
|
|
|
assertEq(sqrtQ, priceTarget);
|
|
assert(sqrtQ < priceAfterWholeInputAmount);
|
|
}
|
|
|
|
function test_computeSwapStep_exactAmountOut_oneForZero_thatGetsCappedAtPriceTargetIn() public pure {
|
|
uint160 priceTarget = Constants.SQRT_PRICE_101_100;
|
|
uint160 price = Constants.SQRT_PRICE_1_1;
|
|
uint128 liquidity = 2 ether;
|
|
int256 amount = 1 ether;
|
|
uint24 lpFee = 600;
|
|
bool zeroForOne = false;
|
|
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee);
|
|
|
|
assertEq(amountIn, 9975124224178055);
|
|
assertEq(amountOut, 9925619580021728);
|
|
assertEq(feeAmount, 5988667735148);
|
|
assert(amountOut < uint256(amount));
|
|
|
|
uint256 priceAfterWholeOutputAmount =
|
|
SqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, uint256(amount), zeroForOne);
|
|
|
|
assertEq(sqrtQ, priceTarget);
|
|
assert(sqrtQ < priceAfterWholeOutputAmount);
|
|
}
|
|
|
|
function test_computeSwapStep_exactAmountIn_oneForZero_thatIsFullySpentIn() public pure {
|
|
uint160 priceTarget = Constants.SQRT_PRICE_1000_100;
|
|
uint160 price = Constants.SQRT_PRICE_1_1;
|
|
uint128 liquidity = 2 ether;
|
|
int256 amount = 1 ether * -1;
|
|
uint24 lpFee = 600;
|
|
bool zeroForOne = false;
|
|
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee);
|
|
|
|
assertEq(amountIn, 999400000000000000);
|
|
assertEq(amountOut, 666399946655997866);
|
|
assertEq(feeAmount, 600000000000000);
|
|
assertEq(amountIn + feeAmount, uint256(-amount));
|
|
|
|
uint256 priceAfterWholeInputAmountLessFee =
|
|
SqrtPriceMath.getNextSqrtPriceFromInput(price, liquidity, uint256(uint256(-amount) - feeAmount), zeroForOne);
|
|
|
|
assert(sqrtQ < priceTarget);
|
|
assertEq(sqrtQ, priceAfterWholeInputAmountLessFee);
|
|
}
|
|
|
|
function test_computeSwapStep_exactAmountOut_oneForZero_thatIsFullyReceivedIn() public pure {
|
|
uint160 priceTarget = Constants.SQRT_PRICE_10000_100;
|
|
uint160 price = Constants.SQRT_PRICE_1_1;
|
|
uint128 liquidity = 2 ether;
|
|
int256 amount = (1 ether);
|
|
uint24 lpFee = 600;
|
|
bool zeroForOne = false;
|
|
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(price, priceTarget, liquidity, amount, lpFee);
|
|
|
|
assertEq(amountIn, 2000000000000000000);
|
|
assertEq(feeAmount, 1200720432259356);
|
|
assertEq(amountOut, uint256(amount));
|
|
|
|
uint256 priceAfterWholeOutputAmount =
|
|
SqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, uint256(amount), zeroForOne);
|
|
|
|
assert(sqrtQ < priceTarget);
|
|
assertEq(sqrtQ, priceAfterWholeOutputAmount);
|
|
}
|
|
|
|
function test_computeSwapStep_amountOut_isCappedAtTheDesiredAmountOut() public pure {
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = SwapMath.computeSwapStep(
|
|
417332158212080721273783715441582, 1452870262520218020823638996, 159344665391607089467575320103, 1, 1
|
|
);
|
|
|
|
assertEq(amountIn, 1);
|
|
assertEq(feeAmount, 1);
|
|
assertEq(amountOut, 1); // would be 2 if not capped
|
|
assertEq(sqrtQ, 417332158212080721273783715441581);
|
|
}
|
|
|
|
function test_computeSwapStep_targetPriceOf1UsesPartialInputAmount() public pure {
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(2, 1, 1, -3915081100057732413702495386755767, 1);
|
|
assertEq(amountIn, Constants.SQRT_PRICE_1_4);
|
|
assertEq(feeAmount, 39614120871253040049813);
|
|
assert(amountIn + feeAmount <= 3915081100057732413702495386755767);
|
|
assertEq(amountOut, 0);
|
|
assertEq(sqrtQ, 1);
|
|
}
|
|
|
|
function test_computeSwapStep_notEntireInputAmountTakenAsFee() public pure {
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(2413, 79887613182836312, 1985041575832132834610021537970, -10, 1872);
|
|
|
|
assertEq(amountIn, 9);
|
|
assertEq(feeAmount, 1);
|
|
assertEq(amountOut, 0);
|
|
assertEq(sqrtQ, 2413);
|
|
}
|
|
|
|
function test_computeSwapStep_zeroForOne_handlesIntermediateInsufficientLiquidityInExactOutputCase() public pure {
|
|
uint160 sqrtP = 20282409603651670423947251286016;
|
|
uint160 sqrtPTarget = (sqrtP * 11) / 10;
|
|
uint128 liquidity = 1024;
|
|
// virtual reserves of one are only 4
|
|
// https://www.wolframalpha.com/input/?i=1024+%2F+%2820282409603651670423947251286016+%2F+2**96%29
|
|
int256 amountRemaining = 4;
|
|
uint24 feePips = 3000;
|
|
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(sqrtP, sqrtPTarget, liquidity, amountRemaining, feePips);
|
|
|
|
assertEq(amountOut, 0);
|
|
assertEq(sqrtQ, sqrtPTarget);
|
|
assertEq(amountIn, 26215);
|
|
assertEq(feeAmount, 79);
|
|
}
|
|
|
|
function test_computeSwapStep_oneForZero_handlesIntermediateInsufficientLiquidityInExactOutputCase() public pure {
|
|
uint160 sqrtP = 20282409603651670423947251286016;
|
|
uint160 sqrtPTarget = (sqrtP * 9) / 10;
|
|
uint128 liquidity = 1024;
|
|
// virtual reserves of zero are only 262144
|
|
// https://www.wolframalpha.com/input/?i=1024+*+%2820282409603651670423947251286016+%2F+2**96%29
|
|
int256 amountRemaining = 263000;
|
|
uint24 feePips = 3000;
|
|
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(sqrtP, sqrtPTarget, liquidity, amountRemaining, feePips);
|
|
|
|
assertEq(amountOut, 26214);
|
|
assertEq(sqrtQ, sqrtPTarget);
|
|
assertEq(amountIn, 1);
|
|
assertEq(feeAmount, 1);
|
|
}
|
|
|
|
function test_fuzz_computeSwapStep(
|
|
uint160 sqrtPriceRaw,
|
|
uint160 sqrtPriceTargetRaw,
|
|
uint128 liquidity,
|
|
int256 amountRemaining,
|
|
uint24 feePips
|
|
) public pure {
|
|
vm.assume(sqrtPriceRaw > 0);
|
|
vm.assume(sqrtPriceTargetRaw > 0);
|
|
vm.assume(feePips >= 0);
|
|
|
|
if (amountRemaining >= 0) {
|
|
vm.assume(feePips < 1e6);
|
|
} else {
|
|
vm.assume(feePips <= 1e6);
|
|
}
|
|
|
|
(uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) =
|
|
SwapMath.computeSwapStep(sqrtPriceRaw, sqrtPriceTargetRaw, liquidity, amountRemaining, feePips);
|
|
|
|
assertLe(amountIn, type(uint256).max - feeAmount);
|
|
|
|
unchecked {
|
|
if (amountRemaining >= 0) {
|
|
assertLe(amountOut, uint256(amountRemaining));
|
|
} else {
|
|
assertLe(amountIn + feeAmount, uint256(-amountRemaining));
|
|
}
|
|
}
|
|
|
|
if (sqrtPriceRaw == sqrtPriceTargetRaw) {
|
|
assertEq(amountIn, 0);
|
|
assertEq(amountOut, 0);
|
|
assertEq(feeAmount, 0);
|
|
assertEq(sqrtQ, sqrtPriceTargetRaw);
|
|
}
|
|
|
|
// didn't reach price target, entire amount must be consumed
|
|
if (sqrtQ != sqrtPriceTargetRaw) {
|
|
uint256 absAmtRemaining;
|
|
if (amountRemaining == type(int256).min) {
|
|
absAmtRemaining = uint256(type(int256).max) + 1;
|
|
} else if (amountRemaining < 0) {
|
|
absAmtRemaining = uint256(-amountRemaining);
|
|
} else {
|
|
absAmtRemaining = uint256(amountRemaining);
|
|
}
|
|
if (amountRemaining > 0) assertEq(amountOut, absAmtRemaining);
|
|
else assertEq(amountIn + feeAmount, absAmtRemaining);
|
|
}
|
|
|
|
// next price is between price and price target
|
|
if (sqrtPriceTargetRaw <= sqrtPriceRaw) {
|
|
assertLe(sqrtQ, sqrtPriceRaw);
|
|
assertGe(sqrtQ, sqrtPriceTargetRaw);
|
|
} else {
|
|
assertGe(sqrtQ, sqrtPriceRaw);
|
|
assertLe(sqrtQ, sqrtPriceTargetRaw);
|
|
}
|
|
}
|
|
|
|
function test_computeSwapStep_swapOneForZero_exactInCapped() public {
|
|
vm.startSnapshotGas("SwapMath_oneForZero_exactInCapped");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_101_100, 2 ether, (1 ether) * -1, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
|
|
function test_computeSwapStep_swapZeroForOne_exactInCapped() public {
|
|
vm.startSnapshotGas("SwapMath_zeroForOne_exactInCapped");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_99_100, 2 ether, (1 ether) * -1, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
|
|
function test_computeSwapStep_swapOneForZero_exactOutCapped() public {
|
|
vm.startSnapshotGas("SwapMath_oneForZero_exactOutCapped");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_101_100, 2 ether, 1 ether, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
|
|
function test_computeSwapStep_swapZeroForOne_exactOutCapped() public {
|
|
vm.startSnapshotGas("SwapMath_zeroForOne_exactOutCapped");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_99_100, 2 ether, 1 ether, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
|
|
function test_computeSwapStep_swapOneForZero_exactInPartial() public {
|
|
vm.startSnapshotGas("SwapMath_oneForZero_exactInPartial");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_1010_100, 2 ether, 1_000 * -1, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
|
|
function test_computeSwapStep_swapZeroForOne_exactInPartial() public {
|
|
vm.startSnapshotGas("SwapMath_zeroForOne_exactInPartial");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_99_1000, 2 ether, 1_000 * -1, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
|
|
function test_computeSwapStep_swapOneForZero_exactOutPartial() public {
|
|
vm.startSnapshotGas("SwapMath_oneForZero_exactOutPartial");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_1010_100, 2 ether, 1_000, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
|
|
function test_computeSwapStep_swapZeroForOne_exactOutPartial() public {
|
|
vm.startSnapshotGas("SwapMath_zeroForOne_exactOutPartial");
|
|
SwapMath.computeSwapStep(Constants.SQRT_PRICE_1_1, Constants.SQRT_PRICE_99_1000, 2 ether, 1_000, 600);
|
|
vm.stopSnapshotGas();
|
|
}
|
|
}
|