Redemption mechanism
Cross-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.
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
1. _requireAfterBootstrapPeriod() // 14 days from deployment
2. _updateRedemptionQuota() // refill bucket by elapsed time
3. par = rateParControl.par() // snapshot par
4. (weights, sumWeights) = _basketWeights()
weights[i] = unshieldedDebtEma[branch_i] // shutdown → 0
// no unshielded debt → 0
5. for each branch i with weights[i] > 0:
base_i = _RDamount * weights[i] / sumWeights
if bad_debt_sink_branch_exists:
base_i *= (1 - redemptionRate[i]) * (1 - BAD_DEBT_REDEEM_FRAC)
redeemed_i = Redemptions[i].redeemCollateral(
msg.sender,
base_i,
hints_i,
underC ? 0 : redemptionRate[i], // fee waived if bad-debt
par
)
totalRedeemed += redeemed_i
6. require(totalRedeemed > 0)
7. require(totalRedeemed <= _redemptionQuota.tokens)
8. _redemptionQuota.tokens -= totalRedeemed
9. if bad_debt_sink_branch_exists: pay_debt_to(sink_branch, feeDebtRequested + badDebtRequested)
A revert in any branch reverts the whole transaction.
Basket weights
weights[i] = unshieldedDebtEma[troveManagers[i]].ema
sumWeights = Σ weights[i] across non-shutdown branches with unshielded debt > 0
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 shielded troves 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 unshielded SortedTroves list in ascending ICR order:
1. find first redeemable trove (uses _firstRedemptionHint as a starting guess)
2. while remaining_rd > 0 and below max_iterations:
trove = current
redeemableFromTrove = min(remaining_rd, trove.netDebt)
coll = redeemableFromTrove / oracle_price * par
fee = coll * redemptionRate
trove.debt -= redeemableFromTrove
trove.coll -= coll
send coll-fee to redeemer
burn redeemableFromTrove RD
if trove.debt ≤ MIN_NET_DEBT + gas_comp:
full-redemption path: close trove, refund 200 RD gas comp,
surplus collateral → CollSurplusPool
remove from sorted list
else:
partial-redemption: reinsert using upper/lower hint
advance to next trove
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.
Shielded book
In normal mode, the shielded SortedTroves is not walked. The unshielded list must be empty for shielded troves to be touched (and at that point you're effectively in a shutdown-like state for that branch).
In shutdown mode, the protocol can walk both books at the shutdown pricing multiplier.
Bad-debt sink branch
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.
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" below).
Redemption quota
RedemptionQuota { uint104 tokens; uint48 lastTs; uint104 lastCap; }
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.
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 unshielded 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 unshielded 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.
Fee accounting
gross_collateral_drawn = (redeemed_rd / oracle_price) × par
fee_in_collateral = gross_collateral_drawn × redemption_fee
collateral_to_redeemer = gross_collateral_drawn − fee_in_collateral
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 "Bad-debt sink branch" above.
Gas
Cross-branch redemption is gas-heavier than a Liquity v1 redemption. Rough 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 Shutdown machinery: the off-the-normal-path version.