Debt-Share Desynchronization / Zero-Share Borrow Inflation

  • Name: Debt-share desynchronization / zero-share borrow inflation
  • Category: exploit pattern / share-based lending accounting failure / debt rounding exploit / empty-borrower-state failure
  • Summary: This exploit family hits share-based lending systems that represent borrower liabilities as a pro-rata claim on aggregate debt through variables like totalDebt, totalDebtShare, base, or elastic. If an attacker can first dominate an empty debt market, then push aggregate debt amount materially out of sync with debt-share state, a later borrow can mint 0 new debt shares after floor rounding or otherwise add grossly understated liability. The protocol hands out real assets, but the borrower’s recorded share liability does not keep up. Public reserve-accrual helpers, hidden prelaunch markets, and debt-management paths that mutate only one side of the debt rebase all make the bug much easier to weaponize.
  • What it is:
    • In share-based debt accounting, user debt is usually stored as debtShare, while market debt lives in aggregate variables like totalDebt and totalDebtShare.
    • The design assumes debt and debt-share growth stay meaningfully coupled: every economically real borrow should mint a nonzero amount of new debt shares.
    • If a protocol lets aggregate debt amount diverge from debt-share state — for example by increasing totalDebt without increasing totalDebtShare, or by zeroing elastic while base remains nonzero — the debt-per-share conversion can stop representing real liability.
    • Once that happens, a nonzero borrow smaller than one full debt share can round down to 0 shares, or later borrow-part math can otherwise undercharge the attacker relative to the assets leaving the pool. The transfer is real; the recorded liability is not.
    • The attacker then repeats the under-share borrow or uses tiny borrow/repay loops to amplify the broken ratio until the protocol’s pool or downstream credit line is empty.
  • Typical preconditions:
    • The attacker can become the only or dominant borrower in a fresh, hidden, or otherwise empty debt market, or can otherwise drive the market into a state where debt shares are concentrated and aggregate debt math is fragile.
    • Debt is tracked via floor-rounded share math or debt-rebase accounting rather than exact per-user principal updates.
    • Some path can increase debt amount without increasing debt-share state, or can zero / distort one side of the debt rebase while leaving the other side live.
    • The protocol does not enforce a minimum 1 debt share for any nonzero debt increase, or otherwise fails to reject impossible aggregate debt states before new borrows continue.
    • Borrowed assets can be monetized locally or through a deeper downstream venue before governance or operators intervene.
  • Common vulnerable surfaces:
    • lending banks that deploy or initialize new borrowable assets before there is any honest borrower base
    • hidden or prelaunch debt markets that are live onchain but assumed to be harmless because the UI does not expose them yet
    • reserve-resolution or fee-accrual functions callable by arbitrary users
    • debt-management helpers such as repayForAll()-style paths that can mutate aggregate debt amount and debt shares asymmetrically
    • protocol-to-protocol credit systems where a local accounting bug can be converted into external borrowable assets
    • spell / strategy / plugin systems that let user-supplied code choreograph borrow and collateral state as long as one end-state invariant appears healthy
  • Exploit sequence:
    1. The attacker finds an empty or near-empty debt market and becomes the only meaningful debt-share holder.
    2. They reduce their position to the smallest nonzero debt-share state that still preserves control of totalDebtShare, often 1 share.
    3. They trigger a function that increases debt amount without increasing debt shares, or otherwise corrupts the aggregate debt rebase so amount and share state stop describing the same market.
    4. The debt-per-share ratio jumps so high that a carefully chosen nonzero borrow now maps to 0 new debt shares after floor division, or later borrow-part calculations otherwise stop charging economically meaningful liability.
    5. The attacker repeatedly borrows real assets or uses tiny borrow/repay loops while their recorded debt-share liability barely changes or does not change at all.
    6. They cash out through the same protocol’s liquid assets or via an external venue, leaving bad debt once the accounting fiction is recognized.
  • Variants:
    • Hidden-pool prelaunch case — the vulnerable debt market exists onchain before operators consider it “live,” so the attacker becomes the first borrower without competition.
    • Public reserve-sync amplifier — a callable reserve or interest-accrual helper creates the debt/share mismatch on demand.
    • Debt-rebase corruption — a helper such as repayForAll() leaves one side of aggregate debt accounting live after the other has been zeroed or distorted, so later borrow-part math is no longer economically meaningful.
    • Protocol-credit externalization — fake internal debt capacity is converted into real assets through a partner credit line or upstream lender.
    • End-state-invariant evasion — a plugin or spell framework allows attacker-written control flow so long as the final collateral check passes, hiding the real exploit in intermediate share math.

Three debt-ledger promotions reviewers should not collapse

  1. An aggregate debt object exists at all

    • totalDebt / totalDebtShare or elastic / base contain nonzero state, and the market can quote a borrower liability object.
    • In thin or hidden markets this is already a privileged moment, because the first borrower may control nearly all debt-share meaning.
  2. That aggregate debt object still deserves to count as economically real borrower liability

    • This is the harder question: did every path that touched aggregate debt keep amount and share state coupled tightly enough that the market still means what later borrow math thinks it means?
    • Alpha’s hidden sUSD case and Abracadabra’s repayForAll() case both fail here. The ledger still looked populated, but the state no longer encoded a truthful exchange rate between real debt and recorded shares.
  3. A later borrow or helper path still deserves to spend against that state right now

    • Even if the market exposes some internally consistent-looking debt object, the protocol still needs a separate boundary before a new borrow, reserve sync, spell step, or partner-credit draw is allowed to monetize it.
    • The recurring mistake is silently promoting aggregate debt object exists into new borrowing authority remains safe, even after helper paths or dust-share states have already made that object economically meaningless.

The family gets much easier to classify once those promotions stay separate: hidden-market exploits usually fail between steps one and two because the first borrower gets to define the debt-share regime cheaply, while helper-path incidents like Abracadabra fail between steps two and three because later borrowing continues after the debt ledger has already stopped meaning what it claims.

  • Detection heuristics:
    • Ask whether any nonzero borrow can ever mint 0 debt shares. If yes, treat that as a critical solvency bug, not a harmless rounding quirk.
    • Model empty-market borrower states explicitly: who owns totalDebtShare when a borrowable asset first becomes reachable?
    • Trace every path that changes aggregate debt amount and verify whether debt-share state stays synchronized, especially across helper paths, reserve syncs, and legacy repayment flows.
    • Stress-test debt-share math around totalDebtShare == 1, dust debt, post-accrual states, and impossible rebase combinations like elastic == 0 && base > 0.
    • Review hidden, paused, or not-yet-announced markets as live attack surface whenever contracts already accept calls against them.
  • Defenses:
    • Enforce borrowShare > 0 for every nonzero borrow, or revert when share math would round a real debt increase to zero.
    • Assert and preserve aggregate debt invariants on every helper path: debt amount and debt-share state must remain mutually meaningful after repayForAll(), reserve syncs, liquidations, or emergency management calls.
    • Do not expose new debt markets until safe initial state, supported assets, and borrower-share hardening are already onchain.
    • Restrict reserve-resolution and similar debt-growing sync functions to trusted control, or redesign them so they cannot create debt/share divergence in empty markets.
    • Seed or quarantine new borrowable assets so no attacker can become the sole meaningful debt-share holder before launch.
    • Treat plugin / spell systems as privileged surfaces and narrow them to audited strategies instead of arbitrary user code.

What sound design looks like

  • A healthy debt-share system makes economically real debt and recorded borrower liability the same state transition everywhere. If assets leave the pool, the protocol either mints nonzero borrower shares or refuses the action; there is no helper, reserve-sync, launch-state, or emergency path allowed to author a debt amount that later borrow math interprets without matching debt-share growth.
  • Aggregate debt objects should fail closed as one security object, not drift as loosely related fields. If totalDebt / totalDebtShare or elastic / base can no longer describe the same market after a helper call, a healthy design stops new borrowing immediately and routes through a bounded repair path instead of letting later users discover the impossible state through 0-share borrows. The positive-design companion there is debt-zero emergency-exit discipline.
  • Helper paths inherit the same ledger semantics as the canonical borrow path. Reserve resolution, bad-debt socialization, repayForAll(), migration helpers, strategy callbacks, and spell systems should reuse one accounting boundary or lose authority to mutate production debt altogether. The sharper control companion there is helper-path invariant reuse and fail-closed discipline.
  • Launch-state debt markets should stage exposure rather than publishing full borrow authority on day zero. A hidden bank, UI-dark market, or freshly listed debt asset should not be reachable until borrower-share floors, helper-path constraints, and safe initial-state checks are already enforced onchain. The nearby rollout companion is fresh-market seeding and activation discipline.
  • The clean design question is: what exact invariant guarantees that every unit of debt handed out still exists as borrower liability after any path that can change aggregate debt? If the answer only covers borrow() but not reserve helpers, launch-state reachability, or emergency debt tooling, the system is still trusting syntax more than balance-sheet truth.

Failure-prevention checklist

  • Make nonzero debt increase => nonzero debt-share increase a hard invariant, not a hoped-for consequence of normal market size. If rounding would mint 0 shares, revert.
  • Fail closed on impossible aggregate debt states such as base > 0 && elastic == 0, dust-share singleton markets, or reserve-sync outcomes that change debt amount without changing debt-share meaning.
  • Reuse one canonical debt-accounting boundary across user-facing borrows, liquidations, reserve resolution, socialized repayment helpers, migrations, and emergency tooling. If a helper path cannot preserve the same ledger semantics, it should not be able to mutate production debt.
  • Treat hidden, deprecated, paused, prelaunch, or UI-dark markets as live attack surface whenever contracts are callable and liquidity can still be externalized.
  • Gate new debt markets with launch sequencing that is enforced in code: deploy, seed / sanity-check, constrain helpers, then expose borrowing. Do not rely on the frontend is not listing it yet as a control.
  • Quarantine debt markets whose first borrower or first meaningful debt-share owner can still be attacker-controlled dust. Empty borrower state is as privileged as empty supplier state.
  • Treat reserve-accrual, bad-debt socialization, and repayForAll()-style helpers as solvency code. Review them with the same paranoia as borrow() itself.
  • Stress-test dust states explicitly: totalDebtShare == 1, tiny base, tiny elastic, post-helper rebase states, and nonzero borrows just below one-share thresholds.
  • Constrain plugin / spell / strategy frameworks so arbitrary user code cannot compose benign-looking debt helpers into privileged exploit choreography under one end-state check.
  • Put blast-radius controls under the market anyway: borrow caps, per-market debt ceilings, isolated liquidity, and fast pause paths should stop one debt-ledger lie from turning into systemwide bad debt.

Audit prompts

  • Can any economically real borrow, fee accrual, reserve resolution, liquidation, or helper call change aggregate debt amount without changing debt-share state proportionally?
  • What exact state transition guarantees that every nonzero borrow mints at least one unit of borrower liability? Where is that guarantee enforced in code?
  • If a market is fresh, hidden, deprecated, or off-UI, who can become the first or only meaningful debt-share holder before operators consider the market live?
  • Which functions can author debt state other than borrow() and repay()? Are those functions provably ledger-equivalent to the main path, or can they leave base / elastic-style variables in combinations the main path would never create?
  • Do reserve-sync helpers, socialized repayment helpers, or emergency debt-management tools stay safe when totalDebtShare is 1, when debt is dust-sized, or when the market has no honest borrower distribution?
  • Can arbitrary spell, plugin, or callback-controlled code sequence helper calls and borrows in one privileged frame while only an end-state solvency check stands guard?
  • If the local market ledger becomes distorted, what downstream venue turns that accounting fiction into real assets: an upstream credit line, sibling pools, shared treasury liquidity, or bridgeable stablecoins?
  • What monitoring would catch this before a full drain: zero-share borrow attempts, singleton debt-share ownership, impossible debt-rebase combinations, or helper calls that move elastic without corresponding base movement?
  • Is there any path where a market operators think is not launched yet is still borrowable, repayable, or helper-callable onchain?
  • What invariant proves that aggregate borrower liability after any helper path still equals the economically real debt the protocol has handed out?

Lessons that should already be standard

  • Empty borrower state is a privileged accounting regime, not a harmless low-usage corner. Whoever owns the dust debt shares often defines what later debt math means.

  • Borrow rounds to zero shares is never a cosmetic precision issue. It is the protocol handing out assets without minting matching liability.

  • Helper functions are part of the solvency perimeter. A bulk repayment or reserve-sync path that can author ledger states the main borrow path would reject is already a lending bug.

  • Hidden markets are not hidden from attackers. If contracts are live, the launch has happened from a security perspective.

  • Aggregate debt variables need semantic coupling, not just syntactic coexistence. base, elastic, totalDebt, and totalDebtShare are only safe when every path preserves their shared economic meaning.

  • End-state solvency checks do not rescue arbitrary intra-transaction authority. A spell or plugin system can still weaponize intermediate debt-state transitions long before the final snapshot looks healthy enough.

  • Protocol-to-protocol credit lines magnify accounting mistakes. A local debt-share bug becomes a treasury event once fake borrow capacity can be externalized into deeper liquidity.

  • Launch sequencing is debt security. Seeding, helper hardening, asset allowlisting, and market exposure order matter just as much on the borrower side as they do for fresh collateral markets.

  • High-signal incidents:

    • zero-share debt inflation exploit (2021-02-13) — the canonical empty-market case where an unannounced sUSD bank, public resolveReserve(), and floor-rounded debt-share math let the attacker borrow real assets while minting 0 new debt shares.
    • [[../../../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 helper-path cousin where repayForAll() left totalBorrow.elastic == 0 while base remained live, and later tiny borrow/repay loops turned that impossible debt-rebase state into real MIM outflows.

Internal linkages

Control surface