// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {Test} from "forge-std/Test.sol"; import {OracleHub} from "../src/OracleHub.sol"; import {UniV3TwapReader, IUniswapV3Pool} from "../src/oracle/UniV3TwapReader.sol"; import {MockAggregator, MockTwap, MockERC20} from "./mocks/Mocks.sol"; contract MockSequencerFeed { int256 public answer; // 0 = up, 1 = down uint256 public startedAt; constructor() { startedAt = block.timestamp; } function set(int256 _answer, uint256 _startedAt) external { answer = _answer; startedAt = _startedAt; } function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { return (1, answer, startedAt, startedAt, 1); } function decimals() external pure returns (uint8) { return 0; } } /// @dev Minimal Uniswap v3 pool mock: constant tick over the whole window. contract MockUniV3Pool { address public token0; address public token1; uint128 public liquidity = type(uint128).max; int56 public tickPerSecond; constructor(address t0, address t1, int56 tick) { token0 = t0; token1 = t1; tickPerSecond = tick; } function setLiquidity(uint128 l) external { liquidity = l; } function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory, uint160[] memory) { int56[] memory ticks = new int56[](secondsAgos.length); uint160[] memory sec = new uint160[](secondsAgos.length); for (uint256 i = 0; i < secondsAgos.length; i++) { // cumulative tick at (now - secondsAgo) with constant tick ticks[i] = tickPerSecond * int56(uint56(block.timestamp - secondsAgos[i])); } return (ticks, sec); } } contract OracleTest is Test { OracleHub hub; MockSequencerFeed seq; MockTwap twap; address timelock = makeAddr("timelock"); MockERC20 rain; MockERC20 usdc; function setUp() public { vm.warp(30 days); // sane timestamp hub = new OracleHub(timelock); seq = new MockSequencerFeed(); twap = new MockTwap(1e18); rain = new MockERC20("RAIN", "RAIN", 18); usdc = new MockERC20("USDC", "USDC", 6); vm.startPrank(timelock); hub.configureAsset( address(rain), OracleHub.AssetConfig(OracleHub.SourceKind.Twap, address(twap), 0, 3000, 1 hours, 5000) ); hub.setSequencerFeed(address(seq), 1 hours); vm.stopPrank(); seq.set(0, block.timestamp - 2 hours); // up, past grace } function test_sequencerDown_blocksPrices() public { seq.set(1, block.timestamp); vm.expectRevert(OracleHub.SequencerDown.selector); hub.getPrice(address(rain)); } function test_sequencerGracePeriod_blocksAfterRecovery() public { seq.set(0, block.timestamp - 10 minutes); // recovered 10 min ago, grace = 1h vm.expectRevert(OracleHub.SequencerGracePeriod.selector); hub.getPrice(address(rain)); seq.set(0, block.timestamp - 2 hours); // past grace (uint256 p,) = hub.getPrice(address(rain)); assertEq(p, 1e18); } function test_twapReader_tickZero_equalsParity() public { // tick 0 => price 1.0 token1/token0. RAIN(18) vs USDC(6): // raw pool price 1.0 means 1e-12 in human terms; reader normalizes decimals. MockUniV3Pool pool = new MockUniV3Pool(address(rain), address(usdc), 0); UniV3TwapReader reader = new UniV3TwapReader(address(pool), address(rain), 30 minutes, 1); // token1/token0 = 1 raw => 1 * 1e18 * 1e18/1e6 / 1e18... expected 1e30 raw-normalized: // human price = raw * 10^(dec0-dec1) = 1 * 10^12 → WAD = 1e12 * 1e18 / 1e18 uint256 p = reader.twapPrice(); assertApproxEqRel(p, 1e30, 1e15); // parity check with decimal normalization } function test_twapReader_rejectsShortWindow() public { MockUniV3Pool pool = new MockUniV3Pool(address(rain), address(usdc), 0); vm.expectRevert(UniV3TwapReader.WindowTooShort.selector); new UniV3TwapReader(address(pool), address(rain), 10 minutes, 1); } function test_twapReader_rejectsLowLiquidity() public { MockUniV3Pool pool = new MockUniV3Pool(address(rain), address(usdc), 0); UniV3TwapReader reader = new UniV3TwapReader(address(pool), address(rain), 30 minutes, 1000); pool.setLiquidity(10); vm.expectRevert(UniV3TwapReader.PoolLiquidityTooLow.selector); reader.twapPrice(); } }