We do not have a reliable USD price for the recorded assets yet.
0x95c1604789c93f41940a7fd9eca11276975a9a65d250b89a247736287dbd2b7e0xdbB20A979a92ccCcE15229e41c9B082D5b5d7E31EthereumOn Ethereum mainnet block 19109730, an unprivileged adversary used transaction 0x95c1604789c93f41940a7fd9eca11276975a9a65d250b89a247736287dbd2b7e to drain 23164877961176708753 PEAS from the single-asset Peapods WeightedIndex at 0xdbB20A979a92ccCcE15229e41c9B082D5b5d7E31. The attacker-controlled helper contract 0xbED4fbF7C3e36727cCDAB4C6706c3C0e17b10397 repeatedly borrowed the index's full PEAS inventory through flash(), bonded that same PEAS back during the flash callback to mint index shares, debonded the accumulated shares, and sold the extracted PEAS into the PEAS/DAI Uniswap V3 pool for DAI. After paying the protocol's fixed flash fees, the helper finished with 49552543223355301726 net DAI more than it started with.
The root cause is a flash-bond share inflation bug. DecentralizedIndex.flash() transfers PEAS out and executes an untrusted callback before it checks repayment, while WeightedIndex.bond() treats any end-of-call PEAS balance increase as valid collateral. That combination let the attacker recycle flash-borrowed PEAS as if it were fresh capital and mint redeemable index shares against assets the protocol already owned.
The victim contract was a PEAS-only index at the exploit pre-state. getAllAssets() returned exactly one constituent token, PEAS at 0x02f92800F57BCD74066F5709F1Daa1A4302Df875, which meant every bond and debond operation mapped directly to the same PEAS reserve that exposed.
flash()The fee configuration also mattered. The verified source and the validated PoC both confirm FLASH_FEE_DAI() == 10, BOND_FEE() == 100, and DEBOND_FEE() == 300. Because the flash fee was flat rather than proportional to borrowed size, the attacker could repeatedly borrow the entire reserve for a fixed 10 DAI charge per loop.
The adversary lifecycle involved two public accounts:
0x835d4928e3447affacf07221f2894607f9a21765, which deployed the helper contract and sent the exploit transaction.0xbED4fbF7C3e36727cCDAB4C6706c3C0e17b10397, which received the flash-borrowed PEAS, minted the inflated index-share position, redeemed it, and exited through Uniswap V3.The vulnerability class is an accounting bug triggered by an untrusted callback across minting-critical state. The intended invariant is straightforward: every newly minted WeightedIndex share must correspond to a net increase in externally supplied underlying assets held by the index. That invariant fails because flash() and bond() observe the same reserve from two incompatible perspectives.
In DecentralizedIndex.flash(), the contract first charges the DAI fee, snapshots the token balance, transfers the requested PEAS out to the recipient, invokes IFlashLoanRecipient(_recipient).callback(_data), and only then checks that the PEAS balance has been restored:
function flash(address _recipient, address _token, uint256 _amount, bytes calldata _data) external override {
IERC20(DAI).safeTransferFrom(_msgSender(), _rewards, FLASH_FEE_DAI * 10 ** IERC20Metadata(DAI).decimals());
uint256 _balance = IERC20(_token).balanceOf(address(this));
IERC20(_token).safeTransfer(_recipient, _amount);
IFlashLoanRecipient(_recipient).callback(_data);
require(IERC20(_token).balanceOf(address(this)) >= _balance, "FLASHAFTER");
}
In WeightedIndex.bond(), the contract calculates the amount of index shares to mint from the attacker-supplied _amount, mints those shares immediately, and then uses _transferAndValidate() to confirm only that the victim's ending balance increased by the expected amount:
function bond(address _token, uint256 _amount) external override noSwap {
uint256 _tokensMinted = (_amount * FixedPoint96.Q96 * 10 ** decimals()) / indexTokens[_tokenIdx].q1;
uint256 _feeTokens = _isFirstIn() ? 0 : (_tokensMinted * BOND_FEE) / 10000;
_mint(_msgSender(), _tokensMinted - _feeTokens);
if (_feeTokens > 0) _mint(address(this), _feeTokens);
_transferAndValidate(IERC20(indexTokens[_i].token), _msgSender(), _transferAmount);
}
The critical validation helper is:
function _transferAndValidate(IERC20 _token, address _sender, uint256 _amount) internal {
uint256 _balanceBefore = _token.balanceOf(address(this));
_token.safeTransferFrom(_sender, address(this), _amount);
require(_token.balanceOf(address(this)) >= _balanceBefore + _amount, "TFRVAL");
}
Because the PEAS sent back during the flash callback satisfies _transferAndValidate(), bond() accepts flash inventory as if it were external collateral. With a single-asset index, the attacker can repeat that cycle as many times as the flat flash fee remains economical, then redeem the synthetic share balance for real PEAS.
The exploit pre-state sigma_B is Ethereum mainnet immediately before transaction 0x95c1604789c93f41940a7fd9eca11276975a9a65d250b89a247736287dbd2b7e in block 19109730. The collected transaction metadata shows the call came from EOA 0x835d4928e3447affacf07221f2894607f9a21765 to helper contract 0xbed4fbf7c3e36727ccdab4c6706c3c0e17b10397, with calldata encoding the victim index, PEAS token, and loop count 20.
The trace demonstrates the recursive minting pattern. Near the end of the exploit trace, the same full-reserve PEAS amount 593301729600583584619 appears in each FlashLoan and Bond event pair:
emit FlashLoan(executor: 0xbED4..., recipient: 0xbED4..., token: PEAS, amount: 593301729600583584619)
emit Bond(wallet: 0xbED4..., token: PEAS, amountTokensBonded: 593301729600583584619, amountTokensMinted: 593301729600583584619)
That is the code-level breakpoint in action. flash() lends the entire PEAS reserve to the helper, the helper immediately calls bond() during the callback, and _transferAndValidate() accepts the returned PEAS as new collateral even though those tokens originated from the victim moments earlier. The flash repayment check then passes because the PEAS balance has been restored by the bond deposit.
After twenty loops, the helper had accumulated 11747374246091554975460 index shares. The same trace then shows the debond and reserve drain:
emit Transfer(from: WeightedIndex, to: 0xbED4..., value: 23164877961176708753)
emit Debond(wallet: 0xbED4..., amountDebonded: 11747374246091554975460)
The collected balance diff confirms the victim reserve loss in state terms:
{
"token": "0x02f92800f57bcd74066f5709f1daa1a4302df875",
"holder": "0xdbb20a979a92cccce15229e41c9b082d5b5d7e31",
"before": "593301729600583584619",
"after": "570136851639406875866",
"delta": "-23164877961176708753"
}
The final monetization step sold the drained PEAS into the PEAS/DAI Uniswap V3 pool at 0xAe750560b09aD1F5246f3b279b3767AfD1D79160. The trace records the corresponding DAI transfer:
emit Transfer(from: 0xAe750560b09aD1F5246f3b279b3767AfD1D79160, to: 0xbED4..., value: 249552543223355301726)
Subtracting the helper's initial 210000000000000000000 DAI and the twenty fixed 10 DAI flash fees yields the reported net profit of 49552543223355301726 DAI. The exploit is therefore fully deterministic, reproducible from public state, and realizable by any unprivileged actor that can fund the fixed flash fees and deploy a callback-capable helper contract.
The adversary flow had three stages. First, transaction 0xdc81426ef7c48c7426e703191ff2cb721af30a119b48c091e531616a91eb6b2b deployed the helper contract. Second, transaction 0x95c1604789c93f41940a7fd9eca11276975a9a65d250b89a247736287dbd2b7e executed the recursive flash-bond loop: the helper paid the 10 DAI flash fee, borrowed the full PEAS reserve, called back into bond(), and repeated that process twenty times. Third, the helper debonded its full inflated index-share position, withdrew PEAS from the victim, and swapped the extracted PEAS for DAI through the Uniswap V3 PEAS/DAI pool.
The attacker did not need privileged access, a compromised key, or hidden state. The required conditions were public and mechanical: the victim had to expose flash() and bond() without a reentrancy guard or provenance check on bonded collateral, the index had to hold PEAS inventory, and the attacker had to fund the flat flash fees. Those conditions define an ACT opportunity rather than an insider-only exploit.
The direct protocol loss was 23164877961176708753 PEAS removed from the WeightedIndex reserve. The transaction-level economic gain was 49552543223355301726 net DAI to the helper contract after fees. The exploit therefore converted victim-held PEAS inventory into attacker-controlled DAI while leaving the protocol with a permanently depleted reserve and an inflated supply path that should never have been reachable.
0x95c1604789c93f41940a7fd9eca11276975a9a65d250b89a247736287dbd2b7e0xdc81426ef7c48c7426e703191ff2cb721af30a119b48c091e531616a91eb6b2bWeightedIndex at 0xdbB20A979a92ccCcE15229e41c9B082D5b5d7E31PEAS at 0x02f92800F57BCD74066F5709F1Daa1A4302Df875PEAS/DAI Uniswap V3 at 0xAe750560b09aD1F5246f3b279b3767AfD1D79160