Architecture
RAI Dollar's contracts are organized in two layers: a per-branch layer that owns trove state and pools for a single collateral, and a singleton layer above that coordinates across branches. The diagram below is the one in Aggregator.sol:
┌──────────────┐
│ Aggregator │ ← Central router
│ - drip() │
│ - redeem() │
│ - quotas │
└──────┬───────┘
│ iterates N branches
┌────────────────┼────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Branch 1 │ │ Branch 2 │ │ Branch N │
│ (WETH) │ │ (wstETH) │ │ (WBTC) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
Each branch contains:
┌─────────────────────────────────────────────┐
│ BorrowerOperations ←→ TroveManager │
│ ↕ ↕ │
│ ActivePool (unshielded) Rewards │
│ ActivePool (shielded) Liquidations │
│ DefaultPool Redemptions │
│ StabilityPool SortedTroves (×2) │
│ CollSurplusPool PriceFeed │
│ FeeRouter MinTrove │
└─────────────────────────────────────────────┘
Singletons:
┌──────────────────────────────────────┐
│ RDToken RateParControl │
│ FEEToken MarketOracle │
│ Aggregator GlobalFeeRouter │
│ LPStaking GlobalHintHelper │
└──────────────────────────────────────┘
Per-branch contracts
A branch is everything needed to run one collateral type. Branches don't share state; they share singletons.
| Contract | Responsibility |
|---|---|
BorrowerOperations | User-facing API: open / adjust / close / shield-toggle. Enforces ICR/TCR/shield rules. |
TroveManager | Trove state machine: storage, ICR computation, liquidation orchestration entry-points. |
Liquidations | Branch-local liquidation logic, offset against SP, redistribute remainder, batch liquidate. |
Redemptions | Branch-local redemption walk over the unshielded (and in shutdown, shielded) sorted list. |
Rewards | Tracks pending redistribution rewards (debt and collateral) for active troves. |
StabilityPool | RD deposits that absorb liquidations. Tracks per-depositor collateral and FEE gains. |
InterestEngine | Drips branch debt interest. Maintains two per-second accumulators: accumulatedRate for the unshielded book (branch_rate − v) and accumulatedShieldRate for the shielded book (branch_rate + u), where the same shield-share curve produces the signed adjustment for each. |
ActivePool (unshielded) | Custody of collateral backing unshielded troves. |
ActiveShieldedPool | Custody of collateral backing shielded troves. |
DefaultPool | Custody of redistributed collateral and debt (from liquidations that couldn't be fully offset). |
CollSurplusPool | Holds collateral surplus owed back to borrowers (post-full-redemption, force-close remainders). |
GasPool | Holds the per-trove 200 RD gas-compensation reserve. |
SortedTroves (unshielded) | Doubly-linked list of unshielded troves, ordered by NICR. |
SortedTroves (shielded) | Same, for shielded troves. |
PriceFeed | Branch-specific collateral USD price feed (Chainlink primary, branch-specific composition for LSTs). |
FeeRouter | Routes fees from this branch: between the local Stability Pool and the singleton GlobalFeeRouter. |
MinTrove | Helper for opening a trove exactly at MIN_NET_DEBT + gas comp with ICR = MCR. |
Singleton contracts
| Contract | Responsibility |
|---|---|
Aggregator | Cross-branch control plane: redemption routing, drip orchestration, issuance quotas, per-branch debt EMAs, delta-rate offsets. |
RDToken | The RD stablecoin (ERC-20). Mint/burn restricted to the protocol's authorized branches and routers. |
FEEToken | The FEE governance/value-capture token. |
RateParControl | The PI controller that produces the system rate and par over time. |
MarketOracle | Balancer-hook TWAP/TWAL feed of RD/USD used by RateParControl. |
GlobalFeeRouter | Splits cross-branch fees between LP staking, FEE staking, keeper rewards, and bad-debt repayment. |
LPStaking | BPT staking; pays FEE (from LpIssuance) and RD (from direct inflows). |
FEEStaking | FEE staking with tiered lockups. |
FLXStaking | FLX staking; one-year linear FEE distribution. |
GlobalHintHelper | Off-chain-style hint computation across branches. |
KeeperRewards | Source of keeper rewards for drip/oracle/par operations. |
LpIssuance / SpIssuance | FEE emission curves for LP and SP rewards. |
Branch ↔ singleton relationships
- Mint/burn: only
BorrowerOperations,TroveManager,StabilityPool,Redemptions, andAggregatorare authorized to mint/burn RD. Authorization is role-based onRDToken. - Drip orchestration:
Aggregator.dripAll()iterates every non-shutdown branch and calls each branch'sTroveManager.drip(), which folds accrued interest intoaccumulatedRateviaInterestEngine. - Debt EMAs: every state-changing borrower op tells the
Aggregatorto refresh that branch's debt EMA. The Aggregator also keeps a per-branch unshielded debt EMA for redemption basket weights. - Rate offsets: the
Aggregatoradds a per-branchdeltaRateOffseton top of the system rate fromRateParControl. The offset is driven by the branch's debt-utilization error. - Redemption: a single
Aggregator.redeemCollateralcall iterates all non-shutdown branches with non-zerounshieldedDebtEma, splits the requested RD across them by EMA weight, and calls each branch'sRedemptions. - Fees: each branch's
FeeRoutercollects fees, sends a (PI-controlled) fraction to the local SP, and forwards the rest toGlobalFeeRouter, which fans out to LP staking, FEE staking, keeper rewards, and bad-debt repayment.
Common message flows
Opening a trove (unshielded):
User → BorrowerOperations.openTrove
→ TroveManager.openTrove (state)
→ Rewards.applyPendingRewards (settle redistribution)
→ InterestEngine.drip (settle interest)
→ ActivePool.addCollateral (pull collateral)
→ RDToken.mint (mint to user)
→ RDToken.mint (mint 200 to GasPool)
→ SortedTroves.insert
→ Aggregator.notifyDebtIncrease (EMA + quota)
Redeeming RD (cross-branch):
User → Aggregator.redeemCollateral
→ RateParControl.par() (read par)
→ for each non-shutdown branch with weight > 0:
→ Redemptions[i].redeemCollateral
→ walks SortedTroves[i] ascending ICR
→ for each trove touched:
→ adjusts coll/debt at par
→ ActivePool[i].sendCollateral → user
→ RDToken.burn from user
→ updates branch debt EMA + quota
Liquidating a trove:
Liquidator → Liquidations[i].liquidate(trove)
→ TroveManager.checkICR (must be below MCR or in recovery mode)
→ StabilityPool[i].offset (burn RD, claim collateral, pro-rata)
→ if residual: redistribute via DefaultPool[i] + Rewards[i]
→ pay 200 RD gas-comp from GasPool to liquidator
→ SortedTroves[i].remove
Why this shape
- Branches are isolated. Adding a new collateral is a new branch deploy, not a global upgrade. Branches don't read each other's pools.
- The Aggregator is read-only with respect to branch internals. It calls into branches via well-typed interfaces; branches push debt-change notifications back via authorized calls. No shared storage.
- The controller is a singleton.
RateParControlproduces one system rate and one par for all branches. Per-branch adjustment happens via theAggregator's additive delta-rate offset, not by giving each branch its own PI loop. - Redemptions are cross-branch but per-branch internally. The Aggregator owns the basket-weight logic; each branch owns its own walk.
Where to read next
- Peg & PI controller: what
RateParControlactually does. - Interest model: what
InterestEnginedoes. - Redemption mechanism: full walk.
- Liquidation paths: offset/redistribute math.
- Fees,
FeeRouterandGlobalFeeRouter.