Settlement#

What happens when a market matures, and how late repayments can still improve lender outcomes.

Overview#

  1. The market reaches maturity.
  2. A 5-minute grace period starts.
  3. The first grace-eligible withdrawal or borrower force-close sets the settlement factor.
  4. Lenders withdraw based on that factor.
  5. 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.

LenderExpectedSettlement (75%)
Alice540,000405,000
Bob324,000243,000
Carol216,000162,000
Total1,080,000810,000

Grace Period#

The protocol enforces a 5-minute grace period after maturity.

ActionDuring Grace Period
DepositNot allowed
BorrowNot allowed
RepayAllowed
WithdrawNot allowed
Force Close PositionNot allowed
Re-settleNot 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 (NotSettled if not).
  • New factor must be strictly higher (SettlementNotImproved otherwise).
  • Anyone can call it (permissionless).
  • Factor can only improve; it never decreases.

Example#

StepVault / OwedFactor
Initial settlement810 / 1,08075%
Late repayments arrive1,080 / 1,080100%
Re-settle calledrecomputed100%

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 Withdraw call)

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:

  1. Per-position state: haircut_owed and withdrawal_sf on the lender's position record the exact unpaid amount and the settlement factor at which the withdrawal occurred.
  2. Market-level accumulator: haircut_accumulator on 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.
  3. HaircutState account: A separate PDA ([SEED_HAIRCUT_STATE, market]) stores claim_weight_sum and claim_offset_sum — a conservative linearised aggregate of all outstanding haircut claims. This is what re_settle uses 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_settle uses the HaircutState aggregate (not the accumulator) so borrower repayments immediately improve the settlement factor for everyone.
  • withdraw_excess / collect_fees still treat haircut_accumulator as reserved value that cannot be swept out of the vault.
  • claim_haircut pays 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 balance
  • R = normalized claim of lenders still in the market (scaled_total_supply * scale_factor / WAD)
  • W = claim_weight_sum from HaircutState (conservative upper-bound weight of prior withdrawers' claims)
  • O = claim_offset_sum from 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#

  1. A lender withdraws at a distressed settlement factor (e.g., 75%) and receives 75% of their entitlement.
  2. The 25% gap is recorded as haircut_owed on the lender's position, anchored at withdrawal_sf = 0.75.
  3. The borrower later repays more, and someone calls re_settle, improving the factor to (e.g.) 90%.
  4. The lender calls claim_haircut. The claimable amount is proportional to the SF improvement:
    claimable = haircut_owed * (current_sf - withdrawal_sf) / (WAD - withdrawal_sf)
    
    In this example: claimable = gap * (0.90 - 0.75) / (1.0 - 0.75) = 60% of the original gap.
  5. The claim is capped at the vault surplus above remaining-lender obligations (defense-in-depth).
  6. 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_haircut on 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#

StepSettlement FactorLender's Haircut OwedAction
Withdraw at distress75%250,000 USDCReceives 750,000 of 1,000,000 owed
Borrower repays more
Re-settle90%250,000 USDCFactor improves
Claim haircut90%100,000 USDCRecovers 150,000 (60% of gap)
Borrower repays fully
Re-settle100%100,000 USDCFactor reaches 100%
Claim haircut100%0 USDCRecovers 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"
}