// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {ISettlementEscrow} from "../interfaces/IUSDR.sol"; /// @dev Resolution/claim views of a live Rain market (rain-sdk-v2 MarketsAbi). /// optionWinner(option) returns the winning side (0 = unresolved); /// getClaimableAmount(user, option) returns the user's payable winnings. interface IRainMarketSettlement { function optionWinner(uint256 option) external view returns (uint8); function getClaimableAmount(address user, uint256 option) external view returns (uint256); function optionClaimed(uint256 option, address user) external view returns (bool); function baseTokenDecimals() external view returns (uint256); // scale factor (e.g. 1e6) function isOptionDisputed(uint256 option) external view returns (bool); function isOptionAppealed(uint256 option) external view returns (bool); } /// @title SettlementRouter — wires live Rain market resolutions to the Settlement Escrow /// @notice White Paper v3.2 §4.4: "A winning position was part of net MaxLoss the moment /// it opened, so its payout is already pre-funded in the Settlement Escrow. /// Settlement to USDT is trustless." /// /// Flow: market resolves via its own oracle (`chooseWinner`, dispute window, /// appeals — existing live machinery, out of scope). Once final, any keeper or /// the winner themself calls `settle(market, option, trader)`: /// 1. verifies the option is RESOLVED and NOT under dispute/appeal, /// 2. reads the trader's claimable winnings from the market contract, /// 3. pays USDT from the Settlement Escrow — funds that were reserved for /// exactly this the moment the exposure opened. /// /// @dev Trustless by construction: the payable amount comes from the market contract's /// own accounting (getClaimableAmount); this router adds no discretion, holds no /// funds, and can only forward escrow money to the verified winner. Double-pay is /// blocked by the local `settled` registry. contract SettlementRouter is ReentrancyGuard { error NotTimelock(); error MarketNotRegistered(); error NotResolved(); error UnderDispute(); error NothingClaimable(); error AlreadySettled(); ISettlementEscrow public immutable escrow; address public immutable timelock; mapping(address => bool) public registeredMarket; // canonical Rain pools only mapping(bytes32 => bool) public settled; // keccak(market, option, trader) event MarketRegistered(address indexed market); event Settled(address indexed market, uint256 indexed option, address indexed trader, uint256 amountWad); constructor(address _escrow, address _timelock) { escrow = ISettlementEscrow(_escrow); timelock = _timelock; } /// @notice Register a canonical Rain pool (timelock-gated; same policy as the /// exposure adapter — only factory-deployed pools). function registerMarket(address market) external { if (msg.sender != timelock) revert NotTimelock(); registeredMarket[market] = true; emit MarketRegistered(market); } /// @notice Settle a winning trader's payout from the pre-funded escrow. /// Permissionless: winner or any keeper may trigger. function settle(address market, uint256 option, address trader) external nonReentrant { if (!registeredMarket[market]) revert MarketNotRegistered(); IRainMarketSettlement m = IRainMarketSettlement(market); if (m.optionWinner(option) == 0) revert NotResolved(); if (m.isOptionDisputed(option) || m.isOptionAppealed(option)) revert UnderDispute(); bytes32 key = keccak256(abi.encodePacked(market, option, trader)); if (settled[key]) revert AlreadySettled(); uint256 claimable = m.getClaimableAmount(trader, option); // base-token units if (claimable == 0) revert NothingClaimable(); settled[key] = true; // effects before interaction uint256 amountWad = claimable * 1e18 / m.baseTokenDecimals(); escrow.paySettlement(trader, amountWad); emit Settled(market, option, trader, amountWad); } }