Skip to main content
This page covers how a redemption is gated after the bond has matured. For the bond minting and 730-day maturity rules, see Claim Bonds.
BondVault enforces a per-epoch redemption throttle so that no single epoch can drain the vault’s LUMINA reserve in one block, even if every bond holder in that epoch tries to redeem simultaneously. The mechanism is a fixed weekly cap (MAX_REDEMPTION_PER_EPOCH_BPS = 108) plus a FIFO queue for the overflow.

The number

ConstantValue
MAX_REDEMPTION_PER_EPOCH_BPS108 (1.08% per epoch / per week)
Epoch length7 days (one week per epoch)
Worst-case drain time for a full epoch≈ 12 weeks (12 × 1.08% ≈ 12.96%)
A single epoch’s bond supply can therefore be drained, in the worst case, across about 12 consecutive weeks — never instantly. The 1.08%/week ceiling is the entire throttle: there’s no separate per-wallet or per-block cap layered on top.

What it actually limits

The throttle limits the LUMINA paid out per epoch per week, expressed as a fraction of that epoch’s total minted face. Concretely, for an epoch with S units of face value minted:
maxRedeemableThisWeek(epoch) = S × 108 / 10_000   // 1.08% of the epoch's face
Bonds redeemed at maturity within this allowance are paid immediately in LUMINA. Bonds beyond the allowance are routed to the FIFO queue.

FIFO queue — burn-now, deliver-later

When a redemption request exceeds the weekly throttle:
  1. The bond tokens are burned at queue time (they leave the user’s wallet immediately — there is no “pending bond” balance to double-spend).
  2. A queue entry is created recording the holder, the USD amount, and the position in the FIFO line.
  3. When processQueue() is called in a later epoch with available weekly allowance, the queue is drained in first-in-first-out order and LUMINA is delivered to the recorded holder addresses.
processQueue() is public — anyone can call it (a keeper, the holder themselves, or an unrelated wallet). The function pays out as many queue entries as the current week’s remaining allowance permits, then stops. In practice, a keeper bot calls it once per epoch.

Why it exists

Two failure modes that the throttle defends against:
  1. Bond-run. Without the throttle, a single big epoch (e.g. a correlated mass-trigger across all 6 shields during a crash) could let bond holders redeem every token at once, dumping the entire LUMINA reserve onto the open market in one block. The throttle spreads that sell pressure over weeks.
  2. Death spiral. If a flash drop both triggers bonds (good) and simultaneously drops the LUMINA/USD reference (bad), unbounded redemption would mint disproportionately more LUMINA per bond, which in turn pushes LUMINA down further, which mints even more on the next redemption, and so on. The throttle slows this feedback loop enough for the buyback / TWAP burn machinery to catch up.

Practical effects for agents

  • Small redemptions are immediate. If you’re redeeming 1% of an epoch’s face or less and you’re early in the week, you get paid in the same transaction.
  • Big redemptions are queued. You’ll receive a RedemptionQueued event instead of BondRedeemed. Your bonds are gone; LUMINA arrives later via RedemptionProcessed events.
  • Listen, don’t poll. Use webhooks (bond_redeemed, redemption_processed) instead of polling BondVault state — the exact epoch when your queue entry pays is data-dependent.
  • Anyone can processQueue(). If you’re tired of waiting and the current epoch has unused allowance, you can call processQueue() yourself.

Reading throttle state

# Total face minted in epoch N:
cast call $BOND_VAULT 'epochSupply(uint256)(uint256)' $EPOCH --rpc-url $BASE_MAINNET_RPC

# Remaining redemption allowance this week for epoch N:
cast call $BOND_VAULT 'remainingAllowance(uint256)(uint256)' $EPOCH --rpc-url $BASE_MAINNET_RPC

# Queue length for epoch N:
cast call $BOND_VAULT 'queueLength(uint256)(uint256)' $EPOCH --rpc-url $BASE_MAINNET_RPC

Worked example

An epoch mints 1,000,000 units of face ($1M notional).
WeekMax redeemableCumulative
110,800 units1.08%
210,8002.16%
310,8003.24%
1210,80012.96%
After 12 weeks, ≈ 13% of the epoch’s face has been redeemed in the worst case. Anything beyond that continues at 1.08%/week into subsequent epochs. The protocol does not promise any redemption schedule faster than this.

Check Redemption Status Programmatically

The on-chain getters above (epochSupply, remainingAllowance, queueLength) tell you the aggregate state of an epoch. To answer the holder-centric question — “will my specific redemption clear now or get queued, and if queued, when?” — use the SDK, which composes those calls with the new throttle-dynamic views (availableCapacityRatioBps, policiesPaused) into a single response. Requires @lumina-org/sdk v0.7.0+.
import { BondQueue, bondVaultAbi } from '@lumina-org/sdk';
import { ethers } from 'ethers';

// getRedemptionStatus lives on the BondQueue helper (NOT lumina.bonds).
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const bv = new ethers.Contract(bondVaultAddress, bondVaultAbi, provider);
const queue = new BondQueue(bv);

const status = await queue.getRedemptionStatus(
  holderAddress,
  epochIdBond,
  usdAmount,
);
console.log(status);
// {
//   status: 'queued',
//   estimatedReleaseDate: '2026-08-15T00:00:00.000Z',
//   queuePosition: 47,
//   throttleInfo: {
//     weeklyCapBps: 108,
//     weeklyCapUsd: '756000',
//     usedThisEpochUsd: '...',
//     remainingThisEpochUsd: '...',
//     nextEpochStart: '...'
//   },
//   policiesPaused: false,
//   availableCapacityBps: 8500
// }
The status field is one of:
  • immediate — the entire usdAmount fits inside the current week’s remaining allowance; calling redeem() now pays out LUMINA in the same transaction.
  • queued — the request exceeds remaining allowance; bonds will be burned and a FIFO entry created. Use estimatedReleaseDate and queuePosition to set holder expectations.
  • partial — part of usdAmount fits in the current week, the rest queues. The SDK splits the response so you can show both halves in the UI.
Bonds are tracked per (holder, epochIdBond) pair — not per individual bondId — because Claim Bonds are ERC-1155 fungible within an epoch. Two holders holding the same epoch share one supply curve but are queued independently.

Auto-injection Mechanism

When BondVault’s available-capacity ratio drops to 50% or below, BondVault automatically pulls 10% of the CEX Reserve’s LUMINA balance into the vault on the next redeem, issue, or processQueue call. This widens the safety margin without requiring a manual rebalance. The injection is bounded by the actual CEX Reserve balance, so once that’s depleted the mechanism becomes a no-op until ops refills the reserve. This is on-chain only — no off-chain keeper required. Every state-changing call that touches BondVault evaluates the trigger, emitting AutoInjectionTriggered(uint256 amountInjected, uint256 newCapacityBps) when it fires.

Floor-Price Soft Pause

If the LUMINA spot price drops at-or-below $0.005, BondVault flips policiesPaused = true. This is a signal that the SDK and frontend should consume to refuse new policy purchases — the contract does not enforce it inside CoverRouterV2, so any integration that bypasses the SDK can still submit purchases on-chain. Recovery requires the price to rise back to 120% of floor ($0.006) — the hysteresis band avoids flapping at the boundary. The SDK exposes this via status.policiesPaused (see the snippet above), and any front-end calling getRedemptionStatus for UX checks gets the flag for free.
  • FIX C-3 + H-6 — solvency floor and LUMINA/USD snapshot alignment; the throttle does not override these — it sits on top of them.
  • FIX M-11BondVault.burnFromReserves 5%-per-tx burn cap (the reserve guard the throttle complements).
  • Sprint Fix Audit Economic — the availableCapacityRatioBps() / policiesPaused() views, the auto-injection trigger, and the SDK getRedemptionStatus() composition described above.

See also