Skip to main content

Redemption mechanism

Multi-branch RD-for-collateral exchange at par, modulo a small per-branch fee. This page covers the on-chain flow in full. User view is Redemption.

This describes normal-mode redemption. Shutdown (of a branch or the whole system) uses a different mechanism and pricing schedule, see Shutdown redemption.

Entry point​

Aggregator.redeemCollateral(
uint256 _RDamount,
address[] _firstRedemptionHint, // one per branch
address[] _upperPartialRedemptionHint, // one per branch
address[] _lowerPartialRedemptionHint, // one per branch
uint256[] _partialRedemptionHintNICR, // one per branch
uint256 _maxIterations // gas cap per branch
)

All four hint arrays must equal troveManagers.length. Pass address(0) and 0 for slots corresponding to shutdown / zero-weight branches; the Aggregator skips them.

Step-by-step​

Aggregator.redeemCollateral(…)anyone can call · reverts during the 14-day bootstrapRefill the redemption quotathe bucket refills for elapsed time, up to the live capSnapshot parrateParControl.par() — one par for every branchCompute basket weightsweight = base debt EMA · shutdown & Redemption-Shield-only → 0For every branch with weight > 0slice = RD × weight ÷ Σ weightssink branch active? each slice is trimmed (see below)Redemptions.redeemCollateral(slice, hints, fee, par)the slice walks that branch’s book — next figurereverts if any branchcan be shutdownAnything redeemed, and within the quota?noRevertyesDeduct the redeemed total from the quotaheavy redemption throttles itselfIf a bad-debt sink branch existsfee + haircut RD → payDebt(…) on the sick branchRedeemer receives a basket of collateral
One call, every branch. The Aggregator snapshots par once, splits the request by basket weight, and lets each branch walk its own book (next section). The quota checks come after the walks, and a revert anywhere reverts everything — there is no partial fulfillment.

A revert in any branch reverts the whole transaction.

Basket weights​

base debt EMABASKET WEIGHTWETH→44.2%wstETH→16.8%WBTC→14.7%rETH→9.5%weETH→5.3%tBTC→weight 0 · Redemption Shield enabled for all trovessUSDS→9.5%PAXG→weight 0 · branch in shutdown
Each branch's weight is its base debt EMA (72-hour half-life), and the weights are normalized across the branches that qualify. A branch in shutdown, or one whose troves all have Redemption Shield enabled, has weight 0 and is skipped entirely. Values are illustrative.

The EMA has 72-hour half-life. Using an EMA rather than spot debt makes the weights smoothly responsive, a sudden borrowing surge on one branch doesn't immediately redirect all redemption pressure there.

A branch with only troves with Redemption Shield enabled has weight 0. A branch in shutdown has weight 0. Both are excluded from normal-mode redemption, they have their own paths (shutdown redemption goes through Redemptions directly).

Per-branch walk​

Inside each branch, Redemptions.redeemCollateral walks the base SortedTroves list in ascending ICR order:

One branch’s slice arrivesRedemptions.redeemCollateral(slice, hints, fee, par)Walk to the first redeemable trove_firstRedemptionHint gives the starting guessbelow-MCR troves and mini-dwarfs are skippedRD left to redeem, under the iteration cap?no — walk endsyesSlice ≥ the trove’s entire net debt?yes — fullno — partialFull redemption — the trove closesdebt repaid at par · collateral drawn200 RD gas comp refunded to the borrowersurplus collateral → CollSurplusPoolPartial redemption — the walk endscapped: the trove keeps ≥ 1,800 RD net debtreinserted into the list at the hinted positiona partial always ends the branch walknext troveCollateral (minus fee) → the redeemerredeemed RD is burned · the fee stays in the trovethe branch reports its redeemed total to the Aggregator
Inside one branch, the slice walks the base book from the lowest ICR up. A full redemption closes the trove and the walk continues; the first partial redemption ends it — capped so the trove is never left below the 1,800 RD minimum net debt. Try it live in the interactive walk below.
0 RDRedeemed Amount
0 RD
min netdebt 1,8004,000108%skip: below MCR5,000112%partial9,000118%untouched≈0121%skip: gas-only4,000126%untouched12,000140%untouched6,000160%untouched← lowest ICRredemption walks low → high ICRhigher ICR →
Requested 0 RD → redeemed 0 (0 closed, 1 partial).
One branch's base book, ordered by ICR. Redemption consumes debt from the lowest-ICR trove upward. Full redemptions close the trove (gas reserve refunded, surplus collateral to the `CollSurplusPool`) and the walk continues; the first partial redemption ends the walk. A partial can never leave a trove below MIN_NET_DEBT (1,800) — if the amount would breach that floor, redemption stops at the floor and the red slice is left unredeemed. Below-MCR and gas-only troves are skipped. Values are illustrative.

Skipped troves​

  • Below MCR. Already in default territory; redemption can't profitably claim them.
  • Mini-dwarfs. Sub-floor troves are swept rather than walked.
  • Oracle-failure pricing. If the branch's price feed is in fallback mode, redemption uses the shutdown pricing schedule instead. See Shutdown redemption.

Troves with Redemption Shield enabled​

In normal mode, the Redemption Shield cohort SortedTroves is never walked, regardless of the base list's state. An empty base list does not redirect redemption into the Redemption Shield cohort book, it simply means the branch contributes nothing to a normal-mode redemption (its basket weight is 0). Only branch shutdown ever routes redemption through troves with Redemption Shield enabled.

In shutdown mode, the protocol can walk both books at the shutdown pricing multiplier.

Putting it together: multi-branch redemption​

The basket split and the per-branch walk, together. A requested redemption is divided across active branches in proportion to their base debt EMA, and each slice then walks that branch's own book from the lowest ICR up. Drag the requested amount, or raise a branch's EMA to pull more of the redemption onto it — and watch a thin branch's slice bottom out against the 1,800 RD floor.

Note: this demo shows only 3 branches, not the full 8 the production protocol has.

18,000 RD
splits across branches by debt-EMA weight → total redeemed 18,000 RD
WETHdebt EMA 24,000 → weight 47% · allocated 8,471 → redeemed 8,471
min 1,8004,000108%skip: below MCR5,000113%closed9,000124%partial6,000140%untouched← lowest ICRhigher ICR →
WBTCdebt EMA 15,000 → weight 29% · allocated 5,294 → redeemed 5,294
min 1,8003,000111%closed7,000128%partial5,000155%untouched← lowest ICRhigher ICR →
sUSDSdebt EMA 12,000 → weight 24% · allocated 4,235 → redeemed 4,235
min 1,8004,000110%closed8,000132%partial← lowest ICRhigher ICR →
A redemption is global. Each slider independently sets a branch's raw debt EMA (72-hour half-life, in RD) — move one and the others stay put. The redemption weight a branch gets is its EMA over the current total (so the weights sum to 100%), and that split determines how much RD hits each branch. Raising one branch's EMA pulls more of the redemption onto it. Each slice then walks that branch's base book from the lowest ICR up — full redemptions close a trove and continue, the first partial ends that branch's walk, and no trove is left below the 1,800 RD floor. Troves with Redemption Shield enabled are never on these lists. Because the EMA is a 72-hour-half-life average, a branch's debt EMA can differ from the current sum of the troves without Redemption Shield shown here. Values are illustrative.

Fee accounting​

Redemption feecollateral leg · 0.5–2.5%normalif a branch is under-collateralizedKept by the redeemed trovestays as residual collateralpost-redemption ICR risesRepairs the bad-debt branchconverted to RD → payDebt(...)autonomous solvency repair
The redemption fee is taken in collateral, not RD. Normally it is retained by the redeemed trove as residual collateral, which nudges that borrower's ICR up. The only exception: while a branch is under-collateralized, the fee is converted to RD and used to repair that bad debt. It never reaches FEE stakers or the protocol.
Gross collateral drawnredeemed RD ÷ oracle price × par100%−Redemption feegross × fee — stays in the trove2.5%=Collateral to the redeemergross − fee97.5%
Every redeemed RD retires debt at par; the collateral drawn against it is priced by the branch oracle. The fee is charged on the collateral leg and never leaves the trove. Shown with an illustrative 2.5% fee — the live per-branch fee ranges 0.5–2.5%.

RedemptionLot.collateralFee is retained by the redeemed trove. The trove's debt decreases by the full redeemed_rd (paid down at par), but its collateral decreases by only collateral_to_redeemer, the fee portion stays inside the trove as residual collateral, increasing its post-redemption ICR. The fee does not flow to the FeeRouter and does not reach FEE stakers.

The redeemer never pays an RD fee; every RD they send is credited against borrower debt at par. The cost is in the collateral leg, and that cost is captured by the redeemed borrower, not the protocol.

The one exception is when a bad-debt sink branch is active. In that case the fee on healthy-branch redemptions is converted into RD and routed to the sink branch's payDebt(...) rather than retained by the healthy redeemed trove, see "Fee accounting with bad debt" below.

Fee accounting with bad debt​

If any branch is under-collateralized (its TCR has dropped below 100%, total collateral worth less than total debt), the Aggregator designates it as the currentUnderCollateralizedTM and routes redemption differently:

  • Healthy-branch allocations are reduced by (1 − redemption_fee) × (1 − BAD_DEBT_REDEEM_FRAC) where BAD_DEBT_REDEEM_FRAC = 0.5%.
  • The fee that would have been paid in collateral by the healthy branches is converted into RD and routed to the sink branch's payDebt(...) to chip away at its bad debt.
  • The bad-debt-haircut slice (another BAD_DEBT_REDEEM_FRAC) is also routed to the sink branch.
Normal redemption — 1,000 RD975 in collateral975 → the redeemer, in collateral (gross − 2.5% fee)25 fee → stays in the redeemed trove as extra collateralBad-debt sink branch active — 1,000 RD≈970 in collateral≈970 → the redeemer, in collateral (the collateral fee is waived)25 fee + 5 haircut → the sink branch as RD, via payDebt(…)every healthy-branch slice is trimmed ↓
The same 1,000 RD redemption, with and without a sick branch. Normally the fee is collateral that stays in the redeemed trove. While any branch is under-collateralized, the fee is collected in RD instead and routed to that branch's payDebt(…), along with an extra 0.5% haircut — the redeemer gives up ~0.5% of output and every redemption repairs the sick branch. Fee shown at an illustrative 2.5%.

In other words: when a branch is sick, every redemption on healthy branches surrenders a small slice of value to repair the sick branch's debt. Redeemers still get collateral; the sink branch gets RD-denominated relief. This keeps all branches solvent without requiring an admin top-up.

When no sink branch exists, the redemption fee is retained by the redeemed trove (see "Fee accounting" above).

Redemption quota​

The Aggregator maintains a token-bucket-style quota. The bucket refills over time up to the current cap (a function of system debt and recent redemption activity), so heavy redemption activity throttles itself.

Read live capacity from Aggregator.redemptionQuotaAvailable():

function redemptionQuotaAvailable() external view returns (
uint256 available,
uint256 redemptionCapacityNow
);

A redemption that would exceed the quota reverts. There's no partial fulfillment at the Aggregator level.

The bucket in motion — refill at REDEMPTION_PCT_PER_DAY = 4% of the global debt EMA per day, pooled up to a 6-hour burst reserve (1% of system debt redeemable instantly). Throw a redemption demand shock at it and drag across the timeline:

Hints​

Each branch needs three hints to insert the partially-redeemed final trove cheaply:

HintWhat it tells the branch
_firstRedemptionHint[i]A trove near (or above) the front of the base list. The walk advances from there to the actual lowest-ICR trove.
_upperPartialRedemptionHint[i]The trove immediately above the post-walk insertion point.
_lowerPartialRedemptionHint[i]The trove immediately below the post-walk insertion point.
_partialRedemptionHintNICR[i]Expected NICR of the final partially-redeemed trove. Mismatch → degraded walk.

The GlobalHintHelper contract computes hints from on-chain state. Front ends and aggregator integrations almost always call it.

If hints are stale (someone else's tx landed first and changed the sorted list), the walk degrades to O(n) on that branch, same final result, more gas.

Two reinsert modes​

The post-walk reinsertion has three modes (mode = 0 / 1 / 2):

  • 0 = regular, normal-mode walk, insert in base sorted list at hinted position.
  • 1 = shutdown_end_reinsert, used during shutdown; insert at the end of the relevant book.
  • 2 = shutdown_local_reinsert, used during shutdown when the trove ends up needing local reinsert.

These are details for redemption batching efficiency in shutdown. The normal-mode user only needs mode 0.

Gas​

Rough redemption gas budget:

  • Aggregator overhead: a few tens of thousands of gas (basket weights, quota refill).
  • Per branch walked: ~50k–80k gas for setup, plus ~30k per trove touched in the walk.
  • Final partial-reinsert: ~80k gas.

_maxIterations is a per-branch cap; setting it too low can leave RD unredeemed in that branch (the Aggregator then reverts with Aggregator: redeemed amount must be less than or equal to quota, depending on what landed).

Deep dive​