Indexed Finance DEFI5 gulp/reindex bug enables SUSHI flash-swap drain
Exploit Transactions
0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aaVictim Addresses
0xfa6de2697d59e88ed7fc4dfe5a33dac43565ea41EthereumLoss Breakdown
Similar Incidents
SorraV2 staking withdraw bug enables repeated SOR reward drain
35%NFTX Doodles collateral accounting flaw enables flash-loan ETH extraction
34%PumpToken removeLiquidityWhenKIncreases Uniswap LP Drain
33%Cream Finance cAmp / Amp Reentrancy Exploit
33%TecraCoin TcrToken burnFrom Allowance Bug Exploits
32%WETH Drain via Unprotected 0xfa461e33 Callback on 0x03f9-62c0
32%Root Cause Analysis
Indexed Finance DEFI5 gulp/reindex bug enables SUSHI flash-swap drain
1. Incident Overview TL;DR
On Ethereum mainnet block 13,417,949, an unprivileged EOA 0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe used the Indexed Finance DEFI5 controller to reindex the DEFI5 pool and configure SUSHI as a new component. Within the same transaction, the adversary combined a SushiSwap flash swap of SUSHI with DEFI5’s gulp, joinswapExternAmountIn, and exitPool mechanics to withdraw more value than deposited. This single adversary-crafted transaction 0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa produced a deterministic net profit of 14.0606163 ETH for the attacker after gas, funded by value drained from the DEFI5 pool and its WETH/SUSHI liquidity.
The root cause is an accounting mismatch in DEFI5’s IndexPool implementation when a newly added component token is gulped: gulp(SUSHI) credits the full borrowed SUSHI balance and weight to the pool’s internal accounting without minting corresponding pool shares. When combined with controller-driven reindexPool and updateMinimumBalance, this breaks the share-to-reserve invariant and allows an unprivileged adversary to use joinswapExternAmountIn and exitPool to extract more ETH-backed value than they contribute, making this an anyone-can-take (ACT) opportunity.
2. Key Background
DEFI5 is an Indexed Finance index pool deployed on Ethereum and implemented as a Balancer-style pool via IndexPool.sol. The pool holds a basket of ERC20 tokens (initially including UNI, AAVE, COMP, CRV, and later SUSHI). Each component token is tracked in a Record struct with fields such as balance and denorm (weight), and pool shares represent proportional ownership of the aggregate underlying token reserves. A separate controller contract orchestrates which tokens are in the index and what their weights should be.
The MarketCapSqrtController at 0x120c6956d292b800a835cb935c9dd326bdb4e011 (accessed via a proxy at 0xf00a38376c8668fc1f3cd3daeef42e0e44a7fcdb) exposes public methods reindexPool and updateMinimumBalance that any EOA can call for DEFI5. These functions compute new desired weights and minimum balances for each component token using TWAP-based pricing and then push configuration changes into the DEFI5 pool contract at 0xfa6de2697d59e88ed7fc4dfe5a33dac43565ea41.
The DEFI5 pool is an IndexPool instance whose implementation includes Balancer-like math and accounting. The vulnerable gulp logic is defined in the Indexed Finance fork of Balancer’s IndexPool.sol:
// Source: Indexed Finance DEFI5 IndexPool (excerpt)
// artifacts/root_cause/data_collector/iter_1/contract/1/0x669693A42B58E87b9e568bA2C6AdD607eb298d95/source/src/temp-contracts/balancer/IndexPool.sol
function gulp(address token) external override _lock_ {
Record storage record = _records[token];
uint256 balance = IERC20(token).balanceOf(address(this));
if (record.bound) {
if (!record.ready) {
uint256 minimumBalance = _minimumBalances[token];
if (balance >= minimumBalance) {
_minimumBalances[token] = 0;
record.ready = true;
emit LOG_TOKEN_READY(token);
uint256 additionalBalance = bsub(balance, minimumBalance);
uint256 balRatio = bdiv(additionalBalance, minimumBalance);
uint96 newDenorm = uint96(badd(MIN_WEIGHT, bmul(MIN_WEIGHT, balRatio)));
if (newDenorm > 2 * MIN_WEIGHT) newDenorm = uint96(2 * MIN_WEIGHT);
record.denorm = newDenorm;
record.lastDenormUpdate = uint40(now);
_totalWeight = badd(_totalWeight, newDenorm);
emit LOG_DENORM_UPDATED(token, record.denorm);
}
}
_records[token].balance = balance;
} else {
_pushUnderlying(token, address(_unbindHandler), balance);
_unbindHandler.handleUnbindToken(token, balance);
}
}
This code assumes that any ERC20 tokens sitting in the pool address are real reserves backing existing pool shares. When gulp is called for a bound but not-yet-ready token, it can mark the token as ready, assign a weight, and set Record.balance equal to the ERC20 balanceOf(pool) — all without minting new pool shares. If that balance comes from a flash-borrowed deposit instead of permanent liquidity, the accounting is incorrect.
The attacker also relies on a SushiSwap SUSHI/WETH LP at 0x795065dcc9f64b5614c407a6efdc400da6221fb0 to source a large amount of SUSHI via a flash swap. A router/orchestrator contract at 0x277e851587eb5da22b52a10f4788576e68150277 coordinates the controller calls, flash swap, gulp, joinswapExternAmountIn, exitPool, and flash swap repayment, all in a single transaction.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an accounting bug in DEFI5’s IndexPool implementation triggered when a new component token is added and then gulped. After the controller configures SUSHI as a component with a minimum balance, a SushiSwap flash swap can deliver a large SUSHI amount directly to the pool contract. When gulp(SUSHI) is called, it credits the entire SUSHI ERC20 balance to the pool’s internal Record.balance and sets a new denorm (weight), but it does not mint any new pool shares to match the increased reserves.
This breaks the intended pool accounting invariant: pool-share supply and pricing assume that token reserves only grow when liquidity providers deposit value in exchange for new shares. Because the flash-borrowed SUSHI is credited entirely to existing shares, subsequent joinswapExternAmountIn and exitPool operations are mispriced. The adversary uses these functions to redeem more ETH-denominated value from DEFI5 and its underlying liquidity than they contribute.
The vulnerability is reachable entirely via publicly callable functions (reindexPool, updateMinimumBalance, gulp, joinswapExternAmountIn, exitPool, and a UniswapV2-style flash swap). An unprivileged EOA with sufficient gas can reproduce the same sequence, making this an anyone-can-take ACT opportunity rather than a governance or privileged-key compromise.
4. Detailed Root Cause Analysis
4.1 Intended invariants
Let S be the total supply of DEFI5 pool shares, and for each component token i let R_i be its ERC20 balance held by the pool contract and w_i be its normalized weight computed from Record.denorm. The intended invariant is that:
- The economic value represented by
Spool shares approximately equals the aggregate value of reserves, i.e.,sharePrice * S ≈ Σ_i value(R_i, w_i, prices). - When reserves increase due to new deposits, either additional pool shares are minted to keep existing holders’ proportional ownership unchanged, or the accounting ensures that existing holders cannot redeem more value than the underlying reserves.
Balancing logic in joinswapExternAmountIn, exitPool, and related functions assumes Record.balance values reflect genuine, share-backed liquidity.
4.2 Controller reindex and minimum balance configuration
The exploit begins by using the public controller API. In the seed transaction’s decoded trace, the EOA’s router calls reindexPool on the DEFI5 controller:
// Seed transaction trace excerpt
// artifacts/root_cause/data_collector/iter_2/tx/1/0x44aad3b8.../call_trace_decoded.json
{
"type": "CALL",
"from": "0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe",
"to": "0x277e851587eb5da22b52a10f4788576e68150277",
"function": { "signature": "0x807a994a" },
"calls": [
{
"type": "CALL",
"from": "0x277e851587eb5da22b52a10f4788576e68150277",
"to": "0xf00a38376c8668fc1f3cd3daeef42e0e44a7fcdb",
"function": { "signature": "reindexPool(address)" },
"decoded_args": "0xfa6de2697D59E88Ed7Fc4dFE5A33daC43565ea41",
"calls": [
{
"type": "DELEGATECALL",
"to": "0x120c6956d292b800a835cb935c9dd326bdb4e011",
"function": { "name": "computeAverageMarketCap" }
}
]
}
]
}
The controller computes average prices for a set of tokens (UNI, AAVE, CRV, COMP, SUSHI) and derives new weights and minimumBalance thresholds. It then configures SUSHI as a new DEFI5 component with a non-zero minimumBalance using public controller functions. No special privileges are required: any EOA can call these entrypoints with suitable parameters.
4.3 Flash swap delivers SUSHI into the pool
Next, the router triggers a SushiSwap flash swap from the SUSHI/WETH LP at 0x795065dcc9f64b5614c407a6efdc400da6221fb0, borrowing a large amount of SUSHI into the DEFI5 pool address. The pre/post balance diff shows this clearly:
// SUSHI balance shifts derived from balance diff
// artifacts/root_cause/seed/1/0x44aad3b8.../balance_diff.json
{
"erc20_balance_deltas": [
{
"token": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"holder": "0x795065dcc9f64b5614c407a6efdc400da6221fb0",
"delta": "-39960196807806751520000",
"contract_name": "SushiToken"
},
{
"token": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"holder": "0xfa6de2697d59e88ed7fc4dfe5a33dac43565ea41",
"delta": "39960196807806751520000",
"contract_name": "SushiToken"
}
]
}
This shows approximately 39,960,196.80780675152 SUSHI moving from the Sushi LP to the DEFI5 pool contract. At this point, these SUSHI tokens are flash-borrowed and must later be repaid to the LP, but IndexPool is not aware of their transient nature.
4.4 Gulp credits flash-borrowed SUSHI without minting shares
After the flash swap deposits SUSHI into the pool, the router calls gulp(SUSHI) on IndexPool. As shown in the code excerpt above, gulp:
- Reads the current
balance = IERC20(token).balanceOf(address(this)). - If the token is bound but not yet ready, and
balance >= minimumBalance, it:- Marks the token as ready (
record.ready = true). - Computes a new
denormbased onMIN_WEIGHTand a ratio of(balance - minimumBalance) / minimumBalance. - Updates
_totalWeightand emits events.
- Marks the token as ready (
- In all bound cases, it sets
_records[token].balance = balance.
Crucially, gulp never mints new pool shares when it increases Record.balance. It assumes that any new ERC20 balance is implicitly backing existing shares. When the SUSHI balance increase comes from a flash swap rather than from additional LP deposits, this assumption is wrong: the pool appears to have more SUSHI reserves than it truly owns economically once the flash loan is repaid.
This is the precise invariant break: the mapping from share supply to underlying reserves is disrupted because the SUSHI balance is inflated without issuing new shares.
4.5 Mispriced joins and exits extract value
With SUSHI now marked as ready and assigned a non-trivial weight, the pool’s internal accounting treats SUSHI as a normal, fully collateralized component. The router then uses joinswapExternAmountIn and exitPool to extract value.
In joinswapExternAmountIn, the pool calculates how many new pool shares to mint for a given tokenAmountIn of a component token using Balancer math over Record.balance, Record.denorm, _totalSupply, and _totalWeight. Because Record.balance for SUSHI is inflated by the flash swap, these pricing calculations are skewed and can allow the adversary to obtain pool shares more cheaply than they should.
In exitPool, the pool burns a specified amount of pool shares and returns proportional amounts of each underlying token based on Record.balance:
// exitPool excerpt showing dependence on Record.balance
// artifacts/root_cause/data_collector/iter_1/contract/1/0x669693A4.../source/src/temp-contracts/balancer/IndexPool.sol
function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut)
external
override
_lock_
{
...
uint256 poolTotal = totalSupply();
uint256 exitFee = bmul(poolAmountIn, EXIT_FEE);
uint256 pAiAfterExitFee = bsub(poolAmountIn, exitFee);
uint256 ratio = bdiv(pAiAfterExitFee, poolTotal);
...
for (uint256 i = 0; i < minAmountsOut.length; i++) {
address t = _tokens[i];
Record memory record = _records[t];
if (record.ready) {
uint256 tokenAmountOut = bmul(ratio, record.balance);
...
_records[t].balance = bsub(record.balance, tokenAmountOut);
_pushUnderlying(t, msg.sender, tokenAmountOut);
}
}
}
Because record.balance for SUSHI was overstated by gulp, the computed tokenAmountOut across all components (including WETH/ETH exposure) is larger than it should be. The adversary sequences a join and an exit in such a way that, after repaying the flash swap, their EOA’s net ETH balance has increased by a large amount.
4.6 Profit validation from on-chain balances and gas
The attacker’s ETH profit is directly validated from on-chain balance diffs and the transaction receipt. The pre/post balance diff for the EOA shows:
// Attacker EOA native balance change
// artifacts/root_cause/data_collector/iter_1/tx/1/0x44aad3b8.../balance_diff_prestate.json
{
"native_balance_deltas": [
{
"address": "0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe",
"before_wei": "1674074821000000000",
"after_wei": "15734691121000000000",
"delta_wei": "14060616300000000000"
}
]
}
This corresponds to:
before=1.674074821ETHafter=15.734691121ETHdelta=14.0606163ETH
From the transaction receipt:
// Gas parameters from receipt
// artifacts/root_cause/data_collector/iter_1/tx/1/0x44aad3b8.../receipt.json
{
"result": {
"gasUsed": "0x5f8f1e",
"effectiveGasPrice": "0x22ecb25c00",
"from": "0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe",
"status": "0x1"
}
}
gasUsed 0x5f8f1e equals 6,262,558 units, and effectiveGasPrice 0x22ecb25c00 equals 150 gwei. The gas fee is therefore:
6,262,558 * 150e-9ETH =0.9393837ETH.
The EOA’s net ETH balance change of +14.0606163 ETH already reflects this gas cost, meaning the adversary’s ETH-denominated portfolio increases from 1.674074821 ETH to 15.734691121 ETH within a single transaction while simultaneously draining SUSHI from the Sushi LP into DEFI5.
5. Adversary Flow Analysis
The adversary-related cluster consists of:
- EOA:
0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe(unprivileged attacker). - Router/orchestrator:
0x277e851587eb5da22b52a10f4788576e68150277.
The minimal profitable ACT transaction sequence b is a single Ethereum mainnet transaction:
- Chain: Ethereum mainnet (
chainid = 1). - Transaction hash:
0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa. - Sender:
0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe. - To:
0x277e851587eb5da22b52a10f4788576e68150277.
The annotated transaction history confirms there are no additional attacker-crafted transactions required for this profit:
// Attacker EOA annotated txlist
// artifacts/root_cause/data_collector/iter_2/address/1/0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe/txlist_annotated.json
{
"result": [
{
"hash": "0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa",
"blockNumber": "13417949",
"to": "0x277e851587eb5da22b52a10f4788576e68150277",
"role": "profit-taking"
},
...
]
}
Within this transaction, the decoded call trace shows the following high-level flow:
-
EOA -> Router
- The attacker EOA calls the router at
0x277e8515...with calldata that encodes the entire exploit sequence.
- The attacker EOA calls the router at
-
Router -> Controller (reindex)
- The router calls
reindexPool(DEFI5)on the controller proxy0xf00a3..., which delegates toMarketCapSqrtController0x120c69...to compute new weights and configure SUSHI as a DEFI5 component.
- The router calls
-
Router -> Sushi LP (flash swap)
- The router initiates a UniswapV2-style flash swap on the SUSHI/WETH LP
0x795065d..., borrowing approximately3.996e22SUSHI tokens into the DEFI5 pool address0xfa6de....
- The router initiates a UniswapV2-style flash swap on the SUSHI/WETH LP
-
Router -> DEFI5 (gulp SUSHI)
- With SUSHI now sitting in the DEFI5 contract, the router calls
gulp(SUSHI)onIndexPool. As analyzed above, this marks SUSHI as ready, assigns it a weight, and setsRecord.balanceto the full ERC20 SUSHI balance without minting new pool shares.
- With SUSHI now sitting in the DEFI5 contract, the router calls
-
Router -> DEFI5 (joinswap + exit)
- The router then calls
joinswapExternAmountInandexitPoolon DEFI5, using the inflated SUSHI-backed accounting to redeem more underlying value (including WETH/ETH) than it contributes. The exact flow is detailed in the decoded trace inartifacts/root_cause/data_collector/iter_2/tx/1/0x44aad3b8.../call_trace_decoded.json.
- The router then calls
-
Router -> Sushi LP (repay flash swap)
- Finally, the router repays the SUSHI flash swap to the Sushi LP. After repayment, DEFI5 remains under-collateralized relative to its share supply, while the attacker EOA holds a net ETH gain of
14.0606163and DEFI5 holds an inflated SUSHI position.
- Finally, the router repays the SUSHI flash swap to the Sushi LP. After repayment, DEFI5 remains under-collateralized relative to its share supply, while the attacker EOA holds a net ETH gain of
Every contract involved (controller, DEFI5 pool, Sushi LP, router) exposes the exercised entrypoints as publicly callable functions. Given publicly available ABIs and verified source code, any unprivileged EOA can construct equivalent calldata and submit the same transaction under standard Ethereum inclusion rules.
6. Impact & Losses
The on-chain impact is measurable from balance diffs and token transfer logs:
- Attacker ETH profit: The attacker EOA’s ETH balance increases from
1.674074821ETH to15.734691121ETH in the seed transaction, a deterministic net gain of14.0606163ETH after paying0.9393837ETH in gas. - WETH / ETH outflows: WETH9 at
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2experiences a-15ETH-equivalent delta during the transaction, consistent with DEFI5 and related logic unwrapping or transferring ETH-denominated value to the attacker. - SUSHI reallocation:
39,960,196,807,806,751,520,000SUSHI move from the Sushi LP0x795065dcc9f64b5614c407a6efdc400da6221fb0to DEFI50xfa6de2697d59e88ed7fc4dfe5a33dac43565ea41. This transfer, combined with the invariant-violatinggulp, leaves DEFI5 with distorted SUSHI exposure and reduced effective backing for its pool share supply.
The primary economic victims are DEFI5 liquidity providers and token holders, whose pool shares are left overvalued relative to true reserves after the attacker extracts ETH and rebalances the pool via the exploit.
7. References
- Seed transaction and balance diffs – Transaction
0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aametadata and pre/post balance changes in:artifacts/root_cause/seed/1/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa/metadata.jsonartifacts/root_cause/seed/1/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa/balance_diff.jsonartifacts/root_cause/data_collector/iter_1/tx/1/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa/balance_diff_prestate.json
- Prestate and receipt evidence – QuickNode prestate tracer output and transaction receipt in:
artifacts/root_cause/data_collector/iter_1/tx/1/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa/state_diff_prestate_raw.jsonartifacts/root_cause/data_collector/iter_1/tx/1/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa/receipt.json
- Decoded call traces and LP reserve changes – Full ABI-decoded trace and Sushi LP reserve evolution in:
artifacts/root_cause/data_collector/iter_2/tx/1/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa/call_trace_decoded.jsonartifacts/root_cause/data_collector/iter_2/tx/1/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa/sushi_lp_reserves_decoded.json
- DEFI5 IndexPool and controller code – Victim pool implementation and controller/Oracle logic in:
artifacts/root_cause/data_collector/iter_1/contract/1/0x669693A42B58E87b9e568bA2C6AdD607eb298d95/source/src/temp-contracts/balancer/IndexPool.solartifacts/root_cause/data_collector/iter_2/contract/1/0x120c6956d292b800a835cb935c9dd326bdb4e011/source/MarketCapSqrtController.sol
- Attacker address activity – EOA transaction history and role annotations in:
artifacts/root_cause/data_collector/iter_2/address/1/0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe/txlist_annotated.json