CoinToken Burn Reserve Drain
Exploit Transactions
0x84bd77f25cc0db493c339a187c920f104a69f89053ab2deabb93c35220e6dfc0Victim Addresses
0x0fdfcfc398ccc90124a0a41d920d6e2d0bd8ccf5BSC0xdbe783014cb0662c629439fbbba47e84f1b6f2edBSCLoss Breakdown
Similar Incidents
CoinToken Burn Supply Collapse
57%Sheep Burn Reserve Drain
51%SafeMoon LP Burn Drain
43%CS Pair Balance Burn Drain
42%ZS Pair Burn Drain
42%BFCToken LP Burn Drain
41%Root Cause Analysis
CoinToken Burn Reserve Drain
1. Incident Overview TL;DR
On BSC block 31528198, transaction 0x84bd77f25cc0db493c339a187c920f104a69f89053ab2deabb93c35220e6dfc0 executed a single-transaction drain against the CoinToken/WBNB PancakeSwap V2 pool at 0xdbe783014cb0662c629439fbbba47e84f1b6f2ed. The attacker bought CoinToken, repeatedly called CoinToken's public burn(uint256), forced the pair's apparent CoinToken balance down to 1, called sync(), and then sold 63 CoinToken for 2230503636641143188923 wei of WBNB. The pool lost 30503636641143188923 wei of BNB-equivalent WBNB, while the attacker EOA realized 30478281508143188923 wei net after gas.
The root cause is a code bug in CoinToken's reflection burn accounting. CoinToken._burn(address,uint256) subtracts the same raw _value from both _rOwned[_who] and _tTotal, mixing reflected units with token units. That breaks the reflection invariant, lets any holder distort tokenFromReflection(_rOwned[pair]), and therefore lets an attacker falsify the pair's balanceOf(pair) without removing the pair's reflected position.
2. Key Background
CoinToken is a reflection-style token. For non-excluded accounts, balanceOf(account) is derived from tokenFromReflection(_rOwned[account]), and tokenFromReflection divides reflected balances by the current rate returned from _getRate(). This means supply accounting must preserve a strict unit-consistency relationship between reflected supply and token supply.
PancakeSwap V2 trusts token balances observed from the token contract. When sync() runs, the pair records current token balances as reserves. If a token reports a manipulated balanceOf(pair), PancakeSwap will write that manipulated balance into reserves and price the next swap against it.
The exploit therefore depends on the interaction of two permissionless components:
- CoinToken's public
burn(uint256)entrypoint. - PancakeSwap V2 reserve synchronization and swap math based on
balanceOf(pair).
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is an accounting bug in a reflection token integrated with an AMM. CoinToken exposes a public burn path that is callable by any holder, but the implementation updates state in inconsistent units. A correct reflection burn would reduce the burner's reflected balance and the reflected global supply proportionally to the token amount being burned. CoinToken instead subtracts only the raw token amount from _rOwned[_who] while also subtracting that same raw amount from _tTotal. Because _tTotal falls faster than reflected balances, the conversion rate becomes artificially large and tokenFromReflection(_rOwned[pair]) collapses. That false balance is then adopted by PancakeSwap when sync() is called. Once the pair reserve is rewritten to 1 CoinToken, a tiny token input can drain almost all WBNB from the pool.
4. Detailed Root Cause Analysis
The critical victim-side code is the public burn entrypoint and the internal _burn implementation:
function burn(uint256 _value) public {
_burn(msg.sender, _value);
}
function _burn(address _who, uint256 _value) internal {
require(_value <= _rOwned[_who]);
_rOwned[_who] = _rOwned[_who].sub(_value);
_tTotal = _tTotal.sub(_value);
emit Transfer(_who, address(0), _value);
}
This logic is broken because _rOwned is tracked in reflected units, while _tTotal is tracked in token units. Subtracting the same raw _value from both variables violates the invariant that reflected balances must remain convertible to token balances through a consistent rate.
The collected trace shows the exploit realizing that bug directly. The attacker-controlled holder repeatedly burns its CoinToken until the pair's reported balance collapses:
CoinToken::burn(99)
...
CoinToken::balanceOf(PancakePair) -> 2
CoinToken::burn(70)
...
CoinToken::balanceOf(PancakePair) -> 1
After the balance collapse, the attacker transfers the last 63 CoinToken to the selling helper and calls PancakePair::sync():
PancakePair::sync()
CoinToken::balanceOf(PancakePair) -> 1
WBNB::balanceOf(PancakePair) -> 2265997190154150201517
emit Sync(reserve0: 1, reserve1: 2265997190154150201517)
At that point the pair's reserve state is corrupted. The next swap is priced against the fake reserve of 1 CoinToken rather than the pool's original reflected position. The same trace then shows the drain:
PancakePair::swap(0, 2230503636641143188923, attackerHelper, 0x)
WBNB::transfer(attackerHelper, 2230503636641143188923)
The balance-diff artifact confirms the economic result. The WBNB contract balance held by the pair decreases by 30503636641143188923 wei, and the sender EOA's native balance increases by 30478281508143188923 wei net after gas. Flash liquidity is not the root cause here; the exploit is enabled by the malformed burn accounting and the AMM's trust in balanceOf(pair).
5. Adversary Flow Analysis
The adversary flow is fully permissionless and fits the ACT model.
- The attacker EOA
0xc892d5576c65e5b0db194c1a28aa758a43bb42a5deploys helper contracts and funds the strategy. - The attacker spends
2200WBNB to buy356286368692880972729922CoinToken through PancakeSwap. - An attacker-controlled holder contract
0x86105b623dec159d69dc010682f2f87a76380b4arepeatedly calls CoinToken's unrestrictedburn(uint256)to manipulate the reflection rate and forceCoinToken.balanceOf(pair)down to1. - The remaining
63CoinToken are moved to attacker helper0xe4fdb3f2ed8f0f755842b6ad7ce0c97969cb4b42. - That helper calls
PancakePair.sync(), which writes the corrupted reserve state into the pair. - The helper sells
63CoinToken back through PancakeSwap and receives2230503636641143188923wei of WBNB. - Profit is withdrawn back to the originating EOA, which ends the transaction up
30478281508143188923wei net after gas.
The key adversary-controlled addresses identified in the trace are:
0xc892d5576c65e5b0db194c1a28aa758a43bb42a5: originating EOA and final profit recipient.0x902aba5b1c299fa7e7707d6e5ba9dc7723c1982d: attacker-created coordinator.0xe4fdb3f2ed8f0f755842b6ad7ce0c97969cb4b42: attacker helper that syncs the pair and executes the final sale.0x86105b623dec159d69dc010682f2f87a76380b4a: attacker helper that holds CoinToken and executes the burn sequence.
6. Impact & Losses
The immediate victimized component is the CoinToken/WBNB PancakeSwap V2 pair at 0xdbe783014cb0662c629439fbbba47e84f1b6f2ed, with the underlying logic bug residing in CoinToken at 0x0fdfcfc398ccc90124a0a41d920d6e2d0bd8ccf5.
Measured losses:
- Pool WBNB depletion:
30503636641143188923wei (30.503636641143188923BNB-equivalent). - Attacker net profit after gas:
30478281508143188923wei (30.478281508143188923BNB).
Beyond direct value loss, the exploit demonstrates that the pool's reserves can be desynchronized from true reflected ownership using only public calls, making the market unsafe as long as the broken CoinToken burn logic remains deployed.
7. References
- Seed transaction:
0x84bd77f25cc0db493c339a187c920f104a69f89053ab2deabb93c35220e6dfc0 - CoinToken:
0x0fdfcfc398ccc90124a0a41d920d6e2d0bd8ccf5 - CoinToken/WBNB PancakeSwap V2 pair:
0xdbe783014cb0662c629439fbbba47e84f1b6f2ed - Pancake Router:
0x10ed43c718714eb63d5aa57b78b54704e256024e - Evidence used:
- Collected CoinToken source code
- Seed transaction trace
- Seed transaction metadata
- Seed transaction balance-diff artifact