// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {Test} from "forge-std/Test.sol"; import {SolvencyGuard} from "../src/SolvencyGuard.sol"; import {ReserveManager} from "../src/ReserveManager.sol"; import {SettlementEscrow} from "../src/SettlementEscrow.sol"; import {RainMarketAdapter, IRainMarket} from "../src/adapters/RainMarketAdapter.sol"; import {MockERC20} from "./mocks/Mocks.sol"; /// @dev Mock of a live Rain pool exposing the exact views the adapter reads /// (signatures from rain-sdk-v2 MarketsAbi). contract MockRainMarket is IRainMarket { uint256 public numberOfOptions; uint256 public totalMarketFunds; uint256 public totalMarketShares; address public baseToken; uint256 public baseTokenDecimals = 1e6; // scale factor, matches live pool behavior uint8 public tradingModel; mapping(uint256 => uint256) public optionWinningPoolShare; mapping(uint256 => uint256) public totalOptionLPShares; mapping(uint256 => uint8) public optionWinner; constructor(uint256 n) { numberOfOptions = n; } function setFunds(uint256 f) external { totalMarketFunds = f; } function setPayout(uint256 o, uint256 p) external { optionWinningPoolShare[o] = p; } } contract RainMarketAdapterTest is Test { SolvencyGuard guard; ReserveManager reserve; SettlementEscrow escrow; RainMarketAdapter adapter; MockERC20 usdt; MockERC20 usdc; address timelock = makeAddr("timelock"); address psm = makeAddr("psm"); bytes32 constant SPORTS = keccak256("SPORTS"); bytes32 constant CRYPTO = keccak256("CRYPTO_PRICE"); function setUp() public { usdt = new MockERC20("USDT", "USDT", 6); usdc = new MockERC20("USDC", "USDC", 6); escrow = new SettlementEscrow(address(usdt), address(usdc)); reserve = new ReserveManager(address(usdt), address(usdc)); guard = new SolvencyGuard(); adapter = new RainMarketAdapter(address(guard), timelock); reserve.setup(psm, address(escrow), address(guard)); escrow.setup(address(reserve)); escrow.finalize(); guard.setup(address(reserve), makeAddr("engine")); guard.addAdapter(address(adapter)); guard.finalize(); } function _fund(uint256 amount6) internal { usdt.mint(address(reserve), amount6); } /// WP S2: balanced $1B book → net MaxLoss ≈ 0 function test_S2_balancedBook_nearZeroExposure() public { _fund(1_000e6); // tiny reserve suffices MockRainMarket m = new MockRainMarket(2); m.setFunds(1_000_000_000e6); // $1B collected m.setPayout(1, 1_000_000_000e6); // France wins → pay exactly the pool m.setPayout(2, 1_000_000_000e6); // Argentina wins → same vm.prank(timelock); adapter.registerMarket(address(m), SPORTS, 10_000); assertEq(guard.stressMaxLoss(), 0); // solvent by construction } /// WP S9: one-sided longshot → exposure computed, depth refused beyond reserve function test_S9_oneSidedBook_computedAndCapped() public { _fund(100_000e6); // $100k reserve MockRainMarket m = new MockRainMarket(2); m.setFunds(10_000e6); // $10k collected m.setPayout(1, 300_000e6); // longshot resolves true → owe $300k // stress = 290k * 1.5 = 435k > 100k reserve → registration (first sync) refused vm.prank(timelock); vm.expectRevert(); adapter.registerMarket(address(m), SPORTS, 10_000); // protocol underwrites only 20% of the book → stress = 87k ≤ 100k → accepted vm.prank(timelock); adapter.registerMarket(address(m), SPORTS, 2_000); assertLe(guard.stressMaxLoss(), 100_000e18); } /// WP S4/S8: correlated markets share a group; aggregate is what the invariant sees function test_S4_S8_correlatedGroupAggregation() public { _fund(200_000e6); MockRainMarket a = new MockRainMarket(2); MockRainMarket b = new MockRainMarket(2); a.setFunds(0); a.setPayout(1, 60_000e6); b.setFunds(0); b.setPayout(2, 60_000e6); vm.startPrank(timelock); adapter.registerMarket(address(a), CRYPTO, 10_000); adapter.registerMarket(address(b), CRYPTO, 10_000); vm.stopPrank(); // both in the CRYPTO group: 2 × 60k × 1.5 = 180k tracked at group level assertEq(guard.groupStressExposure(CRYPTO), 180_000e18); assertEq(guard.stressMaxLoss(), 180_000e18); // a third correlated market pushing past the reserve is refused MockRainMarket c = new MockRainMarket(2); c.setFunds(0); c.setPayout(1, 60_000e6); vm.prank(timelock); vm.expectRevert(); adapter.registerMarket(address(c), CRYPTO, 10_000); } /// resolved market retires → exposure released function test_retire_releasesExposure() public { _fund(200_000e6); MockRainMarket m = new MockRainMarket(2); m.setFunds(0); m.setPayout(1, 50_000e6); vm.startPrank(timelock); adapter.registerMarket(address(m), SPORTS, 10_000); assertGt(guard.stressMaxLoss(), 0); adapter.retireMarket(address(m)); vm.stopPrank(); assertEq(guard.stressMaxLoss(), 0); } /// permissionless sync keeps exposure current as the book moves function test_sync_updatesWithBook() public { _fund(500_000e6); MockRainMarket m = new MockRainMarket(3); m.setFunds(100_000e6); m.setPayout(1, 150_000e6); vm.prank(timelock); adapter.registerMarket(address(m), SPORTS, 10_000); uint256 before = guard.stressMaxLoss(); m.setPayout(1, 120_000e6); // book rebalances adapter.syncMarket(address(m)); // anyone assertLt(guard.stressMaxLoss(), before); } }