All incidents

CoinToken Burn Supply Collapse

Share
Nov 15, 2023 03:28 UTCAttackLoss: 0.53 WBNBPending manual check1 exploit txWindow: Atomic
Estimated Impact
0.53 WBNB
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Nov 15, 2023 03:28 UTC → Nov 15, 2023 03:28 UTC

Exploit Transactions

TX 1BSC
0x2b251e456c434992b9ac7ec56dc166550c4cd7db3adefbf7eb3ab91cef55f9bf
Nov 15, 2023 03:28 UTCExplorer

Victim Addresses

0x570ce7b89c67200721406525e1848bca6ff5a6f3BSC
0xe633c651e6b3f744e7ded314cdb243cf606a5f5bBSC

Loss Breakdown

0.530802WBNB

Similar Incidents

Root Cause Analysis

CoinToken Burn Supply Collapse

1. Incident Overview TL;DR

CoinToken on BSC was exploited in block 33503557 through transaction 0x2b251e456c434992b9ac7ec56dc166550c4cd7db3adefbf7eb3ab91cef55f9bf. The adversary used a public DODO WBNB flash loan, bought almost all CoinToken from the CoinToken/WBNB Pancake pair at 0xe633c651e6b3f744e7ded314cdb243cf606a5f5b, invoked CoinToken's broken burn(uint256) path, forced the pair to sync() against the corrupted token accounting, then sold the remaining CoinToken back for WBNB and repaid the flash loan in the same transaction.

The root cause is a reflected-accounting unit mismatch inside CoinToken at 0x570ce7b89c67200721406525e1848bca6ff5a6f3. For non-excluded holders, CoinToken stores balances in reflection units (_rOwned) but _burn checks and subtracts a token-unit input directly from _rOwned, while also reducing _tTotal by that token-unit value. That lets any non-excluded holder burn vastly more visible tokens than it actually owns, collapse total supply, preserve an outsized reflected ownership share, and drain the paired asset after the AMM pair accepts the manipulated balance during sync().

2. Key Background

CoinToken is a reflection token. Non-excluded balances are not stored directly as token balances; they are stored in _rOwned, and the user-visible balance is derived from _rOwned / currentRate. The conversion rate depends on _rTotal / _tTotal, so changes to _tTotal must stay consistent with changes to reflected balances.

The Pancake pair holds CoinToken and WBNB reserves and trusts token balances when sync() is called. If a token's own accounting is corrupted before sync(), the pair will commit the corrupted balances as the new reserves.

The exploit was permissionless. The funding source was the public DODO WBNB pool at 0xfeafe253802b77456b4627f8c2306a9cebb5d681, and the execution path used only public contracts: DODO flash loan, PancakeRouter at 0x10ed43c718714eb63d5aa57b78b54704e256024e, CoinToken burn(uint256), and Pancake pair sync().

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-category ACT exploit caused by broken token accounting in the victim contract. CoinToken correctly exposes reflected accounting through balanceOf and tokenFromReflection, but its internal _burn function does not operate in the same unit system. Instead of converting the burn amount into reflection units, it compares raw token units against _rOwned[_who] and subtracts that raw token amount directly from _rOwned[_who].

That violates the core invariant for reflected tokens: a burn of t visible tokens must reduce a non-excluded account's reflected balance by t * currentRate, not by t. Because _tTotal is reduced by the full raw token amount while the attacker's _rOwned barely changes in relative terms, the visible total supply collapses while the attacker still retains nearly all reflected ownership. Once the pair calls sync(), it adopts the manipulated CoinToken balance as its official reserve, making the WBNB side grossly mispriced. The attacker then sells the residual CoinToken balance into that distorted reserve state and extracts WBNB.

Verified CoinToken source shows the invariant break directly:

function balanceOf(address account) public view override returns (uint256) {
    if (_isExcluded[account]) return _tOwned[account];
    return tokenFromReflection(_rOwned[account]);
}

function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
    require(rAmount <= _rTotal, "Amount must be less than total reflections");
    uint256 currentRate =  _getRate();
    return rAmount.div(currentRate);
}

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

The bug is that _value is interpreted as visible token units in burn(uint256), while _rOwned is stored in reflection units.

4. Detailed Root Cause Analysis

The exploit opportunity existed in BSC pre-state sigma_B at the end of block 33503556, immediately before the adversary transaction. At that point:

  • CoinToken total supply was 100000000000000000000000000000.
  • The CoinToken/WBNB pair held meaningful liquidity.
  • The DODO WBNB pool held enough WBNB to fund the trade.
  • The burn bug was callable by any non-excluded holder.

The seed trace shows the exact sequence. First, the helper contract borrows 2562001604398998683661 wei of WBNB:

DPPOracle::flashLoan(2562001604398998683661, 0, 0x7c359d942717fA492e2ba319728728AAEa0858F4, 0x6e6f726d616c)
  WBNB::transfer(0x7c359d942717fA492e2ba319728728AAEa0858F4, 2562001604398998683661)
  0x7c359d942717fA492e2ba319728728AAEa0858F4::DPPFlashLoanCall(...)

Next, the exploit contract swaps the borrowed WBNB into CoinToken through PancakeRouter. After the buy, the pair still has 43502994093751897288319708 CoinToken and 2563091604398998683661 WBNB, while the attacker holds almost all visible CoinToken. The critical breakpoint follows immediately:

CoinToken::totalSupply() -> 100000000000000000000000000000
CoinToken::burn(99999999999999999999999995404)
  storage @ totalSupply -> 4596
CoinToken/WBNB Pair::sync()
  CoinToken::balanceOf(pair) -> 1
  WBNB::balanceOf(pair) -> 2563091604398998683661

This is the decisive invariant violation. The burn reduces total supply from 1e29 to 4596, but the attacker's reflected ownership is not reduced proportionally. The pair's CoinToken balance becomes 1 after sync(), even though the pair still holds the same WBNB reserve. That creates an artificial price where a few residual CoinToken units can extract almost the full WBNB side.

The trace then shows the attacker still owns 4594 CoinToken after the burn:

CoinToken::balanceOf(0x7B11Ae85f73B7eE6AA84cc91430581Bd952D9ffA) -> 4594
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(4594, 0, [CoinToken, WBNB], ...)
CoinToken/WBNB Pair::swap(0, 2562532406577152568030, ...)

This demonstrates why the bug is exploitable: the attacker burns nearly the entire supply yet still retains sellable tokens because _rOwned was not debited in reflection units. After selling those 4594 tokens, the pair is left with only 559197821846115631 wei of WBNB.

The exploit conditions were straightforward and public:

  • The attacker needed a non-excluded CoinToken balance, obtainable permissionlessly by swapping borrowed WBNB into the public pair.
  • The buggy burn(uint256) implementation had to remain callable.
  • The pair had to allow sync() on the manipulated balance.
  • The pair needed WBNB liquidity to be drained.

5. Adversary Flow Analysis

The adversary cluster consisted of:

  • EOA 0xea75aec151f968b8de3789ca201a2a3a7faeefba, which sent the exploit transaction and paid gas.
  • Contract 0x7c359d942717fa492e2ba319728728aaea0858f4, which initiated and repaid the DODO flash loan.
  • Contract 0x7b11ae85f73b7ee6aa84cc91430581bd952d9ffa, which executed the swaps, burn, and sync.
  • EOA 0x3eb469163892ac241661dc5f3c5114be9a72cb21, which received final profit.

The end-to-end execution flow was:

  1. 0xea75... calls the attacker contract 0x7b11....
  2. 0x7b11... routes through helper 0x7c35... to borrow WBNB from the DODO pool.
  3. The borrowed WBNB is transferred into 0x7b11... and swapped into CoinToken through PancakeRouter.
  4. 0x7b11... calls CoinToken::burn(99999999999999999999999995404).
  5. 0x7b11... calls PancakePair::sync() so the pair commits the manipulated CoinToken balance of 1.
  6. 0x7b11... sells the remaining 4594 CoinToken back into the pair for 2562532406577152568030 wei of WBNB.
  7. The helper contract repays 2562001604398998683661 wei of WBNB to DODO.
  8. The exploit contract forwards the remaining 530802178153884369 wei of WBNB to 0x3eb469163892ac241661dc5f3c5114be9a72cb21.

The final profit transfer is explicit in the seed trace:

WBNB::balanceOf(0x7B11Ae85f73B7eE6AA84cc91430581Bd952D9ffA) -> 530802178153884369
WBNB::transfer(0x3eB469163892Ac241661DC5F3c5114bE9A72CB21, 530802178153884369)

This transaction is ACT because every step was available to any unprivileged actor with the same public chain state and public contract interfaces.

6. Impact & Losses

The direct monetary loss captured by the adversary cluster was 530802178153884369 wei of WBNB after flash-loan repayment. The tx sender paid 2698365000000000 wei of gas, recorded as the only native balance delta, so the cluster remained net positive.

The victim pool impact was larger than the profit number alone. Before the manipulation, the pair held 43502994093751897288319708 CoinToken and 2563091604398998683661 wei of WBNB after the initial buy. After the manipulated burn and sync(), the pair reported only 1 CoinToken while still holding the same WBNB reserve. After the attacker sold the residual 4594 CoinToken, the pair's WBNB reserve fell to 559197821846115631 wei. LPs therefore lost at least 0.530802178153884369 WBNB and the pair's price state became meaningless until rebalanced.

Affected public components:

  • CoinToken at 0x570ce7b89c67200721406525e1848bca6ff5a6f3
  • Pancake Pair (CoinToken/WBNB) at 0xe633c651e6b3f744e7ded314cdb243cf606a5f5b

7. References

  1. Seed exploit transaction: 0x2b251e456c434992b9ac7ec56dc166550c4cd7db3adefbf7eb3ab91cef55f9bf
  2. Verified CoinToken source: 0x570ce7b89c67200721406525e1848bca6ff5a6f3
  3. Pancake pair: 0xe633c651e6b3f744e7ded314cdb243cf606a5f5b
  4. DODO WBNB pool: 0xfeafe253802b77456b4627f8c2306a9cebb5d681
  5. Seed trace artifact showing flash loan, burn, sync, swap, repayment, and profit transfer
  6. Seed balance-diff artifact showing the tx sender gas cost
  7. Seed metadata artifact confirming sender, target contract, gas price, and block context