bZx / Fulcrum ETH-WBTC Margin Pump shouldLiquidate() Bypass (2020-02-15)
- Name: bZx / Fulcrum ETH-WBTC margin pump
shouldLiquidate()bypass - Date: 2020-02-15
- Protocol: bZx / Fulcrum
- Chain: Ethereum
- Category: unhealthy-position check bypass / slippage exploitation / toxic-order-flow leakage / flash-loan-assisted arbitrage
- Loss: commonly framed as roughly
$350Kof attacker profit after unwinding the Compound leg, while bZx’s lending side absorbed roughly$600K+of equity damage from a massively undercollateralized WBTC short position - Summary: In the first February 2020 bZx exploit, the attacker did not begin by spoofing an oracle. Instead, they used a 10,000 ETH dYdX flash loan to assemble a two-leg trade around Fulcrum’s
sETHwBTC5x margin product. After posting 1,300 ETH to open the short, bZx itself borrowed about 5,637.62 ETH and routed the trade through Kyber into Uniswap, receiving only about 51.3456 WBTC because the order was far too large for available liquidity. That trade should have left the new position immediately liquidatable, but bZx’s finalshouldLiquidate()sanity check was skipped becauseloanDataBytes.length == 0andsentAmounts[6] == sentAmounts[1]made therequire(...)short-circuit true. Once bZx had paid to move the WBTC/ETH market violently out of line, the attacker dumped 112 WBTC borrowed from Compound back into the same distorted Uniswap pool for about 6,871.41 ETH, repaid dYdX, and later unwound the profitable Compound leg while bZx kept the toxic short. - Trigger / entry point:
- Fulcrum’s ETH/WBTC 5x token path let a user open a very large leveraged short with only 1,300 ETH of posted capital and protocol-borrowed ETH providing the rest of the trade size.
- bZx routed the borrowed ETH through Kyber, which in practice selected the KyberUniswap reserve as the only route deep enough to fill the order.
- The protocol relied on a final
shouldLiquidate()check to reject any newly opened position whose slippage had already pushed it below maintenance margin. - A logic bug in
takeOrderFromiToken()let that final health check be skipped exactly on the path used by the attacker.
- Root cause:
- bZx let an enormous market order execute before proving that the resulting position was still solvent.
- The intended post-trade protection existed, but a Boolean short-circuit in
takeOrderFromiToken()treated the path as if no meaningful liquidation check was needed whenloanDataByteswas empty andwithdrawalAmount == newLoanAmount. - Once that check was skipped, the protocol effectively socialized the slippage from its own oversized Uniswap-routed swap into the lending pool, creating a toxic position the attacker could arbitrage against.
- This was adjacent to oracle risk but not the same thing as the 2020-02-18 bZx sUSD oracle attack. The central failure here was unhealthy-position admission after catastrophic execution slippage.
- Exploit path:
- The attacker executed transaction
0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f170a4bc8cd032f768fc5219838in Ethereum block9484688. - Inside the transaction, the attacker flash-borrowed 10,000 ETH from dYdX.
- The attacker deposited 5,500 ETH into Compound and borrowed 112 WBTC, creating inventory they could later dump into a distorted market.
- The attacker then used 1,300 ETH to open a 5x ETH/BTC short on Fulcrum via
mintWithEther, cascading intomarginTradeFromDeposit(). - bZx borrowed about
5,637.62376238 ETHon behalf of the position and routed the trade through Kyber, which sourced liquidity from the KyberUniswap reserve and exchanged that ETH for only about51.34557581 WBTC. - That execution implied roughly
109.8 ETH per WBTC, versus a normal market level closer to the high-30s ETH per WBTC at the time. The new position should have been instantly unhealthy because slippage had already eaten through the maintenance margin. - In
takeOrderFromiToken(), the checkrequire((loanDataBytes.length == 0 && sentAmounts[6] == sentAmounts[1]) || !OracleInterface(oracle).shouldLiquidate(...), "unhealthy position")passed without ever callingshouldLiquidate(), because the attacker used the exact branch where both left-side conditions were true. - With Uniswap now badly skewed by bZx’s own trade, the attacker sold the 112 WBTC borrowed from Compound back into the pool and received about
6,871.41273887 ETH. - The attacker repaid the 10,000 ETH dYdX flash loan, kept residual ETH in the transaction, and later repaid the Compound WBTC debt over time to unlock the far more valuable 5,500 ETH collateral.
- bZx was left holding the underwater side of the short: a position whose catastrophic execution slippage had already transferred value out to Uniswap and then to whoever sold WBTC back into the pool first.
- The attacker executed transaction
- What the attacker needed to be true:
- Fulcrum had to allow a large enough ETH/WBTC short that its own trade would move the destination pool far beyond the product’s safety margin.
- Kyber had to route the order into a liquidity source shallow enough that the protocol’s borrow-funded swap would create extreme slippage instead of reverting or being size-limited.
- The
takeOrderFromiToken()branch with emptyloanDataBytesandsentAmounts[6] == sentAmounts[1]had to remain reachable soshouldLiquidate()would never run. - A second venue leg had to exist for the attacker to monetize the distortion immediately; here that leg was the 112 WBTC hoarded from Compound.
- The whole sequence had to fit atomically around the dYdX flash loan so the attacker did not need to pre-fund 10,000 ETH.
- Technical anatomy worth remembering:
- This exploit is best remembered as
protocol pays catastrophic slippage on a leveraged trade, final health check is skipped, attacker harvests the mispricing from the other side. - The attacker did not need to pre-pump Uniswap before calling bZx. Fulcrum’s own borrowed-trade path created the distortion, and the attacker simply arranged to be the best-positioned seller into that distortion.
- Lev Livnev’s trace analysis showed why the safety check disappeared:
marginTradeFromDeposit()eventually set upsentAmounts[6] = sentAmounts[1], and becauseloanDataByteswas empty, the left side of therequirebecame true and short-circuited the liquidation call entirely. - PeckShield’s breakdown usefully separates the trade into five stages: flash-loan borrow, WBTC hoard on Compound, margin pump on bZx, WBTC dump on Uniswap, and flash-loan repayment.
- Economically, the exploit transferred value from bZx’s lending pool into Uniswap through a toxic trade, then captured a large share of that leaked value by dumping hoarded WBTC back through the now-mispriced pool.
- The case is a strong reminder that
post-trade solvency checksare part of the execution path, not optional cleanup. If a leveraged product can borrow first and validate later, then any branch that skips validation becomes a direct solvency bug.
- This exploit is best remembered as
- Representative transactions and artifacts:
- Main exploit transaction:
0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f170a4bc8cd032f768fc5219838 - Attacker EOA highlighted in public analyses:
0x148426fdc4c8a51b96b4bed827907b5fa6491ad0 - Primary attacker contract highlighted by Palkeo:
0x4f4e0f2cb72e718fc0433222768c57e823162152 - dYdX-calling helper contract highlighted by Palkeo:
0x0de0dd63d9fb65450339ef27577d4f39d095eb85 - Key vulnerable entrypoints discussed in public analyses:
mintWithEther,marginTradeFromDeposit(),takeOrderFromiToken(), andshouldLiquidate()
- Main exploit transaction:
- Response / mitigation:
- bZx paused affected protocol functionality during the incident response window.
- The team patched the condition so the health check would execute even on the branch that had previously been treated like an overcollateralized path.
- bZx also moved toward tighter oracle / trade protections and maximum trade-size constraints, recognizing that giant route-through-AMM execution was itself part of the danger surface.
- The broader lesson is to fail before honoring toxic execution, not after. If a route cannot support the trade size without destroying maintenance margin, the protocol should never finalize the position.
Pattern tags
Three execution-and-solvency promotions this incident collapsed
- A borrow-funded route existed: Fulcrum was willing to finance a 5x ETH/WBTC short, and Kyber’s route discovery found a real executable path into Uniswap even though that path was far too shallow for the size. The first dangerous promotion was treating
there is some route that fillsas if it were alreadythis route can support the financed position without consuming maintenance margin. - That toxic fill then became canonical position truth: after bZx sold about
5,637.62 ETHfor only51.3456 WBTC, the newly opened short should have failed admission as immediately unhealthy. Instead, theloanDataBytes.length == 0 && sentAmounts[6] == sentAmounts[1]branch lettakeOrderFromiToken()promotetrade already happenedintoposition may now existwithout forcingshouldLiquidate()to re-derive solvency from the realized fill. - Once that false position truth was accepted, it still inherited live lender-balance-sheet authority: bZx left the underwater short on its books while the attacker sold
112 WBTCinto the same distorted Uniswap pool and harvested the slippage that Fulcrum had already paid for. Healthy review should keep separateroute can execute,realized fill still supports a solvent position, andthat accepted position deserves lender-funded market exposure.
Why the exploit worked
- bZx let one execution path spend borrowed ETH into a real market before proving the resulting position still cleared maintenance margin. That made market impact part of the solvency boundary rather than an implementation detail.
- The intended safety check was only conditionally enforced. On the attacker-chosen
marginTradeFromDeposit(..., loanDataBytes = "")path,amountIsADeposit = trueeventually madesentAmounts[6] == sentAmounts[1], so therequire(...)intakeOrderFromiToken()treated the trade as safe without ever callingshouldLiquidate(). - Fulcrum sized the short against notional leverage, but the real route went through Kyber into Uniswap liquidity far too thin for the borrowed trade size. Once the protocol itself crossed that depth boundary, it authored the mispricing the attacker later harvested.
- The attacker did not need an independent oracle bug or a pre-manipulation phase. They only needed the protocol to honor its own toxic execution and leave enough time inside the same transaction to sell hoarded WBTC back into the skewed pool.
Detection and audit pivots
- Trace every borrow-funded trade path to the exact point where post-trade health is enforced. If a leverage-opening function can route assets out to live liquidity before the unavoidable solvency assertion runs, the protocol is lending against execution assumptions instead of settled collateral.
- Compare
quotedprice logic withrealizedroute shape. Here the dangerous object was not the mid-price read from Kyber but the actual executable depth of the Kyber-selected Uniswap reserve under full trade size. - Search for Boolean short-circuits around final solvency checks, especially branches that treat some input combinations as
obviously safeand therefore skip the canonical liquidation / health test. - Model same-transaction monetization after a protocol-authored bad trade. If the protocol can pay catastrophic slippage into a pool, ask who can immediately stand on the other side of that pool and capture the leak.
Failure-prevention checklist
- Make post-trade solvency validation mathematically unavoidable. No leverage-opening path should be able to satisfy a convenience branch that skips the same health check the core engine depends on.
- Bind maximum trade size to executable route depth, not only to nominal collateral factor or ex-ante quoted price. If the destination liquidity cannot absorb the protocol-sized order without crossing maintenance margin, revert before opening the position.
- Collapse
quote,execution, andhealthinto one path-complete risk model. The protocol should validate the exact route and realized fill assumptions it is about to honor, not a cleaner proxy price from earlier in the call chain. - Test protocol-funded slippage as a first-class attack model:
protocol opens oversized leveraged trade -> pool becomes distorted -> attacker harvests opposite side in same transaction. - Treat route-selection modules and aggregators as solvency-critical code. Kyber / router choice is not an execution detail once a leveraged product is willing to finance the trade from lender capital.
Audit prompts
- Which exact line proves a newly opened leveraged position is healthy after all route-dependent execution has already happened, and can any branch skip that line without reverting?
- Are quoted prices, reserve-selection logic, and realized executable depth all describing the same market object, or can the protocol validate against one view and settle against another?
- What is the largest borrow-funded order the protocol can push through each allowed route before maintenance margin is consumed by slippage alone?
- If the protocol itself moves the market during position opening, who can immediately capture the other side of that distortion inside the same transaction or same block?
- Do any
safe path,Kyber only,no extra calldata, or similar helper conditions silently stand in for the actual post-trade health assertion?
Lessons that should already be standard
- A leverage protocol does not get to outsource market-impact risk to
execution. If lender capital finances the trade, execution depth is part of solvency. Final safety check exists in codeis not enough; the meaningful question is whether every economically reachable path is forced through it.- Mid-price or aggregator quote sanity does not protect a system that will still honor route-specific toxic fills at full size.
- A protocol can create its own exploitable market dislocation. In this case the attacker mostly arranged financing and timing; Fulcrum itself paid to skew the pool.
- Post-trade validation belongs after the trade and before the position becomes real. Anything weaker is permission to socialize slippage into lenders.
Why this incident matters
- It is one of DeFi’s earliest canonical
flash loan + multi-protocol atomic exploitincidents, but the flash loan was only the financing convenience. - The deeper lesson is about execution-path solvency: a leveraged protocol cannot safely route huge trades into shallow liquidity and rely on a final conditional check unless that check is mathematically unavoidable.
- It also pairs cleanly with bZx’s second February 2020 exploit: first the protocol failed by admitting an already-unhealthy leveraged trade, then days later it failed by honoring manipulated collateral pricing.
Internal linkages
- Core recurring pattern: post-trade-solvency-validation-bypass.
- Positive-design companion: borrowed-execution-depth-and-post-fill-solvency-discipline — the preserved healthier question is how Fulcrum should have fused
route may be financed,this exact Uniswap-via-Kyber path can absorb the size, andthe realized short is still solvent after fillbefore lender-funded execution moved the market. - Closely related follow-up incident from the same protocol: bzx-susd-oracle-manipulation-2020-02-18.
- Adjacent pricing-risk family, but not the cleanest primary label here: oracle-manipulation.
- Adjacent accounting family: solvency-check-bypass.
Control surface
-
The leverage sat in Fulcrum’s leverage sizing, Kyber routing assumptions, allowable trade size relative to real liquidity, the placement and reachability of
shouldLiquidate(), and the protocol’s willingness to finalize a borrow-funded swap before proving the resulting position was still solvent. -
bZx did not merely suffer from
flash-loan complexity. It suffered from an execution-to-solvency bug: the protocol let its own oversized trade leak value out first and only conditionally checked whether the resulting position should exist at all. -
Sources:
-
Last reviewed: 2026-08-05 UTC