TOKENbnb Reflection Drain
Exploit Transactions
0x81fd00eab3434eac93bfdf919400ae5ca280acd891f95f47691bbe3cbf6f05a5Victim Addresses
0x664201579057f50d23820d20558f4b61bd80bddaBSC0x50f2b2a555e5fa9e1bb221433dba2331e8664a69BSCLoss Breakdown
Similar Incidents
FIREDRAKE Reflection Drain on PancakeSwap
44%BUNN Reflection Drain via PancakePair
43%OceanLife Reflection Drain on PancakeSwap
43%BEVO reflection accounting lets anyone manufacture PancakeSwap input and drain WBNB
38%CoinToken Burn Reserve Drain
35%Sheep Burn Reserve Drain
35%Root Cause Analysis
TOKENbnb Reflection Drain
1. Incident Overview TL;DR
On BSC block 57744491, transaction 0x81fd00eab3434eac93bfdf919400ae5ca280acd891f95f47691bbe3cbf6f05a5 used a one-transaction flash-swap strategy to drain native BNB from TOKENbnb at 0x664201579057f50d23820d20558f4b61bd80bdda. The attacker bought PDZ, triggered TOKENbnb.burnToHolder, and then immediately redeemed inflated reward accounting through receiveRewards, draining 3606408757000000000 wei and finishing with a net 3352094698712248469 wei gain.
The root cause is a reward-accounting mismatch inside TOKENbnb. receiveRewards pays out native BNB based on balanceOf(attacker) - burnAmount[attacker], but internal reflection churn in burnFeeRewards -> arriveRewards can increase balanceOf(attacker) without increasing burnAmount[attacker]. That lets a public caller convert reflected-balance inflation into withdrawable BNB.
2. Key Background
PDZ is the token users burn through TOKENbnb. In TOKENbnb.burnToHolder, the contract asks the Pancake router for the WBNB value of the chosen PDZ amount, calls PDZ.burnToholder(sender, amount, deserved), burns the PDZ to the dead address, and then credits TOKENbnb-side rewards.
The critical victim code is:
function receiveRewards(address payable to) external {
address addr = msg.sender;
uint256 balance = balanceOf(addr);
uint256 amount = balance.sub(burnAmount[addr]);
require(amount > 0 );
to.transfer(amount.mul(10**9));
_transfer(addr, address(this), balance);
burnAmount[addr]=0;
}
This redemption path trusts the reflected TOKENbnb balance. TOKENbnb also keeps _taxFee = 50, and arriveRewards performs three internal transfers:
function arriveRewards(uint256 increase) private {
_transfer(address(this), fee1, increase * 2);
_transfer(fee1, fee2, increase * 2);
_transfer(fee2, address(this), balanceOf(fee2));
}
Because TOKENbnb is a reflection token, those taxed internal transfers can change a holder's reflected balance even though the burnAmount principal ledger is unchanged.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class accounting bug in a public reward-redemption flow. TOKENbnb intends burnAmount[a] to represent the principal that should not be redeemed as reward, but receiveRewards instead derives redeemable value from the difference between the current reflected token balance and that ledger. burnFeeRewards adds only the nominal credited amount to burnAmount[sender], then calls arriveRewards, which performs taxed internal transfers through fee1 and fee2. Those transfers modify reflection state globally and can increase the attacker's balanceOf(sender) without any corresponding ledger update. Once balanceOf(sender) > burnAmount[sender], receiveRewards treats the excess as earned rewards and transfers out native BNB at amount * 1e9. The trace shows exactly that sequence. The exploit is permissionless because every step uses public entrypoints, live pool liquidity, and a freshly deployed helper contract.
4. Detailed Root Cause Analysis
The seed transaction first flash-borrowed 10 WBNB from pair 0x231d9e7181e8479a8b40930961e93e7ed798542c, then swapped it through PancakeSwap for 1262449812487 PDZ. With TOKENbnb holding 3630503056723304190 wei before the exploit, the attacker computed the PDZ amount needed to obtain a matching router quote and called:
TOKENbnb.burnToHolder(5467668273, address(0))
Inside TOKENbnb, burnToHolder queried getAmountsOut, called PDZ burnToholder(sender, amount, deserved), and then executed burnFeeRewards(sender, deserved). The PDZ side confirms the burn helper forwards native balance to TOKENbnb when enough balance exists:
function burnToholder(address to,uint256 amount,uint256 balance) external {
require(msg.sender == address(burnHolder), 'only burns');
super._transfer(to, address(burnHolder), amount);
uint256 _balance = payable(address(this)).balance;
if (_balance >= balance) {
payable(address(burnHolder)).transfer(balance);
}
}
The key accounting bug appears immediately after that. burnFeeRewards divides the quoted value by 1e9, transfers that many TOKENbnb units to the user, stores the same amount in burnAmount[sender], and then calls arriveRewards. In the trace, this produced:
emit Transfer(..., dst: 0x81F1..., wad: 8373767728)
emit ArriveFeeRewards(param0: 0x81F1..., param1: 8373767728, param2: 21706940593)
But the later redemption event shows:
emit Transfer(src: 0x81F1..., dst: TOKENbnb, wad: 11980176485)
emit ReceiveReward(param0: 0x81F1..., param1: 3606408757, param2: 9700338304)
So the helper's reflected TOKENbnb balance became 11980176485 while the tracked principal remained 8373767728, leaving an unbacked delta of 3606408757. receiveRewards multiplies that delta by 1e9 and transfers exactly 3606408757000000000 wei, which matches the victim's native balance loss in the balance diff.
The explicit broken invariant is: for any account a, balanceOf(a) - burnAmount[a] must not increase solely because TOKENbnb moved reflected supply among its own internal accounts. The concrete breakpoint is TOKENbnb receiveRewards, which pays out that difference as BNB after arriveRewards has already altered reflection balances without updating the ledger.
5. Adversary Flow Analysis
The adversary-controlled EOA was 0x48234fb95d4d3e5a09f3ec4dd57f68281b78c825. It deployed helper contract 0x81f1acd2dad2a9fe2d879e723fb80b7aecdc1337 inside the same seed transaction.
The on-chain execution flow was:
1. Flash-borrow 10 WBNB from pair 0x231d9e7181e8479a8b40930961e93e7ed798542c.
2. Swap 10 WBNB for 1262449812487 PDZ through PancakeRouter.
3. Call TOKENbnb.burnToHolder(5467668273, address(0)).
4. Let burnFeeRewards -> arriveRewards inflate reflected TOKENbnb balance beyond burnAmount.
5. Call TOKENbnb.receiveRewards(...) and withdraw 3606408757000000000 wei.
6. Sell the remaining 1256982144214 PDZ back to WBNB/BNB.
7. Wrap enough BNB to repay 10.25 WBNB to the flash-swap pair.
8. Leave the EOA with net profit.
This flow is permissionless and reproducible. It requires no privileged caller, no private state, and no attacker-specific deployed bytecode beyond a fresh helper contract that any searcher could deploy.
6. Impact & Losses
The direct victim loss was native BNB held by TOKENbnb:
{
"token_symbol": "BNB",
"amount": "3606408757000000000",
"decimal": 18
}
The balance diff shows TOKENbnb falling from 3630503056723304190 wei to 24094299723304190 wei, a loss of 3606408757000000000 wei. The attacker EOA's native balance increased from 98312048200000000 wei to 3450406746912248469 wei, for a net gain of 3352094698712248469 wei. Operationally, the bug makes TOKENbnb's native treasury drainable whenever the contract holds BNB and public liquidity exists to source the required PDZ.
7. References
- Seed transaction:
0x81fd00eab3434eac93bfdf919400ae5ca280acd891f95f47691bbe3cbf6f05a5 - Victim contract:
TOKENbnbat0x664201579057f50d23820d20558f4b61bd80bdda - Burn token:
PDZat0x50f2b2a555e5fa9e1bb221433dba2331e8664a69 - Flash pair:
0x231d9e7181e8479a8b40930961e93e7ed798542c - PDZ/WBNB pair:
0x7b51150f5a61e97f62447e59c7947660822438ab - Evidence used: seed transaction metadata, full trace, balance diff, TOKENbnb source, and PDZ source in the collector artifacts.