Peg & PI controller
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 signal
Input: price = MarketOracle.read(), a TWAP of RD/USD from a curated Balancer pool. The price is bounded [$0.80, $1.20] at input; anything outside is clamped.
Error: error = par − price (with separate rate-error and par-error views inside the contract). Positive error means RD is trading below par; negative means above.
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 switches based on dwell timers, once the price error exceeds the entry threshold for the dwell period, the baton flips:
PAR_ENTRY_DWELL = 24 hours (RATE → PAR)
PAR_EXIT_DWELL = 12 hours (PAR → RATE)
Dwell prevents thrashing: a brief price spike that crosses the threshold and returns won't switch modes.
Deadbands
There are two deadbands, both applied to the price error:
DB_R = 0.3% // rate-mode deadband
DB_P = 0.8% // 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.
Rate PI
The rate output is bounded:
Lower: 0.1% APR (per-second equivalent 1.0000000000316939e27)
Upper: 30% APR (per-second equivalent 1.0000000083195163e27)
Bias: 2% APR (per-second equivalent 1.0000000006279372e27)
Two pairs of gains (Kp, Ki), different for over-peg and under-peg, because the system is asymmetric:
Over-peg (price > par):
Kp_over = 1e20
Ki_over = Kp_over / 5 days (5-day integral time constant)
Under-peg (price < par):
Kp_under = 3 × Kp_over = 3e20
Ki_under = Kp_under / 5 days
The under-peg gain is 3× stronger. Falling-RD is the more dangerous failure mode, so the controller is more aggressive at recovering from it.
Slew limit: max delta per hour ≈ 0.25% APR at the 2% bias point (about 6% APR/day). The rate cannot move arbitrarily fast.
Par PI
When the baton hands over to par, par moves:
Lower: $0.85
Upper: $1.20
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 = 2 hours
UPDATE_INTERVAL_3 = 8 hours
UPDATE_MAX_REWARD = 10 RD
MAX_CONTROL_DT = 1 day (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 = 1 day. 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 (this is enforced at the Aggregator and Redemptions level, not the controller). The controller itself runs normally, it can start moving rate during bootstrap. By the time redemption is enabled, the controller should have converged.
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() / parExitStart() | Dwell-timer arming timestamps. 0 = not armed. |
What you should remember
- Rate is the everyday tool. PAR mode is a stress response, not normal operation.
- The controller is asymmetric. It responds harder to RD-below-par than RD-above-par.
- Everything is slow. Max rate slew 0.25%/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
- Oracles: how the input price is constructed.
- Whitepaper (
/papers/), stability proofs, simulation results.