bZx Flash-Loan Oracle (2020)
Smart contract security audit by TierZero · 2023-02-15
Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.
Findings summary
Independent research — not a commissioned audit. This report analyzes public incident data and is not based on access to bZx's private codebase or a client engagement.
What the protocol did
bZx ran two connected products on Ethereum — Fulcrum (margin trading) and Torque (collateralized lending) — that let users open leveraged positions and borrow against deposited collateral. To decide how much a position was worth and whether a loan was safely collateralized, bZx's contracts needed a live price for the traded asset pair. Instead of aggregating multiple independent sources, the price-reading logic pulled a spot price directly from a single Uniswap V1 pool's reserves. That pool, like most AMM pools at the time, was thin relative to the capital that flash loans made available.
This is the design decision the rest of the report turns on: a lending protocol used an easily-moved, permissionless, single-block price as if it were a trusted oracle.
The vulnerability
The bug class is spot-price oracle manipulation via flash loan, one of the defining DeFi vulnerability patterns of 2020. The flawed pattern has three ingredients:
- Price is read as an instantaneous ratio of reserves in one AMM pool (
price = reserveB / reserveA), not a time-weighted or multi-source figure. - The contract trusts that price within the same transaction it's read in, with no minimum liquidity check or deviation bound against a reference price.
- Flash loans remove the capital constraint that would normally make moving that price expensive — an attacker can borrow, swap, misprice, act, and repay, all atomically, with the loan itself costing only a fee.
Illustrative pseudocode of the flawed pattern:
// naive oracle read — vulnerable
function getPrice(address pair) public view returns (uint256) {
(uint256 reserveA, uint256 reserveB,) = IUniswapV1(pair).getReserves();
return reserveB * 1e18 / reserveA; // spot price, no TWAP, no bounds
}
function openPosition(uint256 collateralAmount) external {
uint256 price = getPrice(wbtcEthPair); // manipulable in-block
uint256 borrowLimit = collateralAmount * price / 1e18;
_lend(msg.sender, borrowLimit); // trusts the manipulated number
}
There is nothing here checking whether getPrice returned a value consistent with the broader market, and nothing making it expensive to move reserveA/reserveB for one block.
How the exploit worked
Two related incidents happened within days of each other in February 2020:
- First attack. The attacker took a large flash loan of ETH (via dYdX), used part of it as collateral to open a heavily leveraged short through bZx, and used another part to dump ETH into a low-liquidity Uniswap ETH/WBTC pool, pushing WBTC's spot price up sharply within the same transaction. bZx's contracts, reading that inflated spot price, valued the attacker's position far above its real market worth. The attacker unwound everything and repaid the flash loan in the same transaction, walking away with the difference. Public reporting put the profit at roughly $350,000.
- Second attack, days later. A similar mechanic was repeated using a different asset pair and a synthetic asset (sUSD), again manipulating the AMM spot price bZx relied on for collateral valuation, this time to mint/borrow against an inflated balance. Reported profit was larger, roughly $645,000. Combined, the two incidents cost bZx on the order of $1 million and forced the team to pause the protocol.
Both attacks share the same root cause and were completed in single transactions — no multi-block window, no need to hold manipulated capital at risk, and no reliance on any bug in Solidity arithmetic or access control. The only thing exploited was the assumption that a spot price is a trustworthy price.
How an audit catches this
This is precisely the class of finding a competent review is built to surface, and it's why oracle design gets dedicated attention in every engagement we run through smart-contract audit: we trace every price read back to its source and ask whether that source can be moved within one transaction by an economically rational attacker with flash-loan-scale capital.
Concretely, the checks are:
- Source mapping. Enumerate every place the protocol reads a price or exchange rate and identify whether it comes from a single pool, a single exchange, or an aggregated/time-weighted feed.
- Manipulation cost modeling. For each single-source price, estimate the capital needed to move it by a meaningful percentage and compare that against amounts obtainable via flash loans (effectively unbounded for major assets) — if manipulation cost is less than the value at risk, that's a finding.
- Atomicity test. Write a proof-of-concept transaction (or fork-based simulation) that borrows, swaps to skew the pool, calls the vulnerable function, and reverses the swap in one transaction. If the protocol's state changes in the attacker's favor, the oracle is exploitable.
- Invariant checks. Assert that collateral valuations move within bounded deviation from a TWAP or external reference over any single block; flag any code path where that invariant doesn't hold.
This kind of composability blast radius — one contract's price feed becoming another contract's attack surface — is the same category of risk we walk through in our piece on cross-program invocation risk in Solana DeFi: trusting an external, attacker-influenceable data source without validating it in-protocol.
Remediation
The fix bZx and the broader ecosystem converged on afterward is now baseline practice: replace single-pool spot prices with time-weighted average prices (TWAP) sampled over multiple blocks, or with a decentralized oracle network (Chainlink and similar) that aggregates off-chain and cross-venue data and is far more expensive to move in a single transaction. Beyond the primary fix, we'd expect remediation to include: deviation bounds that reject a price update too far from the last accepted value without a time delay; minimum-liquidity checks before trusting any AMM-derived price; and circuit breakers that pause borrowing/lending when price feeds disagree beyond a threshold. Contracts that accept external tokens or approvals as part of these flows deserve the same scrutiny we give gasless-approval patterns in our review of Permit2 and EVM sniping-bot exposure — any mechanism that lets an outside party influence protocol state cheaply and atomically is worth pressure-testing before launch.
If your protocol prices collateral, liquidations, or swaps off any on-chain source, it's worth having someone map that dependency chain before an attacker does — that's exactly the review we run in smart-contract audit engagements.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit