ZeroLend PT-LBTC Actual-Balance / Scaled-Liquidity liquidityIndex Exploit (2025-02-23)

  • Name: ZeroLend PT-LBTC actual-balance / scaled-liquidity liquidityIndex exploit
  • Date: 2025-02-23
  • Protocol: ZeroLend
  • Chain: Base
  • Category: supplier-index inflation / actual-balance-vs-scaled-liquidity boundary mismatch / rounding-window collateral drain
  • Loss: about 3.92307607 LBTC borrowed and left unpaid (roughly $370K at the time)
  • Summary: A tiny ZeroLend PT Lombard LBTC 29MAY2025 reserve on Base was exploitable because flash loans could borrow against the aToken contract’s real ERC-20 balance while supplier accounting and cumulateToLiquidityIndex() still divided by a dust-sized scaled-liquidity denominator. The attacker first transferred PT-LBTC directly to the aToken contract instead of supplying through the pool, then repeatedly flash-borrowed and repaid that balance so the flash-loan premium compounded into liquidityIndex even though bookkept totalLiquidity stayed near the market’s tiny seed level. Once the index was inflated to roughly 2015 × 1e27, the attacker used rayDiv rounding windows to run repeated small supply() / larger withdraw() round trips that drained PT-LBTC from the same reserve, re-supplied the drained PT-LBTC as collateral, and borrowed real LBTC from the connected LBTC reserve in three borrow transactions.
  • Trigger / entry point:
    • ZeroLend had a PT Lombard LBTC 29MAY2025 market on Base whose initialization transaction 0xf7d6adc8bebacb6897277334949b2d952a7266eaae47085f9035923d0f5f94a5 seeded only 32,680 base units of PT-LBTC, about 0.000327 PT-LBTC.
    • That left liquidityIndex = 1e27, variableBorrowIndex = 1e27, and essentially no meaningful supplier base.
    • The attacker then transferred about 5,032,680 base units of PT-LBTC directly to the aToken contract address instead of using Pool.supply(), creating a large real token balance that flash loans could touch without increasing scaled supplier accounting.
    • Because the same lending pool also held a deeper LBTC reserve, any fake collateral value created in PT-LBTC could be turned into real LBTC outflows.
  • Root cause:
    • ZeroLend inherited an Aave-v3-style accounting boundary where flash loans settled against the aToken’s real ERC-20 balance, but totalLiquidity for cumulateToLiquidityIndex() was derived from scaled supplier accounting.
    • Direct token transfers to the aToken contract did not pass through supply(), so they increased flash-loanable balance without increasing the divisor used for supplier-index updates.
    • Flash-loan premiums were immediately socialized through cumulateToLiquidityIndex(), which was catastrophic when the divisor remained at dust scale.
    • After index inflation, the same liquidityIndex widened the rayDiv() rounding window used by both supply() and withdraw(), letting the attacker drain PT-LBTC from the reserve via round trips that netted zero scaled balance but positive underlying token flow.
    • The protocol then accepted that freshly drained PT-LBTC back as collateral at face value, because post-maturity PT-LBTC was priced 1:1 versus LBTC by the oracle, and allowed borrowing against the deeper LBTC reserve.
  • Exploit path:
    1. The attacker targeted the PT-LBTC market after it had been initialized with only 32,680 base units of supply, leaving totalLiquidity tiny.
    2. They transferred about 0.0503268 PT-LBTC (5,032,680 base units) directly to the aToken contract instead of calling supply(). That increased the reserve’s real token balance but did not mint scaled supplier balance or grow bookkept totalLiquidity.
    3. In transaction 0xa14b926bd19ce0e23b17d94802816700e1ea2353f7ceb8e493ae0ddcca4cbad6, they repeatedly flash-loaned and repaid that PT-LBTC balance. Each loop borrowed 5,032,680 base units, repaid 5,035,196, and pushed a 2,516 base-unit premium into cumulateToLiquidityIndex().
    4. Because the denominator stayed around 32,680, each premium increased the index by roughly 1 + 2516 / 32680, or about 7.7% per iteration. Repetition compounded the supplier index until it reached about 2,015 × 1e27.
    5. With the index now extreme, rayDiv(amount, liquidityIndex) mapped a wide range of raw PT-LBTC amounts into the same integer scaled amount. Around 2015 × 1e27, the scaledAmount = 1 window was roughly [1008, 3022] base units.
    6. The attacker then ran repeated supply(small) / withdraw(larger) round trips that minted and burned the same scaled amount while moving more underlying out than in. A representative transaction was 0xbaf678512e38435f869aa811686a5f835036784576fa5bd4dc171586576130f8, which packed 1,151 round trips and drained about 1,154,739 base units (~0.01155 PT-LBTC) from the aToken balance.
    7. Across three larger exploit iterations, the attacker accumulated and re-supplied about 10.9955337 PT-LBTC of collateral. That collateral was largely the same PT-LBTC previously drained from the protocol itself.
    8. Against that collateral, the attacker borrowed LBTC in three transactions: 0xdf1c69feb8e63c70f874cdff22bba7c53eb42a5245e9695713e850966c54ce2a for 0.95324998 LBTC, 0x47fbcdc986c08bf779cb66267c3f6baa0dd43d6a8591f548dbcda5a1c9fce2d2 for 1.47687998 LBTC, and 0xc02cea219b2748ccb8e28b2b23c14d7f6d3d144724ba1b9e17adbf07e70e51a3 for 1.492946 LBTC.
    9. The attacker never repaid. ZeroLend was left with unpaid LBTC debt backed by PT-LBTC collateral that had been fabricated economically from the protocol’s own accounting mismatch and same-market draining loop.
  • What the attacker needed to be true:
    • The PT-LBTC reserve had to have dust-sized scaled liquidity so flash-loan premiums would produce outsized liquidityIndex jumps.
    • Flash loans had to settle against the aToken’s real ERC-20 balance rather than the scaled supplier book.
    • Direct transfers to the aToken contract had to remain off-path from supply() accounting, so the attacker could enlarge lendable balance without enlarging totalLiquidity.
    • The same liquidityIndex had to drive both collateral presentation and rayDiv() conversions in supply() / withdraw().
    • ZeroLend had to let the PT-LBTC reserve count as collateral against a connected, deeper LBTC reserve.
  • Technical anatomy worth remembering:
    • This is an Aave-v3-family supplier-index exploit, but the key setup is not only “empty market.” It is the sharper accounting boundary where flash loans see real token balance while supplier accounting and fee distribution still see only scaled liquidity.
    • The PT-LBTC direct transfer was not itself recognized as a deposit. It was valuable because it created a flash-loanable pool balance that cumulateToLiquidityIndex() later treated as if premiums were being earned on a healthy reserve.
    • The drain phase was not classic same-path precision loss like Radiant or HopeLend. Here the inflated index widened the rayDiv() rounding window enough that repeated supply / withdraw round trips could pull net PT-LBTC from the reserve while finishing each loop with zero scaled balance.
    • The collateral the protocol later honored was economically circular: the attacker drained PT-LBTC from the market, re-supplied that same PT-LBTC, and then borrowed a stronger asset from a sibling reserve.
    • Asset choice mattered operationally. PT-LBTC was an exotic Pendle principal token with tiny seed liquidity, while LBTC was the deeper, cash-out-worthy reserve in the same pool.
  • Representative transactions and artifacts:
    • Attacker EOA: 0x218C572b1Ab6065D74bEbcB708a3f523D14F7719
    • Market initialization tx: 0xf7d6adc8bebacb6897277334949b2d952a7266eaae47085f9035923d0f5f94a5
    • Flash-loan index-inflation tx: 0xa14b926bd19ce0e23b17d94802816700e1ea2353f7ceb8e493ae0ddcca4cbad6
    • Representative rounding-window arb tx: 0xbaf678512e38435f869aa811686a5f835036784576fa5bd4dc171586576130f8
    • Borrow txs: 0xdf1c69feb8e63c70f874cdff22bba7c53eb42a5245e9695713e850966c54ce2a, 0x47fbcdc986c08bf779cb66267c3f6baa0dd43d6a8591f548dbcda5a1c9fce2d2, 0xc02cea219b2748ccb8e28b2b23c14d7f6d3d144724ba1b9e17adbf07e70e51a3
  • Response / mitigation:
    • Public disclosure appears to have lagged badly; outside researchers later pointed to the unpaid debt token and the unchanged collateral position as evidence the borrow had never been repaid.
    • The incident is a strong argument for treating direct underlying transfers to receipt-token contracts as a security boundary, not harmless dust.
    • More generally, Aave-style forks need explicit minimum-liquidity launch discipline, safer fee-routing in empty markets, and controls preventing exotic collateral markets from underwriting deeper sibling reserves before they are operationally mature.

Three reserve-ledger promotions this incident collapsed

  1. Raw PT-LBTC sitting at the aToken was still allowed to become reserve-like state

    • The attacker did not begin by minting new supplier claims through Pool.supply().
    • They first created a wider raw-balance object by transferring PT-LBTC directly to the aToken, so the reserve now physically held inventory that flash-loan paths could touch even though scaled-liquidity accounting had not adopted the same value.
    • The dangerous fact was not merely extra tokens arrived; it was that a fresh, attacker-authored custody balance was already close enough to reserve truth to matter operationally.
  2. That wider raw-balance object was still promoted into canonical supplier-side solvency truth through liquidityIndex

    • Flash-loan premiums generated off the enlarged raw balance were socialized through cumulateToLiquidityIndex() while the denominator stayed near the tiny initialized scaled-liquidity book.
    • ZeroLend therefore treated attacker-authored fee loops as if they were ordinary supplier earnings on a healthy reserve, even though the reserve’s real-balance ledger and supplier-denominator ledger still disagreed.
    • This was the decisive accounting promotion: the protocol converted raw balance can be used into supplier-side collateral truth has grown without first restoring denominator health or reserve-ledger coherence.
  3. That published supplier-side truth still inherited immediate same-reserve extraction and sibling-reserve borrowing authority

    • Once the inflated liquidityIndex widened the rayDiv() rounding window, the attacker could drain PT-LBTC from the same reserve through repeated supply() / withdraw() loops while finishing with the same scaled balance.
    • ZeroLend then accepted that locally drained PT-LBTC back as ordinary collateral and let it underwrite real LBTC borrowing from the sibling reserve.
    • The terminal failure was therefore not just the index got big; it was that the reserve’s weaker local accounting story still retained full extraction and cross-market debt authority.

Pattern tags

  • supplier-index-inflation
  • entrypoint-only-policy-off-path-state-mutation
  • balance-delta-provenance-failure
  • The preserved shape is sharper than generic empty market: a dust-seeded exotic reserve was already allowed to matter cross-market, direct transfers to the aToken enlarged real flash-loanable balance without enlarging scaled liquidity, and the same inflated liquidityIndex later widened the rayDiv() rounding window used to drain PT-LBTC and print borrowable LBTC-facing collateral.
  • This was also a clean listed reserve != mature collateral object failure. The PT-LBTC market did not need to be large to be dangerous; it only needed enough borrow-side authority that one actor could turn a tiny seed denominator and off-path balance growth into sibling-market debt.

Why the exploit worked

  • ZeroLend let a reserve initialized with only 32,680 base units of PT-LBTC — about 0.000327 tokens — stay economically meaningful against a deeper LBTC market. That meant the protocol had already granted cross-market solvency authority to a market whose denominator could still be dominated by one actor.
  • The attacker’s direct transfer to the aToken created the decisive accounting split. Flash loans and raw balance checks could see about 5,032,680 base units of PT-LBTC sitting in the contract, while supplier accounting and cumulateToLiquidityIndex() still socialized premiums over the tiny scaled-liquidity view left by initialization.
  • Once that split existed, flash-loan premiums stopped behaving like ordinary LP income and became attacker-directed index growth. Public reconstruction shows the reserve’s liquidityIndex climbing from 1e27 to roughly 2015e27 while liquidityRate itself stayed at 0, which is the tell that protocol-authored fee recycling — not real lending demand — was manufacturing the solvency story.
  • The inflated index then widened the rayDiv() rounding window enough that repeated supply(small) / withdraw(larger) round trips could drain net PT-LBTC from the same reserve while ending each loop with the same scaled balance. ZeroLend was not only fooled at collateral admission time; it also let the same printed index make same-market extraction mechanically profitable.
  • Downstream borrowing completed the failure. ZeroLend accepted the freshly drained PT-LBTC back as collateral at face value against the deeper LBTC reserve, so the protocol effectively treated its own previously extracted exotic collateral as new security for real money-good assets.

Detection and audit pivots

  • Reconstruct the whole reserve object, not only liquidityIndex: actual underlying balance, scaled supply, totalLiquidity, flash-loan premium routing, and collateral checks should all reconcile before the reserve can matter outside itself.
  • Force raw ERC-20 transfers into the aToken during testing and then run flash loans, fee settlement, and borrow-cap checks. If direct transfers can enlarge flash-loanable balance without enlarging the supplier-index denominator, the reserve still has a live off-path balance mutation problem.
  • Ask the lifecycle question explicitly: what onchain fact proved this PT-LBTC reserve had earned cross-market collateral relevance rather than merely being listed with a dust seed? If the answer is only the market exists, the launch-state hardening was never real.
  • Fuzz rayDiv()-driven mint / burn windows at extreme index values for 8-decimal assets. The interesting bug surface here was not just index inflation, but that the inflated index widened an exploitable scaledAmount = 1 window big enough to make repeated round trips net-positive.
  • Check whether a reserve can accept locally drained assets back as ordinary collateral against a sibling reserve without any second distrust boundary. If same-market extraction can immediately finance cross-market borrowing, the protocol is still collapsing local accounting artifact into system-wide borrow truth.

Failure-prevention checklist

  • Keep exotic or newly listed reserves at collateral factor 0 until they have irreversible seed liquidity, accounting-parity checks, and reserve-specific monitoring in place. Market exists is not enough evidence that market may underwrite sibling-market debt.
  • Make raw transfers into the aToken economically inert until one explicit recognition path adopts them. Direct transfers, rescue top-ups, over-repayments, and settlement leftovers should not become flash-loanable balance, supplier-index-bearing value, or collateral-visible backing by accident.
  • Reconcile underlying.balanceOf(aToken), scaled supply, totalLiquidity, flash-loan capacity, and collateral value after every off-path balance mutation. If those ledgers can disagree, document exactly which one each subsystem trusts and fail closed when they diverge.
  • Stop fee socialization when the denominator is dust. Flash-loan premiums and similar reserve income should route to treasury, suspense accounting, or a quarantined lane whenever scaled liquidity is too thin for safe supplier distribution.
  • Bound same-reserve extraction loops before the reserve can matter cross-market. If an inflated liquidityIndex can widen a profitable rayDiv() window, the reserve should not still be allowed to mint collateral power against a deeper sibling market.
  • Fuzz supply() / withdraw() / flash-loan settlement at extreme index values for low-decimal assets. The interesting bug surface here was not only fee-driven index growth, but that scaledAmount = 1 remained a profitable steady state once the index became enormous.
  • Alert on production-impossible states: liquidityRate == 0 while liquidityIndex jumps sharply, real reserve balance exceeding scaled-liquidity accounting by large multiples, one meaningful supplier, or a reserve whose collateral position can only be explained by locally drained assets being re-supplied.
  • Keep borrow-side blast radius small even if local accounting slips: isolated mode, collateral caps, reserve freezes, and fast collateral-factor zeroing should stop one thin exotic reserve from draining a deeper money-good market.

Audit prompts

  • What exact ledger defines flash-loanable PT-LBTC here: the aToken’s raw ERC-20 balance, scaled supply, reserve cache, or some hybrid? Can a direct transfer enlarge that ledger without minting any supplier claim?
  • When raw underlying arrives at the aToken outside Pool.supply(), which variables move immediately: flash-loan capacity, totalLiquidity, liquidityIndex, collateral value, utilization, or none of the above?
  • What onchain fact proved this PT-LBTC market had graduated from dust-seeded launch object to ordinary collateral object before it was trusted against LBTC?
  • Can the same reserve both create the fake collateral and serve as the extraction source? If locally drained PT-LBTC can be re-supplied moments later and honored at face value against LBTC, where is the second distrust boundary?
  • At extreme liquidityIndex values, what range of raw amounts still maps to scaledAmount = 1 or scaledAmount = 2 under rayDiv()? Which mint / burn / withdraw paths become net-positive because integer rounding stopped tracking economics?
  • Does fee routing assume that any flash-loan premium is honest supplier income, or is there a minimum-liquidity / minimum-breadth condition before the reserve is allowed to socialize it?
  • If the reserve is post-maturity and oracle-priced 1:1, what additional check proves the posted collateral is economically external value rather than the protocol’s own freshly drained inventory returning through supply()?
  • Which control would have stopped this exploit first in production: reserve freeze, flash-loan disable, collateral-factor zeroing, unexpected-balance quarantine, or per-block index-growth clamp?

Lessons that should already be standard

  • A listed reserve is not automatically a mature collateral object. Dust-seeded exotic markets should be treated as effectively fresh until code proves otherwise.
  • Flash-loan fee routing is solvency code. If the same actor can manufacture the premium and monopolize the denominator it is socialized over, the protocol is printing collateral for them.
  • Raw token balance sitting in an aToken is not the same thing as supplier-owned liquidity. If flash loans trust one ledger while supplier accounting trusts another, the gap is already part of the attack surface.
  • Extreme index values turn tiny rounding choices into real money. Once scaledAmount = 1 spans a wide raw-amount interval, rounding bug is just another name for withdraw authority.
  • Exotic collateral needs a second trust boundary beyond local pricing. Oracle says PT-LBTC is 1:1 post-maturity was not enough to justify treating freshly drained PT-LBTC as money-good security for LBTC.
  • Protocols should assume an attacker will try to borrow against the protocol’s own previously extracted inventory. If that sounds impossible, the reserve and collateral ledgers are probably still too loosely coupled.
  • Silence is not containment. An unpaid debt token and frozen-withdrawal symptoms are incident signals, not optics problems, and should trigger user disclosure and hard containment rather than UI excuses.

Why this incident matters

  • ZeroLend preserves a distinct supplier-index-inflation variant beyond Radiant and HopeLend. The printable variable was still liquidityIndex, but the exploit path hinged on real-balance-versus-scaled-liquidity divergence before the rounding drain and collateral borrow.
  • It is a strong exemplar for why off-path balance changes and flash-loan accounting cannot be analyzed separately. The dangerous state came from their composition.
  • It also shows how a protocol can end up accepting its own freshly drained exotic collateral as security for a deeper reserve, which is a particularly vivid form of cross-market balance-sheet self-deception.
  • The note is also useful as a launch-discipline reminder: a market seeded with dust can still be effectively fresh even if it is technically live, and an exotic reserve should not earn sibling-market borrow authority before the protocol can prove that off-path balance changes, fee socialization, and rounding behavior still share one coherent balance sheet.

Internal linkages

Control surface