Clober DEX Liquidity Vault / Rebalancer burnHook Malicious-Strategy Reentrancy Drain (2024-12-10)

  • Name: Clober DEX Liquidity Vault / Rebalancer burnHook malicious-strategy reentrancy drain
  • Date: 2024-12-10
  • Protocol: Clober DEX Liquidity Vault / Rebalancer
  • Chain: Base
  • Category: reentrancy / arbitrary strategy admission / hook-ordering failure / liquidity-vault drain
  • Loss: about 133.7 ETH on Base, roughly $501k at the time
  • Summary: Clober’s Base-side liquidity vault let anyone open a pool with an arbitrary strategy contract, then trusted that strategy during the LP burn flow. In the vulnerable Rebalancer, burning LP shares did not fully close the accounting boundary before calling pool.strategy.burnHook(...). The attacker flash-borrowed 267.4 WETH, opened a WETH/fake-token pool with a malicious strategy they controlled, minted LP shares, and then used burnHook to re-enter the burn path while the first burn still relied on stale reserve state. That let the inner and outer burns both cash out against effectively the same WETH reserves. The attacker withdrew about 401.1 WETH from a pool funded with only 267.4 WETH, repaid the flash loan principal, and kept roughly 133.7 ETH as profit.
  • Trigger / entry point:
    • The exploited contract was Clober’s Base Rebalancer / Liquidity Vault at 0x6A0b87D6b74F7D5C92722F6a11714DBeDa9F3895.
    • Public incident reports identify the main exploit transaction as 0x8fcdfcded45100437ff94801090355f2f689941dca75de9a702e01670f361c04.
    • Clober’s open() path accepted a caller-supplied strategy address for a new pool.
    • The attacker used that freedom to register their own malicious strategy contract, then later invoked burn() so the vault would call back into attacker-controlled logic through burnHook.
  • Root cause:
    • Clober treated a user-supplied strategy contract as trusted execution inside the LP lifecycle.
    • The burn path handed control to pool.strategy.burnHook(...) before the protocol had made the whole burn operation non-reenterable in practice.
    • Because the callback fired while the first burn still depended on pre-finalized accounting, the attacker could re-enter burn() and make the vault compute a second withdrawal against reserve state that had not been safely sealed against that nested call.
    • The deeper failure was not just missing nonReentrant; it was letting arbitrary per-pool strategy code sit directly on the asset-withdrawal boundary.
  • Exploit path:
    1. The attacker used Morpho Blue to flash-loan 267.4 WETH.
    2. They deployed or used a malicious strategy contract at 0x32Fb1BedD95BF78ca2c6943aE5AEaEAAFc0d97C1 and a fake paired token at 0xd3c8d0cd07Ade92df2d88752D36b80498cA12788.
    3. They called Clober’s open() flow to create a new pool keyed to the WETH/fake-token pair while setting the strategy to their malicious contract.
    4. They minted LP shares by providing 267.4 WETH and the matching nominal amount of the fake token.
    5. They initiated a burn() call for part of the LP position.
    6. During that burn, Clober invoked pool.strategy.burnHook(...) on the attacker-controlled strategy.
    7. The malicious hook re-entered the vault’s burn path before the first withdrawal flow had been safely closed, so the nested burn could still cash out against reserve/accounting assumptions the outer frame was already using.
    8. Across the inner and outer burns, the attacker extracted about 401.1 WETH from a pool that only ever held 267.4 WETH of real value.
    9. They repaid the 267.4 WETH flash loan and kept the surplus 133.7 ETH, then bridged proceeds from Base to Ethereum.
  • What the attacker needed to be true:
    • Clober needed to allow arbitrary caller-supplied strategy contracts during pool creation.
    • The strategy contract needed a reachable callback like burnHook during asset withdrawal.
    • The burn flow needed to be re-enterable, or otherwise still rely on stale reserve/accounting state when the hook fired.
    • The attacker needed one real asset side with meaningful value (WETH) and one attacker-controlled side that was cheap to fabricate.
    • The pool only needed enough real WETH for the protocol to overpay the nested burn sequence; the fake token side did not need outside market value.
  • Technical anatomy worth remembering:
    • This was not a generic random user drains arbitrary pool event. The attacker first created a pool whose strategy boundary they controlled, then used that boundary as the reentrancy trampoline.
    • The fake-token leg mattered because it let the attacker satisfy Clober’s pool-creation and minting flow without contributing a second real asset.
    • The profitable step was not the mint; it was the burn, where one LP position was unwound through nested callbacks against incompletely finalized reserve logic.
    • The incident is a useful reminder that plugin-style strategy architecture widens the attack surface even when the strategy interface itself looks narrow. A hook that is “just for strategy bookkeeping” becomes a custody surface the moment it runs during mint or burn.
    • Reports on the exact storage-write ordering differ in emphasis, but they agree on the operational invariant failure: burnHook was reachable early enough for attacker-controlled code to re-enter and make the vault overpay WETH on exit.
  • Representative transactions and addresses:
    • Vulnerable Rebalancer / Liquidity Vault: 0x6A0b87D6b74F7D5C92722F6a11714DBeDa9F3895
    • Exploit transaction: 0x8fcdfcded45100437ff94801090355f2f689941dca75de9a702e01670f361c04
    • Attacker Base EOA: 0x012Fc6377F1c5CCF6e29967Bce52e3629AaA6025
    • Attack strategy contract: 0x32Fb1BedD95BF78ca2c6943aE5AEaEAAFc0d97C1
    • Fake paired token used in public analyses: 0xd3c8d0cd07Ade92df2d88752D36b80498cA12788
    • Post-bridge Ethereum recipient addresses highlighted in incident reports: 0x7760d838192f6e526721a0f6b160627bae989a3e and 0x711c87a0767101fa6f3893facb670b5689621e23
  • Response / mitigation:
    • Clober publicly disclosed the incident, said the breach was limited to the Base Liquidity Vault, and stated that the core order-book protocol remained unaffected.
    • The team offered the attacker a 20% bounty in exchange for returning the remaining funds.
    • Post-incident discussion focused heavily on the gap between prior audits and the code/users actually exposed to the exploit path, especially around post-audit changes and the live hook surface in the vault.

Pattern tags

Why the exploit worked

  • open() treated strategy like a configuration field rather than like a trust boundary. Once the attacker could register their own contract there, Clober had already admitted untrusted code onto the mint / burn path of a real-asset pool.
  • The burn flow then authenticated the wrong object. It acted as if this contract satisfies the strategy interface for this pool also meant this callback is safe to run before reserve and withdrawal state are sealed, which is exactly the semantic leap the attacker monetized.
  • burnHook fired while the outer burn still depended on pre-finalized reserve state, so the nested burn inherited a live withdrawal frame that still described the pool as if the first withdrawal had not already spent part of its WETH-side inventory.
  • The fake-token leg mattered because it collapsed the attacker’s capital requirement to one real asset side. Clober did not need both pool assets to be money-good for the burn to overpay; it only needed the WETH-side reserve math to remain authoritative during re-entry.
  • Put differently: the hook was not ancillary bookkeeping. It was an execution surface sitting directly on the custody boundary, and the vault let that surface observe and reuse half-finished withdrawal truth.

Detection and audit pivots

  • Treat user-supplied plugin endpoints like custody surfaces when they run during mint, burn, rebalance, withdraw, or any path that computes how much real inventory leaves the system.
  • Fuzz full pool-lifecycle sequences rather than isolated function calls: open(attacker strategy) -> mint(real asset + fake asset) -> burn() -> burnHook() -> nested burn() is the meaningful adversarial trace here.
  • Ask whether interface conformance is being mistaken for trustworthiness. If strategy, hook, market, or adapter is accepted because it exposes the right methods, what prevents a malicious implementation from re-entering or redefining the economic meaning of the flow?
  • Audit reserve sealing order, not just transfer order. The critical question is what exact reserve / share / withdrawal object remains authoritative when the callback fires, whether the external edge is a token transfer, a strategy hook, or both.
  • Model one-real-asset / one-worthless-asset pools explicitly. If a vault can be opened against an attacker-fabricated sidecar asset, then the real review question is whether the protocol can be tricked into overpaying the money-good side on exit.
  • Review whether flexible strategy execution is inventory-scoped. The healthier architecture is usually to run arbitrary plugin logic in a stripped-down trampoline that cannot directly inherit reserve-bearing burn authority from the vault.

Why this incident matters

  • It preserves a very clean attacker-chosen plugin hook becomes the reentrancy primitive variant of a familiar bug class.
  • It shows that letting users install arbitrary strategy code is not merely an extensibility feature; it becomes part of the protocol’s withdrawal trust boundary.
  • It is also a compact example of why fake-asset pool legs matter in exploit design: the attacker only needed one side of the pool to be real, as long as the vulnerable burn math would still honor LP claims against the real side.
  • The sharper healthy-side lesson is architectural: when flexible per-pool strategy logic is a product feature, the vault still has to prove that strategy execution never inherits reserve-bearing authority broad enough to turn one in-flight burn into two withdrawals.

Failure-prevention checklist

  • Treat strategy admission as privileged code admission, not as harmless pool configuration. If user-created pools may point at strategy code, the vault should either whitelist reviewed implementations or strictly confine strategy execution to an inventory-free trampoline that cannot inherit burn or withdrawal authority.
  • Seal reserve, share-supply, and payout state before burnHook or any other strategy callback can run. The invariant to write down is not burn finishes correctly; it is after every callback edge, no nested burn can still redeem the same WETH-side inventory again.
  • Separate flexible strategy bookkeeping from live custody math. If a hook only exists to update strategy-side state, it should run after vault-side burn accounting is already final or receive only pre-committed, non-authoritative data.
  • Model one-real-asset / one-worthless-asset pool creation explicitly in tests and reviews. If an attacker can fabricate one leg of the pair, the protocol must still prove that LP mint/burn cannot overpay the money-good side.
  • Apply reentrancy controls at the full mint/burn lifecycle boundary, not only around one internal helper. The dangerous question is whether any nested call can still observe reserve truth that an in-flight withdrawal is already spending.
  • Make plugin execution inventory-scoped. A strategy callback should never inherit ambient approval, vault balances, or reserve-accounting authority broader than the exact asset delta already committed for that step.
  • Re-run security review whenever post-audit product changes expand hook reachability, strategy mutability, or user-controlled endpoint admission. Audited interface is not the same thing as audited live trust boundary.

Audit prompts

  • Which exact reserve, share, and payout variables are still authoritative when burnHook fires, and could a nested call spend them twice?
  • Is strategy being authenticated as safe code to run on a withdrawal boundary, or merely as a contract that implements the expected ABI?
  • Can a user-created pool pair one real asset with one attacker-fabricated asset and still obtain LP claims that the vault will honor against the real side during burn?
  • If strategy code re-enters burn, mint, rebalance, or any sibling helper, what object proves the outer frame has already consumed its withdrawal right?
  • Does the protocol expose any ambient approvals, vault inventory, or temporary capability bits to plugin code that are broader than the single step being executed?
  • Which product or deployment changes occurred after audit, and did any of them widen strategy admission, hook timing, or burn-path authority without a fresh hostile-plugin review?
  • Could the same architecture fail even with a nonReentrant modifier if strategy callbacks still inherit too much pre-committed inventory or stale reserve truth through another path?

Lessons that should already be standard

  • User-extensible strategy systems are custody systems the moment they run during mint, burn, or rebalance. Plugin is a product word, not a trust-boundary exemption.
  • Interface conformance is not behavioral trust. A contract satisfying burnHook(...) says nothing about whether it is safe to call before reserves and payouts are final.
  • A fake asset leg is often enough. If exit math can overpay the real side, the attacker does not need a second money-good asset to make the exploit economic.
  • Reentrancy review has to follow authority, not just function names. The important question is which in-flight withdrawal object stays spendable across the callback, not whether one helper remembered to add a guard.
  • Post-audit hook-surface drift is itself a recurring exploit class. Teams keep shipping architectures that were reviewed as static integrations and later become user-programmable reserve boundaries.
  • The healthy design target is stronger than strategy can’t do anything too weird: strategy execution should never inherit reserve-bearing authority broad enough to turn one honest LP burn into two withdrawals.

Internal linkages

Control surface