Harvest Finance (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, on-chain, and disclosed information about a historical incident. It does not claim to reveal any new or undisclosed vulnerability.
Harvest Finance is a case every auditor working on yield-bearing vaults should know cold. On October 26, 2020, an attacker extracted roughly $24 million from Harvest's USDC and USDT vaults in under fifteen minutes, using nothing more exotic than large, well-timed swaps against a Curve pool. No flash loan was required — the attacker traded with their own capital, in a loop, across a short sequence of blocks. That detail matters: it shows the bug wasn't about capital efficiency tricks, it was a pricing design flaw that any well-funded actor could hit.
What the protocol did
Harvest Finance operated auto-compounding yield vaults ("farms"). Users deposited stablecoins (USDC, USDT) or LP tokens into a vault contract, received an interest-bearing share token (fUSDC, fUSDT) in return, and the vault's strategy contract routed the underlying assets into yield sources — in this case, Curve's stablecoin pools — to farm trading fees and liquidity incentives. Deposits and withdrawals were priced by converting between the share token and the underlying asset using a "price per full share" calculation, computed at the moment of the transaction.
The vulnerability
The vault's share-price computation derived value from the Curve pool's instantaneous reserve composition rather than from a manipulation-resistant reference. Curve pools price assets according to a stableswap invariant that depends on the current ratio of reserves in the pool. That ratio is directly and cheaply movable within a single transaction by executing a large swap. Harvest's strategy read that same distorted ratio to compute how many vault shares a deposit was worth, and how much underlying a withdrawal should return.
In simplified form, the flawed pattern looked like this:
// Vulnerable: share price derived from spot AMM reserves
function getPricePerFullShare() public view returns (uint256) {
if (totalSupply() == 0) return 1e18;
// reads the pool's *current* balances — manipulable in-block
uint256 poolValue = curvePool.calc_withdraw_one_coin(shares, i);
return poolValue * 1e18 / totalSupply();
}
function deposit(uint256 amount) external {
uint256 shares = amount * 1e18 / getPricePerFullShare();
_mint(msg.sender, shares);
// funds routed into the same Curve pool being priced above
}
There is no separation between the price used to value the pool and the price an attacker can move by trading against that same pool one block earlier. This is the same bug class behind most AMM-oracle exploits: trusting a spot price, or a value derived from spot reserves, as if it were a time-resistant reference.
How the exploit worked
- The attacker deposited a large amount of USDT into Curve's Y pool via a single swap, sharply skewing the pool's internal reserve ratio and, with it, the pool's reported per-token value.
- In the same or an immediately following transaction, the attacker deposited USDC/USDT into Harvest's vault. Because the vault priced shares off the now-distorted pool state, it minted the attacker more shares than the assets were economically worth.
- The attacker then reversed the initial Curve swap, pushing the pool's reserves — and its computed price — back toward equilibrium.
- The attacker immediately withdrew from the Harvest vault. Redemption now used the restored, "corrected" price, so the shares minted cheaply in step 2 converted back to more underlying asset than was deposited.
- This four-step round trip was repeated across roughly thirty transactions in quick succession, draining value from other depositors' pooled funds each time. The attacker converted proceeds and moved them off-chain before the team could pause the strategy; roughly $2.5 million was later voluntarily returned.
The vault's total value per share dropped sharply and visibly on-chain during the attack window — the kind of anomaly a real-time monitor, not just a pre-deployment audit, should catch.
How an audit catches this
This is a textbook finding for anyone who has done a smart contract audit on a vault or aggregator strategy. The concrete checks we run:
- Flag any pricing function that reads AMM reserves,
balances(), or a pool's spotget_virtual_price()-style output directly, without a time-weighted or multi-source check. Grep forcalc_withdraw_one_coin,calc_token_amount,balances(, and similar calls inside any function that mints or burns shares. - Fork-test the exact attack shape: simulate a large single-block swap against the underlying pool, then call deposit and withdraw in the same block, and assert that the attacker's realized profit is bounded (ideally zero, modulo fees).
- Invariant test: total vault value per share should not move by more than a small, explainable delta within one block absent a legitimate yield event. Wire this into CI as a fuzzed property, not just a unit test.
- Check for deposit/withdraw fee or cooldown logic that would make same-block or same-transaction round trips uneconomical.
This kind of pattern-matching is exactly what a focused code review and audit engagement is built to surface before mainnet deployment, and it pairs well with the review discipline described in our piece on gasless approval mechanisms and EVM sniping exposure, where similarly transaction-ordering-sensitive assumptions caused real losses.
Remediation
The fix Harvest and the broader DeFi ecosystem converged on after this incident is now standard practice: never let a single in-transaction AMM read set the price for minting or redeeming shares. Concretely:
- Replace spot-derived pricing with a time-weighted average price (TWAP) or an external oracle (Chainlink or equivalent) for any value used in share issuance or redemption math.
- Add a sanity-bound check comparing the AMM-derived value against a secondary reference; revert or pause if the deviation exceeds a configured threshold.
- Introduce deposit/withdrawal fees or minimum holding periods that make single-block or single-transaction round trips unprofitable.
- Add a circuit breaker that halts vault deposits and withdrawals if price-per-share moves more than a set percentage within one block.
- Treat any strategy contract that routes funds through a third-party AMM as an oracle dependency, subject to the same scrutiny you'd apply during smart contract development of a price feed integration, not just a yield integration.
If your protocol prices anything — shares, collateral, liquidation thresholds — off a DEX pool's current state, that's the first thing we look for when you bring us in for a smart contract audit.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit