Staging, 2026-09-17: the USD market went INSOLVENT and every later supply was refused with MARKET_INSOLVENT, which the app renders as "This market is frozen."
Cause: USD was opened with no house capital. A customer supplied 200,
borrowed 105 and repaid 105 on max. The 0.001105947158663302 of accrued
interest left over fell inside max_repay_residual_forgive and was written
off by writeOffDust, which takes the residual off total_borrows with no
money arriving to replace it. A forgiveness is spent protocol equity and
nothing else, and the pool's entire buffer was 0.000165892074222496 -- its
reserve cut. protocol_equity_free closed at -0.000940055084440806 and
closeMarketIfInsolvent shut the market in the same transaction. USDC
survived three write-offs over the same period only because an operator had
injected 1 USDC of house capital into it.
Three changes, and the second is the one that actually closes the hole:
-
The USD thresholds.
max_repay_residual_forgivegoes to zero -- a residual of a cent or more is money the customer can pay and is not written off on their behalf -- anddebt_dustgoes to one transferable unit. Both are needed: zeroing the cap alone strands customers, becauserequestRepayfloors amaxto the market's decimals and refuses the resulting 0.00 with REPAY_EXCEEDS_DEBT, and its own comment names the debt-dust path as the way out. -
An activation gate. A market must hold max(forgive cap, debt dust) of free equity before it may enter ACTIVE. No exemption for an empty book: CAPITAL_INJECTION has an empty row in the status matrix, so the house funds a FROZEN market and the real order is preflight, inject, activate. This is what would have refused USD on 2026-09-16.
-
A per-forgiveness check.
BorrowService.equityAbsorbsre-asks the same question where the money actually leaves, so a pool that opened one forgiveness deep cannot pay for a second. It covers MAX_REPAY_RESIDUAL only; DUST must never be refused, or the sub-unit residual it exists to clear is stranded instead.
Settlement now reads max(quote snapshot, live settings) for both
thresholds. The snapshot is a floor under what the customer was promised,
not a ceiling over what they may receive: a threshold moved against them is
held at the quote, one moved in their favour applies at once. Reading
either alone was wrong in one direction -- live settings broke the promise
requestRepay documents, and the snapshot alone made a repay quoted before
this migration miss the raised dust floor and strand.
Known limit, deliberate: a pool that has already spent its buffer can still be pushed negative by an unrefusable DUST write-off. INSOLVENT is the correct verdict in that state rather than a defect. Holding the buffer across every equity-decreasing path needs a reserved-equity concept in EquityService and is left to its own change.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com