Peg control
The controller's job is to keep RD's market price close to $1. It has two levers to do that: an interest rate that all borrowers pay, and an internal price called par that drives how redemption arbitrage values RD. Most of the time only the rate lever moves; the par lever is reserved for sustained stress.
RateParControl is the singleton split-range PI controller that produces both outputs from one input: the RD/USD market price. This page covers what the controller does, the constants it uses, and the conditions under which each lever fires. The control-theory proofs and simulation results live in the whitepaper (/papers/).
The exact controller runs against a simulated market in the interactive Peg Control simulator — including a low-liquidity severe-underpeg path that exercises the seven-day fallback.
The signal
Input: price = MarketOracle.getResultWithValidity(), a 24-hour TWAP of RD/USD from the RD-USDC-USDT-USDS Balancer pool. The price is bounded [$0.80, $1.20] at input; anything outside is clamped.
Error: error = $1.00 − price — measured against the fixed $1.00 peg (CO_BIAS_PAR), not the live par output (separate rate-error and par-error views exist inside the contract). Positive error means RD is trading below $1; negative means above.
MarketOracle (the RD-USDC-USDT-USDS Balancer pool), clamped to [$0.80, $1.20], and turns it into an error against the fixed $1.00 peg. That single error drives both outputs — the borrow rate and par.Two outputs, one baton
RateParControl uses a baton that's held by exactly one of the two outputs at a time:
RATEmode (default). The rate PI is active. Par leaks slowly toward its bias ($1.00) on a ~7-day time constant.PARmode. Active only after sustained, large deviation. Par PI is active; rate leaks toward its bias (2% APR).
Why the split
Two reasons:
- Rate moves fast, par moves slow. Acting on rate first lets the market correct cheaply (borrowers reprice immediately). Letting par drift is the heavier intervention.
- Avoid simultaneous double-action. Without a baton, par and rate would both react to the same error and amplify each other. The baton ensures one tool at a time.
Mode transitions
The baton has two independent RATE → PAR entry paths. The fast path requires the rate to be pinned at a bound and still pushing; the fallback only requires a sustained, same-direction error. Whichever finishes first flips the baton:
PAR_ENTRY_DWELL = 24 hours (rate at bound + still pushing)
PAR_ENTRY_FALLBACK_DWELL = 7 days (same-direction |error| > 1¢)
PAR_EXIT_DWELL = 12 hours (PAR → RATE)
The fallback matters when market liquidity is low. Liquidity authority scales the measured error, and its 20% floor can still leave the rate short of a bound; without a second path, that could strand the controller in RATE during the crisis PAR exists to handle. The fallback gives rate a full seven-day attempt, then admits PAR even if rate never saturates. It resets if price returns inside the 1¢ entry threshold or the committed error flips direction, so oscillation cannot accumulate time toward a handoff.
Dwell prevents thrashing: a brief price spike that crosses the threshold and returns won't switch modes. The two entry clocks are independent, so a liquidity drop that pulls rate off its bound resets the 24-hour clock without erasing progress on the seven-day clock.
For the mechanism without the controller internals, watch What happens when increasing rates can’t fix the peg?. For the full low-liquidity path—including admitted authority, both entry clocks, and the fast-path reset—watch Restoring the peg under collateral impairment + liquidity crisis.
Deadbands
There are two deadbands, both applied to the price error:
DB_R = 0.15% // rate-mode deadband
DB_P = 0.7% // par-mode deadband
PAR_ON = 1.0% // PAR-mode entry threshold (with hysteresis)
Below the active deadband, the controller does nothing: output holds, integrators don't accumulate. This filters oracle noise.
The fact that DB_R < DB_P is intentional: rate reacts to smaller errors than par. By the time par is moving at all, the system has already been pushing rate for a while.
DB_R = 0.15% the controller does nothing — output holds, integrators don't accumulate, so oracle noise is filtered. Past it, the rate PI works the error. Only once the error holds beyond PAR_ON = 1.0% (for 24 h) does par arm; while in par mode its own deadband is the wider DB_P = 0.7%. Note DB_R < DB_P: rate reacts to smaller errors than par.Rate PI
The rate output is bounded:
Lower: 0.25% APY (per-second equivalent 1.000000000079175551708715280e27)
Upper: 50% APY (per-second equivalent 1.000000012857214317438491660e27)
Bias: 2% APY (per-second equivalent 1.0000000006279372e27)
Asymmetric proportional gain, symmetric integral gain:
Proportional (asymmetric):
Kp_over = 1e20
Kp_under = 4.5 × Kp_over = 4.5e20
Integral (symmetric — one Ki):
Ki = Kp_under / 5 days (5-day integral time constant)
The under-peg proportional gain is 4.5× stronger — falling-RD is the more dangerous failure mode, so the controller is more aggressive at recovering from it. The integral gain is deliberately not asymmetric: a sign-switched Ki would reprice the accumulated integrator whenever the error crosses the deadband boundary, cliffing the commanded rate. Instead, the integrator decays with a ~60-day half-life while the rate loop is active (RATE_MODE_ZR_LEAK_PER_SEC): a sustained rate level persists as equilibrium memory, fading slowly unless the market re-earns it — which lets the controller track secularly falling equilibrium rates without manufactured peg deviations.
Slew limit: max delta per hour ≈ 1% APR at the 2% bias point (about 24% APR/day). The rate cannot move arbitrarily fast.
Par PI
When the baton hands over to par, par moves:
Lower: $0.75
Upper: $1.30
Bias: $1.00
Gains:
Kp_par = 1e18
Time constant = 7 days
Ki_par = 0.7e18 / 7 days
Slew limit: MAX_PAR_DELTA_PER_HOUR = $0.001. Par cannot move more than a tenth of a cent per hour. That's the contract.
Leaks
When a mode is inactive, its output leaks toward its bias:
Par leak rate = 999998853923969325151379472 / 1e27 per second (~7-day half-life toward $1.00)
Rate leak rate = 999998853923969325151379472 / 1e27 per second (~7-day half-life toward 2% APR)
After a stress event ends and the baton returns to RATE mode, par drifts slowly back to $1.00 over ~weeks.
Update cadence
UPDATE_INTERVAL = 3 hours
UPDATE_INTERVAL_3 = 8 hours
UPDATE_MAX_REWARD = 10 RD
MAX_CONTROL_DT = 12 hours (cap on accumulated dt per update)
Anyone can call RateParControl.update(...). The first call after UPDATE_INTERVAL pays a keeper reward (up to 10 RD). The reward scales cubically with elapsed time within [UPDATE_INTERVAL, UPDATE_INTERVAL_3], early callers earn less so the system doesn't burn money calling every block.
If the controller hasn't been touched in a long time (e.g. nobody triggered an update for days), the accumulated dt is capped at MAX_CONTROL_DT = 12 hours. This prevents a stale oracle from causing a single huge step on first update.
Bootstrap period
BOOTSTRAP_PERIOD = 14 days
For the first 14 days after deployment, redemptions are disabled (enforced at the Aggregator / Redemptions level). The controller is also frozen during bootstrap: it commits no control step, so rate and par hold at their bias values the entire time. RateParControl gates this itself — lastUpdateTime is initialized to the bootstrap-end timestamp, so no control time elapses until bootstrap is over. Only once bootstrap ends does the controller begin moving rate and par.
lastUpdateTime is seeded to the bootstrap-end timestamp, so no control step is committed and rate and par simply hold at their biases (2% APR and $1.00). Redemptions are disabled over the same window. Only once bootstrap ends does the controller start moving either output.Reading the controller
| Public getter | Meaning |
|---|---|
par() | Current par value, e.g. 1.000123e18 for $1.000123. |
rate() | Current per-second rate accumulator (in 1e27 precision). |
mode() | 0 = RATE, 1 = PAR. |
Zr() / Zp() | Rate / par integrator state. |
lastEr() / lastEp() | Last deadbanded errors. |
lastUpdateTime() | Timestamp of last commit. |
parEntryStart() | Fast 24-hour PAR-entry timer. 0 = not armed. |
parEntryFallbackStart() | Seven-day sustained-error PAR-entry timer. 0 = not armed. |
parExitStart() | Twelve-hour RATE-return timer. 0 = not armed. |
What you should remember
- Rate is the everyday tool. PAR mode is a stress response, not normal operation.
- Low authority slows control; it cannot veto PAR forever. Sustained same-direction error triggers the seven-day fallback.
- The controller is asymmetric. It responds harder to RD-below-par than RD-above-par.
- Everything is slow. Max rate slew 1%/hour, max par slew $0.001/hour. If you see a sudden jump, it's a UI artifact, not the controller.
- The controller is autonomous. No admin can set par or rate directly.
Deep dive
- More Visualizations: shareable animations and X-ready previews.
- RD Market Oracle: how the input price is constructed.
- Whitepaper (
/papers/), stability proofs, simulation results.