All incidents

BUNN Reflection Drain via PancakePair

Share
Jun 21, 2023 20:26 UTCAttackLoss: 52 WBNBPending manual check1 exploit txWindow: Atomic
Estimated Impact
52 WBNB
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Jun 21, 2023 20:26 UTC → Jun 21, 2023 20:26 UTC

Exploit Transactions

TX 1BSC
0x24a68d2a4bbb02f398d3601acfd87b09f543d935fc24862c314aaf64c295acdb
Jun 21, 2023 20:26 UTCExplorer

Victim Addresses

0xb4b84375ae9bb94d19f416d3db553827be349520BSC
0xc54aaecf5fa1b6c007d019a9d14dfb4a77cc3039BSC

Loss Breakdown

52WBNB

Similar Incidents

Root Cause Analysis

BUNN Reflection Drain via PancakePair

1. Incident Overview TL;DR

On BSC transaction 0x24a68d2a4bbb02f398d3601acfd87b09f543d935fc24862c314aaf64c295acdb in block 29304628, an unprivileged attacker drained 52 WBNB from the BUNN/WBNB pair at 0xb4b84375ae9bb94d19f416d3db553827be349520. The attacker borrowed BUNN from the pair during a callback-enabled swap, called the public BUNN.deliver(uint256) function inside the callback, and used the reflection-side balance jump to make the pair accept phantom BUNN input. The transaction ended with WBNB.withdraw(52000000000000000000) and a net sender-side gain of 51996961216000000000 wei, which matches the balance-diff artifact.

The root cause is the interaction between BUNN's reflection accounting and PancakePair's balance-based swap settlement. BUNN lets any non-excluded holder call deliver, which reduces global reflected supply without transferring tokens. Because the pair was not excluded from reflections, balanceOf(pair) increased after deliver even though no attacker-to-pair BUNN transfer occurred. Verified PancakePair code then interpreted the inflated post-callback balance1 as legitimate amount1In and released WBNB.

2. Key Background

BUNN at 0xc54aaecf5fa1b6c007d019a9d14dfb4a77cc3039 is a reflection token. Non-excluded balances are not stored directly as token balances; instead they are derived from reflected ownership:

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);
}

This matters because reducing _rTotal changes the token-per-reflection exchange rate for every non-excluded holder, including the AMM pair if the pair is not excluded.

The victim pair 0xb4b84375ae9bb94d19f416d3db553827be349520 is a verified PancakePair. Its swap implementation is the standard post-callback balance-difference design:

if (data.length > 0) IPancakeCallee(to).pancakeCall(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));

uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, "Pancake: INSUFFICIENT_INPUT_AMOUNT");

That design is correct for ordinary ERC-20 transfers because the pair infers input from token balances after the callback. It becomes unsafe when the token itself can mutate the pair's observable balance without an inbound transfer.

The ACT opportunity is defined on the public BSC state immediately before block 29304628, especially:

  • The pair held more than 53 WBNB.
  • The pair's token0 was WBNB and token1 was BUNN.
  • BUNN.deliver was publicly callable by any non-excluded holder.
  • Any unprivileged account could submit a contract-creation transaction, call public swap with callback data, and invoke public deliver after temporarily obtaining BUNN inside that same transaction.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-category bug, not a pure MEV unwind. The broken invariant is: an AMM pair must only treat real inbound transfers as input when deciding how much counter-asset to release. BUNN violates that invariant when composed with PancakePair because deliver changes the derived pair balance without a transfer. PancakePair then compounds the issue by computing amount1In from post-callback balances rather than from actual transfer events. Once the attacker temporarily holds BUNN, the pair's own callback mechanism gives them a permissionless place to call deliver inside the swap. The pair therefore sees a larger balance1, computes positive amount1In, passes the input check, and releases WBNB even though the attacker never returned corresponding BUNN. The exploit is deterministic because both relevant entrypoints, swap and deliver, are public and the necessary pre-state existed in the live pool.

The violated security principles are straightforward:

  • AMM-facing token accounting must not let arbitrary users mutate a pool's observable balance without a transfer.
  • Reflection or rebase tokens must exclude liquidity pairs from reflection side effects, or the pair must be synchronized before balance-based pricing is trusted.
  • Public supply-adjustment primitives must be reviewed against downstream protocols that treat balanceOf as transfer-based accounting.

4. Detailed Root Cause Analysis

The attacker starts from the public pre-state at block 29304627 and sends one adversary-crafted transaction. The trace shows two contracts created inside that transaction: 0x731049ac78c13e1bf8e10f993ce7f1ba45ae9a44 and 0x5090906fe938ce8f0746ec46d482658bebcb82d1. The inner helper is then used as the swap callback target.

The first exploit leg borrows 44 WBNB and 1000000000000 BUNN from the pair. The trace then immediately enters pancakeCall on the helper and invokes BUNN.deliver(990000000000):

0xc54AAecF5fA1b6c007d019a9d14dFb4a77CC3039::transfer(..., 1000000000000)
0x5090906F...::pancakeCall(..., 44000000000000000000, 1000000000000, ...)
0xc54AAecF5fA1b6c007d019a9d14dFb4a77CC3039::deliver(990000000000)

At that moment, _rTotal decreases while the pair's reflected holdings remain unchanged. Because balanceOf(pair) is computed from _rOwned[pair] and the new reflection rate, the pair's observable BUNN balance increases without any attacker-to-pair transfer. The trace confirms the result: after the callback, BUNN.balanceOf(pair) is read as 52240860222579, and the pair emits:

Swap(... amount1In: 44537507669584, amount0Out: 44000000000000000000, amount1Out: 1000000000000 ...)

This proves the pair credited positive BUNN input even though the only logged BUNN transfer in that swap was the initial transfer from the pair to the helper.

The attacker repeats the same pattern in a second leg, now pulling another 8 WBNB plus 1000000000000 BUNN and calling deliver(990000000000) again. After the second callback, the trace reads BUNN.balanceOf(pair) as 347268423609734 and emits:

Swap(... amount1In: 296027563387155, amount0Out: 8000000000000000000, amount1Out: 1000000000000 ...)
Sync(1453031216447006008, 347268423609734)

The final sync shows the pair nearly drained of WBNB, down to 1453031216447006008 wei. The helper then unwraps all stolen WBNB:

WBNB::withdraw(52000000000000000000)

The balance diff independently confirms the economic result. The WBNB contract's native balance drops by 52000000000000000000 wei, while the sender EOA 0xe2512f5a3714f473ab2bc3d95e3459fde7cc4b28 gains 51996961216000000000 wei net after fees. That closes the loop from code-level invariant break to measurable on-chain loss.

The exploit conditions are also explicit in the reconstructed path:

  • The pair must hold BUNN while remaining inside the reflection accounting set.
  • The attacker must obtain some BUNN before calling deliver; the pair's own callback-enabled swap provides that temporary inventory.
  • The pair must settle swaps from post-callback balances using the PancakePair balance-difference formula so the reflection-created balance increase is accepted as input.

5. Adversary Flow Analysis

The adversary strategy is a single-transaction callback attack using fresh helper contracts. The sender EOA 0xe2512f5a3714f473ab2bc3d95e3459fde7cc4b28 deploys an outer helper, which creates the inner helper 0x5090906fe938ce8f0746ec46d482658bebcb82d1. The inner helper is initialized with the pair, WBNB, and BUNN addresses.

Stage 1 is the first reflection-backed drain. The inner helper calls pair.swap(44 ether, 1_000_000_000_000, helper, data), receives WBNB and BUNN from the pair, and in pancakeCall calls deliver(990000000000). PancakePair then re-reads the pool balances, treats the reflection-created BUNN balance jump as 44537507669584 of real input, and releases 44 WBNB.

Stage 2 repeats the pattern with pair.swap(8 ether, 1_000_000_000_000, helper, data). A second deliver(990000000000) causes the pair to accept another phantom BUNN input of 296027563387155 and release 8 WBNB. At that point the helper holds the full 52 WBNB drained from the pool.

Stage 3 realizes profit. The helper calls WBNB.withdraw(52000000000000000000) to convert the stolen WBNB to native BNB and then forwards the value back along the helper chain to the original sender. The trace also records the helper self-destruct path, matching the account cluster identified in root_cause.json.

Victim-side addresses are:

  • BUNN token: 0xc54aaecf5fa1b6c007d019a9d14dfb4a77cc3039
  • PancakePair BUNN/WBNB pool: 0xb4b84375ae9bb94d19f416d3db553827be349520

Adversary-side addresses are:

  • Sender EOA: 0xe2512f5a3714f473ab2bc3d95e3459fde7cc4b28
  • Outer helper: 0x731049ac78c13e1bf8e10f993ce7f1ba45ae9a44
  • Inner helper: 0x5090906fe938ce8f0746ec46d482658bebcb82d1

6. Impact & Losses

The measurable loss is 52 WBNB, encoded on-chain as "52000000000000000000" with 18 decimals. The pair's final WBNB balance falls to 1453031216447006008 wei, leaving less than 2 WBNB in reserve. The sender EOA's balance diff shows 3038784000000000 wei paid in fees and a net gain after fees of 51996961216000000000 wei, while the gross protocol-side WBNB outflow is the full 52 WBNB.

This incident affects Bunny Protocol's BUNN token integration with the PancakePair pool. The key failure is compositional: a public reflection-supply adjustment primitive was left active against an AMM pair that trusted balanceOf as if it only changed through real transfers.

7. References

  1. Seed transaction metadata for 0x24a68d2a4bbb02f398d3601acfd87b09f543d935fc24862c314aaf64c295acdb
  2. Seed transaction opcode trace showing both deliver calls, emitted Swap events, Sync, and WBNB.withdraw
  3. Balance-diff artifact confirming 52 WBNB outflow and 51.996961216 BNB net profit
  4. Verified BUNN source excerpt covering balanceOf and deliver
  5. Verified PancakePair source excerpt covering post-callback balance reads and amount1In computation
  6. Etherscan v2 verified-source response for pair 0xb4b84375ae9bb94d19f416d3db553827be349520