Skip to main content

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.

ContractResponsibility
BorrowerOperationsUser-facing API: open / adjust / close / shield-toggle. Enforces ICR/TCR/shield rules.
TroveManagerTrove state machine: storage, ICR computation, liquidation orchestration entry-points.
LiquidationsBranch-local liquidation logic, offset against SP, redistribute remainder, batch liquidate.
RedemptionsBranch-local redemption walk over the unshielded (and in shutdown, shielded) sorted list.
RewardsTracks pending redistribution rewards (debt and collateral) for active troves.
StabilityPoolRD deposits that absorb liquidations. Tracks per-depositor collateral and FEE gains.
InterestEngineDrips 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.
ActiveShieldedPoolCustody of collateral backing shielded troves.
DefaultPoolCustody of redistributed collateral and debt (from liquidations that couldn't be fully offset).
CollSurplusPoolHolds collateral surplus owed back to borrowers (post-full-redemption, force-close remainders).
GasPoolHolds 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.
PriceFeedBranch-specific collateral USD price feed (Chainlink primary, branch-specific composition for LSTs).
FeeRouterRoutes fees from this branch: between the local Stability Pool and the singleton GlobalFeeRouter.
MinTroveHelper for opening a trove exactly at MIN_NET_DEBT + gas comp with ICR = MCR.

Singleton contracts

ContractResponsibility
AggregatorCross-branch control plane: redemption routing, drip orchestration, issuance quotas, per-branch debt EMAs, delta-rate offsets.
RDTokenThe RD stablecoin (ERC-20). Mint/burn restricted to the protocol's authorized branches and routers.
FEETokenThe FEE governance/value-capture token.
RateParControlThe PI controller that produces the system rate and par over time.
MarketOracleBalancer-hook TWAP/TWAL feed of RD/USD used by RateParControl.
GlobalFeeRouterSplits cross-branch fees between LP staking, FEE staking, keeper rewards, and bad-debt repayment.
LPStakingBPT staking; pays FEE (from LpIssuance) and RD (from direct inflows).
FEEStakingFEE staking with tiered lockups.
FLXStakingFLX staking; one-year linear FEE distribution.
GlobalHintHelperOff-chain-style hint computation across branches.
KeeperRewardsSource of keeper rewards for drip/oracle/par operations.
LpIssuance / SpIssuanceFEE emission curves for LP and SP rewards.

Branch ↔ singleton relationships

  • Mint/burn: only BorrowerOperations, TroveManager, StabilityPool, Redemptions, and Aggregator are authorized to mint/burn RD. Authorization is role-based on RDToken.
  • Drip orchestration: Aggregator.dripAll() iterates every non-shutdown branch and calls each branch's TroveManager.drip(), which folds accrued interest into accumulatedRate via InterestEngine.
  • Debt EMAs: every state-changing borrower op tells the Aggregator to refresh that branch's debt EMA. The Aggregator also keeps a per-branch unshielded debt EMA for redemption basket weights.
  • Rate offsets: the Aggregator adds a per-branch deltaRateOffset on top of the system rate from RateParControl. The offset is driven by the branch's debt-utilization error.
  • Redemption: a single Aggregator.redeemCollateral call iterates all non-shutdown branches with non-zero unshieldedDebtEma, splits the requested RD across them by EMA weight, and calls each branch's Redemptions.
  • Fees: each branch's FeeRouter collects fees, sends a (PI-controlled) fraction to the local SP, and forwards the rest to GlobalFeeRouter, 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. RateParControl produces one system rate and one par for all branches. Per-branch adjustment happens via the Aggregator'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.