Loss: about $16 million across the DEFI5 and CC10 index pools, with knock-on damage to FFF because it held those index tokens
Summary: Indexed let its controller estimate an index pool’s total value from the balance of the first initialized token with nonzero target weight. During a reindex, newly added tokens remained uninitialized and were repriced from that extrapolated pool value through updateMinimumBalance(), while the pool’s mint and burn paths were still willing to spend against whatever initialization state the controller and pool had most recently published. The attacker flash-borrowed the already-initialized constituents, traded them into DEFI5 and CC10 to drain the reference token balance used by extrapolatePoolValueFromToken(), then called updateMinimumBalance() so the controller believed the pool was far smaller than reality. That let the attacker initialize SUSHI with an absurdly low minimum balance, make gulp() assign it an outsized initial weight, mint massively overpriced pool shares with joinswapExternAmountIn(), and finally burn those shares with exitPool() for a proportional basket of real underlying assets.
Trigger / entry point:
Indexed’s MarketCapSqrtController used extrapolatePoolValueFromToken() to infer pool value from a single already-initialized token balance and its target weight rather than from the full basket.
updateMinimumBalance() then set an uninitialized token’s minimumBalance to 1% of that extrapolated pool value.
Because reweights were time-limited while live balances could be moved instantly with flash liquidity, the attacker could slash the observed balance of the reference token without changing the weight denominator the controller trusted.
The protocol also allowed a reindex and the follow-up minimum-balance update in the same transaction, so there was no cooling-off period between binding the new token and repricing its initialization threshold.
Root cause:
Indexed treated one token’s current pool balance as a safe proxy for the whole pool’s value even though the attacker could temporarily evacuate that token and stuff the rest of the pool with other assets in the same transaction.
The pool’s slowly updating weights were assumed to stay economically aligned with instantaneous balances, but flash swaps broke that assumption.
Once minimumBalance for the new token was pushed down far enough, the token could be initialized from a manipulated state and given an initial denorm / weight that had no relation to a fair 1%-of-pool allocation.
reindexPool() / updateMinimumBalance() / gulp() were effectively one publication pipeline for the pool’s live initialization truth, but Indexed let joinswapExternAmountIn() and exitPool() spend against that truth immediately instead of quarantining it.
In short: Indexed embedded a manipulable internal oracle into its rebalance path, then let pool-share minting and burning honor that false oracle as if it represented real NAV.
Exploit path:
The attacker funded EOA 0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe from Tornado Cash and attacked DEFI5 first in transaction 0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa, then CC10 in transaction 0xbde4521c5ac08d0033019993b0e7e1d29b1457e80e7743d318a3c27649ca4417.
For DEFI5, the attacker first triggered reindexPool() so SUSHI was newly bound but still uninitialized. At that moment UNI was the first initialized token with positive target weight, so UNI became the reference asset for extrapolatePoolValueFromToken().
The attacker flash-borrowed roughly $156 million of initialized pool assets such as UNI, AAVE, COMP, CRV, MKR, and SNX from SushiSwap and Uniswap V2.
Using those borrowed assets, the attacker bought almost all of the pool’s UNI in dozens of chunks to respect Balancer-style MAX_IN_RATIO / MAX_OUT_RATIO limits. That left the controller looking at a tiny UNI balance even though the pool had just received massive value in the other constituents.
The attacker called updateMinimumBalance() on the controller. Because pool value was extrapolated from the now-drained UNI balance, Indexed concluded the whole pool was worth only about 29,851 SUSHI (around $300k) and reset SUSHI’s minimum balance to an absurdly low level instead of the roughly $126k threshold it should have required.
The attacker used the previously purchased UNI to mint DEFI5 shares via joinswapExternAmountIn(), inflating pool supply while the new-token initialization math was already corrupted.
The attacker then borrowed a huge amount of SUSHI, transferred it into the pool, and called gulp() so SUSHI became ready. Because minimumBalance had been forced down, the pool treated this manipulated SUSHI state as the correct initialization point and assigned SUSHI an abnormally large initial denorm / weight.
With SUSHI now drastically overweighted, the attacker could feed SUSHI into joinswapExternAmountIn() and mint far more DEFI5 than honest NAV justified.
Finally, the attacker burned the inflated pool shares through exitPool(), which returned a proportional basket of real underlying tokens. After repaying the flash liquidity, the remaining basket was pure profit. CC10 was then exploited with the same pattern.
What the attacker needed to be true:
The pool-value oracle for uninitialized assets needed to depend on one manipulable constituent balance instead of the aggregate basket.
Weight updates needed to be slow and divorced from the instantaneous balance changes flash swaps could cause.
The attacker needed enough flash liquidity to drain the reference token balance and still initialize / overweight the new token in the same transaction.
joinswapExternAmountIn() and exitPool() needed to accept the manipulated weights and minimum-balance state as genuine accounting truth.
The same-transaction sequencing from reindexPool() to updateMinimumBalance() to gulp() needed to remain open.
Technical anatomy worth remembering:
This was not a conventional external-price oracle attack. The decisive oracle lived inside Indexed’s rebalance controller: extrapolatePoolValueFromToken() inferred total pool value from one token and one stale weight.
The exploit combined two separate trust mistakes: an internal NAV approximation that could be flash-manipulated, and a pool-share mint path that treated the manipulated initialization state as permanent weight truth.
updateMinimumBalance() and then gulp() were the decisive publication surfaces. The first rewrote what counted as enough SUSHI to become live; the second hardened the attacker-shaped SUSHI balance into the token’s starting denorm / weight.
The profit leg came from a familiar AMM fact: if share minting is distorted but share burning still pays out real basket assets, the attacker can round-trip through mint and redeem to cash accounting fiction into reserves.
More sharply: this was a publication-vs-spend collapse. Indexed let a same-transaction sequence reindex -> publish new threshold -> harden manipulated balance -> mint -> exit turn a fresh local estimate into immediate reserve authority.
The attack also shows why delayed reweighting is not a harmless market-making detail. If weights update on a slow schedule while balances move instantly, the gap is an oracle surface.
Indexed’s own postmortem said the fix was to remove the approximation function and replace it with logic that values the combined balances held by the pool rather than one chosen token.
The team also concluded that allowing both reindexing and minimum-balance updates in the same transaction was unsafe and should have been separated by at least a day or two.
The incident remains one of the clearest demonstrations that rebalancing controllers and initialization thresholds belong inside the protocol’s solvency boundary, not in a helper math bucket.
Three pool-publication promotions this incident collapsed
1. One initialized constituent still counted as a live pool-value proxy
Indexed’s own postmortem preserved the first true fact clearly: when a new asset was being added, extrapolatePoolValueFromToken() still picked the first fully initialized token with positive target weight and used that one balance as a stand-in for whole-pool value.
In the DEFI5 exploit, that proxy token was UNI. Before manipulation, the same mechanism produced a reasonable SUSHI minimum balance of about 11,926 SUSHI (roughly $126k) because the live UNI balance still loosely tracked basket reality.
At this stage the system only knew one initialized token still exists and the controller can extrapolate from it; that should not yet have meant the whole pool's initialization truth is now safe to rewrite from this one balance under adversarial flow.
2. Indexed still promoted that partial-basket proxy into canonical new-token initialization truth
The decisive second promotion happened when the attacker flash-drained nearly all of the pool’s UNI, then called updateMinimumBalance() while the controller was still willing to treat that now-tiny UNI balance as a trustworthy whole-pool proxy.
Indexed’s own writeup recorded the resulting false publication: despite the pool having just absorbed well over $100M of other constituent inventory, the controller concluded the pool was worth only about 29,851 SUSHI (around $300k) and reset SUSHI’s minimum balance from a sane threshold to an absurdly low one.
That is the control split worth preserving: a proxy balance can still be read and that proxy may now define the canonical readiness threshold for a freshly bound asset were different authority promotions, but Indexed collapsed them into one controller update.
3. That published initialization truth still inherited immediate share-mint and basket-exit authority
Indexed did not leave the rewritten threshold in a staging lane. The attacker could immediately use the manipulated state to initialize SUSHI, let gulp() harden it into live denorm / weight truth, mint massively overpriced DEFI5 and CC10 via joinswapExternAmountIn(), and then burn those shares through exitPool() for real underlying assets.
Indexed’s own postmortem summarized the downstream effect directly: after the minimum-balance exploit, the attacker used borrowed SUSHI to mint additional DEFI5 at the extremely inflated valuation, then burned the DEFI5 for the full basket and repeated the loop.
So the dangerous ladder was one constituent still serves as a pool-value proxy, that proxy is promoted into canonical new-token initialization truth, and that freshly published truth still deserves immediate mint and redemption authority against real reserves. The exploit worked because Indexed treated those as one continuous permission instead of three separate promotions.
Indexed’s dangerous oracle was not a borrowed feed; MarketCapSqrtController itself published the attacker-shaped what is 1% of this pool worth? answer that later decided whether SUSHI initialization and DEFI5 / CC10 share issuance were treated as fair.
The sharp failure was not just one-token extrapolation is manipulable, but controller publication became reserve authority with no quarantine. reindexPool() bound the new asset, updateMinimumBalance() rewrote the admissible initialization threshold from a partial-basket view, and gulp() then hardened that manipulated state into live denorm / weight truth that mint and burn paths immediately spent against.
This is therefore a publication-vs-spend incident as much as an oracle-manipulation incident: once Indexed wrote a new internal mark for fresh-token readiness, the pool treated that same-window write as money-good NAV rather than as untrusted staging state.
Why the exploit worked
Indexed’s own postmortem already points at the core miss: extrapolatePoolValueFromToken() valued the whole pool from the first initialized token with positive target weight. In the live DEFI5 exploit, that token was UNI, so draining UNI let the attacker rewrite pool-wide value while hundreds of millions of dollars of the other constituents were simultaneously being stuffed into the pool.
BlockSec’s reconstruction makes the second boundary clearer: the controller’s target weights and the pool’s total weight moved on a slow reweight schedule, while flash-loaned balances changed instantly. That meant the extrapolation denominator still looked legitimate even after the balance numerator had been violently manipulated, so updateMinimumBalance() published a SUSHI threshold that no longer described 1% of real basket value at all.
The exploit only turned into theft because Indexed then let that controller-authored threshold flow straight into live initialization and reserve-spending semantics. After the attacker transferred in enough SUSHI to pass the manipulated minimum, gulp() marked SUSHI ready and assigned it an attacker-shaped starting denorm / weight; joinswapExternAmountIn() and exitPool() were still willing to mint and burn DEFI5 / CC10 against that just-published state.
In other words, Indexed did not merely read a bad price. It let one same-transaction pipeline reindex -> publish lower minimum -> harden manipulated SUSHI state -> mint -> exit turn a fresh local controller estimate into immediate basket-withdrawal truth.
The missing healthy-side control was therefore broader than value all tokens, not one. Indexed also lacked a reserve-spend boundary between controller just rewrote initialization math and pool shares may now be minted and redeemed against that state as if it were mature NAV.
Detection and audit pivots
Treat controller-side threshold updates as solvency-critical publications, not rebalance helpers. If a function like updateMinimumBalance() can change which assets are considered initialized or how much of a token is enough to become live, audit it with the same suspicion as mint, redeem, and collateral-pricing code.
Look for mixed-speed state assumptions. If weights, target denorms, or virtual balances drift on a scheduled cadence while live balances can move instantly via flash liquidity, test whether the slow variable is being used as if it still economically explained the fast one.
Refuse partial-basket extrapolation when the result can authorize reserve spend. Any estimate whole pool from one constituent shortcut should be presumed adversarial if later mint, burn, borrow, or redemption paths will honor it directly.
Separate staging state from spendable state. Reindexing, threshold resets, token initialization, and new-weight activation should not share the same transaction window as share issuance or proportional exits unless a stronger invariant proves the newly published state is manipulation-resistant.
Trace the exact publication pipeline from freshly bound token to live share-mint authority. In Indexed’s case the important question was not only can minimumBalance be manipulated?, but what downstream paths immediately start trusting the rewritten minimum and the subsequent gulp-hardened denorm?
Failure-prevention checklist
Do not let estimate whole pool value from one initialized constituent authorize any initialization threshold, mint rate, or reserve-spending path. If the protocol wants a cheap local estimate for UI or rebalance planning, keep it advisory-only until a full-basket or otherwise manipulation-bounded valuation confirms it.
Treat reindexing, token binding, minimum-balance resets, gulp()-style readiness flips, and initial denorm assignment as one security-sensitive publication pipeline. If any step can be called permissionlessly, model the entire sequence under adversarial ordering rather than team-run happy-path timing.
Require a delay or at least a separate confirmation state between new asset or threshold published and share mint / redemption may spend against that state. Indexed’s own postmortem is the clean reminder that reindex + updateMinimumBalance in one transaction was already too much authority in one economic window.
Recompute pool value from the full realizable basket, not from a single constituent and a stale target weight. If a constituent can be flash-drained while the denominator stays slow-moving, the valuation shortcut is already an oracle surface.
Keep initialization thresholds and starting weights bounded by independent sanity checks: if one updated threshold would imply an absurdly small or absurdly large initial denorm relative to the rest of the pool, fail closed instead of letting gulp() canonize it.
Audit Balancer-derived mint and burn paths together with controller-side publication logic. The bug was not finished when minimumBalance went wrong; it paid only because joinswapExternAmountIn() and exitPool() were still willing to honor the new state as fair basket truth.
Stress same-transaction manipulate -> publish -> harden -> mint -> exit loops explicitly in testing. If the protocol depends on multi-block economic mean reversion to stay safe, attackers will compress the whole state machine into one bundle.
Instrument alerts for one constituent balance collapsed while pool value allegedly did not, newly bound token became ready unusually cheaply, and new token initialization immediately preceded outsized share minting or redemption.
Keep fresh-token staging paths economically local. A newly added asset should not gain the ability to reshape pool-wide share pricing until its initialization state has survived beyond the window in which flash liquidity can author it.
Treat controller math as treasury code. If a helper decides what 1% of the pool means, it already sits inside the solvency boundary whether the team labels it a rebalance convenience or not.
Audit prompts
What exact function decides how much of a newly bound token is enough to become ready, and can an attacker cheaply distort the state that function reads before calling it?
Does the protocol ever extrapolate whole-pool value from one constituent, one active range, one stale target weight, or another partial-state proxy, then let mint / burn / borrow logic honor the result directly?
Can a public caller sequence bind or reindex -> reset threshold -> finalize readiness -> mint -> exit inside one transaction or one MEV bundle?
If a token’s live balance can move instantly but its target weight or denominator only updates gradually, what stops the protocol from treating that stale denominator as still economically meaningful?
Which downstream consumers trust the freshly published initialization state: share issuance, proportional exits, collateral factors, treasury reallocations, or all of them?
Are there hard bounds on how much one readiness update can change implied starting weight, mint price, or reserve claim, or does the protocol assume the publisher already produced something sane?
If a newly initialized token suddenly represents a very large fraction of share-mint credit after one gulp() or cache-write step, what explicit invariant catches that before users can redeem against the basket?
Can the protocol explain why a just-published threshold or denorm deserves immediate reserve authority rather than staging-only status?
What monitoring would distinguish constituent temporarily drained, threshold rewritten from partial state, and freshly hardened token now dominating mint economics before the attack reaches the redemption leg?
If governance or operators intend a safe multi-step rollout for new assets, what onchain state forces that order when the first executor is adversarial rather than cooperative?
Lessons that should already be standard
Internal controller math is still oracle math. If the protocol authors a basket value from one manipulable constituent, it has built itself an oracle whether or not it imports Chainlink.
Freshly published is not the same thing as money-good. A threshold reset, readiness flip, or starting-weight assignment should not become immediate mint and redemption truth just because the protocol itself wrote it.
Mixed-speed state is a real exploit surface. Slow weights plus instant balance movement is not harmless implementation detail; it is enough to make partial-basket valuation lie violently.
Rebalance and initialization code belong inside the solvency boundary. The dangerous question is not just can users trade the new token yet? but can this onboarding path already move share pricing or reserve claims for the whole pool?
Same-transaction convenience is often same-transaction catastrophe. If binding a token, repricing its threshold, hardening its balance, and redeeming against the basket can all happen in one bundle, the protocol has not actually separated staging from spend.
Mint-side bugs become reserve-drain bugs when burn-side redemption still pays honest assets. Distorted share issuance is already treasury loss if exits keep honoring the inflated supply.
A protocol should be able to name the exact moment a local estimate becomes reserve authority. If that answer is vague, attacker-shaped staging state is probably one function call away from solvency truth.
Why this incident matters
Indexed is a crisp example of an AMM / index product being broken by its own internal price-discovery shortcut rather than by a bad Chainlink feed or a naïve spot oracle.
It preserves a distinct exploit family where the attacker does not merely pump collateral price; they corrupt how the protocol decides what an uninitialized asset and a newly minted pool share are worth.
It is also a useful warning for any Balancer-style system that stages new assets, virtual balances, or delayed weights: the initialization path is part of the attack surface, not a temporary onboarding detail.
The healthier design lesson is broader than value the whole basket. Fresh controller-written thresholds and freshly hardened token-init state should not gain immediate mint / redemption authority in the same economic window.
Sharper internal-pricing lens: protocol-authored-market-state-oracle-failure — the decisive oracle was Indexed’s own rebalance controller extrapolating whole-pool value from one manipulable constituent balance and stale target weights.
Publication-authority pattern: state-publication-authority-failure — the exploit only paid once updateMinimumBalance() and later gulp() hardened a manipulated partial-basket view into canonical initialization and minting truth.
Publication-authority lens: sync-harvest-and-cache-write-authority-discipline — updateMinimumBalance() and later gulp() were not neutral maintenance steps; they were the publication surfaces that hardened attacker-shaped partial-basket state into live initialization and minting truth.
Positive-design companion: internal-mark-publication-quarantine-and-reserve-spend-separation — Indexed collapsed controller-side publication and pool-side reserve spend into one authority chain, so a fresh local basket estimate immediately became mint / burn truth.
Timing companion: delayed-snapshots-and-same-block-quarantine — the exploit only paid because reindexing, threshold reset, token initialization, mint, and exit could all be chained in one economic window.
Adjacent pattern: lp-token-oracle-mispricing — not because DEFI5 itself priced an LP token, but because the cash-out step still relied on Balancer-style pool-share mint / burn logic honoring a manipulated internal basket valuation.
Control surface
The decisive leverage sat in MarketCapSqrtController’s extrapolatePoolValueFromToken() and updateMinimumBalance() functions, the lack of delay between reindexing and new-token initialization, and Balancer-derived mint / burn paths that trusted the resulting weights as if they represented real pool NAV.
updateMinimumBalance() and gulp() were the crucial publication surfaces: they took a temporarily attacker-shaped partial view of pool state and promoted it into canonical initialization truth that later mint and burn paths treated as fair basket economics.
The missing control was not only use a fuller valuation function; it was also force publication and reserve spend to be different states. Indexed gave same-transaction reserve authority to freshly rewritten minimum-balance and initialization state.
Indexed did not fail because one constituent token had a bad market price. It failed because its own rebalance controller let a single temporarily drained balance stand in for total pool value, then let that fiction harden into share-minting truth.