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
A revert in any branch reverts the whole transaction.
Basket weights
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:
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.
Fee accounting
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)whereBAD_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.
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:
| Hint | What 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
- Redemption (user view): same content, less detail.
- Shutdown redemption and Collateral shutdown: the off-the-normal-path version.