This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x633e538ecf0bee1a18c2edfe10c4da0d6e71e77bBSC0x8cb88701790f650f273c8bb2cc4c5f439cd65219BSC0x542c06a5dc3f27e0fbdc9fb7bc6748f26d54ddb0BSCOn BSC, the adversary first created a small USDT/WBNB LP position in PancakeBunny's VaultFlipToFlip vault in transaction 0x88fcffc3256faac76cde4bbd0df6ea3603b1438a5a0409b2e2b91e7c2ba3371a, then exploited the reward path in transaction 0x897c2de73dd55d7701e1b69ffb3a17b0f4801ced88b0c75fe1551c5fcce6a979. The profitable transaction used nested PancakeSwap flash swaps plus a public Bank USDT flashloan to distort AMM reserves, trigger getReward(), mint an outsized amount of BUNNY, repay all borrowed liquidity, and exit with large residual WBNB and BUNNY.
The root cause is that PancakeBunny treated a flashloan-manipulable LP spot valuation as a trustworthy reward oracle. VaultFlipToFlip.getReward() forwards LP-denominated performance fees into BunnyMinterV2.mintForV2(), and BunnyMinterV2 converts that fee into BUNNY/WBNB LP and values it with PriceCalculatorBSCV1.valueOfAsset() using instantaneous AMM reserves inside the same transaction.
PancakeBunny's VaultFlipToFlip vault tracks principal and LP-denominated profit for each depositor. When a user calls getReward(), the vault calculates earned(msg.sender), withdraws that LP-denominated profit from MasterChef, computes a performance fee, and sends the fee into the Bunny minter rather than paying it back directly to the caller.
The minter path is critical because it converts the fee's economic value into newly minted BUNNY. In BunnyMinterV2.mintForV2(), the fee asset is zapped into the LP token, that LP position is valued in BNB, and the resulting BNB-denominated contribution feeds .
BUNNY/WBNBamountBunnyToMint(...)The price calculator is not oracle-backed. PriceCalculatorBSCV1.valueOfAsset() prices a Pancake LP directly from live reserves and total supply. For LPs containing WBNB, it computes LP value from the current WBNB reserve share, which means a same-transaction reserve distortion directly changes the mint basis.
The vulnerability is an economic attack in the reward-mint pipeline. The vault correctly computes that the attacker has a small positive LP-denominated reward, but the downstream minter incorrectly assumes that the fee LP can be converted and valued at an honest market price within the same transaction. That assumption fails because both the source LP economics and the destination BUNNY/WBNB LP valuation are exposed to flashloan-driven reserve distortion. The exploit therefore does not need broken access control or arithmetic bugs. It only needs a valid vault position, public flash liquidity, and a reward path that prices LP output from live AMM state. The code-level breakpoint is BunnyMinterV2.mintForV2() calling priceCalculator.valueOfAsset(BUNNY_BNB, bunnyBNBAmount) after _zapAssetsToBunnyBNB(...). At that point, the protocol translates attacker-controlled reserve state directly into newly minted BUNNY.
The verified VaultFlipToFlip source shows that getReward() realizes LP-denominated profit and forwards the performance fee into the minter:
function getReward() external override {
uint amount = earned(msg.sender);
...
uint performanceFee = canMint() ? _minter.performanceFee(amount) : 0;
if (performanceFee > DUST) {
_minter.mintForV2(address(_stakingToken), 0, performanceFee, msg.sender, depositTimestamp);
amount = amount.sub(performanceFee);
}
_stakingToken.safeTransfer(msg.sender, amount);
}
The verified BunnyMinterV2 source shows the vulnerable mint step:
uint bunnyBNBAmount = _zapAssetsToBunnyBNB(asset, feeSum, true);
(uint valueInBNB,) = priceCalculator.valueOfAsset(BUNNY_BNB, bunnyBNBAmount);
uint contribution = valueInBNB.mul(_performanceFee).div(feeSum);
uint mintBunny = amountBunnyToMint(contribution);
_mint(mintBunny, to);
The verified PriceCalculatorBSCV1 source shows why the valuation is manipulable:
(uint reserve0, uint reserve1, ) = IPancakePair(asset).getReserves();
if (IPancakePair(asset).token0() == WBNB) {
valueInBNB = amount.mul(reserve0).mul(2).div(IPancakePair(asset).totalSupply());
} else if (IPancakePair(asset).token1() == WBNB) {
valueInBNB = amount.mul(reserve1).mul(2).div(IPancakePair(asset).totalSupply());
}
The attack trace confirms that the exploit passes through exactly this path. The run(...) transaction includes VaultFlipToFlip::getReward(), then BunnyMinterV2::mintForV2(...), then PriceCalculatorBSCV1::valueOfAsset(...), and then a large mint:
0xd415e6Ca...::getReward() [delegatecall]
0x8cB88701...::mintForV2(..., 156535657809713, 0xcc598232..., 1621463485)
0x542c06a5...::valueOfAsset(0x7Bb89460..., 105609410271999464040453) [staticcall]
BunnyToken::mint(6972455699137117802944353)
This sequence is sufficient to establish the invariant break. A tiny LP-denominated fee inside the vault was converted into more than 6.972e24 BUNNY units because the valuation input was based on reserve state that the attacker was actively distorting with public flash liquidity.
The adversary cluster consists of EOA 0xa0acc61547f6bd066f7c9663c17a312b6ad7e187 and helper contract 0xcc598232a75fb1b361510bce4ca39d7bc39cf498. The setup transaction funded the helper with 1 WBNB, zapped into the public USDT/WBNB Pancake LP at 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae, and deposited 9.275133809515340750 LP tokens into the Bunny vault. That position later accrued a small positive earned() balance.
The exploit transaction then chained six PancakeSwap flash swaps and a public Bank flashloan of 2,961,750,450,987,026,369,366,661 USDT units. During the deepest callback, the helper manipulated the staking LP and related markets, then called IVaultFlipToFlip(VAULT).getReward(). That reward call consumed the accrued LP profit, invoked mintForV2, and caused the minter to price BUNNY/WBNB LP output against the manipulated live reserves.
After minting, the helper unwound the manipulated positions, swapped out residual BUNNY through two BUNNY/WBNB pairs, topped up USDT as needed to repay the Bank loan plus fee, repaid every flash-swap leg, and finally transferred the remaining WBNB and BUNNY back to the controlling EOA. Every component in this sequence was public and callable by an unprivileged actor.
The measurable outcome is a direct, permissionless extraction of value from PancakeBunny's reward mint path. The collector balance diffs show the attacker EOA receiving 697245569913711780294436 BUNNY units during the exploit transaction. The validated root-cause artifact reports direct WBNB sequence profit of 114630542114869651730975 wei-scaled WBNB after subtracting the initial 1 WBNB setup capital, with additional BUNNY upside beyond that figure.
The victim set for this root cause is the PancakeBunny reward path composed of the vault at 0x633e538ecf0bee1a18c2edfe10c4da0d6e71e77b, the minter at 0x8cb88701790f650f273c8bb2cc4c5f439cd65219, and the price calculator at 0x542c06a5dc3f27e0fbdc9fb7bc6748f26d54ddb0. The exploit is ACT because no privileged keys, private orderflow, or attacker-only infrastructure were required.
0x88fcffc3256faac76cde4bbd0df6ea3603b1438a5a0409b2e2b91e7c2ba3371a0x897c2de73dd55d7701e1b69ffb3a17b0f4801ced88b0c75fe1551c5fcce6a979VaultFlipToFlip source for getReward()BunnyMinterV2 source for mintForV2() and _zapAssetsToBunnyBNB()PriceCalculatorBSCV1 source for valueOfAsset()getReward -> mintForV2 -> valueOfAsset -> BunnyToken::mint