# USDR Protocol — Technical Specification v0.1 **Implements:** Rain Dollar (USDR) White Paper v3.2 **Target chain:** Arbitrum One (Solidity 0.8.24, OpenZeppelin v5) **Status:** Pre-audit implementation spec. Every section maps to a whitepaper requirement. --- ## 0. Design goals (from WP v3.2) 1. **The invariant:** `Stress-MaxLoss(t) <= Stable Reserve(t)` enforced in code at every state transition that can increase exposure or decrease the reserve. 2. **No admin-mint.** USDR is minted only by the PSM (1:1 stables) and the CDP vault (over-collateralized), both subject to code-enforced debt ceilings. The minter set is finalized at deployment and can never be extended. 3. **No reflexivity.** There is **no code path** that converts volatile collateral (RAIN/ARB/ETH/wBTC) into USDT/USDC. CDP exit returns collateral. Liquidation transfers collateral to keepers who pay USDR. The protocol never sells volatile collateral to honor redemption. 4. **Two-tier guarantee.** Winning-trader settlement is pre-funded in a Settlement Escrow sized to `Stress-MaxLoss(t)`; everyone else redeems best-effort from free slack (`Reserve − Escrow`). 5. **Immutable core, timelocked risk params.** The invariant logic, ceiling enforcement, and liquidation logic have no upgrade path. Risk parameters (ratios, ceilings, stress factors) are adjustable only through a timelock. Emergency pause is scope- and duration-bounded. --- ## 1. Module map ``` ┌─────────────────┐ │ Timelock / │ (risk params only; │ Governance │ core is immutable) └────────┬────────┘ │ ┌───────────┐ mint/burn ┌────┴─────┐ mint/burn ┌───────────┐ │ PSM ├─────────────►│ USDR │◄──────────────┤ CDPVault │ │ (stables) │ │ ERC-20 │ │(volatile) │ └─────┬─────┘ └──────────┘ └─────┬─────┘ │ USDT/USDC in │ prices ▼ ▼ ┌───────────────┐ escrow top-up ┌──────────────┐ ┌──────────┐ │ReserveManager │─────────────────►│ Settlement │ │OracleHub │ │ (USDT/USDC) │ │ Escrow │ │(CL+TWAP+ │ │ slack ⇄ escrow│ │(traders only)│ │ breaker) │ └───────┬───────┘ └──────────────┘ └──────────┘ │ invariant check ▼ ┌───────────────┐ exposure reports ┌──────────────────┐ │ SolvencyGuard │◄───────────────────────┤ Market adapters │ │ MaxLoss/Stress│ │ (LMSR + CLOB) │ └───────────────┘ └──────────────────┘ ┌───────────────┐ ┌──────────────────┐ ┌────────────────────┐ │ SurplusBuffer │ │ LiquidationEngine│ │ EmergencySettlement│ │ (vig→buffer→ │ │ (Dutch, partial/ │ │ (global shutdown, │ │ burn excess) │ │ staggered) │ │ pro-rata claims) │ └───────────────┘ └──────────────────┘ └────────────────────┘ ``` ## 2. Contracts ### 2.1 `USDR.sol` — the token - ERC-20 + ERC20Permit ("Rain Dollar", "USDR", 18 decimals). No transfer fee, no hooks. - `mint/burn` restricted to the **minter set**: PSM and CDPVault, added during deployment, then `finalizeMinters()` locks the set **forever** (no owner override — WP §10 "no admin-mint"). ### 2.2 `ReserveManager.sol` — stable reserve accounting (WP §4.2) - Holds USDT + USDC (6-decimals normalized to 18 internally). - `reserveBalance()` = USDT + USDC held (escrow excluded). - `rebalanceEscrow()` (permissionless, keeper-callable): moves funds so `escrow.balance == SolvencyGuard.stressMaxLoss()`. Escrow is funded **first**; slack is what remains. - `slack() = reserve − requiredEscrow` (floored at 0). - `redeemFromSlack(to, amount)` — only callable by PSM; reverts if `amount > slack()`. First-come, first-served. This is the *only* stable-out path besides the escrow. ### 2.3 `SettlementEscrow.sol` — traders' winnings only (WP §4.2, §4.4) - Holds USDT. Funded only by ReserveManager top-ups. - `paySettlement(trader, amount)` — only callable by registered **market adapters** (the live RAIN market contracts), only for resolved winning positions. No other withdrawal path exists. - Over-funded escrow (exposure dropped) flows back to ReserveManager via `rebalanceEscrow()`. ### 2.4 `SolvencyGuard.sol` — the invariant (WP §4.1, §4.3) **[immutable core]** - Registered market adapters report per-market exposure: `reportExposure(marketId, groupId, normalMaxLoss, stressMaxLoss)`. - Correlated-group aggregation: exposures are summed **within groups** and across groups (conservative upper bound; group-level netting can only lower it, so summing is safe). - `cdpShortfallRisk` reported by LiquidationEngine (in-flight liquidation risk across full breaker window at stress drawdown — WP §4.6). - **Enforcement:** any call that *increases* stress exposure reverts unless `newStressMaxLoss + cdpShortfallRisk <= ReserveManager.reserveBalance()`. Decreases always succeed (never trap a de-risking). - Publishes both `maxLoss()` (normal) and `stressMaxLoss()` for the Solvency Dashboard. ### 2.5 `CDPVault.sol` — volatile collateral issuance (WP §3, §5.4, §6) - Per-collateral config: `{ collateralRatioBps, debtCeiling, liquidationPenaltyBps, correlatedGroup }`. Launch: ETH 140%, wBTC 140%, ARB 400%, RAIN 400% (ARB+RAIN share a correlated ceiling group). - `open/depositCollateral/withdrawCollateral/borrow(mint USDR)/repay/close`. - Health: `collateralValue * BPS >= debt * ratioBps`. Withdraw/borrow revert if unhealthy after. - **Debt ceilings with hysteresis (WP §5.4):** ceilings are set by the timelock from liquidity data. Lowering a ceiling **only blocks new mints**; existing debt is untouched. Correlated group ceiling = shared cap across RAIN+ARB. - **No-reflexivity:** `close/withdraw` return **collateral only**. There is no function that pays USDT/USDC out of the vault. Verified by tests (§5). ### 2.6 `PSM.sol` — Peg Stability Module (WP §3, §4.4, S10) - `mint(stable, amount)`: 1:1, USDT/USDC in → USDR out (stables forwarded to ReserveManager). - `redeem(stable, amount)`: burns USDR, pays 1:1 **only from slack** via `ReserveManager.redeemFromSlack`. Reverts when slack exhausted — by design (best-effort exit). ### 2.7 `OracleHub.sol` — prices + circuit breaker (WP §9) - ETH/wBTC/USDC: Chainlink adapters with staleness bounds. - RAIN/ARB: Uniswap v3 TWAP adapter (30–60 min window) — interface here; TWAP reader wired at deployment. - **Circuit breaker:** if price moves more than `breakerThresholdBps` within `breakerWindow`, the breaker trips for that asset: liquidations become **partial + rate-limited** (never fully frozen — WP §9), new borrows against that asset pause. - Cross-check: divergence between sources beyond threshold throttles liquidations. ### 2.8 `LiquidationEngine.sol` — keeper auctions (WP §7 step 2, §9) - Dutch auction: starts at oracle price + penalty discount decaying over time; keeper pays USDR (burned against the position's debt), receives collateral. - Breaker active ⇒ per-block liquidation rate limit (partial/staggered, WP §9); the engine reports the un-cleared stress shortfall to SolvencyGuard as `cdpShortfallRisk`. - Residual bad debt → `SurplusBuffer.absorbBadDebt()`. ### 2.9 `SurplusBuffer.sol` — waterfall steps 3–4 (WP §7, §8) - Receives vig revenue (USDT). Fills to `bufferTarget` first; only excess is available to `buybackAndBurn()` (converts to RAIN via external market **buy** — buying RAIN is allowed; *selling* protocol RAIN is only via the capped backstop). - `absorbBadDebt(amount)` — callable by LiquidationEngine. - `backstopSellRain(cap)` — timelock-gated, per-epoch capped treasury-RAIN sale (waterfall step 4; “measured, capped, never reflexive mint”). ### 2.10 `EmergencySettlement.sol` — waterfall step 5 (WP §7) - One-way `trigger()` by guardian **through timelock**; freezes mint/borrow, snapshots collateral pool and USDR supply; holders `claim()` pro-rata share of all collateral at $1 valuation. - Pause powers elsewhere are duration-bounded (`MAX_PAUSE = 14 days`) and scope-limited. ### 2.11 Governance - OZ `TimelockController` (48h min delay) owns all risk parameters. - Guardian multisig: emergency-pause only (bounded), cannot mint, cannot move funds, cannot change the invariant. ## 3. Key flows **Mint via PSM:** user → PSM.mint(USDC, 100) → USDC to ReserveManager → 100 USDR minted. Reserve ↑ ⇒ invariant slack ↑. No check needed (mint can't breach invariant). **Mint via CDP:** user opens RAIN CDP, deposits 400 RAIN($400), borrows ≤100 USDR. Checks: health ratio, per-asset ceiling, correlated-group ceiling, asset not breaker-paused. **Market opens depth:** adapter calls `SolvencyGuard.reportExposure(...)` with the new stress figure → reverts if `Σ stress + shortfall > reserve`. **Depth is refused, never insolvency.** **Trader wins:** market adapter calls `SettlementEscrow.paySettlement(winner, x)` — pre-funded, trustless, cannot be consumed by PSM redeemers. **PSM run (S10):** redeems served from slack until empty, then revert. Escrow untouched. **RAIN −80% in 1h (S1):** breaker trips → borrows pause, liquidations go partial/staggered, shortfall priced into invariant, keepers clear positions, residual → buffer → capped backstop. No RAIN is ever sold by the protocol for USDT. ## 4. Units & precision - Internal accounting: 18 decimals (WAD). USDT/USDC (6 dec) normalized at the boundary. - Prices from OracleHub: WAD USD. - Rounding: protocol-favoring (round debt up, collateral-out down). ## 5. Audit-readiness checklist - [x] Invariant enforced on every exposure increase (`SolvencyGuard`) - [x] Minter set finalized, no admin-mint (`USDR.finalizeMinters`) - [x] No volatile→stable code path (grep + test `NoReflexivity.t.sol`) - [x] Escrow-first reserve accounting; PSM cannot touch escrow - [x] Ceiling hysteresis: lowering blocks only new mints - [x] Breaker: liquidation rate-limited, never frozen; shortfall priced in - [x] Reentrancy guards on all external-token flows; CEI ordering - [x] SafeERC20 everywhere (USDT non-standard return) - [ ] External: Uniswap v3 TWAP reader (deployment wiring) - [ ] External: market-adapter integration for live RAIN LMSR/CLOB - [ ] Independent audit + economic review (WP §14) ## 6. Out of scope (live RAIN protocol, per WP) Market structure, LMSR pricing, order books, resolution oracle — these exist and operate live. This repo exposes the `IMarketAdapter`/`ISolvencyGuard` boundary they integrate against.