Furucombo Aave V2 Proxy Whitelist / Mutable-Implementation Approval Drain (2021-02-27)

  • Name: Furucombo Aave V2 proxy whitelist / mutable-implementation approval drain
  • Date: 2021-02-27
  • Protocol: Furucombo
  • Chain: Ethereum
  • Category: approval-by-address / mutable-code execution / whitelist drift / delegatecall storage-context confusion / approval drain
  • Loss: roughly $15M across 22 users and 21 assets, according to Furucombo’s postmortem
  • Summary: Furucombo’s proxy let users delegatecall into registry-approved handlers. The dangerous part was that the registry used the same isValid() gate for who may call the proxy and what the proxy may delegatecall into, so the Aave V2 LendingPool proxy (0x7d2768...) was treated as a valid handler even though it was only meant to be an approved integration endpoint. Because that Aave contract was itself an upgradeability proxy with a public initialize(address _logic, bytes _data) path, the attacker first used Furucombo’s own delegatecall surface to write a malicious implementation pointer into Furucombo proxy storage. A second call through the same still-whitelisted Aave proxy then delegatecalled into attacker code, which used victims’ standing token allowances to execute transferFrom() drains. The result was not an Aave compromise; it was a Furucombo authorization failure where approved address stopped meaning approved behavior once proxy indirection and caller-controlled storage context were combined.
  • Trigger / entry point:
    • Furucombo’s batchExec() path delegated execution to arbitrary registry-approved handler addresses from the main proxy at 0x17e8Ca1b4798B97602895f63206afCd1Fc90Ca5f.
    • The registry at 0xd4258B13c9FAdb7623cA4B15DdA34b7b85B842C7 used one isValid() check for both trusted callers and trusted delegatecall targets, so the Aave V2 LendingPool proxy at 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9 became a valid Furucombo handler.
    • The Aave V2 proxy exposed a public initialize(address _logic, bytes _data) function that only required its implementation slot to be unset in the current storage context.
    • Many users had already granted broad or effectively unlimited ERC-20 allowances to the Furucombo proxy, so once attacker logic executed in that proxy’s context, token theft reduced to ordinary transferFrom() calls.
  • Root cause:
    • Furucombo authenticated a handler by address, not by the stable code identity and execution envelope that would actually run under delegatecall.
    • The protocol conflated approved integration endpoint with safe delegatecall target, even though the approved address was an upgradeable proxy whose behavior depended on mutable implementation state.
    • delegatecall into the Aave proxy made the Aave proxy read and write its EIP-1967 implementation slot inside Furucombo’s storage, not Aave’s own storage, so Furucombo unknowingly gave a whitelisted external proxy the ability to install attacker logic in Furucombo context.
    • The exploit only monetized because users had already approved the Furucombo proxy to move many tokens on their behalf.
  • Exploit path:
    1. The attacker deployed a malicious implementation contract at 0x86765DDe9304bEA32f65330d266155c4Fa0c4f04 and prepared a Furucombo call that used the whitelisted Aave V2 LendingPool proxy as the sole handler.
    2. In setup transaction 0x6a14869266a1dcf3f51b102f44b7af7d0a56f1766e5b1908ac80a6a23dbaf449, the attacker made Furucombo delegatecall into the Aave proxy and invoke initialize(attackerImplementation, "").
    3. Because the call happened under delegatecall, Aave’s proxy code checked the implementation slot in Furucombo storage, found it unset, and wrote the attacker-controlled implementation address into Furucombo’s EIP-1967 implementation slot.
    4. In subsequent drain transactions such as 0x8bf64bd802d039d03c63bf3614afc042f345e158ea0814c74be4b5b14436afb9 and 0x5af11a27e98a167b61b01fea093cf612d5ec76c20fd2032f2d1aa49c8b1ee529, the attacker again routed execution through the same whitelisted Aave proxy.
    5. This time the Aave proxy fallback performed a second delegatecall to the implementation address stored in Furucombo storage, which now pointed to attacker code rather than Aave logic.
    6. The malicious implementation executed in Furucombo proxy context and called ERC-20 transferFrom() against victims who had approved the Furucombo proxy, draining assets including stETH, USDC, USDT, DAI, CRV, LINK, cTokens, aTokens, and other positions.
    7. Furucombo removed the Aave V2 LendingPool proxy from its registry after discovery, causing later attacker attempts such as 0xfaf46447572617a7eef027be43525b3168e4b650c07a50338faa453dda1f1940 to revert.
  • What the attacker needed to be true:
    • A whitelisted Furucombo handler had to be an upgradeable or otherwise mutable execution surface rather than a tightly scoped inert adapter.
    • The whitelisted handler needed an initialization or implementation-selection path that would behave differently when executed against Furucombo’s storage rather than its own deployed storage.
    • Furucombo had to let arbitrary users choose that handler in batchExec().
    • Victims needed pre-existing allowances to the Furucombo proxy so the attacker could convert code execution into token theft without any additional approvals.
  • Technical anatomy worth remembering:
    • This is a useful approval-by-address case because the address never changed. What changed was the logic that the approved address resolved to once its proxy machinery started reading from attacker-shaped Furucombo storage.
    • The decisive mistake was not just delegatecall is dangerous; it was delegatecall into an approved proxy whose mutable implementation slot now lives in the caller's storage context.
    • The registry-design bug mattered. If Furucombo had separated valid external caller from valid handler target, the Aave LendingPool proxy would not have become an attacker-choosable handler in the first place.
    • The exploit also preserves a recurring lesson from approval-drain incidents: wallet risk can sit latent in innocuous old allowances until a separate authorization bug turns a trusted router or proxy into attacker code.

Three execution-envelope promotions this incident collapsed

  1. A reputable, registry-valid address existed at all
    • Furucombo’s registry answered isValid(AaveV2LendingPoolProxy) == true, and that was a real operational fact in the live system.
    • At this stage the strongest true claim was only this address is on Furucombo's approved list, not delegatecalling this address will run one stable, storage-safe handler envelope inside Furucombo context.
  2. That approved address was promoted into canonical truth that this exact delegatecall envelope was safe to execute as Furucombo
    • This was the decisive failure. Furucombo’s own postmortem says the same isValid() gate was used for both trusted callers and trusted callees, so an Aave proxy that was acceptable as an integration endpoint inherited handler authority too.
    • Once execution happened under delegatecall, the meaningful object was no longer Aave proxy address in the abstract; it was Aave proxy fallback + foreign-storage EIP-1967 implementation slot + reachable initialize() branch + second-stage delegatecall target. Furucombo collapsed those two meanings and treated the first as if it safely committed the second.
  3. That accepted execution envelope still deserved immediate spender authority against users’ standing approvals
    • After the attacker installed their implementation into Furucombo’s own storage, the same still-whitelisted Aave proxy path retained full proxy-context reach and could call ERC-20 transferFrom() as the already-approved Furucombo spender.
    • The system therefore kept almost no blast-radius separation between one approved address was misinterpreted and many old wallet allowances are now live theft authority, which is why the exploit cashed out directly as token drains rather than getting trapped inside a narrower local invariant break.
  • Representative addresses and control points:
    • Furucombo proxy: 0x17e8Ca1b4798B97602895f63206afCd1Fc90Ca5f
    • Furucombo registry: 0xd4258B13c9FAdb7623cA4B15DdA34b7b85B842C7
    • Aave V2 LendingPool proxy used as handler: 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9
    • Attacker EOA: 0xb624E2b10b84a41687caeC94BDd484E48d76B212
    • Malicious implementation contract: 0x86765DDe9304bEA32f65330d266155c4Fa0c4f04
    • Replacement Furucombo proxy after incident: 0xA013AfbB9A92cEF49e898C87C060e6660E050569
  • Response / mitigation:
    • Furucombo removed the Aave V2 LendingPool proxy from the registry within about an hour of the first exploit and temporarily disabled Aave-related cubes.
    • The team redeployed the Furucombo proxy so old allowances against the compromised proxy address would no longer authorize future transfers.
    • The UI was changed to default toward approving only the amount being spent instead of encouraging broad standing allowances.
    • Furucombo said affected users would be compensated and that the proxy / registry design would be upgraded to close the integration bug.

Pattern tags

  • one registry approval bit covered both who may call the proxy and what code the proxy may delegatecall into, so caller trust silently became code-execution trust
  • whitelisted proxy indirection let an approved external address reinterpret Furucombo storage as its own upgrade state, then retarget later execution without changing the approved address
  • delegatecall into mutable proxy code turned integration allowlisting into root storage authority, so a reputable upstream endpoint became an attacker-chosen implementation installer in foreign context
  • standing ERC-20 allowances to the Furucombo proxy converted code-execution drift into immediate wallet drains once the execution envelope was captured

Why the exploit worked

  • Furucombo authenticated the wrong object. The protocol treated Aave LendingPool proxy address is valid as if that meant the code path reached by delegatecall is a safe Furucombo handler, but the real privileged object was the full delegatecall execution envelope: target code shape, storage context, upgrade semantics, and inherited spender authority.
  • The registry collapsed two different trust questions into one boolean. A contract that was acceptable as an integration endpoint or caller was not automatically acceptable as a delegatecall target with Furucombo’s own storage and token-spender authority attached.
  • Proxy indirection made the approved address non-binding. The Aave proxy address stayed constant, but under delegatecall its implementation slot lived in Furucombo storage, so initialize() became a way to install attacker code inside the already-approved execution path.
  • Furucombo’s privilege boundary also sat outside its own contracts in users’ old allowances. Once attacker code ran in proxy context, the exploit no longer needed fresh signatures or approvals; it only needed to exercise ERC-20 rights users had already delegated to the proxy.
  • The resulting incident was therefore bigger than initializer bug or whitelist bug in isolation. Address approval, proxy mutability, foreign storage context, and ambient spender authority were one joined authorization failure.

Detection and audit pivots

  • Separate valid caller, valid external integration, and valid delegatecall target inventories during review. If one registry bit authorizes more than one privilege shape, assume trust-surface confusion until proven otherwise.
  • Treat every whitelisted proxy, beacon, router, or module as a live code-identity problem rather than a static address-book entry. Review what storage it reads under delegatecall, who controls implementation changes, and whether initialization or upgrade branches behave differently in foreign context.
  • Compare the execution primitive with the reputation heuristic. Aave is trusted may justify a plain external call to a known interface; it does not justify delegatecall into an upgradeable proxy with caller storage authority.
  • Enumerate all standing user approvals to privileged routers and proxies, then ask what happens if one approved execution path drifts. Many integration bugs only become protocol-scale once old wallet allowances are attached.
  • Search for initializer and implementation-setting code reachable through user-chosen delegatecall targets, even when those targets are thought to be already initialized in their own native deployments.
  • Review post-incident containment speed as part of the design surface: if removing one registry entry instantly halts further drains, the registry was already acting as a root execution-control plane and deserves root-grade monitoring.

Failure-prevention checklist

  • Split approval domains by privilege shape. Use separate allowlists and review criteria for external callers, plain call integrations, and delegatecall-capable handlers.
  • Pin delegatecall targets to stable execution envelopes: expected runtime hash or implementation family, allowed selectors, storage assumptions, and whether proxy indirection is forbidden outright.
  • Avoid delegatecalling into third-party upgradeable proxies whenever possible. Wrap upstream integrations in local adapters with narrow, audited logic instead of inheriting foreign upgrade semantics and storage expectations.
  • Reject or quarantine targets with public initializer, implementation-setting, module-install, or fallback-dispatch behavior unless those paths are proven harmless under foreign storage context.
  • Treat standing ERC-20 approvals as part of the protocol blast radius. Minimize default approval size, rotate spender addresses after incidents, and design for narrower per-action approval objects where possible.
  • Add runtime monitoring for handler registry changes, unexpected delegatecall targets, implementation-slot writes in proxy storage, and unusual bursts of transferFrom() executed from routing contracts.
  • Test hostile-composition cases explicitly: whitelisted proxy target, foreign-context initializer, changed implementation pointer, and attacker-controlled second-stage delegatecall should all be invariant tests, not just audit thought experiments.
  • Preserve a kill switch that can deauthorize one execution surface quickly without requiring whole-protocol shutdown, but do not mistake that for sufficient prevention.

Audit prompts

  • Does the system distinguish safe to call from safe to delegatecall, or does one allowlist silently authorize both privilege shapes?
  • If a whitelisted target is itself a proxy, beacon, or dispatcher, what exact code will run under delegatecall today, and what can change that tomorrow without changing the address?
  • Which storage slots from the target contract become meaningful when executed in the proxy’s context: implementation slot, admin slot, initialization flags, module pointers, allowance bookkeeping, or auth config?
  • Can any user-chosen target execute an initializer or upgrade-like path whose safety assumption is only true in the target’s own storage context?
  • What wallet authority is attached to successful execution here: unlimited token allowances, NFT approvals, permit-style rights, vault shares, or router spend permissions?
  • If the approved integration were compromised only by being a delegatecall target, not by being externally called, would current review tooling and registry semantics catch that difference?
  • What telemetry would show approved address still the same, executed code meaning has changed before funds move: implementation-slot writes, unusual selector use, or new fallback dispatch behavior?
  • Can defenders revoke one bad handler fast enough to beat repeated drain attempts, and are those revocation rights monitored like root privileges?

Lessons that should already be standard

  • A reputable upstream contract is not automatically a safe delegatecall target. Trust in business logic does not transfer to trust in foreign-context storage behavior.
  • Approved address is a weak security object when proxy indirection or upgrade semantics sit behind it. The safe question is what exact code and privilege shape will execute, not whose address book entry looked familiar.
  • Allowlist reuse is dangerous. A boolean that means both may interact with us and may execute as us usually collapses two different security reviews into one skipped review.
  • Initializers are contextual, not universal. A contract can be fully initialized in its own storage and still expose live setup authority when executed through someone else’s storage via delegatecall.
  • Old approvals are part of today’s exploit surface. Wallet risk often matures long before the triggering bug appears, then cashes out the moment one trusted spender’s execution boundary drifts.
  • Integration review has to model composition, not only standalone code quality. The sharp question is often what does this upstream contract become when our protocol gives it our storage and our approvals?
  • Fast registry removal helped contain Furucombo, but it also proves the registry was a root execution-control plane. Systems should monitor and harden such registries accordingly.

Why this incident matters

  • It preserves a sharp non-governance instance of the approved address, different executed code family. Tornado Cash shows the metamorphic-governance version; Furucombo shows the integration-whitelist / proxy-indirection version.
  • It demonstrates that upstream protocol reputation is not a sufficient whitelist heuristic. Aave’s code was not compromised, but whitelisting Aave’s proxy as a delegatecall target still became a code-smuggling path.
  • It is also a good reminder that approval-drain risk often begins earlier than the exploit transaction. The theft became possible only because many users had left the Furucombo proxy with standing permission to move assets.
  • The preserved healthy boundary was not just separate caller allowlists better. It was authenticate one exact delegatecall execution envelope and keep wallet-spender authority narrower than raw standing allowance, so an approved integration address could not silently become attacker code with old approvals attached.

Internal linkages

Control surface