// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Test, console} from "forge-std/Test.sol"; import {PluginOracleAdapter} from "../../src/PluginOracleAdapter.sol"; import {ChainlinkScaledAdapter} from "../../src/ChainlinkScaledAdapter.sol"; import {PriceSnapshotUpkeep} from "../../src/automation/PriceSnapshotUpkeep.sol"; import {XSwapV2TwapSource, IV2Pair} from "../../src/XSwapV2TwapSource.sol"; import {IPluginFeed, IAggregatorV3Like} from "../../src/interfaces/IPluginFeed.sol"; interface ITypeAndVersion { function typeAndVersion() external view returns (string memory); } /// @notice Read-only fork tests against XDC mainnet. No keys, no broadcast. /// Skipped unless XDC_FORK_RPC is set: /// XDC_FORK_RPC=https://rpc.xinfin.network forge test --match-path 'test/fork/*' contract PluginFeedForkTest is Test { // From deepdive/plugin_feeds_mainnet_2026-10-21.csv address constant XDC_USDT = 0x896CC6fE8AD04dF9f481332606c445768e286076; // 16 dp, multi-oracle address constant PLI_USDT = 0x2baa1AEfa06D58454c9c06C56436CdD5B7904248; address constant SPY_USD_EAC = 0x6dFe684eD467359c25a3DD4ba36DFEF91F9755A7; // reverts "No access" address constant BTC_USDT = 0xEe28565FF7583dcB188554BE434665b4235fBCDb; // reverts with empty data address constant VRF_COORD = 0xCc017FEe2fbD6722168c07418D9dD6A2509E10B4; // XSwap V2 (docs.xspswap.finance): factory 0x347D…352f; WXDC/USDC pair via getPair address constant XSWAP_WXDC_USDC = 0xf490b8574a7D7063027B5E77B721b8418C62784A; address constant WXDC = 0x951857744785E80e2De051c32EE7b25f9c458C42; bool forked; function setUp() public { string memory rpc = vm.envOr("XDC_FORK_RPC", string("")); if (bytes(rpc).length == 0) return; vm.createSelectFork(rpc); assertEq(block.chainid, 50, "not XDC mainnet"); forked = true; } function test_Fork_XdcUsdtDefaultStaleness() public { if (!forked) vm.skip(true); PluginOracleAdapter a = new PluginOracleAdapter(IPluginFeed(XDC_USDT), 0, 0, 0); assertEq(a.maxStaleness(), 3600); assertEq(a.decimals(), 16); uint256 p = a.latestPrice(); console.log("XDC/USDT (16dp):", p); assertGt(p, 0.001e16); assertLt(p, 10e16); (, int256 s8,,,) = new ChainlinkScaledAdapter(IAggregatorV3Like(address(a))).latestRoundData(); assertEq(uint256(s8), p / 1e8); } function test_Fork_PliUsdt() public { if (!forked) vm.skip(true); assertGt(new PluginOracleAdapter(IPluginFeed(PLI_USDT), 0, 0, 0).latestPrice(), 0); } function test_Fork_StaleAfterWarp() public { if (!forked) vm.skip(true); PluginOracleAdapter a = new PluginOracleAdapter(IPluginFeed(XDC_USDT), 0, 0, 0); vm.warp(block.timestamp + 2 hours); vm.expectRevert(); a.latestRoundData(); } function test_Fork_NoAccessFeed() public { if (!forked) vm.skip(true); bytes memory denied = abi.encodeWithSelector(PluginOracleAdapter.FeedAccessDenied.selector, SPY_USD_EAC); try new PluginOracleAdapter(IPluginFeed(SPY_USD_EAC), 1 hours, 0, 0) returns (PluginOracleAdapter a) { vm.expectRevert(denied); // decimals() open, data getters gated a.latestRoundData(); } catch (bytes memory reason) { assertEq(reason, denied); // decimals() gated too } } function test_Fork_EmptyRevertFeedIsWrapped() public { if (!forked) vm.skip(true); vm.expectRevert(abi.encodeWithSelector(PluginOracleAdapter.FeedReverted.selector, BTC_USDT, "")); new PluginOracleAdapter(IPluginFeed(BTC_USDT), 1 hours, 0, 0); } function test_Fork_UpkeepSnapshot() public { if (!forked) vm.skip(true); PriceSnapshotUpkeep u = new PriceSnapshotUpkeep(new PluginOracleAdapter(IPluginFeed(XDC_USDT), 0, 0, 0), 15 minutes, address(this)); u.setForwarder(address(0xF00D)); (bool needed, bytes memory data) = u.checkUpkeep(""); assertTrue(needed); vm.prank(address(0xF00D)); u.performUpkeep(data); assertGt(u.lastPrice(), 0); } function test_Fork_VrfCoordinatorVersion() public { if (!forked) vm.skip(true); assertEq(ITypeAndVersion(VRF_COORD).typeAndVersion(), "VRFCoordinatorV2 1.0.0"); } function test_Fork_XSwapTwapAndLiquidity() public { if (!forked) vm.skip(true); IV2Pair pair = IV2Pair(XSWAP_WXDC_USDC); XSwapV2TwapSource s = new XSwapV2TwapSource(pair, WXDC, 16, 1e6); // >= 1 USDC (uint256 c0,) = s.priceCumulative(); vm.warp(block.timestamp + 1 hours); // reserves unchanged on fork => spot price (uint256 c1,) = s.priceCumulative(); uint256 p; unchecked { p = s.quote((c1 - c0) / 3600); } console.log("XSwap WXDC/USDC TWAP (16dp):", p); assertGt(p, 0.001e16); assertLt(p, 10e16); // Recommended floor for a real fallback: >= 50,000 USDC of quote reserve. The WXDC/USDC pool // (~42 USDC on 2026-10-21) MUST fail it; that is why TwapFallback ships with the fallback off. XSwapV2TwapSource sensible = new XSwapV2TwapSource(pair, WXDC, 16, 50_000e6); vm.expectPartialRevert(XSwapV2TwapSource.InsufficientLiquidity.selector); sensible.quote(1); } }