// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @title SettlementEscrow — traders' winnings only (White Paper v3.2 §4.2, §4.4) /// @notice Pre-funds winning-trader payouts with stable assets equal to Stress-MaxLoss(t). /// "Accessible only to traders, and only for settlement of winning positions." /// Merely holding USDR — or having minted it against RAIN — grants NO access. /// @dev Only registered market adapters (the live RAIN market contracts) can pay out. /// Only the ReserveManager can reclaim excess. No other withdrawal path exists. contract SettlementEscrow is ReentrancyGuard { using SafeERC20 for IERC20; error NotDeployer(); error NotAdapter(); error NotReserveManager(); error AlreadyFinalized(); error InsufficientEscrow(); address public immutable deployer; IERC20 public immutable usdt; IERC20 public immutable usdc; uint256 private constant SCALE = 1e12; address public reserveManager; bool public finalized; mapping(address => bool) public isAdapter; event SettlementPaid(address indexed trader, uint256 amountWad); event RefundedToReserve(uint256 amount6); event AdapterAdded(address indexed adapter); constructor(address _usdt, address _usdc) { deployer = msg.sender; usdt = IERC20(_usdt); usdc = IERC20(_usdc); } function setup(address _reserveManager) external { if (msg.sender != deployer) revert NotDeployer(); if (finalized) revert AlreadyFinalized(); reserveManager = _reserveManager; } function addAdapter(address adapter) external { if (msg.sender != deployer) revert NotDeployer(); if (finalized) revert AlreadyFinalized(); isAdapter[adapter] = true; emit AdapterAdded(adapter); } function finalize() external { if (msg.sender != deployer) revert NotDeployer(); finalized = true; } /// @notice Pay a winning trader. Trustless: only market adapters, only settlement. function paySettlement(address trader, uint256 amountWad) external nonReentrant { if (!isAdapter[msg.sender]) revert NotAdapter(); uint256 amount6 = amountWad / SCALE; uint256 t = usdt.balanceOf(address(this)); if (t >= amount6) { usdt.safeTransfer(trader, amount6); } else { if (t > 0) usdt.safeTransfer(trader, t); uint256 rem = amount6 - t; if (usdc.balanceOf(address(this)) < rem) revert InsufficientEscrow(); usdc.safeTransfer(trader, rem); } emit SettlementPaid(trader, amountWad); } /// @notice Excess (exposure dropped) flows back to the main reserve — only. function refundToReserve(uint256 amount6) external nonReentrant { if (msg.sender != reserveManager) revert NotReserveManager(); uint256 t = usdt.balanceOf(address(this)); uint256 sendT = amount6 > t ? t : amount6; if (sendT > 0) usdt.safeTransfer(reserveManager, sendT); uint256 rem = amount6 - sendT; if (rem > 0) usdc.safeTransfer(reserveManager, rem); emit RefundedToReserve(amount6); } function escrowBalance() external view returns (uint256) { return (usdt.balanceOf(address(this)) + usdc.balanceOf(address(this))) * SCALE; } }