All incidents

BEVO reflection accounting lets anyone manufacture PancakeSwap input and drain WBNB

Share
Jan 30, 2023 09:37 UTCAttackLoss: 337 WBNBManually checked1 exploit txWindow: Atomic
Estimated Impact
337 WBNB
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Jan 30, 2023 09:37 UTC → Jan 30, 2023 09:37 UTC

Exploit Transactions

TX 1BSC
0xb97502d3976322714c828a890857e776f25c79f187a32e2d548dda1c315d2a7d
Jan 30, 2023 09:37 UTCExplorer

Victim Addresses

0xc6cb12df4520b7bf83f64c79c585b8462e18b6aaBSC
0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a633BSC

Loss Breakdown

337WBNB

Similar Incidents

Root Cause Analysis

BEVO reflection accounting lets anyone manufacture PancakeSwap input and drain WBNB

1. Incident Overview TL;DR

On 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.

2. Key Background

BEVO is a reflection token. For non-excluded addresses, balanceOf(account) does not read a direct token balance; it converts _rOwned[account] through the current reflection rate. This means balances can change when the global reflection supply changes even if no transfer touches the 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.

3. Vulnerability Analysis & Root Cause Summary

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.

4. Detailed Root Cause Analysis

4.1 Code-Level Breakpoint

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.

4.2 On-Chain Realization

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.

4.3 ACT Conditions

The exploit was permissionless and reproducible from public state:

  • The attacker only needed a normal EOA and a self-deployed helper contract.
  • Flash liquidity came from public PancakeSwap pair 0xd99c7f6c65857ac913a8f880a4cb84032ab2fc5b.
  • BEVO deliver(uint256) was public to any non-excluded holder.
  • The BEVO/WBNB pair remained non-excluded from reflection accounting.
  • Public PancakeSwap 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.

4.4 Security Principles Violated

  • AMM pool addresses that must not receive passive rebases or reflections were left inside BEVO’s reflection set.
  • A public value-redistribution primitive (deliver) could create externally claimable reserve drift inside a third-party AMM.
  • The integration failed to preserve the core AMM invariant that spendable balances and recorded reserves must not diverge under attacker control.

5. Adversary Flow Analysis

The adversary cluster consisted of:

  • EOA 0xd3455773c44bf0809e2aeff140e029c632985c50, which deployed the helper and submitted the exploit transaction.
  • Helper contract 0xbec576e2e3552f9a1751db6a4f02e224ce216ac1, which executed the flash swap, BEVO purchase, deliver, skim, and final drain.
  • Profit-recipient EOA 0x5599cec4ba078d7c0c9214a19b690732d0924d0e, which received the WBNB proceeds and supplied CHI gas tokens used by the helper.

The on-chain flow was:

  1. In deployment transaction 0xdb64e23314606b53f9a9d156dc87497d4b96b554f80b2d1a475f9d4516e6c578, the attacker EOA created helper contract 0xbec576e2e3552f9a1751db6a4f02e224ce216ac1.
  2. In exploit transaction 0xb97502d3976322714c828a890857e776f25c79f187a32e2d548dda1c315d2a7d, the helper flash-borrowed 192.5 WBNB from the USDC/WBNB pair.
  3. The helper swapped that WBNB into BEVO through PancakeSwap and acquired 3028267986646483923 BEVO.
  4. The helper called BEVO.deliver(3028267986646483923), forcing the non-excluded BEVO/WBNB pair to gain passive BEVO balance.
  5. The helper called PancakePair.skim, extracting the first reflected surplus and ending with more BEVO than it held immediately after the first deliver.
  6. The helper called BEVO.deliver(4544947785463603859) again, increasing the pair’s passive BEVO surplus a second time.
  7. The helper called PancakePair.swap(337 ether, 0, helper, ""), causing PancakeSwap to interpret the reflected BEVO drift as real swap input and release 337 WBNB.
  8. The helper repaid 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.

6. Impact & Losses

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:

  • Direct WBNB profit recipient: 0x5599cec4ba078d7c0c9214a19b690732d0924d0e gained exactly 144 WBNB.
  • Gas payer: 0xd3455773c44bf0809e2aeff140e029c632985c50 spent 10.007043105111882249 BNB equivalent in gas.
  • Net adversary-cluster profit after fees: 133.992956894888117751 WBNB equivalent.
  • Public victim components: BEVO token 0xc6cb12df4520b7bf83f64c79c585b8462e18b6aa and PancakeSwap pair 0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a633.

7. References

  • Seed exploit transaction: 0xb97502d3976322714c828a890857e776f25c79f187a32e2d548dda1c315d2a7d
  • Helper deployment transaction: 0xdb64e23314606b53f9a9d156dc87497d4b96b554f80b2d1a475f9d4516e6c578
  • BEVO token contract: 0xc6cb12df4520b7bf83f64c79c585b8462e18b6aa
  • BEVO/WBNB PancakeSwap pair: 0xa6eb184a4b8881c0a4f7f12bbf682fd31de7a633
  • Flash-loan pair used for initial liquidity: 0xd99c7f6c65857ac913a8f880a4cb84032ab2fc5b
  • Validated evidence sources:
    • BEVO verified source collected from BscScan
    • Seed transaction metadata and balance-diff artifacts
    • Full cast run -vvvvv trace for the seed exploit transaction