test(oracle): add twap oracle tests
This commit is contained in:
1 parent
ec7e6b4b46
commit
9b63845065
1 file changed
+224
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user