Skip to main content

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/).

Try it live

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.

MarketOracleRD-USDC-USDT-USDS poolRD/USD · 24h TWAPRateParControlerror = $1.00 − pricesplit-range PIratepar
The controller's only input is the RD/USD price. It reads a 24-hour TWAP from the 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:

  • RATE mode (default). The rate PI is active. Par leaks slowly toward its bias ($1.00) on a ~7-day time constant.
  • PAR mode. Active only after sustained, large deviation. Par PI is active; rate leaks toward its bias (2% APR).
deadbandRATE modeRATE modePAR modePAR mode−1.0%+1.0%−0.15%+0.15%0$1.00 peg← RD below parRD above par →
Price error bands, colored by which output holds the baton. A dead band in the center where nobody acts; the RATE mode owns everyday errors just outside it; and only a large, sustained error hands the baton to PAR mode further out. One mode is responsible for any given error — never both.

Why the split​

Two reasons:

  1. 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.
  2. 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.

RATE mode · defaultrate PI activepar leaks → $1.00 (~7 d)PAR modepar PI activerate leaks → 2% APR (~7 d)|error| > 1.0% · same directionrate at bound: 24 hfallback without bound: 7 derror subsidesPAR_EXIT_DWELL 12 hthe inactive output leaks toward its bias on a ~7-day half-life
A baton is held by exactly one output at a time, so rate and par never act on the same error at once. RATE is the default — cheap, fast correction while par leaks back to $1.00. The baton flips to PAR through either a 24-hour rate-bound dwell or a seven-day sustained-error fallback, then flips back once the error subsides for its own dwell. The fallback means low liquidity can slow active control without vetoing PAR indefinitely.

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.

holdrate PI actsrate PI actspar armspar armsPAR_ON1.0%DB_R0.15%DB_P0.7%0$1.00 peg24 h fast · 7 d fallback24 h fast · 7 d fallback← RD below parRD above par →
Everything keys off the price error (market vs par). Inside 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.

Par leaks → $1.00while RATE mode is active$1.00$1.05~7d ½-lifetime →Rate leaks → 2% APRwhile PAR mode is active2%8%~7d ½-lifetime →
Whichever output isn't holding the baton doesn't freeze — it leaks back toward its bias on a ~7-day half-life. So after a stress event ends and the baton returns to RATE, a par that was pushed off $1.00 drifts home over weeks; and while par holds the baton, the rate relaxes toward its 2% bias.

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.

bootstrap · controller frozenday 14controller actsrateheld @ 2%parheld @ $1.00time (days) →
For the first 14 days the controller is frozen — 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 getterMeaning
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.