// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {PluginOracleAdapter} from "../src/PluginOracleAdapter.sol"; import {TwapFallback, ITwapSource} from "../src/TwapFallback.sol"; import {XSwapV2TwapSource, IV2Pair} from "../src/XSwapV2TwapSource.sol"; import {IPluginFeed} from "../src/interfaces/IPluginFeed.sol"; import {MockPluginFeed, MockTwap, MockToken, MockV2Pair} from "./mocks/Mocks.sol"; contract TwapFallbackTest is Test { MockPluginFeed feed; PluginOracleAdapter adapter; MockTwap twap; TwapFallback fb; function setUp() public { vm.warp(1_000_000); feed = new MockPluginFeed(8); feed.set(1e8, block.timestamp); adapter = new PluginOracleAdapter(IPluginFeed(address(feed)), 1 hours, 0, 0); twap = new MockTwap(1e8); fb = new TwapFallback(adapter, ITwapSource(address(twap)), 0.5e8, 2e8, 30 minutes, 6 hours, address(this)); fb.setFallbackEnabled(true); } function test_FallbackOffByDefault() public { TwapFallback off = new TwapFallback(adapter, ITwapSource(address(twap)), 0.5e8, 2e8, 30 minutes, 6 hours, address(this)); assertFalse(off.fallbackEnabled()); (uint256 p, bool used) = off.price(); // primary fine -> primary assertEq(p, 1e8); assertFalse(used); vm.warp(block.timestamp + 2 hours); // primary stale -> no silent fallback vm.expectRevert(TwapFallback.FallbackDisabled.selector); off.price(); vm.prank(address(0xBEEF)); vm.expectRevert(); off.setFallbackEnabled(true); // owner only } function test_CheckpointIsPermissionless() public { vm.warp(block.timestamp + 1 hours); vm.prank(address(0xCAFE)); fb.checkpoint(); assertEq(fb.cpTimestamp(), block.timestamp); } function test_UsesPrimary() public view { (uint256 p, bool fallback_) = fb.price(); assertEq(p, 1e8); assertFalse(fallback_); } function test_FallbackWhenStale() public { vm.warp(block.timestamp + 30 minutes); twap.setPrice(1.2e8); vm.warp(block.timestamp + 31 minutes); (uint256 p, bool fallback_) = fb.price(); assertTrue(fallback_); assertEq(p, (uint256(1e8) * 1800 + uint256(1.2e8) * 1860) / 3660); } function test_FallbackWhenOutOfBounds() public { vm.warp(block.timestamp + 30 minutes); feed.set(5e8, block.timestamp); (uint256 p, bool fallback_) = fb.price(); assertTrue(fallback_); assertEq(p, 1e8); } function test_FallbackOnNoAccess() public { vm.warp(block.timestamp + 30 minutes); feed.setNoAccess(true); (, bool fallback_) = fb.price(); assertTrue(fallback_); } function test_RevertWindowTooShortOrLong() public { feed.set(0, block.timestamp); vm.expectRevert(); fb.price(); vm.warp(block.timestamp + 7 hours); vm.expectRevert(); fb.price(); fb.checkpoint(); vm.warp(block.timestamp + 1 hours); (, bool fallback_) = fb.price(); assertTrue(fallback_); } } contract XSwapV2TwapSourceTest is Test { MockToken wxdc; MockToken usdc; MockV2Pair pair; function setUp() public { vm.warp(1_000_000); wxdc = new MockToken(18); usdc = new MockToken(6); pair = new MockV2Pair(address(wxdc), address(usdc)); pair.sync(1_000_000e18, 35_000e6); // 0.035 USDC per WXDC } function _src(address base, uint256 minQ) internal returns (XSwapV2TwapSource) { return new XSwapV2TwapSource(IV2Pair(address(pair)), base, 16, minQ); } function test_TwapPriceBaseToken0() public { XSwapV2TwapSource s = _src(address(wxdc), 1_000e6); (uint256 c0, uint256 t0) = s.priceCumulative(); vm.warp(block.timestamp + 1 hours); (uint256 c1, uint256 t1) = s.priceCumulative(); uint256 p = s.quote((c1 - c0) / (t1 - t0)); assertApproxEqRel(p, 0.035e16, 1e12); // 16 dp } function test_TwapPriceBaseToken1() public { XSwapV2TwapSource s = _src(address(usdc), 1_000e18); // quote = WXDC (uint256 c0,) = s.priceCumulative(); vm.warp(block.timestamp + 1 hours); (uint256 c1,) = s.priceCumulative(); assertApproxEqRel(s.quote((c1 - c0) / 3600), 28.5714285714e16, 1e12); } function test_TimeWeighted() public { XSwapV2TwapSource s = _src(address(wxdc), 1_000e6); (uint256 c0,) = s.priceCumulative(); vm.warp(block.timestamp + 30 minutes); pair.sync(1_000_000e18, 45_000e6); // 0.045 vm.warp(block.timestamp + 30 minutes); (uint256 c1,) = s.priceCumulative(); assertApproxEqRel(s.quote((c1 - c0) / 3600), 0.04e16, 1e12); } function test_MinLiquidity() public { XSwapV2TwapSource s = _src(address(wxdc), 50_000e6); vm.expectRevert(abi.encodeWithSelector(XSwapV2TwapSource.InsufficientLiquidity.selector, 35_000e6, 50_000e6)); s.quote(1); } function test_WithFallback() public { MockPluginFeed feed = new MockPluginFeed(16); feed.set(0.035e16, block.timestamp); PluginOracleAdapter a = new PluginOracleAdapter(IPluginFeed(address(feed)), 1 hours, 0, 0); TwapFallback fb = new TwapFallback(a, _src(address(wxdc), 1_000e6), 0.01e16, 0.1e16, 30 minutes, 6 hours, address(this)); fb.setFallbackEnabled(true); vm.warp(block.timestamp + 2 hours); // primary stale (uint256 p, bool used) = fb.price(); assertTrue(used); assertApproxEqRel(p, 0.035e16, 1e12); } }