All incidents

BFCToken LP Burn Drain

Share
Sep 09, 2023 19:16 UTCAttackLoss: 42,342.67 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
42,342.67 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Sep 09, 2023 19:16 UTC → Sep 09, 2023 19:16 UTC

Exploit Transactions

TX 1BSC
0x8ee76291c1b46d267431d2a528fa7f3ea7035629500bba4f87a69b88fcaf6e23
Sep 09, 2023 19:16 UTCExplorer

Victim Addresses

0x595eac4a0ce9b7175a99094680fbe55a774b5464BSC
0x71e1949a1180c0f945fe47c96f88b1a898760c05BSC

Loss Breakdown

42,342.67USDT

Similar Incidents

Root Cause Analysis

BFCToken LP Burn Drain

1. Incident Overview TL;DR

On BSC block 31599444, transaction 0x8ee76291c1b46d267431d2a528fa7f3ea7035629500bba4f87a69b88fcaf6e23 drained the public BFC/USDT pool at 0x71e1949a1180c0f945fe47c96f88b1a898760c05. The attacker flash-borrowed USDT, bought BFC, repeatedly collapsed the pair's tracked BFC reserve, then sold retained BFC into the broken curve to pull almost all USDT from the pair. The root cause is BFCToken's deferred sell-tax logic: it stores a 2% sell tax in lastTx, burns that amount from the LP pair on the next sell, and immediately calls sync(). That lets any holder use public transfer and skim calls to destroy LP reserves without paying an equivalent cost.

2. Key Background

BFCToken at 0x595eac4a0ce9b7175a99094680fbe55a774b5464 is a fee-on-transfer token trading on a Pancake-style BFC/USDT pair. Pancake/Uniswap V2 pairs maintain tracked reserves and expose skim(address) to return token balances held above those tracked reserves. That matters because BFCToken's transfer hook directly mutates the pair's token balance and then forces a reserve refresh through sync(), so the pair's accounting can be driven by token-side side effects rather than normal AMM liquidity operations.

The attacker also used the public USDT/WBNB pair at 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae as a flash-loan source and the public Pancake router at 0x10ED43C718714eb63d5aA57B78B54704E256024E for swaps. No privileged keys, owner rights, or private infrastructure were required.

3. Vulnerability Analysis & Root Cause Summary

The vulnerable branch is in BFCToken _transfer when to is the BFC/USDT AMM pair. If lastTx != 0, the token first executes super._transfer(address(uniswapV2PairUSDT), DESTROY, lastTx), reduces total supply, and calls IUniswapPair(uniswapV2PairUSDT).sync(). Only after that does it compute the new 2% sell tax for the current transfer and store it back into lastTx.

if (ammPairs[to]) {
    if (lastTx != 0) {
        super._transfer(address(uniswapV2PairUSDT), DESTROY, lastTx);
        _totalSupply = _totalSupply - lastTx;
        IUniswapPair(uniswapV2PairUSDT).sync();
        lastTx = 0;
    }
    uint256 subAmount = amount.div(100).mul(2);
    lastTx = lastTx + subAmount;
}

This breaks the AMM safety invariant that pair reserves should only change through value-conserving pair actions such as swaps, mint, burn, or legitimate liquidity removal. The prior seller never actually pays the stored lastTx during the same sell. Instead, the next seller causes the pair itself to be debited and re-synchronized. Because skim(address) is public, an attacker can feed BFC into the pair, reclaim most of it as excess, and repeatedly force the pair to burn LP-held BFC from the previous deferred tax. The reserve collapse is therefore deterministic and permissionless.

4. Detailed Root Cause Analysis

The exploit starts from the public pre-state before block 31599444, where the BFC/USDT pair still holds substantial BFC and USDT liquidity. The attacker uses a flash borrow of 400000 USDT from the USDT/WBNB pair, buys BFC from the public market, and then enters a transfer/skim loop.

In each loop iteration, the attacker transfers BFC to the BFC/USDT pair. That sell stores a new deferred tax in lastTx. The attacker then calls skim(address) to reclaim excess BFC that now sits above the pair's tracked reserve. On the next transfer into the pair, BFCToken burns the previously stored lastTx from the pair's own balance and calls sync(), lowering the tracked BFC reserve. Because the attacker recovers most of the newly transferred BFC via skim, the attacker does not need to permanently spend the same amount of BFC that the pair loses.

The seed trace shows this pattern directly: repeated PancakePair::skim(...) calls interleaved with repeated PancakePair::sync() calls before the terminal swap. The seed balance diff also shows the end state of the reserve destruction. The BFC/USDT pair's BFCToken balance falls from 22419094799340585333382 to 2587469245664025216121, while its USDT balance falls from 42342665553163949036807 to 171. The dead address receives 3344709912733564822643 BFC, matching the burn path.

Once the tracked BFC reserve is nearly dust, the attacker performs one final BFC sell into the pair. Because the denominator on the BFC side has been collapsed, the swap extracts nearly all remaining USDT. The flash borrow is repaid, residual USDT is converted to native BNB, and the sender EOA finishes with a net gain of 174954934067014145209 wei.

5. Adversary Flow Analysis

The adversary cluster is centered on EOA 0x7cb74265e3e2d2b707122bf45aea66137c6c8891, which sends the exploit transaction and receives the profit. The trace also shows two attacker-controlled helper contracts: 0x9180981034364f683ea25bcce0cff5e03a595bef and the CREATE2-deployed helper 0xa2120a9147a4f6c834fc9b73a5d0ec454e2ae45d.

The on-chain flow is:

  1. 0x16b9...daE::swap(400000 USDT, 0, helper, data) provides flash liquidity.
  2. The helper buys BFC through the Pancake router.
  3. The helper repeatedly transfers BFC to 0x71e1...c05 and calls skim(address(this)), causing the next transfer to burn the prior lastTx from the pair and forcing sync().
  4. After the tracked BFC reserve is nearly zero, the helper sells retained BFC back into the pair and drains USDT down to 171 raw units.
  5. The helper repays 401203.610832497492477433 USDT to the flash-loan pair, swaps remaining USDT to BNB, and exits profit to the attacker EOA.

Representative trace evidence:

0x16b9...daE::swap(400000000000000000000000, 0, 0xa212...45d, ...)
PancakePair::skim(0xa212...45d)
PancakePair::sync()
... repeated reserve-collapse loop ...
PancakePair::swap(440287233893506239553239, 0, 0xa212...45d, 0x)
PancakeRouter::swapExactTokensForETH(..., 0x7cb7...8891, ...)

6. Impact & Losses

The public BFC/USDT pool lost 42342665553163949036636 raw USDT units in the incident transaction, leaving only 171 raw units in the pair. LP holders also lost BFC-side inventory because BFCToken burned pair-held BFC during the loop and synchronized the lower balance into reserves. The measurable attacker profit was 174.954934067014145209 BNB net of gas.

7. References

  1. Seed transaction: 0x8ee76291c1b46d267431d2a528fa7f3ea7035629500bba4f87a69b88fcaf6e23
  2. BFCToken verified source: /workspace/session/artifacts/collector/seed/56/0x595eac4a0ce9b7175a99094680fbe55a774b5464/src/Contract.sol
  3. Seed trace: /workspace/session/artifacts/collector/seed/56/0x8ee76291c1b46d267431d2a528fa7f3ea7035629500bba4f87a69b88fcaf6e23/trace.cast.log
  4. Seed metadata and receipt: /workspace/session/artifacts/collector/seed/56/0x8ee76291c1b46d267431d2a528fa7f3ea7035629500bba4f87a69b88fcaf6e23/metadata.json
  5. Seed balance diff: /workspace/session/artifacts/collector/seed/56/0x8ee76291c1b46d267431d2a528fa7f3ea7035629500bba4f87a69b88fcaf6e23/balance_diff.json