Lendf.Me imBTC ERC-777 Reentrancy (2020-04-19)

  • Name: Lendf.Me imBTC ERC-777 reentrancy
  • Date: 2020-04-19
  • Protocol: Lendf.Me (dForce)
  • Chain: Ethereum
  • Category: lending-market integration failure / ERC-777 hook reentrancy / collateral-accounting overwrite
  • Loss: about $25.24M across 10+ asset pools, per PeckShield’s incident accounting
  • Summary: Lendf.Me listed imBTC as collateral and treated transferFrom() like an inert ERC-20 transfer. It was not inert. imBTC followed ERC-777-style hook semantics, so the attacker’s tokensToSend() callback could run in the middle of Lendf.Me’s supply() path. By entering supply() with a dust imBTC deposit and calling withdraw() from inside the hook before supply() finalized storage, the attacker made Lendf.Me erase the real withdrawal and restore the old collateral balance from stale local state. Repeating that loop printed fake imBTC collateral inside the protocol and let the attacker borrow nearly every liquid asset market against collateral that was no longer there.
  • Trigger / entry point:
    • Lendf.Me’s collateral deposit path called doTransferIn() / IMBTC.transferFrom() before finalizing the user’s collateral bookkeeping.
    • imBTC exposed ERC-777 sender-hook behavior through the ERC1820 registry and tokensToSend().
    • The attacker registered a malicious sender implementer, initiated supply() with imBTC, and used the callback window to invoke withdraw() before the outer supply() call wrote back final balances.
  • Root cause:
    • The core mistake was checks-effects-interactions failure in a lending-market collateral path: external token transfer first, authoritative accounting second.
    • Because Lendf.Me cached balance data locally, the re-entrant withdraw() could correctly reduce collateral, then the outer supply() call would resume and overwrite storage with the stale pre-withdraw balance plus the new tiny deposit.
    • The issue was not that ERC-777 itself was broken. The issue was that Lendf.Me integrated a callback-capable token without modeling the callback as a full control-flow surrender.
  • Exploit path:
    1. The attacker first supplied a real imBTC position to establish collateral inside Lendf.Me; public writeups use roughly 289.99999999 imBTC as the representative starting amount.
    2. The attacker then called supply() again with only 0.00000001 imBTC.
    3. Inside IMBTC.transferFrom(), the ERC-777 sender hook tokensToSend() fired via the ERC1820 implementer the attacker had registered.
    4. During that callback, the attacker called Lendf.Me withdraw() and removed about 290 imBTC from the protocol.
    5. The re-entrant withdraw() correctly reduced the attacker’s stored collateral, but execution then returned to the outer supply() path.
    6. The outer supply() path wrote back collateral state using stale local variables, restoring the old collateral balance as if the withdrawal had not happened and adding the dust deposit on top.
    7. By repeating the same pattern, the attacker inflated Lendf.Me’s internal record of imBTC collateral to tens of millions of dollars while holding the real imBTC outside the protocol.
    8. With that fake collateral mark in place, the attacker borrowed across 10+ markets and drained stablecoins, synthetic BTC assets, and WETH from available pool liquidity.
  • What the attacker needed to be true:
    • A callback-capable token had to be admitted as collateral in a path that updated state too late.
    • The attacker needed control of the ERC1820 sender-hook registration for the depositing address so tokensToSend() would execute attacker logic during transferFrom().
    • withdraw() had to remain callable during the outer supply() callback window.
    • The protocol had to trust the mutated imBTC collateral balance enough to let the attacker borrow stronger assets from sibling markets.
    • Sufficient liquid assets needed to be available across Lendf.Me pools for the fake collateral to be monetizable.
  • Technical anatomy worth remembering:
    • This was a clean collateral-accounting overwrite, not merely a generic recursive withdraw loop. The money came from restoring stale collateral after a real withdrawal had already happened.
    • The callback surface was on the sender side of ERC-777 transfer semantics, not just on the receiver side many engineers casually picture.
    • PeckShield’s trace highlights the vulnerable pair as supply() plus re-entrant withdraw(), with the decisive transfer happening inside doTransferIn().
    • Quantstamp emphasizes the end state clearly: from the protocol’s perspective the attacker had huge imBTC collateral; economically, that imBTC was already back in the attacker’s wallet.
    • The incident also echoed the earlier Uniswap/imBTC issue from the previous day: the same callback-capable asset interacted badly with DeFi code that had assumed token transfer was passive.

Three collateral-authority promotions this incident collapsed

  1. A callback-capable collateral-supply edge became live at all
    • Lendf.Me’s supply() path had already crossed the critical execution boundary once doTransferIn() called IMBTC.transferFrom() and the attacker’s ERC-777 sender hook could run.
    • At this stage the strongest safe statement was only collateral intake is mid-flight and attacker-controlled code can still reach sibling paths, not the protocol has already finalized one settled collateral story for this account.
  2. That mid-flight storage view was promoted back into canonical collateral truth
    • This was the decisive failure. The re-entrant withdraw() correctly removed real imBTC, but the outer supply() path still resumed from stale local variables and wrote the pre-withdraw balance back into storage as if it were the current truth.
    • The dangerous promotion was therefore not merely callback happened; it was obsolete pre-withdraw state still counted as the canonical collateral answer after the callback returned.
  3. That accepted collateral truth still deserved immediate cross-market borrow authority
    • Once the stale collateral story had been restored in storage, sibling Lendf.Me markets still treated it as money-good borrow capacity for stablecoins, WETH, and other assets.
    • The exploit therefore became protocol-wide only because a locally false imBTC balance still inherited ordinary cross-market lending authority after the callback window closed.
  • Representative transactions and artifacts:
    • Representative Lendf.Me exploit transaction analyzed by PeckShield: 0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b
    • Representative exploit transaction cited by Quantstamp: 0x9743677ccf9a95f0a0521558d1bf853226f3a87c077d40cf3df73199ba211102
    • Tokenlon / imBTC incident notice documenting suspension timeline: 2020-04-19 notice “About Recent Uniswap and Lendf.Me Reentrancy Attacks”
  • Response / mitigation:
    • Lendf.Me paused protocol functions after the exploit.
    • Tokenlon’s public timeline shows how unstable the integration boundary already was operationally: imBTC transfers were first suspended at 12:49 on 2020-04-18 after the Uniswap incident, resumed at 17:00 after partner confirmations, then suspended again at 10:12 on 2020-04-19 once Lendf.Me reported its own reentrancy attack.
    • dForce later reported that all stolen funds had been recovered and that by 2020-05-04 users had been made whole.
    • The broader lesson dForce itself highlighted was tighter audit process, risk management, and more careful treatment of composability hazards between token standards and DeFi contracts.

Why this incident matters

  • It is one of the clearest early lending-market examples of ERC-777 sender-hook reentrancy turning stale collateral accounting into protocol-wide bad debt.
  • Unlike a plain withdraw()-before-balance-update theft, this case shows how a dust deposit can be used to overwrite a prior real withdrawal and manufacture collateral that only exists in storage.
  • Alongside the later C.R.E.A.M. AMP exploit, it is a canonical reminder that token-standard integration risk is often lending-protocol risk in disguise.

Pattern tags:

  • Sender-side callback-capable collateral: the exploitable control transfer happened in the ERC-777 tokensToSend() hook triggered during transferFrom(), not in some obviously dangerous receiver-only path.
  • Stale-state collateral restoration: the attacker did not need to invent collateral from nothing inside one call; they used re-entrant withdraw() to make the outer supply() restore obsolete pre-withdraw storage.
  • Cross-market counterfeit collateral monetization: the fake imBTC balance only mattered because sibling Lendf.Me markets were willing to treat it as borrow-authoritative collateral.
  • ERC1820-mediated semantic drift: the deeper integration failure was assuming an asset that looked ERC-20-compatible still preserved ERC-20-style inert transfer semantics.

Why the exploit worked

  • Lendf.Me put the external token transfer before authoritative collateral-state finalization, so the protocol surrendered control while its accounting still depended on stale local variables.
  • imBTC preserved ERC-777 sender-hook behavior through ERC1820, which meant transferFrom() was not a passive asset movement but an attacker-controlled execution window.
  • Re-entrant withdraw() mutated the true collateral state mid-flight, but the outer supply() path still resumed with the old balance snapshot and wrote that obsolete state back into storage.
  • The protocol also failed at market composition, not only local accounting: once counterfeit imBTC collateral existed in storage, other asset pools were happy to lend against it.
  • Operationally, the incident was already foreshadowed by the prior-day Uniswap/imBTC attack and Tokenlon’s temporary transfer suspension, but Lendf.Me still resumed exposure to the same callback-capable asset surface.

Detection and audit pivots

  • Inventory every listed collateral token for callback-capable semantics, including sender-side hooks and ERC1820-based behavior, not just explicit receiver callbacks.
  • In deposit paths, trace whether any authoritative balance, collateral, or share variable is cached before an external token transfer and later written back after control can be re-entered.
  • Fuzz deposit(dust) -> callback -> withdraw(full) and nearby sequences against listed collateral assets, then check whether storage ends with a larger balance than the protocol actually custody-holds.
  • Review cross-market monetization explicitly: if one exotic collateral market can be made locally inconsistent, how much sibling liquidity can that false state immediately borrow or drain?
  • Treat ERC20-compatible as a marketing phrase, not a security property. Ask what control-flow and callback semantics the token actually preserves at transfer time.
  • Ask whether protocol pause or asset-disable runbooks are fast enough to react when an upstream token integration shows the same exploit family somewhere else first.

Failure-prevention checklist

  • Finalize collateral ownership, account liquidity, and any borrow-authoritative storage before doTransferIn() or any token transfer path that can invoke sender- or receiver-side hooks. A dust top-up should never be able to reopen an old collateral snapshot and write it back over a real withdrawal.
  • Treat collateral listing as token-semantics admission, not just ticker admission. If an asset can dispatch ERC777 / ERC1820 hooks or any other callback-time logic, prove the supply, withdraw, and borrow paths remain safe at that callback edge or quarantine the asset from ordinary collateral use.
  • Reuse the same callback invariant across sibling markets. If one collateral market can be made locally false, other pools should not immediately lend stablecoins, WETH, or synthetic BTC against that false state.
  • Regression-test supply(real collateral) -> supply(dust) -> callback -> withdraw(full) -> borrow siblings sequences against hook-capable mocks in CI, not only same-function recursion on a toy withdraw path.
  • Avoid stale local-balance restore patterns. If a function caches a user balance or collateral figure before external control leaves, the post-callback write path should recompute from canonical storage or fail closed rather than overwrite with an obsolete snapshot.
  • Keep operational asset-disable and market-isolation runbooks ready for upstream semantic surprises. Tokenlon’s own timeline showed the same imBTC callback surface had already caused a near-identical exploit elsewhere less than a day earlier.
  • Prefer explicit collateral quarantine, tighter borrow caps, or isolated markets for assets whose transfer semantics are not as passive as the core lending code expects. Transfer succeeds is weaker than transfer cannot rewrite solvency timing.

Audit prompts

  • Which listed collateral assets can execute code during transferFrom(), and where is that fact documented in listing review instead of buried in token-standard folklore?
  • Does supply() or deposit() cache collateral balances, share balances, or exchange-rate inputs before an external transfer, then later write those cached values back after control could have been re-entered?
  • During a collateral-supply callback window, which sibling entrypoints remain reachable: withdraw(), borrow(), collateral toggles, liquidation helpers, or market-entry bookkeeping?
  • If one callback-capable collateral market becomes locally inconsistent for one block, how much sibling liquidity can that false state immediately borrow or drain before anyone can react?
  • Are tests proving only re-entrant withdraw changes state, or also the stronger property post-callback storage never ends higher than actual custody after deposit(dust) -> callback -> withdraw(full)?
  • What exact control would have stopped Lendf.Me from re-exposing imBTC after the prior-day Uniswap incident: listing review, asset quarantine, borrow-cap discipline, or a faster incident-response rule?
  • If an exotic collateral token keeps its own escrow sound while lending integrations fail, who owns the boundary that says this token is still unsuitable as immediate borrow-authoritative collateral here?
  • Are pause and asset-disable procedures fast enough to beat copycat exploitation when the same semantic surface breaks a neighboring protocol first?

Lessons that should already be standard

  • ERC20-compatible is not a security property. imBTC did not need to be economically broken; Lendf.Me only needed to misunderstand what a token transfer was allowed to do mid-function.
  • Sender-side hooks matter as much as receiver-side hooks. Many teams mentally picture callback risk only at the receiving contract, but Lendf.Me was broken by the sender-side ERC777 path hidden inside transferFrom().
  • A dust deposit can be a collateral-forgery primitive when stale state is restored after a real withdrawal. The exploit was not withdraw twice; it was withdraw once, then let the outer deposit resurrect the old collateral story.
  • Local accounting inconsistency becomes protocol-wide loss when sibling markets trust it as immediate borrow authority. The fake imBTC balance only mattered because every other pool was willing to lend against it.
  • Asset listing policy and solvency policy are the same conversation. If a token can rewrite control flow during transfer, that fact belongs in collateral factors, isolation rules, and market admission — not just in a token-compatibility spreadsheet.
  • Upstream warnings count. The prior-day Uniswap/imBTC incident was already telling every integrator that imBTC transfer semantics could rewrite DeFi timing assumptions.
  • The clean design question is blunt: if this token can run attacker code during transfer, what exact storage truth is already final before that code runs, and which sibling markets are still willing to trust it?

Internal linkages

Control surface