Calculated from recorded token losses using historical USD prices at the incident time.
0xb97502d3976322714c828a890857e776f25c79f187a32e2d548dda1c315d2a7d0xc6cb12df4520b7bf83f64c79c585b8462e18b6aaBSC0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a633BSCOn BSC block 25230703, transaction 0xb97502d3976322714c828a890857e776f25c79f187a32e2d548dda1c315d2a7d used attacker-created helper contract 0xbec576e2e3552f9a1751db6a4f02e224ce216ac1 to flash-borrow 192.5 WBNB, buy BEVO, call BEVO.deliver(uint256) twice, extract the resulting reflected surplus with PancakePair.skim, and then drain 337 WBNB from the BEVO/WBNB pair 0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a633. The helper repaid 193 WBNB to the flash-loan pair and transferred 144 WBNB to profit-recipient EOA 0x5599cec4ba078d7c0c9214a19b690732d0924d0e.
The root cause is a contract-level accounting flaw in BEVO (0xc6cb12df4520b7bf83f64c79c585b8462e18b6aa), not privileged access. BEVO exposed public reflection redistribution through deliver(uint256) while leaving the BEVO/WBNB PancakeSwap pair inside the reflection set. That combination let any non-excluded holder inflate balanceOf(pair) without a transfer or reserve update, then convert the synthetic balance into real assets with public PancakeSwap functions.
BEVO is a reflection token. For non-excluded addresses, balanceOf(account) does not read a direct token balance; it converts through the current reflection rate. This means balances can change when the global reflection supply changes even if no transfer touches the account.
_rOwned[account]The relevant BEVO logic is:
function balanceOf(address account) public view override returns (uint256) {
if (_isExcluded[account]) return _tOwned[account];
return tokenFromReflection(_rOwned[account]);
}
function deliver(uint256 tAmount) public {
address sender = _msgSender();
require(!_isExcluded[sender], "Excluded addresses cannot call this function");
(uint256 rAmount,,,,,,) = _getValues(tAmount);
_rOwned[sender] = _rOwned[sender].sub(rAmount);
_rTotal = _rTotal.sub(rAmount);
_tFeeTotal = _tFeeTotal.add(tAmount);
}
function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
uint256 currentRate = _getRate();
return rAmount.div(currentRate);
}
BEVO computes its current rate after removing only addresses listed in _excluded. At the incident block, the BEVO/WBNB pair was not excluded from reflection accounting. PancakeSwap pairs, however, assume token balances only change via explicit transfers followed by reserve updates. If a token contract changes balanceOf(pair) passively while reserve0/reserve1 remain stale, public methods such as skim and swap can monetize the mismatch.
This incident is an ATTACK-class ACT exploit against BEVO’s reflection accounting model. The broken invariant is: a public token-side action must not let an unprivileged user increase an AMM pair’s spendable balanceOf(pair) without a matching transfer or reserve update. BEVO violates that invariant because deliver(uint256) reduces _rTotal, and every non-excluded holder’s token balance is derived from _rOwned / currentRate. Since the PancakeSwap pair was not excluded, its apparent BEVO balance increased after each public deliver call even though no BEVO was transferred into the pair. The attacker then used skim to pull the first synthetic surplus out of the pair and a final swap to make PancakeSwap treat the much larger reflected surplus as real amount1In. No privileged role, signature, attacker artifact, or private orderflow was required; any holder that can acquire BEVO and call public PancakeSwap functions could realize the same path.
The decisive breakpoint is BEVO deliver(uint256). It subtracts the caller’s reflected amount from both _rOwned[sender] and _rTotal, but it does not touch _rOwned[pair]. Because balanceOf(pair) is derived from tokenFromReflection(_rOwned[pair]), lowering _rTotal lowers the reflection rate and therefore raises the pair’s token balance in place.
BEVO’s supply calculation makes the issue exploitable because it excludes only addresses recorded in _excluded:
function _getCurrentSupply() private view returns(uint256, uint256) {
uint256 rSupply = _rTotal;
uint256 tSupply = _tTotal;
for (uint256 i = 0; i < _excluded.length; i++) {
if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply) return (_rTotal, _tTotal);
rSupply = rSupply.sub(_rOwned[_excluded[i]]);
tSupply = tSupply.sub(_tOwned[_excluded[i]]);
}
if (rSupply < _rTotal.div(_tTotal)) return (_rTotal, _tTotal);
return (rSupply, tSupply);
}
Supplementary state observations show that pair 0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a633 was not excluded. That made the pair a passive recipient of every reflection-rate change created by public deliver.
The seed trace shows the exploit sequence deterministically:
CoinToken::deliver(3028267986646483923)
PancakePair::skim(0xBeC576E2e3552F9A1751Db6A4f02E224Ce216aC1)
CoinToken::balanceOf(PancakePair) -> 6844218532359160336
emit Transfer(... to: 0xBeC576..., value: 4544041574685364973)
CoinToken::deliver(4544947785463603859)
PancakePair::swap(337000000000000000000, 0, 0xBeC576..., 0x)
emit Sync(reserve0: 1221197780523651391, reserve1: 728234950164515176689)
emit Swap(sender: 0xBeC576..., amount0In: 0, amount1In: 725936136828400254595, amount0Out: 337000000000000000000, amount1Out: 0, to: 0xBeC576...)
The first BEVO purchase left the BEVO/WBNB pair with reserves of 338.221197780523651391 WBNB and 2298813336114922094 BEVO. After the first deliver, the pair’s actual BEVO balance rose to 6844218532359160336 while reserves still recorded 2298813336114922094. Public skim therefore transferred 4544041574685364973 BEVO from the pair to the helper contract without any new attacker transfer into the pair.
The second deliver amplified the mismatch further. By the time of the final drain swap, the pair’s actual BEVO balance was 728234950164515176689 while PancakeSwap still treated the old reserve baseline as input accounting. The pair therefore emitted a Swap event with synthetic amount1In = 725936136828400254595 and paid out 337 WBNB.
The exploit was permissionless and reproducible from public state:
0xd99c7f6c65857ac913a8f880a4cb84032ab2fc5b.deliver(uint256) was public to any non-excluded holder.skim and swap calls were sufficient to withdraw the reflected surplus.These conditions satisfy the ACT model. The original helper contract was attacker-created in transaction 0xdb64e23314606b53f9a9d156dc87497d4b96b554f80b2d1a475f9d4516e6c578, but the exploit did not depend on any privileged attacker-owned infrastructure.
deliver) could create externally claimable reserve drift inside a third-party AMM.The adversary cluster consisted of:
0xd3455773c44bf0809e2aeff140e029c632985c50, which deployed the helper and submitted the exploit transaction.0xbec576e2e3552f9a1751db6a4f02e224ce216ac1, which executed the flash swap, BEVO purchase, deliver, skim, and final drain.0x5599cec4ba078d7c0c9214a19b690732d0924d0e, which received the WBNB proceeds and supplied CHI gas tokens used by the helper.The on-chain flow was:
0xdb64e23314606b53f9a9d156dc87497d4b96b554f80b2d1a475f9d4516e6c578, the attacker EOA created helper contract 0xbec576e2e3552f9a1751db6a4f02e224ce216ac1.0xb97502d3976322714c828a890857e776f25c79f187a32e2d548dda1c315d2a7d, the helper flash-borrowed 192.5 WBNB from the USDC/WBNB pair.3028267986646483923 BEVO.BEVO.deliver(3028267986646483923), forcing the non-excluded BEVO/WBNB pair to gain passive BEVO balance.PancakePair.skim, extracting the first reflected surplus and ending with more BEVO than it held immediately after the first deliver.BEVO.deliver(4544947785463603859) again, increasing the pair’s passive BEVO surplus a second time.PancakePair.swap(337 ether, 0, helper, ""), causing PancakeSwap to interpret the reflected BEVO drift as real swap input and release 337 WBNB.193 WBNB to the flash-loan pair and transferred the remaining 144 WBNB to 0x5599cec4ba078d7c0c9214a19b690732d0924d0e.The success predicate in the audited root-cause artifact is therefore correct: the adversary cluster realized positive WBNB profit from the public exploit path, with no dependence on privileged capabilities.
The BEVO/WBNB PancakeSwap pair lost almost all of its WBNB inventory in one transaction. Its WBNB reserve fell from 338.221197780523651391 WBNB to 1.221197780523651391 WBNB, a depletion of 337 WBNB.
Measured impact:
0x5599cec4ba078d7c0c9214a19b690732d0924d0e gained exactly 144 WBNB.0xd3455773c44bf0809e2aeff140e029c632985c50 spent 10.007043105111882249 BNB equivalent in gas.133.992956894888117751 WBNB equivalent.0xc6cb12df4520b7bf83f64c79c585b8462e18b6aa and PancakeSwap pair 0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a633.0xb97502d3976322714c828a890857e776f25c79f187a32e2d548dda1c315d2a7d0xdb64e23314606b53f9a9d156dc87497d4b96b554f80b2d1a475f9d4516e6c5780xc6cb12df4520b7bf83f64c79c585b8462e18b6aa0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a6330xd99c7f6c65857ac913a8f880a4cb84032ab2fc5bcast run -vvvvv trace for the seed exploit transaction