APE2 Pair Burn Exploit
Exploit Transactions
0x8d35dfd9968ce61fb969ffe8dcc29eeeae864e466d2cb0b7d26ce63644691994Victim Addresses
0xb47955b5b7eaf49c815ebc389850eb576c460092BSC0xee2a9d05b943c1f33f3920c750ac88f74d0220c3BSCLoss Breakdown
Similar Incidents
CS Pair Balance Burn Drain
46%UN Burn-Skim Exploit
41%GPT Public LP-Burn Exploit
40%Sheep Burn Reserve Drain
39%StarlinkCoin Pair Drain
37%Carson Pair Reserve Siphon
37%Root Cause Analysis
APE2 Pair Burn Exploit
1. Incident Overview TL;DR
At BNB Chain transaction 0x8d35dfd9968ce61fb969ffe8dcc29eeeae864e466d2cb0b7d26ce63644691994, an attacker-controlled helper contract 0x45aa258ad08eeeb841c1c02eca7658f9dd4779c0 used five public DODO flash loans to borrow USDT, bought APE2, repeatedly recycled that APE2 through the official APE2/USDT Pancake pair with skim(), then called the public goDead() function to burn pair-held APE2 and collapse the pool reserve. The exploit was possible because APE2 records a 20% deferred burn liability on sell-classified transfers in amountToDead, but it never escrows the backing tokens for that liability. When goDead() runs, it pays the queued burn directly from the LP pair and immediately syncs reserves, so liquidity providers absorb the debit. The attacker ended the transaction with 7127248018299132277754 USDT, and even after converting the BNB gas cost into USDT at block 30072293, the net profit remained 7104753978095161372836 USDT.
2. Key Background
Three protocol behaviors matter for this incident.
First, PancakeSwap V2 pairs keep reserves in storage and expose a permissionless skim(address) method that transfers any token balance sitting above the stored reserves to an arbitrary receiver. That means a token can be sent to the pair, left unsynced, and then pulled back out by anyone allowed to call skim().
Second, APE2 classifies transfers into the official pair as sells unless the live pair balance and reserve delta look like an add-liquidity pattern. Sell-classified transfers incur token-side fees and, crucially, increment the amountToDead accumulator.
Third, the queued dead-burn is not backed by seller-funded escrow. APE2 only transfers 5% of the gross sell amount into token-controlled balances while separately increasing amountToDead by 20% of the gross amount. That mismatch is what lets the later public burn spend LP reserves instead of seller funds.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class accounting flaw in the victim token, not a benign MEV strategy. The violated invariant is that any deferred burn amount must already be fully collateralized before it is allowed to debit third-party assets. In APE2, the sell path records amountToDead += amountx * 20 /100 after a sell-classified transfer, but the contract never escrows that extra 20% anywhere. The public goDead() function later settles the queued amount by executing _rawTransfer(address(pair), address(0xdEaD), amountToDead); and then pair.sync();, so the liquidity pair becomes the source of truth and the source of payment. Because skim() is permissionless, the attacker can keep reusing almost the same APE2 balance across many sell-classified transfers while amountToDead keeps growing. Once goDead() burns a large amount directly from the pair, the pool’s APE2 reserve collapses and the attacker can dump the remaining APE2 into a badly distorted market for large USDT extraction.
4. Detailed Root Cause Analysis
The core bug is visible directly in the verified APE2 source:
if (_transferType == 2){
if (isOpenToDead){
amountToDead += amountx * 20 /100;
}
}
function goDead() public{
if (amountToDead > 0){
_rawTransfer(address(pair), address(0xdEaD), amountToDead);
pair.sync();
amountToDead = 0;
}
}
This code establishes the concrete breakpoint. During a sell-classified transfer, the token books a future burn equal to 20% of the gross amount, but the earlier fee logic only moves 2% to address(this), 2% to the NFT distributor, 1% to address(this) again, and optionally a time-based sell tax. No escrow is created for the extra 20%, so amountToDead is an unbacked liability.
The trace shows how the attacker turns that accounting bug into a reusable primitive. After buying APE2 with 19000000000000000000000 USDT, the attacker transfers APE2 into the pair, which the token classifies as a sell, and then immediately pulls the unsynced excess balance back out with skim():
APE2::transfer(PancakePair, 2126740767132717497770)
emit Transfer(from: attacker, to: PancakePair, value: 2126740767132717497770)
PancakePair::skim(attacker)
APE2::transfer(attacker, 2126740767132717497770)
emit Transfer(from: PancakePair, to: attacker, value: 2020403728776081622883)
The important semantic effect is not the exact fee dust but the repeated state transition: every sell-classified transfer increases amountToDead, while skim() returns most of the same APE2 back to the attacker so it can be used again. The exploit trace shows this recycle pattern sixteen times before the attacker triggers the burn.
Once the queued burn is large enough, the attacker calls goDead() and the pair pays the liability:
APE2::goDead()
emit Transfer(from: PancakePair, to: 0x000000000000000000000000000000000000dEaD, value: 3708941327967849359474)
PancakePair::sync()
emit Sync(reserve0: 27993453783576933576082, reserve1: 31146393415761651955)
PancakePair::swap(26127248018299132278755, 0, attacker, 0x)
That is the exact invariant break in realized form. Before goDead(), LP reserves still implicitly support the APE2/USDT market. After goDead() and sync(), the pair’s APE2 reserve is only 31146393415761651955, so the attacker’s remaining 437148509290176342673 APE2 becomes enough to pull 26127248018299132278755 USDT from the pair in the final swap. The collected balance diff confirms the pair lost 19155190926317811152304 USDT and the attacker contract retained 7127248018299132277754 USDT after repaying all five flash loans.
5. Adversary Flow Analysis
The exploit is a single adversary-crafted BNB Chain transaction by EOA 0x10703f7114dce7beaf8d23cde4bf72130bb0f56a, routed through helper contract 0x45aa258ad08eeeb841c1c02eca7658f9dd4779c0.
- The helper contract takes five permissionless DODO flash loans, for a total of
2153560542292103331545884USDT. - It approves PancakeRouter
0x10ED43C718714eb63d5aA57B78B54704E256024Eand swaps19000000000000000000000USDT into APE2. - It cycles the purchased APE2 through
APE2.transfer(pair, ...)andPancakePair.skim(attacker)sixteen times, inflatingamountToDeadwithout surrendering the full token position. - It calls public
APE2.goDead(), which burns3708941327967849359474APE2 from pair0xee2a9d05b943c1f33f3920c750ac88f74d0220c3and syncs the pair to a tiny APE2 reserve. - It sends
1001USDT dust plus its remaining APE2 into the pair, computes the swap output withPancakeRouter.getAmountsOut, and extracts26127248018299132278755USDT. - It repays each flash-loan pool and keeps the residual USDT profit.
All external components used in this path are permissionless: the DODO flash-loan pools, PancakeRouter, PancakePair skim() and swap(), and the public APE2 goDead() entrypoint. That satisfies the ACT framing.
6. Impact & Losses
The measured victim-side loss is the USDT drained from the APE2/USDT Pancake pair: 19155190926317811152304 raw units of USDT with 18 decimals. The pair also lost 4501944705070935349078 APE2 over the transaction, including the direct dead-address burn that forced the reserve collapse.
The attacker contract ended with 7127248018299132277754 USDT. The transaction receipt shows gasUsed=18835861 and effectiveGasPrice=5000000000, which equals 94179305000000000 wei of BNB gas. Replaying PancakeRouter getAmountsOut(94179305000000000, [WBNB, USDT]) at block 30072293 yields 22494040203970904918 USDT, so the fee-adjusted profit is still 7104753978095161372836 USDT. The affected public components are the APEDAO token contract 0xb47955b5b7eaf49c815ebc389850eb576c460092 and its official APE2/USDT Pancake pair 0xee2a9d05b943c1f33f3920c750ac88f74d0220c3.
7. References
- Exploit transaction:
0x8d35dfd9968ce61fb969ffe8dcc29eeeae864e466d2cb0b7d26ce63644691994 - Attacker EOA:
0x10703f7114dce7beaf8d23cde4bf72130bb0f56a - Attacker helper contract:
0x45aa258ad08eeeb841c1c02eca7658f9dd4779c0 - Victim token:
0xb47955b5b7eaf49c815ebc389850eb576c460092 - Victim pair:
0xee2a9d05b943c1f33f3920c750ac88f74d0220c3 - Verified APE2 source used for validation:
artifacts/collector/seed/56/0xb47955b5b7eaf49c815ebc389850eb576c460092/src/kape/APE2.sol - Seed trace used for validation:
artifacts/collector/seed/56/0x8d35dfd9968ce61fb969ffe8dcc29eeeae864e466d2cb0b7d26ce63644691994/trace.cast.log - Balance diff used for loss and profit accounting:
artifacts/collector/seed/56/0x8d35dfd9968ce61fb969ffe8dcc29eeeae864e466d2cb0b7d26ce63644691994/balance_diff.json - Transaction receipt and historical pricing independently re-queried by the validator from BNB Chain RPC and PancakeRouter at block
30072293