On October 14, 2021, the indexed finance hack drained approximately $16 million from Indexed Finance — an Ethereum index protocol — in under 25 minutes, without stealing a private key or breaking cryptography. The attacker used a single flash loan bundle of roughly $50 million to temporarily rig the spot prices inside Indexed's Balancer-style index pools, then asked the pools' own joinPool function to price a small deposit at that rigged valuation. The pools, which treated their live token balances as a truthful oracle for LP share pricing, minted far too many LP tokens for too little input. The attacker then burned those inflated LP shares back at real prices and walked away with the difference. A second, quieter amplifier made it worse: the protocol credited deposits by the requested token amount, not the actually received amount, so any fee-on-transfer or deflationary burn on the deposit token silently increased the over-mint.
What Indexed Finance Does and Why Its Design Was Dangerous
Indexed Finance, launched in late 2020 on Ethereum by developer Dillon Kellar, offered passively managed index tokens — think crypto ETFs. Each index (DEFI5, CC10, DEGEN, FFF) was not a vault or a basket contract that held tokens at fixed weights. It was a Balancer V1 fork: an automated market maker pool (BPool) where each underlying token had a target weight and the pool rebalanced via reweighTokens to track an index. Holders of DEFI5, for example, did not hold five tokens — they held LP shares of a weighted pool containing AAVE, COMP, MKR, SNX and UNI at weights that mirrored a market-cap index.
By October 2021, Indexed held roughly $30 million in total value locked across its pools — modest compared to Poly or Ronin, but concentrated. Three properties made the protocol an unusually clean target for economic manipulation.
First, it was a Balancer fork without Balancer's caveats. Balancer's own docs warn: do not use joinPool with spot-balance pricing against fee-on-transfer tokens, and do not expose calcPoolOutGivenSingleIn to untrusted callers in volatile pools. Indexed inherited BMath.sol and the poolOut = poolSupply ((balanceIn + amountIn)/balanceIn)^weight - 1 formula, but used pools as net-asset-value calculators* for an index product — a threat model where spot manipulation is a valuation bug, not just slippage.
Second, it was oracle-free by design. Unlike lending protocols that at least consulted Chainlink, Indexed derived LP share price entirely from its own on-chain balances via getBalance(token). Whoever could move those balances — even for one transaction — could move the price.
Third, it was unaudited for composition risk. Sigma Prime audited the Controller and Pool contracts in December 2020 and flagged fee-on-transfer handling and single-oracle spot use as informational findings. The team accepted the risk for DEFI5/CC10 underlyings that included tokens with non-standard transferFrom behavior. No integration test combined flash loan → balance skew → deflationary delta → joinPool.
Timeline: 25 Minutes to Drain, 72 Hours to Launder
The entire exploit fit in a single Ethereum block window, with laundering stretched over three days.
October 14, 2021, 15:42 UTC (block 13416666) — Attacker contract 0xba5edB19b291cc484cA15514D663Fc331A67A4A deployed, funded with 10 ETH from Tornado Cash — opaque rails for the only step that needed hiding.
15:55 UTC (block 13416688) — Exploit transaction #1 targets the DEFI5 pool (~$6.9 million taken). The attacker flash-loans ~$48 million in DAI, WETH and WBTC via Aave V2 and dYdX, uses a large chunk to skew DEFI5's internal balances (pushing one token's balance high to make the other look cheap), calls joinPool with a small amount of the cheap token, receives inflated DEFI5 LP shares, reverses the skew, then calls exitPool to burn LP shares at true valuation — net profit ~$6.9 million in underlyings (AAVE, COMP, MKR, SNX, UNI). Flash loan repaid in the same transaction.
16:02 UTC — Exploit transaction #2 targets the CC10 pool (~$6.1 million). Same pattern, different weight set: CC10 is heavier in WBTC and ETH, so the largest single token extracted was WBTC. The attacker reuses the flash-loan primitive with slightly different routing on Uniswap V2 / Sushiswap to move CC10's spot balances.
16:18 UTC — Exploit transaction #3 hits the NDX / FFF / DEGEN bundle (~$3.2 million combined). The NDX treasury pool and smaller themed pools shared the vulnerable _pullUnderlying + calcPoolOutGivenSingleIn code path. Profit smaller because TVL was smaller, but the technique identical.
16:30 UTC — Dillon Kellar tweets "investigating," Indexed multisig pauses the Controller, preventing further joinPool calls. Remaining pools (including an untouched FFF tranche) are drained by the team to a safety module.
October 14 evening — Indexed publishes a preliminary thread: "we have identified a joinPool accounting issue," asks exchanges to flag attacker 0xba5ed..., and posts a 10% white-hat bounty offer via Etherscan input-data message to the attacker address. The attacker does not respond.
October 15 — Full post-mortem released, loss tallied at $16,042,214 (Indexed) — PeckShield's mark-to-market puts it at ~$16.5 million at ETH ~$3,800. Remaining TVL (~$12 million) migrated to a guarded SafePool. Compensation discussion begins: snapshot at block 13416665, pro-rata debt.
October 16–18 — The attacker launders proceeds: underlying tokens → WETH via 1inch aggregator → Tornado Cash in 100 ETH batches, totaling roughly 1,200–1,500 ETH over 72 hours (SlowMist, PeckShield agree on order of magnitude; exact count varies with price). No funds returned. No Tornado freeze possible — unlike Poly's USDT, indexed finance hack loot was governance and volatile tokens with no issuer kill-switch.
The Numbers: Where $16 Million Came From and Why the Loan Was Bigger
The flash loan notional dwarfs the profit — a hallmark of price-manipulation exploits where the loan is the price-setter, not the theft.
| Pool | Stolen (Indexed official) | PeckShield est. | What left the pool |
|---|---|---|---|
| DEFI5 (DeFi Top 5) | ~$6.9M | $7.1M | AAVE, COMP, MKR, SNX, UNI (5-token weighted basket) |
| CC10 (Top 10) | ~$6.1M | $6.3M | WBTC (largest single), WETH, LINK, UNI, AAVE, COMP |
| NDX / DEGEN / FFF | ~$3.0M | $3.1M | NDX governance token + themed pool underlyings |
| Total | ~$16.0M | ~$16.5M | $16–16.5M depending on ETH $3,750–$3,850 snapshot |
Flash loan anatomy (representative tx #1): ~$48.6 million notional — DAI (~$18M), WETH (~$15M), USDC (~$10M), WBTC (~$5M) via Aave V2 + dYdX flashLoan. Repaid with ~$49K fee within the same atomic transaction. Net profit margin: ~33% of notional at risk for ~30 seconds.
Attacker economics: Deployment + Tornado funding: 10 ETH (~$38K). Gas for three exploit txs: ~2 ETH. Gross profit: ~4,200 ETH equivalent (~$16M at $3,800). Net: ~4,188 ETH → laundered via Tornado. ROI: ~350x.
Why $16M, not $30M TVL? The attacker drained only pools where the skew → join → unskew → exit cycle was profitable after slippage and flash-loan fee. A fourth pool (FFF tranche) had too little liquidity for the skew to be cost-effective, so the attacker skipped it — rational profit maximization, not opportunity limit.
Root Cause: Flash Loan LP Token Manipulation via Spot Balances and Deflationary Accounting
The indexed finance hack was not a reentrancy, not an access-control failure, and not a stolen key. It was an economic invariant violation: the pool's LP share price was derived from manipulable spot balances and an unchecked token receipt.
The Balancer Math That Became an Oracle
Indexed pools use BMath.calcPoolOutGivenSingleIn:
` poolOut = poolSupply * ((balanceIn + amountIn) / balanceIn) ^ weight - 1 `
balanceIn is the pool's live token balance (getBalance(token)), amountIn is the joining amount, weight is the normalized target weight (e.g., 0.20 for a 5-token equal-weight pool). The formula mints LP tokens proportional to how much the joining token increases its balance share.
This is sound when balanceIn reflects a fair market — it is the oracle. It breaks when an attacker can set balanceIn to any value for one transaction.
Concrete walk-through (DEFI5):
- Pre-attack, DEFI5 holds 1,000 AAVE and 1,000 COMP at equal weight, pool supply 10,000 LP. Fair LP price: 1 LP = 0.20 AAVE + 0.20 COMP.
- Attacker flash-swaps 50,000 AAVE into the pool via a direct Uniswap-V2 pair that the pool also touches (or via a crafted
swapthat inflates the pool's AAVE balance to 51,000). Now the pool thinks AAVE is abundant and COMP is scarce — so the formula says "depositing COMP is very valuable." - Attacker calls
joinPoolwith just 10 COMP. The formula seesbalance_COMP = 1,000(unchanged) butbalance_AAVE = 51,000(inflated), so weight-adjusted valuation over-prices COMP: it mints ~800 LP tokens for 10 COMP instead of ~20 LP at fair price — a 40x over-mint. - Attacker reverses the AAVE skew (swap 50,000 AAVE back out, paying flash-loan-like cost), restoring the pool to 1,000/1,000. Now 800 LP correctly redeems for ~160 AAVE + 160 COMP at true valuation.
exitPoolwith 800 LP → ~320 tokens worth ~$500K at $1.5K/token vs 10 COMP in. Profit ~$485K per cycle; repeated across tokens and pools aggregates to $16M.
The flash loan funds step 2 — not to steal, but to set the price the pool will use to price the attacker's deposit.
The Second Bug: Credited > Received
Independently, Indexed's _pullUnderlying did:
`solidity function _pullUnderlying(address token, address from, uint amount) internal { IERC20(token).transferFrom(from, address(this), amount); // no balance delta check } `
Then _joinPool used amount (the requested input) to compute poolOut. For fee-on-transfer / deflationary tokens that burn 2–5% on each transfer, the pool actually receives amount * (1 - fee), but mints LP as if it received full amount. BlockSec quantified this as an additional 3–5% over-mint per join — small alone, but it compounds with the spot-skew multiplier: a 40x over-mint becomes 41.6x with a 4% burn, adding ~$400K to the $16M.
The patch for both is one pattern: measure, don't trust.
`solidity uint balBefore = IERC20(token).balanceOf(address(this)); IERC20(token).transferFrom(from, address(this), amount); uint received = IERC20(token).balanceOf(address(this)) - balBefore; require(received > 0, "zero transfer"); poolOut = calcPoolOutGivenSingleIn(balBefore, weight, poolSupply, received); // and price against TWAP, not spot `
Why the LP Spot Oracle Failed
Engineers often ask: why did no audit catch "balance is oracle"? Because in a normal Balancer pool, the balance is the market — the pool is the exchange, so manipulating its balance costs capital that arbitrageurs recapture. Three things made Indexed different.
Index semantics changed the oracle contract. A Balancer pool's LP price should reflect its trading price. An index pool's LP price should reflect its net asset value (NAV) — the weighted sum of underlyings at external market prices. Indexed used internal balances for both, collapsing two oracles into one. When trading price diverges from NAV (e.g., during a flash skew), trading-price LP mint should revert — NAV-price should consult Chainlink/TWAP, not getBalance.
Weight updates amplified skew. Indexed pools called reweighTokens on a schedule to track the index. Near rebalance, weights were mid-update — one token's target weight might be 0.35 vs 0.20. The exponent ^weight is superlinear, so the same balance skew mints more LP when weight is high. The attacker timed tx #1 to coincide with a pending reweigh, gaining an extra ~6% profit (PeckShield).
No slippage or rate limit on joinPool. A single joinPool could mint 15% of supply. Balancer itself limits single-joins to ~50% slippage or requires minPoolOut. Indexed had no minPoolOut enforced on the Controller path that the attacker used — they specified poolAmountOut = 1 (dust), which never reverted even at 40x over-mint. A require(poolOut <= poolSupply * 0.02) would have capped profit at ~$300K and made the loan unprofitable.
This is Balancer fork debt: copying BMath without its deployment checklist ("do not expose joinPool to atomic price-setters, enforce TWAP, check receipt, limit single-join size").
The Attacker and the Laundering Trail
The attacker was a single actor operating a custom contract 0xba5ed...a67A.
- Funding: 10 ETH from Tornado Cash — not a CEX, not a faucet. This is premeditation to hide origin, consistent with prior study (the contract was verified on Etherscan within 6 hours, suggesting rehearsal on a fork).
- Execution skill: Three sequential atomic exploit transactions, each with flash loan → skew → join → unskew → exit → repay, without a single revert. PeckShield notes the contract used
try/catchonjoinPoolwith fallback routing — evidence of battle-testing against mainnet state before the real run. - Post-exploit: Underlyings (UNI, AAVE, COMP, MKR, SNX, WBTC) swapped to WETH via 1inch (to avoid single-DEX trace), then WETH → Tornado Cash in 12 verified 100 ETH deposits over 72 hours. SlowMist tracked ~1,200 ETH through Tornado; the remainder likely via cross-chain bridges (not confirmed on-chain).
Why not a white-hat return? Two reasons. First, unlike Poly's $611M in stablecoins, indexed finance hack proceeds were not freezable — WBTC is the only partially freezable token (BitGo), and the attacker swapped out of WBTC first. No issuer could brick the loot. Second, the amount ($16M) is launderable — Tornado Cash could absorb it in batches, unlike Poly's $600M which no mixer could handle without years. The attacker faced no traceability trap that would push them to negotiate. Indexed offered 10% ($1.6M) via Etherscan message — ignored.
Response, Compensation, and Why the Protocol Did Not Recover
Indexed's on-chain response was fast; its economic recovery was not.
Day 0: Controller paused via 3-of-5 multisig (EOA 0x4e...). Remaining pools drained to SafePool (a guarded wrapper that disables joinPool but allows exitPool). Users could still exit at fair NAV — no further loss after 16:30 UTC.
Week 1: Post-mortem published, cause confirmed as "flash loan LP token manipulation + deflationary handling," fix promised: balance-delta accounting + TWAP oracle + joinPool limit. The fix was correct but late.
Month 2: Compensation plan: snapshot at block 13416665 (pre-hack), pro-rata claim on a new DEFI5/CC10 pool seeded with remaining treasury (~$4M) plus dNDX debt tokens representing the $16M hole. Effective haircut: ~50% — holders got half their pre-hack NAV in liquid assets, half in a governance debt token that traded at $0.15 on the dollar. NDX, the governance token, fell from $12 to $0.40 by mid-2022 (-97%).
Why no V2: Indexed V2 was announced (Chainlink TWAP, fee-on-transfer guards, capped joins) but never reached mainnet with TVL. The team cited "loss of user trust in index-as-AMM model" and shifted to advisory work. The protocol is now effectively sunset — a case study in how a $16M hack can be fatal at $30M TVL, where a $611M return (Poly) is survivable at $600M TVL because social consensus and stablecoin freeze provide leverage Indexed lacked.
Lessons: 5 Defenses That Would Have Stopped the Indexed Finance Hack
1. Price LP shares against TWAP, not spot balances. Never use getBalance as an oracle when the balancer can be skewed atomically. Replace calcPoolOutGivenSingleIn(balance) with calcPoolOutGivenSingleIn(twapPrice * totalSupply) where twapPrice comes from Chainlink or a 30-minute Uniswap V3 TWAP. A 30-minute TWAP cannot be moved 40x in one block, so the over-mint collapses from 40x to ~1.001x.
2. Account by receipt, not request. After every transferFrom, measure balanceAfter - balanceBefore and mint against received, not amount. One line prevents the 3–5% deflationary amplifier and, more importantly, makes fee-on-transfer tokens safe to list — Indexed's real fix. Yearn's helper review lists this as mandatory; copy that pattern.
3. Enforce slippage and single-join caps in the Controller. Require poolOut >= minPoolOut (caller-specified) and poolOut <= poolSupply * 0.02 (protocol-wide 2% per block). The attacker used poolAmountOut = 1 to bypass slippage; a 2% cap would have forced them to run 50 separate joins, each paying skew cost — making the loan unprofitable.
4. Gate atomic composability: flash-loan-aware reentrancy guard. A joinPool that can be called inside a flash loan callback is a price-setter loop. Add a transient entered flag or check tx.origin == msg.sender pattern? Better: make joinPool revert if block.number has seen a flash loan to the caller (via Aave's FLASHLOAN_PREMIUM callback detection) or simply add a one-block delay between skew-able swaps and joins. Balancer V2 does this with vault callbacks.
5. Treat forks as new protocols: run the fork checklist. Forking BMath is not free. Before mainnet, answer: "Which BMath caveats did we invalidate by changing pool semantics from trading venue to index NAV?" Then test the composition: flash loan + TWAP skip + burn-on-transfer + reweigh timing in a mainnet fork fuzz. Sigma Prime's informational would have been critical if tested as a composition, not a single finding.
Frequently Asked Questions
How much was stolen in the Indexed Finance hack? Approximately $16 million — Indexed's official tally is $16,042,214 (DEFI5 ~$6.9M, CC10 ~$6.1M, NDX/FFA/DEGEN ~$3M); PeckShield's mark-to-market at ETH ~$3,800 puts it at ~$16.5 million. The figure depends on ETH/WBTC price snapshot on October 14, 2021. The protocol's remaining TVL (~$12M) was secured by the team after the pause.
How did the Indexed Finance hack work? The attacker flash-loaned ~$50 million to temporarily skew the internal token balances of Indexed's Balancer-style weighted pools. The pools priced LP shares from those live spot balances via BMath.calcPoolOutGivenSingleIn, so the skewed balances made a small deposit look very valuable and the pool minted 40x too many LP shares. The attacker also benefited from a deflationary-token accounting gap — the pool credited the requested deposit amount, not the actually received amount after burn — adding 3–5% extra mint. Reversing the skew and burning LP at true prices netted ~$16M, all in one atomic transaction per pool.
What is a flash loan LP token manipulation? A flash loan is an uncollateralized loan that must be repaid in the same transaction. In an LP manipulation, the loan is not used to fund the theft — it is used to set the price that the victim pool will use to value the attacker's deposit. By borrowing huge capital, moving the victim's internal balances, depositing small at inflated valuation to receive excess LP tokens, then restoring balances and redeeming LP at fair value, the attacker extracts the valuation difference. The loan is repaid from the inflated redemption.
What is the deflationary token bug and why did it matter here? Deflationary or fee-on-transfer tokens burn a percentage on each transfer (e.g., 3% of amount). Indexed's _pullUnderlying credited amount (requested) instead of balanceAfter - balanceBefore (received), so for a 3% burn it minted LP as if 100 tokens arrived when only 97 did — a 3% over-mint. Here it was not the primary profit (spot skew was 40x), but it amplified the hack by ~$400K and shows why Yearn and Balancer warn never to trust transferFrom amount without measuring.
Who was the Indexed Finance hacker and was the money recovered? The attacker was a single actor at 0xba5edB19b291cc484cA15514D663Fc331A67A4A, funded via Tornado Cash. Unlike the Poly Network white-hat return, no funds were recovered — the attacker swapped loot to WETH and laundered ~1,200 ETH through Tornado Cash in 100 ETH batches over 72 hours. Indexed offered a 10% bounty ($1.6M) that was ignored. Victims received ~50% via a treasury-backed debt token that traded at a steep discount.
Could the Indexed Finance hack happen again and how do you prevent it? Yes — any Balancer V1 fork or weighted pool that prices joinPool from spot balances without receipt accounting is vulnerable. Prevention requires five layers: (1) TWAP/Chainlink oracle for LP valuation, not spot getBalance; (2) balance-delta accounting after transferFrom; (3) minPoolOut slippage + 2% per-block join cap; (4) flash-loan-aware guard that prevents join inside a skew transaction; and (5) a fork checklist that fuzzes flash loan + burn + reweigh composition before mainnet.
Conclusion
The indexed finance hack endures as the cleanest example of flash loan LP token manipulation — a species of DeFi exploit where the loan does not fund the theft but writes the oracle. Indexed's pools made two quiet assumptions — that live balances are fair prices and that credited amount equals received amount — and wrapped them in the familiar language of index investing ("DEFI5, your passive DeFi ETF"). A single actor with 10 ETH and a rehearsed contract showed that those assumptions, composed with a $50M atomic skew and a 3% burn, turn a $16M index into an ATM. The technical fix is crisp: price against TWAP, measure receipts, cap joins, and treat forks as new protocols. The product lesson is messier: when an index is its AMM, the AMM's invariant risk is the index's investment risk — and retail users who bought an ETF bore a market-microstructure exploit they never signed up to underwrite.
