// SPDX-License-Identifier: MIT pragma solidity 0.8.36; import {Test} from "forge-std/Test.sol"; import {StdInvariant} from "forge-std/StdInvariant.sol"; import {FluxClFactory} from "../src/FluxClFactory.sol"; import {FluxClPool} from "../src/FluxClPool.sol"; import {TestErc20} from "../src/TestErc20.sol"; import {IErc20} from "../src/interfaces/IErc20.sol"; import {IFluxMintCallback} from "../src/interfaces/IFluxMintCallback.sol"; import {IFluxSwapCallback} from "../src/interfaces/IFluxSwapCallback.sol"; import {TickMath} from "../src/libraries/TickMath.sol"; contract ClHandler is IFluxMintCallback, IFluxSwapCallback { FluxClPool public pool; TestErc20 public token0; TestErc20 public token1; int24 private constant spacing = 60; int256 public ghost0; int256 public ghost1; struct Range { int24 lower; int24 upper; } Range[] public ranges; mapping(bytes32 => uint128) public liquidityOf; constructor(FluxClPool poolAddress, TestErc20 first, TestErc20 second) { pool = poolAddress; token0 = first; token1 = second; token0.mint(address(this), 1e33); token1.mint(address(this), 1e33); } function fluxMintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { if (amount0Owed > 0) token0.transfer(msg.sender, amount0Owed); if (amount1Owed > 0) token1.transfer(msg.sender, amount1Owed); ghost0 += int256(amount0Owed); ghost1 += int256(amount1Owed); } function fluxSwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { if (amount0Delta > 0) token0.transfer(msg.sender, uint256(amount0Delta)); if (amount1Delta > 0) token1.transfer(msg.sender, uint256(amount1Delta)); ghost0 += amount0Delta; ghost1 += amount1Delta; } function doMint(uint128 amountSeed, int24 lowerSeed, int24 widthSeed) external { uint128 amount = uint128(bound(uint256(amountSeed), 1e12, 1e24)); int24 lower = int24(bound(int256(lowerSeed), -30000, 30000)) / spacing * spacing; int24 width = int24(bound(int256(widthSeed), 1, 100)) * spacing; int24 upper = lower + width; if (upper > TickMath.maxTick) return; try pool.mint(address(this), lower, upper, amount, "") { bytes32 key = keccak256(abi.encodePacked(lower, upper)); if (liquidityOf[key] == 0) ranges.push(Range({lower: lower, upper: upper})); liquidityOf[key] += amount; } catch {} } function doSwap(uint256 amountSeed, bool zeroForOne) external { int256 amountSpecified = int256(bound(amountSeed, 1e6, 1e26)); uint160 limit = zeroForOne ? TickMath.minSqrtRatio + 1 : TickMath.maxSqrtRatio - 1; try pool.swap(address(this), zeroForOne, amountSpecified, limit, "") {} catch {} } function doBurn(uint256 rangeSeed, uint128 amountSeed) external { if (ranges.length == 0) return; Range memory r = ranges[rangeSeed % ranges.length]; bytes32 key = keccak256(abi.encodePacked(r.lower, r.upper)); uint128 have = liquidityOf[key]; if (have == 0) return; uint128 amount = uint128(bound(uint256(amountSeed), 1, have)); try pool.burn(r.lower, r.upper, amount) { liquidityOf[key] -= amount; (uint128 c0, uint128 c1) = pool.collect(address(this), r.lower, r.upper, type(uint128).max, type(uint128).max); ghost0 -= int256(uint256(c0)); ghost1 -= int256(uint256(c1)); } catch {} } function bound(uint256 x, uint256 min, uint256 max) private pure returns (uint256) { if (max <= min) return min; return min + (x % (max - min + 1)); } function bound(int256 x, int256 min, int256 max) private pure returns (int256) { if (max <= min) return min; uint256 range = uint256(max - min) + 1; uint256 offset = uint256(x < 0 ? -x : x) % range; return min + int256(offset); } function rangesLength() external view returns (uint256) { return ranges.length; } } contract FluxClInvariantTest is StdInvariant, Test { uint160 private constant priceOne = 79228162514264337593543950336; FluxClFactory private factory; FluxClPool private pool; TestErc20 private token0; TestErc20 private token1; ClHandler private handler; function setUp() public { factory = new FluxClFactory(); TestErc20 tokenA = new TestErc20("Alpha", "ALPHA", 0); TestErc20 tokenB = new TestErc20("Beta", "BETA", 0); (address a, address b) = address(tokenA) < address(tokenB) ? (address(tokenA), address(tokenB)) : (address(tokenB), address(tokenA)); token0 = TestErc20(a); token1 = TestErc20(b); pool = FluxClPool(factory.createPool(a, b, 3000)); pool.initialize(priceOne); handler = new ClHandler(pool, token0, token1); bytes4[] memory selectors = new bytes4[](3); selectors[0] = ClHandler.doMint.selector; selectors[1] = ClHandler.doSwap.selector; selectors[2] = ClHandler.doBurn.selector; targetSelector(FuzzSelector({addr: address(handler), selectors: selectors})); targetContract(address(handler)); } function invariantPoolBalanceMatchesAccounting() public view { assertEq(int256(token0.balanceOf(address(pool))), handler.ghost0()); assertEq(int256(token1.balanceOf(address(pool))), handler.ghost1()); } function invariantPriceStaysWithinTickRange() public view { (uint160 sqrtP, int24 tick,,,,) = pool.slot0(); assertGe(sqrtP, TickMath.minSqrtRatio); assertLt(sqrtP, TickMath.maxSqrtRatio); assertGe(tick, TickMath.minTick); assertLe(tick, TickMath.maxTick); } function invariantPoolNeverOwesMoreThanItHolds() public view { assertGe(handler.ghost0(), 0); assertGe(handler.ghost1(), 0); } }