Alpha Homora V2 Hidden sUSD Pool / Zero-Share Debt Inflation Exploit (2021-02-13)

  • Name: Alpha Homora V2 hidden sUSD pool / zero-share debt inflation exploit

  • Date: 2021-02-13

  • Protocol: Alpha Homora V2

  • Chain: Ethereum

  • Category: debt-share desynchronization / zero-share borrow inflation / hidden-pool fresh-market exploit / bad-debt drain

  • Loss: about $38 million extracted, leaving Alpha Homora with tens of millions of dollars of debt to Iron Bank

  • Summary: The attacker found that Alpha Homora V2 had an unannounced sUSD bank deployed onchain ahead of any public release. Because that bank had no existing borrowers, the attacker could become the sole debt-share holder, then repay down to 1 debt share and 1 mini-sUSD of debt. A publicly callable resolveReserve() call increased totalDebt without increasing totalDebtShare, creating a state where borrowing amounts just below totalDebt mapped to 0 new debt shares after floor rounding. The attacker then repeatedly borrowed real sUSD while the protocol recorded no new borrower liability, used flash-swapped USDC to deepen Iron Bank sUSD liquidity, and finally turned Alpha’s protocol-level Iron Bank credit line into about $38M of real WETH, DAI, USDC, and USDT outflows.

  • Trigger / entry point:

    • Alpha Homora V2 had an sUSD pool at the contract level “in preparation for the upcoming release,” but it was neither exposed in the UI nor publicly announced.
    • HomoraBankV2.execute() accepted arbitrary spells as long as the final collateral check passed, so the attacker could deploy a custom spell contract and route the whole exploit through it.
    • The attacker used a UNI-WETH LP position deposited into WERC20 as collateral so the bank’s end-of-call collateralValue >= borrowValue check would not block setup.
    • Because the sUSD bank had no other borrowers, the attacker became the first and only debt-share holder.
  • Root cause:

    • Alpha left an empty, reachable sUSD debt market live onchain before it was operationally hardened or publicly launched.
    • The debt-share accounting let a carefully chosen borrow amount round down to 0 new debt shares when totalDebt had been made much larger than totalDebtShare.
    • resolveReserve() could be called by anyone and increased totalDebt without increasing totalDebtShare, which was exactly the mismatch the attacker needed.
    • The spell system treated arbitrary spell code as acceptable so long as the final collateral check passed, which let the attacker automate borrow / repay / transfer choreography inside Alpha’s privileged protocol context.
    • Alpha’s integration with Iron Bank meant the accounting failure did not stay local: fake internal sUSD borrowing power could be converted into real external assets borrowed on Alpha’s protocol credit line.
  • Exploit path:

    1. The attacker deployed an “evil spell” contract at 0x560a8e3b79d23b0a525e15c6f3486c6a293ddad2 in tx 0x2b419173c1f116e94e43afed15a46e3b3a109e118aba166fcca0ba583f686d23.
    2. In tx 0x4441eefe434fbef9d9b3acb169e35eb7b3958763b74c5617b39034decd4dd3ad, the attacker swapped ETH into UNI, formed a UNI-WETH LP position for collateral, swapped more ETH into sUSD, and deposited sUSD into Cream’s Iron Bank to get cysUSD.
    3. In tx 0xcc57ac77dc3953de7832162ea4cd925970e064ead3f6861ee40076aca8e7e571, they called execute() through the evil spell, opened position 883, borrowed 1000e18 sUSD, and became the first borrower with 1000e18 debt shares.
    4. In tx 0xf31ee9d9e83db3592601b854fe4f8b872cecd0ea2a3247c475eea8062a20dd41, they repaid 1000000098548938710983 sUSD against an actual debt of 1000000098548938710984, leaving exactly 1 mini-sUSD of debt and 1 debt share.
    5. In tx 0x98f623af655f1e27e1c04ffe0bc8c9bbdb35d39999913bedfe712d4058c67c0e, they called resolveReserve() on the sUSD bank. That accrued 19709787742196 debt units to reserves while leaving totalShare at 1, producing totalDebt = 19709787742197 and totalShare = 1.
    6. With that skewed ratio in place, the attacker re-entered execute() and repeatedly borrowed totalDebt - 1-style amounts. Because each borrow was just below the debt-per-share threshold, the calculated borrow share rounded down to 0, so Alpha transferred real sUSD but recorded no new debt shares. Alpha’s postmortem highlights tx 0x2e387620bb31c067efc878346742637d650843210596e770d4e2d601de5409e3 as a 16-iteration doubling loop and tx 0x64de824a7aa339ff41b1487194ca634a9ce35a32c65f4e78eb3893cc183532a4 as a further 10-iteration doubling loop.
    7. Once the attacker needed deeper sUSD liquidity, they flash-borrowed USDC from Aave, swapped it into sUSD, deposited that sUSD into Iron Bank, and resumed the zero-share doubling path in txs 0x7eb2436eedd39c8865fcc1e51ae4a245e89765f4c64a13200c623f676b3912f9, 0xd7a91172c3fd09acb75a9447189e1178ae70517698f249b84062681f43f0e26e, and 0xacec6ddb7db4baa66c0fb6289c25a833d93d2d9eb4fbe9a8d8495e5bfa24ba57.
    8. After manufacturing enough apparent credit, the attacker borrowed real assets through Alpha’s Iron Bank relationship. Alpha’s postmortem attributes roughly 13,244.63 WETH, 4.263M DAI, 3.998M USDC, 5.647M USDT, and an additional 426,659 USDC draw to the exploit, with the multi-asset cash-out centered in tx 0x745ddedf268f60ea4a038991d46b33b7a1d4e5a9ff2767cdba2d3af69f43eb1b.
    9. The attacker then moved stablecoins into Aave and Curve’s a3Crv system, staked the LP in tx 0xc60bc6ab561af2a19ebc9e57b44b21774e489bb07f75cb367d69841b372fe896, and later routed funds through Tornado Cash while sending 1,000 ETH each to Alpha’s and Cream’s deployer addresses.
  • What the attacker needed to be true:

    • The hidden sUSD bank had to remain callable onchain even though Alpha had not surfaced it in the UI.
    • No other borrower could hold meaningful sUSD debt shares; otherwise the attacker could not cheaply dominate totalDebtShare.
    • Borrow-share minting had to floor-round downward rather than forcing a minimum 1 share for any nonzero debt increase.
    • resolveReserve() had to be public and able to increase totalDebt without increasing totalDebtShare.
    • Alpha had to honor custom spells so long as the final collateral check passed.
    • Iron Bank had to extend protocol-level credit to Alpha so the printed internal debt state could be externalized into real borrowable assets.
  • Technical anatomy worth remembering:

    • This was not an oracle hack and not a classic callback bug. It was a share-accounting failure in the debt side of a lending market.
    • The decisive invariant was totalDebt staying proportional to totalDebtShare. Once Alpha let reserves raise totalDebt while the attacker still owned all shares, one extra debt share became extremely expensive and anything below that threshold could round to zero.
    • The hidden-pool detail matters. From Alpha’s UI perspective the sUSD bank was not live; from Ethereum’s perspective it was live enough to become the exploit surface.
    • Alpha had recently added an onlyEOA gate intended to hinder flash-loan-style attacks. That forced a multi-transaction exploit, but it did not fix the underlying accounting bug.
    • The exploit also shows how protocol-to-protocol credit lines magnify local bugs. Alpha’s broken debt accounting became Iron Bank bad debt.
  • Representative transactions and artifacts:

    • Attacker EOA: 0x905315602ed9a854e325f692ff82f58799beab57
    • Evil spell: 0x560a8e3b79d23b0a525e15c6f3486c6a293ddad2
    • Alpha Homora V2 proxy: 0x090ece252cec5998db765073d07fac77b8e60cb2
    • Spell deployment: 0x2b419173c1f116e94e43afed15a46e3b3a109e118aba166fcca0ba583f686d23
    • Initial setup / collateral and sUSD preparation: 0x4441eefe434fbef9d9b3acb169e35eb7b3958763b74c5617b39034decd4dd3ad
    • First borrow into position 883: 0xcc57ac77dc3953de7832162ea4cd925970e064ead3f6861ee40076aca8e7e571
    • Near-total repay to 1 debt share: 0xf31ee9d9e83db3592601b854fe4f8b872cecd0ea2a3247c475eea8062a20dd41
    • Reserve-resolution skew step: 0x98f623af655f1e27e1c04ffe0bc8c9bbdb35d39999913bedfe712d4058c67c0e
    • Zero-share borrow doubling loops: 0x2e387620bb31c067efc878346742637d650843210596e770d4e2d601de5409e3, 0x64de824a7aa339ff41b1487194ca634a9ce35a32c65f4e78eb3893cc183532a4, 0x7eb2436eedd39c8865fcc1e51ae4a245e89765f4c64a13200c623f676b3912f9, 0xd7a91172c3fd09acb75a9447189e1178ae70517698f249b84062681f43f0e26e, 0xacec6ddb7db4baa66c0fb6289c25a833d93d2d9eb4fbe9a8d8495e5bfa24ba57
    • Multi-asset cash-out: 0x745ddedf268f60ea4a038991d46b33b7a1d4e5a9ff2767cdba2d3af69f43eb1b
    • Curve gauge parking: 0xc60bc6ab561af2a19ebc9e57b44b21774e489bb07f75cb367d69841b372fe896
  • Response / mitigation:

    • Alpha paused new borrowing on Homora V2 while investigations proceeded, and Iron Bank temporarily cut Alpha’s credit line to 0 while pausing and then re-enabling its own markets after checking they were functioning normally.
    • Alpha’s patched bank restricted execute() to whitelisted spells, made resolveReserve() governor-only, and limited borrow / repay assets to ETH, DAI, USDC, and USDT.
    • The incident ultimately became a long-lived protocol-to-protocol debt problem between Alpha and Iron Bank, not just an instant one-block loss.

Three debt-authority promotions this incident collapsed

  1. A hidden, not-yet-released bank still counted as live debt-share genesis state

    • The dangerous fact was not merely that an sUSD pool contract existed. It was that Alpha left an onchain-reachable bank with full borrowing semantics live before any honest borrower distribution, so the attacker could become the one address that defined what a debt share meant.
    • Once the attacker repaid down to 1 debt share and 1 mini-sUSD of debt, the bank’s entire borrower-side denominator had effectively become attacker-owned singleton state.
  2. That attacker-owned singleton state was still promoted into canonical debt truth

    • resolveReserve() then raised totalDebt without raising totalDebtShare, which meant Alpha promoted a reserve-side bookkeeping mutation into canonical borrower-side debt meaning even though the only meaningful share owner was the attacker.
    • After that skew, borrow() calls just below the one-share threshold could mint 0 new debt shares while still moving real sUSD, so Alpha was no longer publishing debt amount and debt ownership as the same security object.
  3. That published debt fiction still deserved external balance-sheet authority

    • The exploit only became a protocol-scale loss because Alpha still treated the corrupted sUSD debt state as good enough to support real borrowing through its Iron Bank credit line.
    • In other words, a hidden bank with attacker-authored share semantics and zero-share borrow growth still retained authority to spend external WETH, DAI, USDC, and USDT liquidity rather than being quarantined as impossible local accounting.

The durable lesson is that the bank exists onchain, its debt/share ratio still defines canonical borrower truth, and that published truth still deserves upstream credit authority are three separate promotions. Alpha collapsed all three.

Why this incident matters

  • It preserves a lending-market exploit family that is easy to miss if you only look for price or callback failures: the debt side can be just as forgeable as the collateral side.
  • It is a strong example of hidden or prelaunch markets still being production attack surface. If users cannot see a pool but contracts can, attackers can usually reach it first.
  • It shows how public reserve-sync helpers and share-based accounting compose into a mint primitive when market state is empty.
  • It also broadens the empty-market corpus beyond supplier-side exchange-rate or liquidityIndex inflation: here the attacker weaponized empty borrower state and debt-share math instead.

Pattern tags

Why the exploit worked

  • Alpha let an onchain-reachable but operationally “not live yet” sUSD bank inherit full production authority before there was any honest borrower distribution, so the attacker could own essentially all debt-share meaning from the start.
  • resolveReserve() was a real balance-sheet mutation, not harmless maintenance. Because it could raise totalDebt without minting matching totalDebtShare, it created the exact impossible debt/share ratio that later borrows could exploit.
  • Borrow-share conversion used downward floor rounding with no nonzero borrow => nonzero debt share backstop. Once one share represented an absurd amount of debt, Alpha still honored borrows that minted 0 new borrower liability.
  • The spell framework expanded blast radius because arbitrary attacker-written control flow could sequence borrow, repay, reserve-resolution, and collateral choreography inside Alpha’s privileged lending context as long as the final collateral snapshot looked acceptable.
  • Iron Bank turned a local accounting lie into systemwide loss. Alpha did not merely misprice internal debt; it could externalize that fake capacity into real WETH, DAI, USDC, and USDT borrowed against its protocol credit line.

Detection and audit pivots

  • Review every borrowable market that exists onchain but is hidden from the UI, paused, deprecated, or prelaunch. If contracts remain reachable, ask who can become the first or only meaningful debt-share holder before operators consider the market live.
  • Trace all functions that can change aggregate debt outside the main borrow path, especially reserve syncs, fee accrual, socialized repayment helpers, and emergency debt tooling. Any path that mutates debt amount without proportional debt-share movement is solvency code.
  • Fuzz totalDebtShare == 1, dust debt, and borrow just below one-share threshold states. A single successful nonzero borrow mints 0 debt shares testcase should be treated as a critical failure.
  • Model plugin, spell, and strategy frameworks as privileged accounting surfaces rather than mere UX helpers. The right question is whether arbitrary user-authored call choreography can string together individually-valid debt mutations into one exploit bundle.
  • Follow externalization paths, not just local pool math. If a manipulated local debt ledger can tap an upstream credit line, sibling pools, or shared treasury liquidity, the real risk is balance-sheet theft at protocol scope rather than isolated market bad debt.

Failure-prevention checklist

  • Do not leave hidden, UI-dark, or prelaunch borrow markets callable with production semantics. If a bank is not live, borrowing, reserve helpers, and custom strategy entry should not be live either.
  • Enforce nonzero borrow => nonzero debt-share mint as a hard invariant. If a borrow amount would round to 0 debt shares, revert instead of lending anyway.
  • Treat reserve-accrual helpers like resolveReserve() as balance-sheet code. If they can move totalDebt, they must also preserve meaningful totalDebt : totalDebtShare coupling under singleton-share and dust-debt states.
  • Re-test debt accounting under adversarial first-borrower conditions: one borrower, one debt share, dust debt, and repeated borrow(totalDebt - 1)-style thresholds should all fail safely.
  • Keep spell, plugin, and strategy systems on a whitelist once they can touch borrowing, repayment, or collateral state. Final collateral check passed is not enough if arbitrary intermediate control flow can author impossible debt state.
  • Stage fresh-market activation in code rather than in ops lore: deploy, seed, constrain helpers, verify borrower-share behavior, then expose the asset through supported spells and interfaces.
  • Put blast-radius limits under protocol-credit integrations. A hidden-market debt bug inside one partner should not be able to drain ordinary WETH, DAI, USDC, and USDT liquidity from an upstream line without earlier ceilings, isolation, or circuit breakers.

Audit prompts

  • Can any path besides borrow() and repay() change aggregate debt for this market, and if so, what proves those paths preserve the same economic meaning of totalDebtShare?
  • What exactly happens when totalDebtShare == 1 and totalDebt is large? Does any positive borrow, however small, still mint at least one unit of borrower liability?
  • Are there any deployed-but-not-exposed markets, spells, or asset configs that remain callable onchain even though operators consider them not launched or not supported yet?
  • Can a user-authored spell sequence reserve sync, debt repayment, and later borrowing inside one privileged bank context while only an end-of-call collateral snapshot stands guard?
  • If one market’s internal debt ledger becomes impossible, what external venue still trusts it enough to hand out real assets: partner credit lines, sibling pools, or treasury liquidity?
  • Which monitoring would have paged earlier here: singleton debt-share ownership, resolveReserve() on a dormant market, borrowShare == 0 events, or hidden-bank activity before public launch?
  • Does the protocol assume UI invisibility or undocumented status reduces attackability anywhere in code or operational rollout?

Lessons that should already be standard

  • Not in the UI yet is not a launch control. If the contract is live, the market is live to attackers.
  • Debt-side share math deserves the same paranoia teams already apply to collateral exchange rates. A borrower ledger that can hand out assets while minting 0 liability is already a mint bug.
  • Reserve collection and maintenance helpers are never neutral once they can change aggregate debt. If they touch totalDebt, they are part of the lending security boundary.
  • Arbitrary strategy or spell systems do not need to break the final collateral check to be dangerous; they only need enough mid-flight authority to compose legitimate-looking steps into an impossible accounting state.
  • Protocol-to-protocol credit turns local accounting mistakes into ecosystem balance-sheet events. Partner integrations need isolation and distrust, not just good intentions.
  • First-borrower and dust-share states are as security-sensitive as first-depositor states. Fresh debt markets should launch with explicit singleton-state review, not with hope that nobody notices them first.

Internal linkages

Control surface