test(oracle): add twap oracle tests

This commit is contained in:
dualfroz committed 2026-08-27 17:12:00 +02:00
1 parent ec7e6b4b46
commit 9b63845065
1 file changed
+224
+224
View File
@@ -0,0 +1,224 @@
// 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());
}
}