zkLend wstETH Empty-Market Accumulator-Inflation Exploit (2025-02-11)

  • Name: zkLend wstETH empty-market accumulator-inflation exploit
  • Date: 2025-02-11
  • Protocol: zkLend
  • Chain: Starknet
  • Category: empty-market accumulator inflation / precision-loss withdrawal / cross-market lending drain
  • Loss: about $9.57 million
  • Summary: The attacker targeted zkLend’s empty wstETH market and used its flash-loan donation path to pump the market-wide lending_accumulator from 1 to roughly 4.069297906051644020e18. With the accumulator inflated, they deposited about 4.069297906051644020 wstETH to obtain a raw_balance of 2, then repeatedly ran deposit/withdraw loops whose truncating burn math reduced raw_balance too slowly. That let a microscopic starting position grow into roughly 1,724 raw units worth about 7,015.4 wstETH on paper. zkLend then treated that fabricated collateral as real solvency, letting the attacker borrow ETH, USDC, USDT, STRK, and other assets before bridging much of the haul to Ethereum.
  • Trigger / entry point:
    • The decisive target was zkLend’s empty wstETH market at 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05.
    • zkLend represented collateral as lending_accumulator * raw_balance, so a global accounting scaler rather than spot oracle price became the printable variable.
    • The market’s flash-loan settlement path accepted over-repayment as donation and folded that extra reserve balance into lending_accumulator through thesettle_extra_reserve_balance().
    • Withdrawal / burn logic used floor-rounded scaling, so once lending_accumulator became enormous, burning large zwstETH amounts could reduce raw_balance by only 1.
  • Root cause:
    • zkLend let an empty market start from effectively zero real liquidity and accepted arbitrary early deposits into that state.
    • Its flash-loan donation mechanism fed directly into the global collateral scaler for suppliers, so excess repayment could print paper collateral value instead of merely donating idle reserves.
    • Its mint / burn conversions truncated during scale-down, which became exploitable once the denominator had been inflated to absurd size.
    • In short: an empty market, a donation-sensitive accumulator, and attacker-favorable precision loss combined into fake collateral that the rest of the protocol honored as real.
  • Exploit path:
    1. Around 17:30 UTC on 2025-02-11, the attacker initialized the empty wstETH market with a 1 wei deposit in transaction 0x01c0f62dc1c0c013fff273b8d25c2e761364c036e327285c3838b9415500d2f1, confirming the market had previously held 0 wstETH and 0 zwstETH supply.
    2. In transaction 0x039b6587b9d545cfde7c0f6646085ab0c39cc34e15c665613c30f148b569687c, they flash-borrowed 1 wei wstETH and repaid 1000 wei, causing the extra 999 wei donation to raise lending_accumulator from 1 to about 851.
    3. They repeated that 1 wei flash-loan / over-repayment pattern ten times until lending_accumulator reached roughly 4,069,297,906,051,644,020.
    4. With the scaler inflated, they deposited 4.069297906051644020 wstETH and received a raw_balance of 2, giving themselves a tiny but strategically positioned share base inside the manipulated market.
    5. In transaction 0x001e3c2ebb5b4eafc4800c178dcbd6aa36233d40733bc419d6bce47f8c48d6e6, and then many follow-on loops, the attacker repeatedly deposited amounts equal to integer multiples of the accumulator and withdrew about 1.5x accumulator-sized chunks.
    6. Because the burn path scaled down with floor rounding, those withdrawals removed more wstETH than the deposits added while decrementing raw_balance too little. The first loop sequence pushed raw_balance from 2 to 3; later sequences repeated the same trick at larger size.
    7. A representative later loop transaction, 0x24cd75cd7cf619718bb62df404ce6d53c17f601645ea882ceae7933d5b07b8a, performed 30 deposits and 30 withdrawals, extracting about 61.39 wstETH in one cycle and re-feeding it into the attack.
    8. After enough iterations, the attacker had inflated raw_balance to about 1,724, which zkLend priced as roughly 7,015.4 wstETH of collateral.
    9. They then borrowed stronger assets from the rest of the protocol, including ETH, USDC, USDT, and STRK, turning the printed collateral into real withdrawable value.
    10. The attacker bridged a large share of the stolen assets to Ethereum via StarkGate, including a message identified by 0xee7887af574928f92cc80384cd9febb41aa4f18c97133c974206f8a2b9baba09, which settled on Ethereum in transaction 0xe107aca6535c8b67356dfa2d741bb3d4c4374f558b58a70f8e34e4c1bf1328e7.
  • What the attacker needed to be true:
    • The wstETH market had to be empty so the first depositor could control the initial accounting state.
    • Flash-loan over-repayment had to count as a donation that immediately increased lending_accumulator for suppliers.
    • The protocol had to let the same actor monopolize the tiny raw share supply while inflating the accumulator.
    • Mint / burn scaling had to floor-round in a way that under-burned raw_balance once the accumulator was huge.
    • zkLend had to accept the resulting paper collateral quickly enough to allow cross-asset borrowing from deeper pools.
  • Technical anatomy worth remembering:
    • This was not a direct theft of other users’ wstETH deposits. The attacker used a tiny amount of wstETH to manufacture collateral value, then stole other assets against that fabricated solvency.
    • The printable variable was neither a classic price oracle nor a Compound-style exchange rate. It was zkLend’s lending_accumulator, a market-wide scaler intended to distribute supplier earnings.
    • The donation path mattered as much as the rounding bug. Without the flash-loan over-repayment mechanism, the attacker could not have blown the denominator up to the level needed for the truncation to matter.
    • The exploit shows the same family resemblance as Hundred, Midas, Onyx, Sonne, and Radiant even though the implementation stack was different: empty market first, inflated accounting variable second, under-burn or under-accounting third, real cross-market borrowing last.
    • zk systems do not get a special exemption from bad integer math. The Starknet setting changed the codebase, not the invariant.
  • Representative transactions and artifacts:
    • Attacker Starknet account: 0x04d7191dc8eac499bac710dd368706e3ce76c9945da52535de770d06ce7d3b26
    • Attack contract: 0x0193da87dc0b317f8418ae0c8fb3e0301698ed2d1a4047191d4641ddabc1e2bf
    • Target wstETH market: 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
    • Empty-market initialization tx: 0x01c0f62dc1c0c013fff273b8d25c2e761364c036e327285c3838b9415500d2f1
    • First accumulator-pump tx: 0x039b6587b9d545cfde7c0f6646085ab0c39cc34e15c665613c30f148b569687c
    • First raw_balance-inflation loop tx: 0x001e3c2ebb5b4eafc4800c178dcbd6aa36233d40733bc419d6bce47f8c48d6e6
    • Representative later loop tx: 0x24cd75cd7cf619718bb62df404ce6d53c17f601645ea882ceae7933d5b07b8a
    • Starknet-to-Ethereum bridge message: 0xee7887af574928f92cc80384cd9febb41aa4f18c97133c974206f8a2b9baba09
    • Ethereum bridge settlement tx: 0xe107aca6535c8b67356dfa2d741bb3d4c4374f558b58a70f8e34e4c1bf1328e7
  • Response / mitigation:
    • zkLend paused withdrawals and later published a post-mortem describing the exploit as the combination of empty-market conditions, donation-driven accumulator inflation, and precision loss.
    • The team publicly offered the attacker a 10% white-hat settlement if funds were returned, but the deadline passed without recovery.
    • Railgun publicly rejected at least one laundering attempt under its exclusion-policy flow, sending funds back rather than shielding them.
    • SlowMist argued onchain links connected the operator to the July 2023 EraLend exploit, suggesting this was not just a copycat technique but potentially a repeat exploiter reusing an empty-market playbook.

Three reserve-authority promotions this incident collapsed

  1. An empty reserve was still allowed to remain attacker-owned denominator state

    • The decisive setup was not only wstETH market exists on zkLend. It was that the reserve still had effectively zero prior supplier breadth, so the attacker could define what the next unit of supplier-side value meant.
    • A 1 wei opening deposit plus repeated flash-loan over-repayments was enough because zkLend let a live collateral market stay economically equivalent to genesis.
  2. That attacker-shaped denominator was still promoted into canonical supplier-side solvency truth

    • The over-repayment path raised lending_accumulator, and zkLend treated that accumulator jump as ordinary supplier value even though the market’s meaningful raw_balance denominator was still attacker-owned dust.
    • In other words, the protocol promoted extra reserve balance appeared into suppliers are now richer in a way collateral math should trust, instead of treating empty-market accumulator growth as immature accounting that still needed quarantine.
  3. That published supplier-side truth still deserved immediate reserve-spending authority elsewhere

    • Once lending_accumulator * raw_balance became enormous on paper, zkLend let the attacker borrow ETH, USDC, USDT, STRK, and other assets from deeper pools.
    • The later under-burn loops mattered because the protocol had already crossed the decisive boundary: a zero-breadth reserve whose collateral meaning was still attacker-authored retained live cross-market borrowing authority instead of staying locally visible but economically quarantined.

The durable lesson is that the reserve is deployed, the reserve has published a supplier-side value, and that value deserves immediate cross-market borrowing power are three separate promotions. zkLend failed all three in the same exploit window.

Pattern tags

  • supplier-index-inflation
  • entrypoint-only-policy-off-path-state-mutation
  • The preserved shape is sharper than generic empty market on Starknet: zkLend let a zero-breadth reserve accept flash-loan over-repayment as supplier-side revenue, let that donation-sensitive lending_accumulator redefine collateral value over a microscopic raw_balance denominator, and then let the printed mark unlock deeper pools.
  • This was also a clean listed market != mature collateral object failure. The wstETH reserve did not need an external oracle failure or a large user base; it only needed enough borrow-side authority that one actor could turn an empty-state accounting object into protocol-wide solvency fiction.

Why the exploit worked

  • zkLend let a market that had previously held 0 wstETH and 0 zwstETH supply behave like a mature collateral source the moment the first depositor arrived. That meant the attacker could define the opening denominator for the entire supplier-side accounting story instead of being diluted by an existing deposit base.
  • The flash-loan repayment path then turned over-repayment into supplier-side value rather than quarantined surplus. In an empty market, that is not harmless protocol revenue; it is attacker-directed accumulator inflation. Once lending_accumulator moved from 1 to about 4.069297906051644020e18, zkLend had already allowed one user to decide what the next unit of supplier income meant.
  • The burn path completed the exploit pipeline. Because mint / withdraw conversions floor-rounded when scaling back down to raw_balance, the inflated accumulator made tiny accounting errors economically huge. Withdrawals could remove much more wstETH than the corresponding raw_balance decrement justified, so the attacker could keep growing the same microscopic position while extracting real assets from the reserve.
  • Downstream borrowing then treated lending_accumulator * raw_balance as ordinary collateral truth. There was no second distrust boundary where a zero-breadth market, a market showing impossible accumulator growth, or a market whose supplier-side value came from flash-loan over-repayment had to remain locally observable but still economically quarantined.
  • The preserved lesson is therefore broader than Starknet rounding bug: empty-market lifecycle control, donation handling, accumulator publication, burn rounding, and collateral admission all belonged to one security object, and zkLend split them apart.

Detection and audit pivots

  • Ask the lifecycle question explicitly: what onchain fact proved this wstETH market had passed out of its dangerous opening state, and what kept that fact true once the first meaningful supplier controlled all raw_balance? If the answer is only the market exists, the reserve was still economically at genesis.
  • Treat flash-loan over-repayment and donation handling as solvency code. If surplus repayment can increase a market-wide supplier scaler in a thin reserve, then flash-loan settlement is already part of collateral design rather than just fee plumbing.
  • Reconstruct the whole supplier-value object, not only lending_accumulator: reserve balance, extra-reserve settlement paths, raw_balance, mint / burn conversions, and cross-market collateral checks should all tell one coherent story before the market can matter system-wide.
  • Simulate the exact edge state the exploit used: one meaningful supplier, repeated micro flash loans with over-repayment, then mint / withdraw loops under extreme accumulator values. If the harness only tests normal-size deposits and withdrawals, it will miss the state where floor rounding becomes catastrophic.
  • Check for a downstream distrust boundary. Can an empty or effectively fresh market stay observable while collateral factor is 0, borrowing is disabled, flash loans are off, or per-market debt caps stay too small for one printed accumulator to drain sibling pools?

Failure-prevention checklist

  • Do not let a newly listed or zero-breadth reserve become borrow-relevant until a non-burnable seed floor or another irreversible denominator guard already exists onchain. First depositor arrived is not a security boundary if that opening state can still be monopolized or recreated.
  • Treat flash-loan over-repayment, donations, and surplus-settlement paths as solvency code. If extra reserve balance can raise a supplier-side scaler like lending_accumulator, that path needs the same review as collateral valuation itself.
  • Keep reserve_balance, lending_accumulator, raw_balance, and cross-market collateral value on one coherent accounting story. If one path can raise supplier-side value without proportionally increasing irreversible supplier ownership, the reserve is still printable.
  • Quarantine empty and effectively empty markets. Collateral factor 0, flash loans off, tight debt caps, or other local-only modes should remain in force until the reserve has real supplier breadth and ordinary accumulator behavior.
  • Make burn math safe at pathological accumulator values and minimum-unit asset granularities. If the economically correct burn is just under 2 raw units, the protocol still needs to consume enough state that a withdrawal cannot mint value.
  • Re-run launch-state protections when a reserve drifts back toward dust supply. A market that later becomes one-supplier or near-empty should lose the same authorities a fresh reserve lacks.
  • Monitor production-impossible growth explicitly: one meaningful supplier, accumulator jumps that imply absurd APY, or supplier-side value expanding much faster than the reserve’s organic earnings should trigger automatic quarantine rather than only dashboards.
  • Keep blast-radius controls underneath the market anyway: per-reserve debt caps, fast pauses, flash-loan toggles, and correlated-borrow restrictions should stop one manipulated scaler from draining the rest of the book.

Audit prompts

  • What exact onchain fact proves this reserve is out of its dangerous opening state, and what keeps that fact true if the first supplier later redeems back toward dust?
  • Can flash-loan over-repayment, direct donation, rescue transfer, or other surplus path raise lending_accumulator while one actor still controls essentially all meaningful raw_balance?
  • At extreme accumulator values, how many raw units should a withdrawal burn economically, and where does floor rounding let the protocol destroy less supplier state than the withdrawal consumes?
  • Are flash-loan settlement, reserve-surplus accounting, supplier-scaler updates, and collateral publication all reconciled over the same denominator, or can one path publish value faster than supplier ownership grows?
  • Can an empty or freshly manipulated reserve immediately define borrow power for deeper sibling assets, or is there a second distrust boundary that keeps local accounting anomalies from becoming system-wide solvency truth?
  • What automated signal would catch this early: one meaningful supplier, impossible accumulator growth, reserve-balance jumps sourced by over-repayment, or collateral-value expansion unsupported by real earnings?
  • If the reserve later becomes dormant or nearly empty again, does it automatically fall back into the same quarantine a fresh reserve should start in?

Lessons that should already be standard

  • Empty markets are privileged accounting states, not harmless low-usage corners. Whoever controls the first meaningful raw units often controls the meaning of the next unit of supplier income.
  • Flash-loan over-repayment is not benign fee plumbing when it can raise a market-wide collateral scaler. In thin markets it becomes an attacker-directed solvency input.
  • accumulator * raw_balance is an oracle by another name once downstream borrowing trusts it. It deserves the same skepticism as any published collateral price.
  • Precision loss only looks small while denominators are healthy. Once one actor can inflate the scaler that prices withdrawals, rounding turns into balance-sheet policy.
  • Cross-market lending monetizes local accounting lies. A manipulated empty reserve becomes dangerous the moment the rest of the protocol agrees to honor its paper collateral.
  • Different execution stacks do not change the invariant. Starknet-specific code still failed on the old question: who was allowed to define borrowable truth in an empty market?

Why this incident matters

  • zkLend broadened the empty-market exploit corpus beyond Compound-v2 and Aave-v2 forks into Starknet-specific accumulator accounting.
  • It shows that accumulator * raw_balance designs can fail in the same structural way as exchange-rate or liquidityIndex systems when an empty market lets one user define the first meaningful state.
  • It is also a strong reminder that flash-loan repayment handling is part of solvency design, not just a convenience feature.
  • The stronger lifecycle lesson is that market deployed on a different stack did not change the underlying launch-state problem: a zero-breadth reserve was still allowed to publish borrowable collateral truth before any irreversible seed floor or borrow-side quarantine existed.

Internal linkages

Control surface