Settlement#
What happens when a market matures, and how late repayments can still improve lender outcomes.
Overview#
- The market reaches maturity.
- A 5-minute grace period starts.
- The first grace-eligible withdrawal or borrower force-close sets the settlement factor.
- Lenders withdraw based on that factor.
- If more funds arrive later, anyone can call re-settlement to improve the factor.
Settlement Factor#
The settlement factor is the payout ratio applied to all lenders.
At settlement time, the program computes:
expected_value = scaled_total_supply * scale_factor / WAD
available_for_lenders = vault_balance
raw_factor = available_for_lenders * WAD / expected_value
settlement_factor = clamp(raw_factor, 1, WAD)
Lender claims are senior to protocol fees. The full vault balance is used — fees are not reserved or subtracted from the settlement computation. Fee collection is separately guarded and only permitted when the market is fully solvent.
If expected_value = 0, the program sets settlement_factor = WAD.
See Protocol Calculations for details.
Pro-Rata Distribution#
All lenders share the same factor.
| Lender | Expected | Settlement (75%) |
|---|---|---|
| Alice | 540,000 | 405,000 |
| Bob | 324,000 | 243,000 |
| Carol | 216,000 | 162,000 |
| Total | 1,080,000 | 810,000 |
Grace Period#
The protocol enforces a 5-minute grace period after maturity.
| Action | During Grace Period |
|---|---|
| Deposit | Not allowed |
| Borrow | Not allowed |
| Repay | Allowed |
| Withdraw | Not allowed |
| Force Close Position | Not allowed |
| Re-settle | Not allowed |
If a settlement-triggering withdrawal or force-close is attempted during grace, the transaction fails with SettlementGracePeriod.
Withdrawal Slippage Protection#
Withdraw includes an optional min_payout parameter. If the computed payout is below min_payout, the transaction fails with PayoutBelowMinimum (ERR-42).
Use this to protect against receiving less than expected when settlement is distressed or when the vault changes between quote and execution.
Re-Settlement#
Re-settlement recalculates the settlement factor after late repayments.
Why It Exists#
Initial settlement uses vault funds available at the first settlement-triggering withdrawal or force-close. If the borrower repays more later, re-settlement improves the factor for both remaining lenders and lenders who already withdrew at a loss. Remaining lenders benefit directly on their next withdrawal; prior withdrawers can recover the difference via claim_haircut.
Rules#
- Market must already be settled (
NotSettledif not). - New factor must be strictly higher (
SettlementNotImprovedotherwise). - Anyone can call it (permissionless).
- Factor can only improve; it never decreases.
Example#
| Step | Vault / Owed | Factor |
|---|---|---|
| Initial settlement | 810 / 1,080 | 75% |
| Late repayments arrive | 1,080 / 1,080 | 100% |
| Re-settle called | recomputed | 100% |
Lenders who withdraw after re-settlement use the improved factor. Lenders who already withdrew at a loss can recover the difference via claim_haircut (see Haircut Recovery below).
Force-Closing Abandoned Positions#
After maturity + grace period, the borrower can call ForceClosePosition to clear lender positions that haven't been voluntarily withdrawn. This is necessary when:
- A lender deposited a dust amount and never withdraws
- A lender lost access to their wallet
- A lender was externally blacklisted (blocking their
Withdrawcall)
Force-close computes the lender's payout using the same formula as Withdraw and transfers it to the lender's token account. The position is then zeroed and scaled_total_supply is decremented, allowing WithdrawExcess to eventually succeed.
If the settlement factor is below 100% (distressed market), the gap between the lender's entitlement and their actual payout is tracked in the market's haircut accumulator.
Haircut Tracking#
When lenders withdraw during market distress (settlement factor below 100%), they receive less than their full entitlement. The difference — the "haircut gap" — is tracked in two complementary layers:
- Per-position state:
haircut_owedandwithdrawal_sfon the lender's position record the exact unpaid amount and the settlement factor at which the withdrawal occurred. - Market-level accumulator:
haircut_accumulatoron the Market account stores the sum of all still-unpaid haircut amounts, preventing the borrower or fee sweeps from draining tokens reserved for haircutted lenders. - HaircutState account: A separate PDA (
[SEED_HAIRCUT_STATE, market]) storesclaim_weight_sumandclaim_offset_sum— a conservative linearised aggregate of all outstanding haircut claims. This is whatre_settleuses to compute the new settlement factor.
Why This Design#
The old approach subtracted haircut_accumulator from the vault when computing the re-settlement factor. That made late borrower repayments invisible to settlement improvement — the accumulator grew on every distressed withdrawal and blocked the factor from rising even when new funds arrived.
The new design separates concerns:
re_settleuses the HaircutState aggregate (not the accumulator) so borrower repayments immediately improve the settlement factor for everyone.withdraw_excess/collect_feesstill treathaircut_accumulatoras reserved value that cannot be swept out of the vault.claim_haircutpays the exact per-position recovery and decrements both the accumulator and the HaircutState aggregate.
Re-Settlement Formula#
new_sf = WAD * (V + O) / (R + W)
Where:
V= current vault balanceR= normalized claim of lenders still in the market (scaled_total_supply * scale_factor / WAD)W=claim_weight_sumfrom HaircutState (conservative upper-bound weight of prior withdrawers' claims)O=claim_offset_sumfrom HaircutState (offset term for prior withdrawers)
The formula solves for the highest settlement factor where vault >= remaining_lender_obligations + prior_withdrawer_claims. It is idempotent — calling re_settle twice with identical state produces the same factor.
Haircut Recovery#
Lenders who withdrew at a loss are not permanently locked out. When the settlement factor improves (via re_settle after late borrower repayments), they can recover the difference.
How It Works#
- A lender withdraws at a distressed settlement factor (e.g., 75%) and receives 75% of their entitlement.
- The 25% gap is recorded as
haircut_owedon the lender's position, anchored atwithdrawal_sf = 0.75. - The borrower later repays more, and someone calls
re_settle, improving the factor to (e.g.) 90%. - The lender calls
claim_haircut. The claimable amount is proportional to the SF improvement:
In this example:claimable = haircut_owed * (current_sf - withdrawal_sf) / (WAD - withdrawal_sf)claimable = gap * (0.90 - 0.75) / (1.0 - 0.75) = 60% of the original gap. - The claim is capped at the vault surplus above remaining-lender obligations (defense-in-depth).
- Any unpaid remainder stays on the position, re-anchored at the new SF, and can be claimed if the factor improves further.
Claim Rules#
- Lender must sign (or borrower can call
force_claim_haircuton their behalf). - Market must be settled (
settlement_factor_wad > 0). - Current SF must be strictly higher than the position's
withdrawal_sf. - Payout is capped at vault surplus above remaining-lender obligations.
- No blacklist check — tokens are already owed, not a new financial action.
- The position's HaircutState contribution is removed before payout and reinserted (if remainder exists) at the new anchor.
Example#
| Step | Settlement Factor | Lender's Haircut Owed | Action |
|---|---|---|---|
| Withdraw at distress | 75% | 250,000 USDC | Receives 750,000 of 1,000,000 owed |
| Borrower repays more | — | — | — |
| Re-settle | 90% | 250,000 USDC | Factor improves |
| Claim haircut | 90% | 100,000 USDC | Recovers 150,000 (60% of gap) |
| Borrower repays fully | — | — | — |
| Re-settle | 100% | 100,000 USDC | Factor reaches 100% |
| Claim haircut | 100% | 0 USDC | Recovers remaining 100,000 |
Viewing the Factor#
On-chain#
const market = decodeMarket(marketAccount.data);
const factorPercent = Number(market.settlementFactorWad) / 1e16;
console.log('Settlement Factor:', factorPercent, '%');
API#
GET /api/markets/:address
{
"success": true,
"data": {
"market": {
"settlementFactorWad": "750000000000000000"
}
},
"requestId": "f5fd7f52-7ec2-4f3e-85c2-2f17f47ebf4e"
}