// SPDX-License-Identifier: MIT pragma solidity 0.8.36; import {Test} from "forge-std/Test.sol"; import {StdInvariant} from "forge-std/StdInvariant.sol"; import {Vm} from "forge-std/Vm.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"; import {OracleLibrary} from "../src/libraries/OracleLibrary.sol"; contract OracleUser is IFluxMintCallback, IFluxSwapCallback { function fluxMintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external { (address token0, address token1) = abi.decode(data, (address, address)); if (amount0Owed > 0) IErc20(token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IErc20(token1).transfer(msg.sender, amount1Owed); } function fluxSwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { (address token0, address token1) = abi.decode(data, (address, address)); if (amount0Delta > 0) IErc20(token0).transfer(msg.sender, uint256(amount0Delta)); if (amount1Delta > 0) IErc20(token1).transfer(msg.sender, uint256(amount1Delta)); } function mint(FluxClPool pool, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data) external { pool.mint(address(this), tickLower, tickUpper, amount, data); } function swap(FluxClPool pool, bool zeroForOne, int256 amountSpecified, uint160 limit, bytes calldata data) external { pool.swap(address(this), zeroForOne, amountSpecified, limit, data); } } contract FluxOracleTest is Test { uint160 private constant priceOne = 79228162514264337593543950336; FluxClFactory private factory; FluxClPool private pool; TestErc20 private token0; TestErc20 private token1; OracleUser private user; bytes private cb; function setUp() public { vm.warp(1_000_000); 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); cb = abi.encode(a, b); pool = FluxClPool(factory.createPool(a, b, 3000)); pool.initialize(priceOne); user = new OracleUser(); token0.mint(address(user), 1e33); token1.mint(address(user), 1e33); user.mint(pool, -600000, 600000, 1e20, cb); } function testCardinalityGrows() public { (,,, uint16 cardinalityBefore, uint16 cardinalityNextBefore,) = pool.slot0(); assertEq(cardinalityBefore, 1); assertEq(cardinalityNextBefore, 1); pool.increaseObservationCardinalityNext(20); (,,,, uint16 cardinalityNextAfter,) = pool.slot0(); assertEq(cardinalityNextAfter, 20); for (uint256 i = 0; i < 25; i++) { vm.warp(block.timestamp + 60); user.swap(pool, true, 1e18, TickMath.minSqrtRatio + 1, cb); } (,,, uint16 cardinalityAfter,,) = pool.slot0(); assertEq(cardinalityAfter, 20); } function testTwapReflectsPriceDrop() public { pool.increaseObservationCardinalityNext(50); for (uint256 i = 0; i < 30; i++) { vm.warp(block.timestamp + 120); user.swap(pool, true, 1e18, TickMath.minSqrtRatio + 1, cb); } vm.warp(block.timestamp + 120); (, int24 spotTick,,,,) = pool.slot0(); int24 meanTick = OracleLibrary.consult(address(pool), 1800); assertLt(meanTick, 0); assertGt(meanTick, spotTick); assertGt(meanTick, TickMath.minTick); } function testObserveInterpolatesCumulative() public { pool.increaseObservationCardinalityNext(10); vm.warp(block.timestamp + 300); user.swap(pool, true, 1e18, TickMath.minSqrtRatio + 1, cb); vm.warp(block.timestamp + 300); uint32[] memory secondsAgos = new uint32[](3); secondsAgos[0] = 500; secondsAgos[1] = 200; secondsAgos[2] = 0; (int56[] memory cumulatives,) = pool.observe(secondsAgos); assertLt(cumulatives[2], cumulatives[1]); assertLe(cumulatives[1], cumulatives[0]); } function testQuoteAtTickAtPriceOne() public view { uint256 quote = OracleLibrary.getQuoteAtTick(0, 1e18, address(token0), address(token1)); assertApproxEqRel(quote, 1e18, 1e12); } function testObserveRevertsForTooOldWindow() public { pool.increaseObservationCardinalityNext(3); vm.warp(block.timestamp + 100); user.swap(pool, true, 1e18, TickMath.minSqrtRatio + 1, cb); uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = 100000; secondsAgos[1] = 0; vm.expectRevert(bytes("Oracle: OLD")); pool.observe(secondsAgos); } } contract OracleMonotonicHandler is IFluxMintCallback, IFluxSwapCallback { Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); FluxClPool public pool; TestErc20 public token0; TestErc20 public token1; bool public orderingHeld = true; 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); } 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)); } function seed() external { pool.mint(address(this), -600000, 600000, 1e24, ""); } function warpAndSwap(uint16 dtSeed, uint256 amountSeed, bool zeroForOne) external { uint256 dt = 1 + (uint256(dtSeed) % 600); vm.warp(block.timestamp + dt); int256 amount = int256(1e12 + (amountSeed % 1e22)); uint160 limit = zeroForOne ? TickMath.minSqrtRatio + 1 : TickMath.maxSqrtRatio - 1; try pool.swap(address(this), zeroForOne, amount, limit, "") {} catch {} _checkOrdering(); } function _checkOrdering() private { (,, uint16 index, uint16 cardinality,,) = pool.slot0(); uint32 prev; bool started; for (uint16 i = 0; i < cardinality; i++) { uint16 slot = uint16((uint256(index) + 1 + i) % cardinality); (uint32 ts,,, bool initialized) = pool.observations(slot); if (!initialized) continue; if (started && ts < prev) orderingHeld = false; prev = ts; started = true; } } } contract FluxOracleInvariantTest is StdInvariant, Test { uint160 private constant priceOne = 79228162514264337593543950336; FluxClPool private pool; OracleMonotonicHandler private handler; function setUp() public { vm.warp(1_000_000); FluxClFactory 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)); pool = FluxClPool(factory.createPool(a, b, 3000)); pool.initialize(priceOne); handler = new OracleMonotonicHandler(pool, TestErc20(a), TestErc20(b)); pool.increaseObservationCardinalityNext(30); handler.seed(); bytes4[] memory selectors = new bytes4[](1); selectors[0] = OracleMonotonicHandler.warpAndSwap.selector; targetSelector(FuzzSelector({addr: address(handler), selectors: selectors})); targetContract(address(handler)); } function invariantObservationTimestampsAreMonotonic() public view { assertTrue(handler.orderingHeld()); } }