3.10.3 bZx (February 2020)

The bZx attacks of February 2020 established two attack primitives that have defined DeFi exploitation ever since: flash loans as a way to wield arbitrary capital for a single transaction, and on-chain DEX prices as oracles that can be manipulated within that same transaction. By dollar value, the bZx incidents were small — under $1M total across two attacks. By influence, they were enormous. Almost every major DeFi exploit in the years that followed used one or both of these patterns.

The attacks also demonstrated something the smart-contract community had not fully internalized: composability is an attack surface. bZx's contracts were not exploited in isolation. The attacker chained calls across dYdX, Compound, Kyber, Uniswap, and Synthetix — five separate protocols — within a single transaction. Each protocol's design was internally sound; the failure mode emerged from how they fit together. The bZx team had not designed their oracle assuming a flash-loan capable adversary could move on-chain prices within a single transaction. After bZx, no DeFi protocol could responsibly make that assumption again.

Section 3.8.5 (Oracle & Price Manipulation) treats this case as foundational. Section 3.11.4 covers flash loans as a capital primitive. The case below traces the mechanics that made these defenses necessary.

Context

bZx (also marketed as "Fulcrum") was a decentralized lending and margin-trading protocol on Ethereum mainnet. Users could deposit assets to earn interest, borrow against collateral, or open leveraged positions. The protocol used external price feeds from Kyber Network (which routed to Uniswap reserves) to determine the value of collateral and the prices at which trades executed.

By February 2020, DeFi was emerging as a recognizable category but was still small by later standards. Total value locked across all DeFi protocols was around $1B. bZx had 27,000 ETH ($7M at the time) locked in its lending pools. The protocol had been audited by ZK Labs.

The attacks took place during ETHDenver, when much of the bZx team was attending the conference. The timing was almost certainly deliberate — exploiting a protocol while its team is on the other side of the country, distracted by conference activities, slows the response.

Two attacks, separated by four days:

  • Attack 1: February 14-15, 2020 — 1,193 ETH ($370K profit) drained via a pump-and-arbitrage scheme exploiting a slippage check that didn't fire
  • Attack 2: February 18, 2020 — 2,378 ETH ($630K profit) drained via direct oracle manipulation of the sUSD price

Combined loss: approximately $954,000. The bZx insurance fund (10% of all earned interest, accumulated for exactly this kind of contingency) eventually covered the losses to users.

The bZx attacks were the first time most of the smart-contract community saw flash loans used offensively. Aave's flash loans had been live for less than two months; dYdX's "flash" feature (which the attacker actually used) had a smaller profile. The attacks made clear that any DeFi protocol with on-chain inputs was, from that moment forward, operating under the assumption that any attacker could borrow tens of millions of dollars for a single transaction at near-zero cost.

Attack 1: The Pump-and-Arbitrage (February 14-15, 2020)

The first attack exploited bZx's leveraged short feature combined with a slippage-check bug. The attack was complex — six steps across five different protocols — but the underlying logic was a pump-and-dump executed and unwound atomically.

The Setup

bZx's leveraged short positions worked as follows: the user deposited margin in ETH; bZx borrowed WBTC from the user's position by selling additional ETH for WBTC via Kyber (which routed to Uniswap). If the ETH/BTC ratio fell, the short profited; if it rose, the position would be liquidated.

The slippage protection: bZx's contracts checked that the executed trade price was within some tolerance of the expected price. If slippage was too high, the trade reverted. This check was the only protection against the trade itself being abusive.

The bug: the slippage check did not fire for overcollateralized positions. The bZx team had reasoned that an overcollateralized position posed no risk to the protocol — if the trade went poorly, the borrower's collateral would cover the loss. So the check was skipped for those cases. But "the protocol covers the loss" assumes the trade itself was honest. When the trade itself is the attack — moving on-chain prices to enable other exploits — skipping the slippage check is the bug.

The Attack Flow

The attack executed as a single transaction:

  1. Flash loan from dYdX. The attacker borrowed 10,000 ETH (~$2.7M at the time) from dYdX's flash loan facility. No collateral required; just had to be repaid in the same transaction.

  2. Borrow WBTC from Compound. The attacker deposited 5,500 ETH as collateral on Compound and borrowed 112 WBTC. Compound's lending was working correctly — the loan was overcollateralized at the actual market price.

  3. Open a leveraged short on bZx. The attacker sent 1,300 ETH to bZx and opened a 5x short position on the ETH/BTC ratio. bZx's logic: to open the short, it needed to acquire WBTC. It would do this by selling some of the attacker's deposited ETH on Kyber.

  4. bZx routes its trade to Uniswap. bZx forwarded the trade to Kyber, which forwarded it to its Uniswap reserve. The trade was large enough (5,637 ETH being swapped for ~51 WBTC) to dramatically move the price on Uniswap's small WBTC/ETH pool. The slippage check would normally have caught this — but the position was overcollateralized at the quoted price, so the check didn't fire.

  5. Arbitrage the now-mispriced Uniswap pool. With the Uniswap WBTC/ETH price now badly skewed (WBTC trading at roughly 3x its market value on Uniswap relative to other venues), the attacker sold their Compound-borrowed 112 WBTC back into Uniswap, receiving substantially more ETH than they would have at fair prices.

  6. Repay the flash loan, keep the profit. The attacker repaid the 10,000 ETH flash loan to dYdX with their newly acquired ETH. Net profit: ~1,193 ETH.

bZx was left holding an under-collateralized short position. The attacker's "loan" from bZx — the WBTC they had effectively borrowed by opening the short — was now worth significantly more in real terms than the collateral the attacker had posted. bZx's lending pool absorbed the difference: approximately 620 ETH of bad debt.

Why It Worked

The slippage check bypass was the proximate cause. The deeper cause was that bZx's logic treated the trade's slippage as a risk to the protocol rather than as a signal that the trade itself was abusive. Skipping the check for overcollateralized positions made sense if you assumed an honest user. With a flash-loan-equipped attacker, the assumption broke.

Attack 2: Direct Oracle Manipulation (February 18, 2020)

The second attack was cleaner and demonstrated the core oracle-manipulation pattern that has since become the canonical DeFi exploit shape. The attacker took out a flash loan, used part of it to move the price on bZx's oracle source, then used the manipulated price to extract value from bZx.

The Setup

bZx used Kyber Network to query the price of sUSD (Synthetix's stablecoin). Kyber routed the query through its various reserves — and one of those reserves was a Uniswap pool that the attacker could manipulate.

The flaw was structural: bZx was using the spot price from a single liquidity pool as its authoritative price feed. There was no time-weighting, no cross-source aggregation, no sanity checking. Whatever the pool said the sUSD/ETH price was at the moment of the query, bZx accepted as truth.

The Attack Flow

This attack also executed as a single transaction:

  1. Flash loan from bZx itself. The attacker borrowed 7,500 ETH directly from bZx's own lending pool. (The attacker had to repay it within the transaction, so this functioned as a flash loan.)

  2. Pump sUSD on Kyber. The attacker swapped 900 ETH for sUSD through Kyber, draining a Uniswap reserve and pushing the Kyber-quoted sUSD price up substantially. After this trade, Kyber reported sUSD trading at roughly $2 instead of its peg-near-$1 price.

  3. Buy sUSD at fair price via Synthetix. The attacker used Synthetix's depot contract to buy 943,837 sUSD by sending 3,518 ETH. Synthetix sold sUSD at its actual peg value, not the manipulated Kyber price. The attacker now held nearly a million dollars worth of sUSD acquired at fair prices.

  4. Deposit sUSD as collateral on bZx, borrow ETH. The attacker posted 1,099,841 sUSD to bZx and borrowed 6,796 ETH. bZx valued the sUSD using its Kyber oracle — which still showed sUSD at the manipulated ~$2 price. So bZx thought the attacker had posted ~$2.2M of collateral and let them borrow ~$1.8M of ETH (6,796 ETH at then-current rates).

  5. Repay the flash loan with the borrowed ETH. 7,500 ETH had been borrowed in step 1; the attacker repaid it. The borrowed ETH from step 4 (6,796 ETH) plus the ETH they had used to pump (which they kept) more than covered it.

  6. Walk away with the profit. Net profit: 2,378 ETH (~$630K at the time).

bZx was left holding 1.1M sUSD as collateral against an under-collateralized 6,796 ETH loan. Because sUSD's actual market price was ~$1, the collateral was worth ~$1.1M; the loan was worth ~$1.8M. The difference — approximately $700K — was the protocol's loss.

Why It Worked

The attack required no bugs in bZx's contracts. Every individual operation worked correctly. The vulnerability was in the assumption that an oracle reading at a single point in time, from a single pool, would be honest. The flash loan made that assumption obviously wrong — anyone with access to a flash loan could move on-chain prices, and the oracle's reading would always be honest as of that exact moment, regardless of whether that moment had been manipulated.

This is the canonical oracle manipulation pattern, and it has been repeated across dozens of protocols since. Section 3.8.5 covers the modern defenses.

Vulnerable Code

A simplified rendering of the bZx oracle pattern that enabled Attack 2:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IKyber {
    function getExpectedRate(address src, address dst, uint256 srcAmount)
        external view returns (uint256 expectedRate, uint256 slippageRate);
}

contract VulnerableLending {
    IKyber public oracle;
    mapping(address => uint256) public collateralSusd;
    mapping(address => uint256) public borrowedEth;

    function getSusdPriceInEth() public view returns (uint256) {
        // BUG: single spot-price query from a manipulable source
        (uint256 rate, ) = oracle.getExpectedRate(SUSD, ETH, 1e18);
        return rate;
    }

    function borrow(uint256 collateralAmount, uint256 ethAmount) external {
        // Transfer in sUSD collateral
        IERC20(SUSD).transferFrom(msg.sender, address(this), collateralAmount);
        collateralSusd[msg.sender] += collateralAmount;

        // Compute collateral value using the (manipulable) oracle
        uint256 susdPrice = getSusdPriceInEth();
        uint256 collateralValueEth = (collateralAmount * susdPrice) / 1e18;

        // Check overcollateralization
        require(collateralValueEth >= ethAmount * 2, "insufficient collateral");

        // Lend out ETH
        borrowedEth[msg.sender] += ethAmount;
        payable(msg.sender).transfer(ethAmount);
    }
}

The bug isn't in the code — it's in the assumption that oracle.getExpectedRate(...) returns a meaningful price. When the attacker has just manipulated the underlying liquidity pool, the returned rate reflects the manipulation, not the market.

Root Cause

The bZx attacks had several root causes:

1. On-chain spot prices used as oracles (Section 3.8.5). This is the fundamental flaw. AMM spot prices are a function of pool reserves; reserves change with every swap; therefore spot prices are manipulable by anyone with enough capital to make a large swap. Flash loans make that capital available to anyone.

2. Single-source oracle. bZx queried Kyber as its sole price feed. Kyber's response depended on its underlying reserves, primarily Uniswap. There was no cross-source aggregation, no fallback, no sanity check against an off-chain feed.

3. No time-weighted averaging. A spot price reflects exactly one moment. A time-weighted average price (TWAP) over a meaningful window would require the attacker to sustain the manipulation for that window, which is far more expensive than manipulating a single block.

4. Slippage check bypassed for overcollateralized trades (Attack 1 only). The first attack additionally exploited a logic bug: the slippage check that would have caught the price impact was skipped because the trade was "overcollateralized." But the slippage was the attack, not a side effect of it.

5. Composability without adversarial threat modeling. bZx's contracts assumed honest interactions with Kyber. Kyber's contracts assumed honest interactions with Uniswap. Uniswap had no concept of who its users were. Each contract was correct in isolation; the failure emerged from their composition.

6. Flash loans as an attack primitive. Aave introduced flash loans in January 2020. dYdX had an equivalent capability. Within weeks, both were being used as the capital base for attacks. The DeFi community had not yet absorbed that unlimited capital for a single transaction was now available to any attacker.

Lessons

The bZx attacks changed how DeFi protocols approach oracles. The conventions that emerged:

1. Never use spot prices from a single DEX as an oracle. This became the canonical first lesson. Modern DeFi protocols use either off-chain aggregated oracles (Chainlink) or on-chain time-weighted averages (Uniswap V3 TWAPs) — sometimes both. Section 3.8.5 covers each approach.

2. Aggregate across multiple sources. Even with Chainlink as the primary oracle, sanity checking against an independent source catches single-feed failures. Section 3.8.5 covers cross-source deviation checks.

3. Time-weighted averages over meaningful windows. A 30-minute TWAP forces an attacker to sustain manipulation for 30 minutes — at the cost of arbitrage during that window. Section 3.11 will cover TWAP windowing in depth.

4. Assume flash-loan-equipped adversaries. Any protocol that reads prices, evaluates balances, or makes any decision based on chain state must assume that an attacker can manipulate that state within a single transaction. The "honest user" mental model died with bZx.

5. Composability requires adversarial threat modeling. When your contract interacts with another contract, model what happens if a third party manipulates the second contract's state before your call. The interactions are composable; so are the attacks.

6. Bug bounties and emergency pause mechanisms. bZx had an admin key that allowed pausing the protocol; the team used it to stop the attacks after they happened. This was the right design choice in 2020 and remains the right design choice now (with a multisig or timelock guarding the pause authority, as covered in Section 3.7.5).

7. Insurance funds for protocol losses. bZx had accumulated a 10% protocol fee in an insurance fund that ultimately covered user losses. This pattern has since been widely adopted — most lending protocols now maintain an insurance fund as standard.

Modern Reproduction

A simplified flash-loan oracle manipulation in modern Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IUniswapV2Pair {
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32);
    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}

interface IFlashLender {
    function flashLoan(uint256 amount) external;
}

// Vulnerable lending protocol that uses spot price as oracle
contract VulnerableProtocol {
    IUniswapV2Pair public immutable pool;
    mapping(address => uint256) public collateral;

    constructor(address _pool) {
        pool = IUniswapV2Pair(_pool);
    }

    function getPriceInWeth() public view returns (uint256) {
        (uint112 r0, uint112 r1, ) = pool.getReserves();
        // BUG: spot price from a single pool
        return (uint256(r1) * 1e18) / uint256(r0);
    }

    function borrow(uint256 collateralAmount, uint256 ethAmount) external {
        // Pull collateral
        IERC20(TOKEN).transferFrom(msg.sender, address(this), collateralAmount);
        collateral[msg.sender] += collateralAmount;

        // Value collateral using vulnerable oracle
        uint256 price = getPriceInWeth();
        uint256 collateralValueEth = (collateralAmount * price) / 1e18;

        require(collateralValueEth >= ethAmount * 2, "undercollateralized");
        payable(msg.sender).transfer(ethAmount);
    }
}

// Attacker contract that flash-loans and manipulates
contract OracleManipulator {
    function attack(
        IFlashLender lender,
        VulnerableProtocol target,
        IUniswapV2Pair pool,
        uint256 loanAmount
    ) external {
        // Take flash loan; lender will call this contract's callback
        lender.flashLoan(loanAmount);
    }

    function flashLoanCallback(uint256 amount) external {
        // 1. Swap large amount into the pool to manipulate spot price
        // 2. Now the oracle reads a manipulated price
        // 3. Deposit normal collateral on target, borrow against inflated value
        // 4. Swap back through the pool to recover (or repay the loan and keep the difference)
    }
}

The Chainlink-based defense:

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract SafeProtocol {
    AggregatorV3Interface public immutable priceFeed;

    function getPrice() public view returns (uint256) {
        (, int256 answer, , uint256 updatedAt, ) = priceFeed.latestRoundData();
        require(answer > 0, "invalid price");
        require(block.timestamp - updatedAt < 1 hours, "stale price");
        // Returned price reflects aggregation across many sources;
        // not manipulable by any single trade
        return uint256(answer);
    }
    // ... rest of protocol uses getPrice() instead of pool spot price
}

The fix moves the price source from a manipulable on-chain pool to an off-chain aggregated feed with staleness checks. This pattern — Chainlink with latestRoundData() + staleness check + sanity check — has become the canonical DeFi oracle pattern, directly traceable to bZx.

Cross-References

  • Oracle manipulation — Section 3.8.5 covers the full vulnerability class including modern defenses (Chainlink, TWAPs, multi-source aggregation)
  • Flash loans — Section 3.11.4 covers flash loans as a capital primitive in DeFi
  • Defensive patterns — Section 3.7.5 covers pause mechanisms and emergency response
  • Composability — Section 3.11.2 covers cross-contract composability and adversarial threat modeling
  • Subsequent oracle exploits — many later exploits used variations of the bZx pattern; Section 3.10.8 (Euler Finance) involves a related class of attack
  • Insurance and incident response — Section 2.9 covers incident response including the insurance-fund pattern that bZx pioneered