mirror of
https://github.com/lux-amm/v4-core.git
synced 2026-10-30 20:54:53 +00:00
191 lines
7.6 KiB
Solidity
191 lines
7.6 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.24;
|
|
|
|
import "forge-std/Test.sol";
|
|
import {V3Helper, IUniswapV3Pool, IUniswapV3MintCallback, IUniswapV3SwapCallback} from "./utils/V3Helper.sol";
|
|
import {Deployers} from "./utils/Deployers.sol";
|
|
import {Currency} from "../src/types/Currency.sol";
|
|
import {Fuzzers} from "../src/test/Fuzzers.sol";
|
|
import {IHooks} from "../src/interfaces/IHooks.sol";
|
|
import {IPoolManager} from "../src/interfaces/IPoolManager.sol";
|
|
import {BalanceDelta, BalanceDeltaLibrary, toBalanceDelta} from "../src/types/BalanceDelta.sol";
|
|
import {PoolSwapTest} from "../src/test/PoolSwapTest.sol";
|
|
import {PoolKey} from "../src/types/PoolKey.sol";
|
|
import {ModifyLiquidityParams, SwapParams} from "../src/types/PoolOperation.sol";
|
|
import {SqrtPriceMath} from "../src/libraries/SqrtPriceMath.sol";
|
|
import {TickMath} from "../src/libraries/TickMath.sol";
|
|
import {SafeCast} from "../src/libraries/SafeCast.sol";
|
|
import {LiquidityAmounts} from "./utils/LiquidityAmounts.sol";
|
|
|
|
abstract contract V3Fuzzer is V3Helper, Deployers, Fuzzers, IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|
function setUp() public virtual override {
|
|
super.setUp();
|
|
deployFreshManagerAndRouters();
|
|
deployMintAndApprove2Currencies();
|
|
}
|
|
|
|
function initPools(uint24 fee, int24 tickSpacing, int256 sqrtPriceX96seed)
|
|
internal
|
|
returns (IUniswapV3Pool v3Pool, PoolKey memory key_, uint160 sqrtPriceX96)
|
|
{
|
|
fee = uint24(bound(fee, 0, 999999));
|
|
tickSpacing = int24(bound(tickSpacing, 1, 16383));
|
|
// v3 pools don't allow overwriting existing fees, 500, 3000, 10000 are set by default in the constructor
|
|
if (fee == 500) tickSpacing = 10;
|
|
else if (fee == 3000) tickSpacing = 60;
|
|
else if (fee == 10000) tickSpacing = 200;
|
|
else v3Factory.enableFeeAmount(fee, tickSpacing);
|
|
|
|
sqrtPriceX96 = createRandomSqrtPriceX96(tickSpacing, sqrtPriceX96seed);
|
|
|
|
v3Pool = IUniswapV3Pool(v3Factory.createPool(Currency.unwrap(currency0), Currency.unwrap(currency1), fee));
|
|
v3Pool.initialize(sqrtPriceX96);
|
|
|
|
key_ = PoolKey(currency0, currency1, fee, tickSpacing, IHooks(address(0)));
|
|
manager.initialize(key_, sqrtPriceX96);
|
|
}
|
|
|
|
function addLiquidity(
|
|
IUniswapV3Pool v3Pool,
|
|
PoolKey memory key_,
|
|
uint160 sqrtPriceX96,
|
|
int24 lowerTickUnsanitized,
|
|
int24 upperTickUnsanitized,
|
|
int256 liquidityDeltaUnbound,
|
|
bool tight
|
|
) internal {
|
|
ModifyLiquidityParams memory v4LiquidityParams = ModifyLiquidityParams({
|
|
tickLower: lowerTickUnsanitized,
|
|
tickUpper: upperTickUnsanitized,
|
|
liquidityDelta: liquidityDeltaUnbound,
|
|
salt: 0
|
|
});
|
|
|
|
v4LiquidityParams = tight
|
|
? createFuzzyLiquidityParamsWithTightBound(key_, v4LiquidityParams, sqrtPriceX96, 20)
|
|
: createFuzzyLiquidityParams(key_, v4LiquidityParams, sqrtPriceX96);
|
|
|
|
v3Pool.mint(
|
|
address(this),
|
|
v4LiquidityParams.tickLower,
|
|
v4LiquidityParams.tickUpper,
|
|
uint128(int128(v4LiquidityParams.liquidityDelta)),
|
|
""
|
|
);
|
|
|
|
modifyLiquidityRouter.modifyLiquidity(key_, v4LiquidityParams, "");
|
|
}
|
|
|
|
function swap(IUniswapV3Pool pool, PoolKey memory key_, bool zeroForOne, int128 amountSpecified)
|
|
internal
|
|
returns (int256 amount0Diff, int256 amount1Diff)
|
|
{
|
|
if (amountSpecified == 0) amountSpecified = 1;
|
|
if (amountSpecified == type(int128).min) amountSpecified = type(int128).min + 1;
|
|
// v3 swap
|
|
(int256 amount0Delta, int256 amount1Delta) = pool.swap(
|
|
// invert amountSpecified because v3 swaps use inverted signs
|
|
address(this),
|
|
zeroForOne,
|
|
amountSpecified * -1,
|
|
zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT,
|
|
""
|
|
);
|
|
// v3 can handle bigger numbers than v4, so if we exceed int128, check that the next call reverts
|
|
bool overflows = false;
|
|
if (
|
|
amount0Delta > type(int128).max || amount1Delta > type(int128).max || amount0Delta < type(int128).min
|
|
|| amount1Delta < type(int128).min
|
|
) {
|
|
overflows = true;
|
|
}
|
|
// v4 swap
|
|
SwapParams memory swapParams = SwapParams({
|
|
zeroForOne: zeroForOne,
|
|
amountSpecified: amountSpecified,
|
|
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT
|
|
});
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
BalanceDelta delta;
|
|
try swapRouter.swap(key_, swapParams, testSettings, "") returns (BalanceDelta delta_) {
|
|
delta = delta_;
|
|
} catch (bytes memory reason) {
|
|
require(overflows, "v4 should not overflow");
|
|
assertEq(bytes4(reason), SafeCast.SafeCastOverflow.selector);
|
|
delta = toBalanceDelta(0, 0);
|
|
amount0Delta = 0;
|
|
amount1Delta = 0;
|
|
}
|
|
|
|
// because signs for v3 and v4 swaps are inverted, add values up to get the difference
|
|
amount0Diff = amount0Delta + delta.amount0();
|
|
amount1Diff = amount1Delta + delta.amount1();
|
|
}
|
|
|
|
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
|
|
currency0.transfer(msg.sender, amount0Owed);
|
|
currency1.transfer(msg.sender, amount1Owed);
|
|
}
|
|
|
|
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
|
|
if (amount0Delta > 0) currency0.transfer(msg.sender, uint256(amount0Delta));
|
|
if (amount1Delta > 0) currency1.transfer(msg.sender, uint256(amount1Delta));
|
|
}
|
|
}
|
|
|
|
contract V3SwapTests is V3Fuzzer {
|
|
function test_shouldSwapEqual(
|
|
uint24 feeSeed,
|
|
int24 tickSpacingSeed,
|
|
int24 lowerTickUnsanitized,
|
|
int24 upperTickUnsanitized,
|
|
int256 liquidityDeltaUnbound,
|
|
int256 sqrtPriceX96seed,
|
|
int128 swapAmount,
|
|
bool zeroForOne
|
|
) public {
|
|
(IUniswapV3Pool pool, PoolKey memory key_, uint160 sqrtPriceX96) =
|
|
initPools(feeSeed, tickSpacingSeed, sqrtPriceX96seed);
|
|
addLiquidity(pool, key_, sqrtPriceX96, lowerTickUnsanitized, upperTickUnsanitized, liquidityDeltaUnbound, false);
|
|
(int256 amount0Diff, int256 amount1Diff) = swap(pool, key_, zeroForOne, swapAmount);
|
|
assertEq(amount0Diff, 0);
|
|
assertEq(amount1Diff, 0);
|
|
}
|
|
|
|
struct TightLiquidityParams {
|
|
int24 lowerTickUnsanitized;
|
|
int24 upperTickUnsanitized;
|
|
int256 liquidityDeltaUnbound;
|
|
}
|
|
|
|
function test_shouldSwapEqualMultipleLP(
|
|
uint24 feeSeed,
|
|
int24 tickSpacingSeed,
|
|
TightLiquidityParams[] memory liquidityParams,
|
|
int256 sqrtPriceX96seed,
|
|
int128 swapAmount,
|
|
bool zeroForOne
|
|
) public {
|
|
(IUniswapV3Pool pool, PoolKey memory key_, uint160 sqrtPriceX96) =
|
|
initPools(feeSeed, tickSpacingSeed, sqrtPriceX96seed);
|
|
for (uint256 i = 0; i < liquidityParams.length; ++i) {
|
|
if (i == 20) break;
|
|
addLiquidity(
|
|
pool,
|
|
key_,
|
|
sqrtPriceX96,
|
|
liquidityParams[i].lowerTickUnsanitized,
|
|
liquidityParams[i].upperTickUnsanitized,
|
|
liquidityParams[i].liquidityDeltaUnbound,
|
|
true
|
|
);
|
|
}
|
|
|
|
(int256 amount0Diff, int256 amount1Diff) = swap(pool, key_, zeroForOne, swapAmount);
|
|
assertEq(amount0Diff, 0);
|
|
assertEq(amount1Diff, 0);
|
|
}
|
|
}
|