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