Calculated from recorded token losses using historical USD prices at the incident time.
0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36c0xb390b07fcf76678089cb12d8e615d5fe494b01fbBSC0x21125d94cfe886e7179c8d2fe8c1ea8d57c73e0eBSC0x7c9e73d4c71dae564d41f78d56439bb4ba87592fBSCOn BNB Smart Chain block 7,457,125, transaction 0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36c drained Alpaca Finance's BUSD auto-compounding pool 13 through a permissionless same-transaction loop. The attacker EOA 0x47f341d896b08daacb344d9021f955247e50d089 called attacker contract 0xef39f14213714001456e2e89eddbdf8c850c3be6, sourced temporary capital from Cream's public crBUSD flash-loan entrypoint, and repeatedly deposited into and emergency-withdrew from BvaultsBank pool 13. The direct on-chain result was a profit of 452868889740007548480164 BUSD to the attacker contract, a 2341271733535381575962 BUSD fee to Cream, and 224119053087641410424550 BUSD left stranded inside the vulnerable strategy.
The root cause is a unit mismatch in Alpaca's BvaultsStrategy.withdraw. BvaultsBank computes a user's withdrawal entitlement in BUSD units, but the strategy forwards that BUSD-denominated _wantAmt directly into FairLaunch and ibBUSD withdrawal functions that interpret the argument as ibBUSD share units. Because ibBUSD share price already exceeded 1 BUSD per share before the exploit, each withdrawal burned too many shares and redeemed more BUSD than the strategy removed from its own accounting.
The affected pool was BvaultsBank pool 13 at 0xb390b07fcf76678089cb12d8e615d5fe494b01fb. That pool accepted BUSD deposits and routed capital to strategy 0x21125d94cfe886e7179c8d2fe8c1ea8d57c73e0e, which in turn deposited BUSD into ibBUSD vault 0x7c9e73d4c71dae564d41f78d56439bb4ba87592f and staked the resulting ibBUSD shares in Alpaca FairLaunch pool 3 at .
0xa625ab01b08ce023b2a342dbb12a16f2c8489a8fThe important accounting distinction is that BvaultsBank and strategy-level user accounting are BUSD-denominated, while ibBUSD is a share token. The ibBUSD implementation exposes withdraw(uint256 share), where the argument is a share amount and the redeemed underlying depends on share * totalToken / totalSupply. At the exploit pre-state, ibBUSD totalToken() exceeded totalSupply(), so one ibBUSD share redeemed more than one BUSD.
The public financing leg was also permissionless. Cream's crBUSD at 0x2bc4eb013ddee29d37920938b96d353171289b7c emitted a flash-loan event in the exploit transaction for 7804239111784605253208456 BUSD with fee 2341271733535381575962, demonstrating that no privileged capital or privileged access was required.
The vulnerability class is an application-level accounting bug in a strategy withdrawal path. BvaultsBank calculates each user's withdrawable amount as a proportional claim on the strategy's wantLockedTotal, which is tracked in BUSD units. That design is valid only if the strategy withdraw path converts BUSD-denominated entitlement into the correct number of ibBUSD shares before unstaking from FairLaunch and redeeming from the vault.
Instead, BvaultsStrategy.withdraw treats _wantAmt as though it were already an ibBUSD share amount. The strategy calls FairLaunch withdraw and ibBUSD withdraw using the same raw _wantAmt, even though the downstream contracts interpret the value in share units. When share price is above 1, burning _wantAmt shares returns more than _wantAmt BUSD, so the strategy receives a surplus on every emergency withdrawal cycle. It then forwards only _wantAmt BUSD back to BvaultsBank and decrements wantLockedTotal by only _wantAmt, leaving the excess BUSD inside the strategy. Repeating deposit plus emergency withdraw re-stakes the retained surplus and compounds the accounting error until the attacker can exit with a large net gain.
The vulnerable bank-side withdrawal logic is visible in the verified BvaultsBank implementation:
uint256 wantLockedTotal = IStrategy(poolInfo[_pid].strategy).wantLockedTotal();
uint256 sharesTotal = IStrategy(poolInfo[_pid].strategy).sharesTotal();
uint256 amount = user.shares.mul(wantLockedTotal).div(sharesTotal);
IStrategy(poolInfo[_pid].strategy).withdraw(msg.sender, amount);
pool.want.safeTransfer(address(msg.sender), amount);
That code establishes the unit for amount: it is a BUSD entitlement derived from the user's share of wantLockedTotal.
The strategy-side breakpoint is the verified BvaultsStrategy.withdraw path:
IFairLaunch(farmContractAddress).withdraw(address(this), pid, _wantAmt);
IVault(vaultContractAddress).withdraw(_wantAmt);
IERC20(wantAddress).safeTransfer(msg.sender, _wantAmt);
wantLockedTotal = wantLockedTotal.sub(_wantAmt);
The bug is that both downstream calls interpret _wantAmt as ibBUSD shares, not BUSD. This mismatch is already exploitable at the recorded pre-state. At block 7457124, ibBUSD reported:
totalToken() = 432774267707418013139542776
totalSupply() = 421341415449018700128004728
Because totalToken / totalSupply > 1, burning _wantAmt shares returns more than _wantAmt BUSD.
The validator execution trace shows the mismatch directly during a representative exploit loop. In one cycle, the strategy calls:
ibBUSD::withdraw(8244829877014047380769405)
and ibBUSD then transfers back:
BUSD::transfer(BvaultsStrategy, 8468552234736835681451947)
The strategy next forwards only:
BUSD::transfer(BvaultsBank, 8244829877014047380769405)
So that single cycle leaves 223722357722788300682542 wei of BUSD equivalent retained inside the strategy before the next deposit/farm step. Repeating this loop increases the strategy's internal surplus while BvaultsBank continues to mint and burn user shares against only the nominal BUSD amount.
The exploit was therefore end-to-end complete at pre-state σ_B: the vulnerable strategy was active on pool 13, ibBUSD share price already exceeded 1, and temporary BUSD liquidity could be sourced permissionlessly from Cream. No privileged access, non-public state, or attacker-only infrastructure was required.
The adversary cluster consists of EOA 0x47f341d896b08daacb344d9021f955247e50d089 and attacker contract 0xef39f14213714001456e2e89eddbdf8c850c3be6. The EOA created the contract and then submitted the exploit transaction to it.
The transaction sequence is a single adversary-crafted transaction:
7804239111784605253208456 BUSD from Cream crBUSD via a public flash loan.emergencyWithdraw(13), forcing BvaultsBank to compute a BUSD entitlement and route it through the strategy's buggy withdraw path.The collector balance diff confirms the final value flow:
{
"holder": "0xef39f14213714001456e2e89eddbdf8c850c3be6",
"delta": "452868889740007548480164"
}
It also shows the vault-side loss signal:
{
"holder": "0x7c9e73d4c71dae564d41f78d56439bb4ba87592f",
"delta": "-679329214561184340480676"
}
Those two artifacts match the semantic exploit pattern: attacker profit and victim-vault depletion caused by the same unit mismatch loop.
The direct attacker profit was 452868889740007548480164 BUSD, denominated in 18-decimal BUSD units. The same exploit transaction also paid 2341271733535381575962 BUSD to Cream as the flash-loan fee and left 224119053087641410424550 BUSD stranded in the vulnerable strategy. The ibBUSD vault itself lost 679329214561184340480676 BUSD of underlying during the transaction, which is the protocol-visible depletion caused by repeated over-redemption.
The affected victim-side components were BvaultsBank pool 13, BvaultsStrategy 0x21125d94cfe886e7179c8d2fe8c1ea8d57c73e0e, and the ibBUSD vault path used by that strategy. The incident is correctly categorized as ATTACK, and specifically as an ACT opportunity because the exploit required only public contracts, public state, and a permissionless financing primitive.
0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36c0x21125d94cfe886e7179c8d2fe8c1ea8d57c73e0e0x406cfaae2c8e30a70b90baa52e753b6c17c1df9c0x7eeaa96bf1abaa206615046c0991e678a2b12da10xee0c0a840cbfc2145580c517b10afabd0b788328forge test execution log confirming the over-redemption cycle on the forked pre-state