Solvency Check Bypass / Accounting Desynchronization

  • Name: Solvency check bypass / accounting desynchronization
  • Category: exploit pattern / lending-accounting failure / bad-debt creation / liquidation-path abuse
  • Summary: These attacks break lending systems by finding a state-changing path that mutates collateral, reserves, share balances, or debt without preserving the same solvency invariants the main borrow / withdraw / transfer paths enforce. The attacker first builds leverage inside the protocol’s normal rules, then uses the unchecked accounting path to desynchronize assets from liabilities, intentionally create bad debt, and finally monetize that mismatch through liquidation, withdrawal, redemption, or later borrowing against a ledger that no longer describes real liabilities.
  • Quick routing rule: Euler is the unchecked reserve-donation / self-liquidation exemplar, Platypus the stale pre-withdraw emergency-exit exemplar, Abracadabra Cauldron V4 the aggregate debt-ledger corruption exemplar, and Abracadabra GMX V2 the phantom-collateral liquidation exemplar. Stay here when the main question is which balance-sheet mutation escaped the canonical post-state solvency boundary?
  • What it is:
    • A lending protocol tracks collateral and liabilities through internal accounting tokens or balances.
    • Most user-facing actions are expected to preserve a health-factor or liquidity invariant after they execute.
    • One secondary path — such as reserve donation, fee skim, share burn, internal transfer, or bookkeeping helper — updates part of the balance sheet without re-running the same invariant checks.
    • The attacker uses that path to reduce realizable collateral, increase liabilities, or otherwise break the protocol’s assumptions about account health.
    • A second module, usually liquidation or redemption, then converts the accounting mismatch into real profit.
  • Typical preconditions:
    • The protocol allows large leverage before the bug is triggered, often through same-asset looping or aggressive collateral factors.
    • At least one state transition can materially change collateral, reserves, or debt without a post-action solvency check.
    • Liquidation, withdrawal, or redemption logic assumes the broken state cannot arise except in narrower benign cases.
    • The attacker can either self-liquidate through a controlled second account or otherwise capture the value released by the broken state.
    • There is enough pool liquidity left for discounted collateral or withdrawal rights to cash out into real assets.
  • Common vulnerable surfaces:
    • reserve-donation functions that burn or move collateral without checking borrower health
    • accounting helpers that update one side of the balance sheet but not the other
    • internal transfer / transferFrom variants whose safety checks diverge from the main path
    • fee, rebate, or penalty logic that can erase user equity while liabilities remain untouched
    • debt-management helpers that can corrupt aggregate debt state without repairing the matching share or liability record
    • liquidation engines that assume collateral shortfall can only arise from ordinary market movement
    • redemption math that honors receipt tokens produced by a prior solvency-breaking state transition
  • Exploit sequence:
    1. The attacker opens a large leveraged position while remaining nominally solvent.
    2. The attacker calls the unchecked path to reduce collateral, move value to reserves, or otherwise desynchronize accounting.
    3. The account becomes underwater in a way the protocol designers did not expect users to be able to manufacture.
    4. The attacker liquidates the position from a controlled address or uses some other downstream path that prices the broken state as if it were legitimate.
    5. Discounted collateral, withdrawal rights, or reserve claims are redeemed for real assets.
    6. The protocol is left with bad debt or depleted pools once the attacker exits.
  • Variants:
    • Unchecked reserve donation — collateral is donated or burned into reserves while debt remains unchanged.
    • Equity burn without liability burn — one tokenized representation of the position is reduced but the matching liability side is not.
    • Helper-path invariant gap — a convenience function skips the health checks present on the core path it economically imitates.
    • Self-liquidation extraction — the attacker controls both the violator and liquidator so liquidation discounts stay inside the attack bundle.
    • Debt-ledger corruption — a helper such as repayForAll() or reserve sync leaves aggregate debt amount and debt-share state disagreeing, so later borrows or solvency checks run against meaningless liabilities.
    • Redemption after desync — a later withdraw or redeem path cashes out receipt tokens created or acquired from the broken accounting state.
    • Cross-module debt-ledger contamination — one module mutates a shared borrow ledger, another module later interprets the resulting zero-debt state as sufficient settlement proof, and collateral is released even though no single canonical close path reconciled the whole position.

Three solvency-boundary promotions reviewers should not collapse

  1. A helper, reserve, liquidation-side, or migration path can touch economically relevant state

    • A side path may move collateral, burn reserve-backed claims, rewrite debt ledgers, consume pending-order value, or release entitlements.
    • That fact alone does not yet mean the protocol has accepted the resulting post-state as canonical solvency truth.
  2. The protocol still interprets that side path’s output as canonical balance-sheet meaning

    • Downstream code treats the helper-authored combination of collateral, debt, reserves, shares, or pending assets as if it were a state the canonical close / borrow / withdraw core could also have produced.
    • This is the dangerous promotion: a weaker mutation path has been allowed to publish state with the same semantic rank as the main mutation-and-validate boundary.
  3. That accepted state still deserves monetization authority

    • Liquidation, redemption, collateral release, or fresh borrowing is still willing to spend against the helper-authored state as if it were economically settled truth.
    • This is where accounting drift becomes cash loss: the protocol does not just store the impossible state, it pays out against it.

Healthy design keeps those three steps separate. A helper changed something, the protocol accepted that changed thing as one coherent balance sheet, and downstream paths may now move real assets against it are different approvals, not one vague missing solvency check story.

  • Detection heuristics:
    • Compare every collateral-changing path against the canonical checkLiquidity / health-factor enforcement path; look for helpers that mutate balances without the same guard.
    • Ask whether any function can reduce user equity, move value into reserves, consume pending collateral, or burn position tokens while leaving debt untouched.
    • Write invariants around realizable collateral, not just token balances: can debt exceed what liquidation can actually seize because of an internal bookkeeping path?
    • Also write invariants around aggregate debt semantics: if a helper changes totalBorrows, elastic, debt shares, reserve balances, or pending-order collateral, can a later canonical path still interpret the resulting state as if those fields were mutually consistent?
    • Simulate attacker-controlled violator and liquidator accounts together; many designs look safe until self-liquidation is modeled explicitly.
    • Review reserve-management and fee-management code with the same seriousness as borrow and liquidation logic.
  • Defenses:
    • Enforce the same solvency checks on every path that changes user collateral, debt, or economically equivalent balances.
    • Centralize accounting mutations so helper functions cannot silently bypass health checks.
    • Require zero debt or explicit safe-state conditions before permitting reserve donations or similar balance-destructive actions.
    • Test liquidation and redemption logic against adversarially manufactured insolvency, not just honest market-driven insolvency.
    • Add invariants asserting that internal state transitions cannot create unbacked debt except through explicitly handled bad-debt resolution flows.

What sound design looks like

  • A side path being able to touch economically relevant state does not automatically promote that path into a balance-sheet author. Helper, reserve, emergency, migration, and liquidation-side code should only mutate through the same post-state mutation-and-validate core that owns canonical solvency meaning.
  • Canonical balance-sheet truth should be explicit and unique: aggregate debt amount, debt shares, reserve balances, pending collateral, and release rights should remain mutually interpretable as one position state rather than loosely synchronized side tables.
  • Monetization authority should lag helper-side state mutation. Even if a side path has touched collateral, reserves, or debt, liquidation, redemption, collateral release, and fresh borrowing should not trust the result until the protocol has re-proved that the post-state still means one coherent solvency object.
  • No path should let value leave, equity be erased, or reserve balances be repurposed while live debt still depends on the thing being removed, unless the protocol is intentionally entering an explicit bad-debt-resolution workflow.
  • When helper output, module topology, or aggregate ledger state stops composing cleanly, the protocol should fail closed rather than silently promote an impossible state into economic truth and let downstream paths spend against it.

Failure-prevention checklist

  • Keep the three promotions mechanically separate in code and reviews: a helper can touch state, that touched state counts as canonical solvency meaning, and downstream flows may monetize it should each require an explicit reason, not one inherited assumption.
  • Route every collateral-, reserve-, debt-, or entitlement-mutating helper through one post-state mutation-and-validate core instead of letting donate, sync, emergency, repayForAll, payoff, or adapter-specific helpers open-code weaker sequencing.
  • Require explicit safe-state gates on destructive helper paths: debt == 0, pending-order value consumed, reserve accounting synchronized, aggregate debt/share pairs mutually meaningful, and no cross-module ledger still interpreting the soon-to-be-released object as backing.
  • Treat helper, migration, reserve, liquidation-side, and adapter-side code as balance-sheet code; give it the same invariant suite as ordinary borrow / withdraw / liquidation flows.
  • Make liquidation, redemption, closeout, and fresh-borrow logic prove they cannot monetize attacker-manufactured insolvency states or stale entitlement objects from a prior helper-path desynchronization.
  • Assert impossible-state combinations at the aggregate ledger layer and fail closed when they appear instead of allowing later user-facing paths to keep pricing corrupted debt or collateral state as canonical truth.

Audit prompts

  • Which functions can reduce realizable collateral, move value into reserves, consume pending-order assets, or mutate aggregate debt without reusing the exact post-state solvency boundary of the canonical core path?
  • If a helper changes totalBorrows, elastic, debt shares, reserve balances, pending-order collateral, or lender-side debt records, what later path still interprets those fields as if they were authored together by one canonical close or borrow routine?
  • Do emergency, migration, rescue, liquidation-settlement, or admin convenience functions read pre-state comfort (solvent before) when the real safety question is post-state meaning (safe after collateral or debt changed)?
  • Can an attacker control both the unhealthy account and the monetization path — self-liquidation, redemption, or helper-assisted closeout — so the protocol’s own discount or payout logic becomes the extraction leg?
  • What helper or legacy path was written before collateral reuse, leverage, asynchronous orders, or cross-module debt ledgers existed, and why is it still allowed to mutate those objects without fresh requalification?

Lessons that should already be standard

  • A helper can touch balance-sheet state, that helper-authored state still counts as canonical solvency truth, and the protocol may now pay against it are three different approvals.

  • A helper that can move collateral, reserves, or debt is not secondary code; it is an alternative economic finality boundary and should be designed like one.

  • Solvent before this action is not a substitute for safe after this action; post-state meaning beats pre-state reassurance.

  • Aggregate debt amount, debt shares, reserve balances, pending-order collateral, and closeout entitlements must stay mutually interpretable as one balance sheet or the protocol should stop and fail closed.

  • Liquidation logic is part of the exploit surface, not just the cleanup path; if users can manufacture insolvency and also capture the liquidator side, the discount itself becomes attacker financing.

  • Legacy helpers should narrow or revert when architecture changes broaden what their touched objects can back, settle, or release.

  • High-signal incidents:

    • bad-debt exploit (2023-03-13) — the cleanest major exemplar: an unchecked reserve-donation path let the attacker delete collateral, preserve debt, self-liquidate at the protocol’s own discount, and drain multiple markets.
    • Platypus Finance USP emergencyWithdraw solvency-check bypass (2023-02-16) — the collateral-escape-hatch variant: emergencyWithdraw() checked stale pre-withdraw collateral state, ignored the meaning of live debt, and let the attacker pull LP collateral out while keeping newly minted USP outstanding.
    • [[../../../incidents/lending-liquidations/abracadabra-cauldron-v4-repayforall-base-elastic-desynchronization-borrow-inflation-2024-01-30|Abracadabra Cauldron V4 repayForAll() base/elastic desynchronization borrow inflation (2024-01-30)]] — the debt-ledger-corruption variant: a legacy helper zeroed aggregate debt amount without clearing the matching debt-share state, and tiny follow-on borrow/repay loops turned that impossible ledger into about $6.5M of unbacked MIM borrowing power.
    • [[../../../incidents/lending-liquidations/openleverage-openlevv1-opborrowing-shared-ltoken-debt-accounting-liquidation-payofftrade-drain-2024-04-01|OpenLeverage OpenLevV1 / OpBorrowing shared-LToken debt-accounting liquidation + payoffTrade drain (2024-04-01)]] — the shared-ledger cross-module variant: liquidation-side repayment mutated the lender’s debt book more aggressively than the surviving trade record expected, and a later payoffTrade() treated borrowBalanceStored == 0 as enough proof to release collateral.
    • Abracadabra GMX V2 Cauldron RouterOrder phantom-collateral self-liquidation drain (2025-03-25) — the asynchronous-order collateral variant: liquidation consumed real RouterOrder assets without consuming the same pending-order value still used by _isSolvent(), so the protocol kept lending against collateral that had already been seized once.

Internal linkages

Control surface