We do not have a reliable USD price for the recorded assets yet.
0x04c80bb477890f3021f03b068238836ee20aa0b8EthereumBarley Finance's WeightedIndex at 0x04c80Bb477890F3021F03B068238836Ee20aA0b8 was drained on Ethereum mainnet in transaction 0x995e880635f4a7462a420a58527023f946710167ea4c6c093d7d193062a33b01 after an attacker prefunded a helper contract with 200 DAI and then ran twenty public flash-loan cycles against the index. In each cycle, the helper borrowed the full BARL reserve through flash, immediately called bond with the borrowed BARL inside the flash callback, and ended the callback with the reserve restored so the flash-loan repayment check still passed. After twenty loops, the helper held an inflated index-share balance with no net BARL contribution, debonded those shares, and extracted 7876244499261782261563980 BARL from the victim reserve.
The root cause is an accounting flaw in the interaction between flash, bond, and _transferAndValidate. bond mints shares before it proves that new collateral has actually increased the index's net assets, and _transferAndValidate only compares the post-transfer balance against the current in-flight balance snapshot. During flash, the index's BARL balance is temporarily zero, so returning flash-loaned BARL through bond is incorrectly treated as fresh collateral and produces redeemable shares for the attacker.
0x995e880635f4a7462a420a58527023f946710167ea4c6c093d7d193062a33b01Barley Finance's WeightedIndex is a BARL-backed index token. bond(address _token, uint256 _amount) mints index shares based on _amount, keeping a 1% bond fee in newly minted shares for the index contract itself. debond(uint256 _amount, ...) burns the caller's shares and releases a proportional amount of the underlying assets, subject to the debond-fee logic in the contract.
The same contract inheritance tree also exposes flash(address recipient, address token, uint256 amount, bytes data) through DecentralizedIndex. That function charges a fixed 10 DAI fee, transfers the requested token out to the recipient, executes the recipient callback, and then checks that the token balance after the callback is at least the pre-loan balance. Because flash is public, any unprivileged contract can borrow the reserve as long as it restores the balance before returning.
The pre-exploit state at block 19106654 is deterministic from the collected artifacts: the victim index held 8611951186321848770844715 BARL, the helper contract held zero BARL, and the attacker-funded DAI budget for flash fees was 200 DAI. Those values are the baseline for both the root-cause analysis and the PoC oracle.
The vulnerability class is an attack-level accounting flaw in collateral recognition during an in-flight flash-loan cycle. The intended invariant is that newly minted index shares must correspond to a real net increase in BARL assets held by the index. The implementation breaks that invariant because WeightedIndex.bond computes and mints shares before proving that the transferred tokens increased the contract's net reserve. The later validation in _transferAndValidate is also too weak for flash-loan contexts because it snapshots the current balance at call time, not the asset position before the flash loan began.
As a result, when flash transfers the full BARL reserve to the attacker's helper, the helper can approve that same BARL back to the index and call bond(BARL, flashAmount) during the callback. bond mints fresh shares immediately, and _transferAndValidate sees balanceBefore == 0, so transferring the borrowed BARL back satisfies the deposit check even though the transaction only restored the flash-loaned reserve. The subsequent flash repayment check also passes because the same returned BARL restores the reserve. Repeating that sequence compounds unbacked shares until debond can convert them into a large BARL withdrawal from the victim.
The relevant victim code shows the flawed ordering directly. In WeightedIndex.bond, the contract computes _tokensMinted, mints 99% of that amount to the caller, optionally mints the fee to itself, and only afterwards calls _transferAndValidate for the underlying token transfer:
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 validation function in DecentralizedIndex only compares the post-transfer balance to the contract's balance at the moment bond is called:
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");
}
That logic is safe only if _balanceBefore represents a stable, pre-deposit asset base. It does not during a flash loan. flash first snapshots the reserve, transfers the asset out, executes the callback, and only afterwards checks whether the final balance recovered:
function flash(address _recipient, address _token, uint256 _amount, bytes calldata _data) external override {
uint256 _balance = IERC20(_token).balanceOf(address(this));
IERC20(_token).safeTransfer(_recipient, _amount);
IFlashLoanRecipient(_recipient).callback(_data);
require(IERC20(_token).balanceOf(address(this)) >= _balance, "FLASHAFTER");
}
The exploit trace shows this exact sequence repeating. During each loop, the victim emits a BARL transfer from the index to helper 0x356e7481b957be0165d6751a49b4b7194aef18d5, then WeightedIndex::bond(BARL, 8611951186321848770844715) mints 8525831674458630283136268 shares to the helper and 86119511863218487708447 fee shares to the index, and the helper transfers the same BARL amount back to the index. The flash-loan event then emits successfully, proving the reserve was restored for that loop.
WeightedIndex::flash(..., BARL, 8611951186321848770844715, 0x)
emit Transfer(from: WeightedIndex, to: helper, value: 8611951186321848770844715)
WeightedIndex::bond(BARL, 8611951186321848770844715)
emit Transfer(from: 0x0, to: helper, value: 8525831674458630283136268)
emit Transfer(from: 0x0, to: WeightedIndex, value: 86119511863218487708447)
emit Transfer(from: helper, to: WeightedIndex, value: 8611951186321848770844715)
emit FlashLoan(...)
After twenty loops, the helper holds 170516633489172605662725360 index shares. It then calls debond once, and the trace shows a BARL transfer of 7876244499261782261563980 from the victim index to the helper. The collected balance diff confirms the victim BARL reserve moved from 8611951186321848770844715 to 735706687060066509280735, which is the exact drained amount stated in the analysis.
The attacker cluster has two roles. EOA 0x7b3a6eff1c9925e509c2b01a389238c1fcc462b6 funded the exploit and ultimately received the monetized output, while helper contract 0x356e7481b957be0165d6751a49b4b7194aef18d5 executed the public protocol calls.
First, the EOA sent 20 DAI in transaction 0xa685928b5102349a5cc50527fec2e03cb136c233505471bdd4363d0ab077a69a and 180 DAI in transaction 0xaaa197c7478063eb1124c8d8b03016fe080e6ec4c4f4a4e6d7f09022084e3390 to prefund the helper with the exact 200 DAI needed for twenty flash-loan fees. Next, in transaction 0x995e880635f4a7462a420a58527023f946710167ea4c6c093d7d193062a33b01, the helper looped twenty times over the same public sequence: approve 10 DAI fee, call flash, receive the full BARL reserve, approve BARL back to the index, and call bond inside the callback. Because each loop both restores the flash reserve and mints new shares, the helper compounds an unbacked share position without introducing new BARL.
Finally, the helper debonded the accumulated shares and received 7876244499261782261563980 BARL. The trace then shows SwapRouter::exactInput swapping that BARL through public Uniswap V3 liquidity, with the final WETH output 52133956080457251524 transferred to the attacker EOA. The ACT property holds because every step used permissionless ERC-20 transfers, public protocol methods, and attacker-controlled contracts only.
The measurable victim loss is the BARL reserve drained from WeightedIndex. The collected balance diff records a direct reduction of 7876244499261782261563980 BARL from the victim contract in the exploit transaction. That amount equals 7,876,244.49926178226156398 BARL at 18 decimals.
The attack also inflated the total supply economics of the index by creating 170516633489172605662725360 attacker-controlled shares that were not backed by new assets. The exploit therefore breaks the core backing invariant of the index and converts the accounting flaw into immediate reserve extraction.
0x995e880635f4a7462a420a58527023f946710167ea4c6c093d7d193062a33b010xa685928b5102349a5cc50527fec2e03cb136c233505471bdd4363d0ab077a69a, 0xaaa197c7478063eb1124c8d8b03016fe080e6ec4c4f4a4e6d7f09022084e3390WeightedIndex / DecentralizedIndex at 0x04c80Bb477890F3021F03B068238836Ee20aA0b80x3e2324342bF5B8A1Dca42915f0489497203d640E, DAI 0x6B175474E89094C44Da98b954EedeAC495271d0FWeightedIndex.sol, and DecentralizedIndex.sol from the collected seed artifacts