Calculated from recorded token losses using historical USD prices at the incident time.
0xd0d13179645985eae599c029574e866d79b286fbea395b66504f87f31629f8590x27f9787dbdca43f92ccc499892a082494c23213fBSC0x84858eff9f49d93039a09d392df2ec6e46d2ea07BSCOn BSC mainnet, transaction 0xd0d13179645985eae599c029574e866d79b286fbea395b66504f87f31629f859 at block 86066209 let an unprivileged adversary drain the AM/USDT PancakeSwap pair at 0x84858eff9f49d93039a09d392df2ec6e46d2ea07. The originating EOA 0x0b9a1391269e95162bfec8785e663258c209333b routed the exploit through helper contract 0x11ab0c24fbc359a585587397d270b5fed2c85fd4, borrowed permissionless liquidity from PancakeSwap V3 and Moolah, borrowed additional USDT from Venus, then manipulated the AM token's transfer hook to collapse the pair's AM reserve to dust before selling into the distorted price.
The root cause is a pair-reserve desynchronization bug in AMBase._update(address,address,uint256). On sells, AM first consumes global toFeeAmount and toBurnAmount against the live pair, calls sync(), and only afterward accounts the current seller's transfer. A second bug makes the buy restriction unenforceable for to == address(0), because the zero-address branch returns before _handleBuy runs. Together, these bugs let any attacker who sees a nonzero public pending burn reshape the AM/USDT reserves and extract nearly all USDT from the pair.
AM maintains two global accumulators, toFeeAmount and toBurnAmount, that persist across transactions and are exposed through public getters. Its sell handler adds the seller's fee to and the seller's net AM amount to . Because these values are global rather than per-order, any unprivileged user can read them before submitting a transaction and predict the side effects that the next sell will trigger.
toFeeAmounttoBurnAmountAM is also directly coupled to the PancakeSwap V2 pair. Instead of treating the pair as an external venue, the token's transfer hook mutates pair balances and calls sync() from inside token logic. That means a token transfer can alter the pair's reserves before Pancake's own swap accounting has finished.
The verified AM source shows both critical behaviors in one place:
if (isZeroAddress) {
super._update(from, to, value);
return;
}
if (is_buy) {
_handleBuy(from, to, value);
} else if (is_sell) {
if (toFeeAmount > 0) {
_autoSwap();
}
if (toBurnAmount > 0) {
super._update(address(uniswapV2Pair), DEAD_ADDRESS, toBurnAmount);
uniswapV2Pair.sync();
toBurnAmount = 0;
}
_handleSell(from, to, value);
}
Origin: verified AM token source.
Two background details matter for the exploit:
_handleBuy enforces require(to == staking, "k009"), so ordinary buys are supposed to go only to the staking contract._handleBuy, so a buy routed to address(0) bypasses that restriction entirely.The vulnerability is an attack-class accounting bug in the AM token, not a generic AMM flaw in PancakeSwap. The broken invariant is that a token transfer hook must not mutate live AMM reserves independently of the current swap input before the swap's consideration has been accounted. AM violates that invariant in the sell branch of AMBase._update, where _autoSwap() can move AM and USDT, and the toBurnAmount branch can remove AM from the pair and call sync(), all before _handleSell(from, to, value) processes the current seller input.
That ordering makes the pair reserves reflect stale global state rather than the active trade. Because toBurnAmount and toFeeAmount are publicly readable, the attacker can predict the exact reserve mutation their sell will trigger. The second bug is the zero-address fast path: from == address(0) || to == address(0) causes an immediate super._update and return, so _handleBuy never enforces the staking-only recipient check when the pair transfers AM to address(0). The attacker used that bypass to buy pairBalance - toBurnAmount AM directly to the zero address, leaving only the pending-burn amount in the pair. Dust transfers then re-triggered the sell hook, burned the remaining AM reserve to dust, and a final sell extracted almost the entire USDT side of the pool.
This violates three security principles called out in the root-cause artifact:
The ACT opportunity existed at the public prestate immediately before the exploit transaction. Direct RPC reads at block 86066208 confirm:
toBurnAmount = 1888700000000000000000
toFeeAmount = 333300000000000000000
pair AM = 3515634091167029175653781
pair USDT = 133376108623827763982079
Origin: independent validator RPC reads against BSC mainnet prestate.
Those values satisfy the exploit conditions in the analysis: the attacker only needed public state plus permissionless capital and a small AM seed inventory.
The exploitable breakpoint is the sell branch in AMBase._update:
if (toFeeAmount > 0) {
_autoSwap();
}
if (toBurnAmount > 0) {
super._update(address(uniswapV2Pair), DEAD_ADDRESS, toBurnAmount);
uniswapV2Pair.sync();
toBurnAmount = 0;
}
_handleSell(from, to, value);
Origin: verified AM token source.
_handleSell then appends the current seller's amounts to the same global accumulators:
uint256 sellFeeAmount = (amount * sellFee) / 100;
uint256 netAmount = amount - sellFeeAmount;
super._update(from, address(this), sellFeeAmount);
super._update(from, to, netAmount);
toFeeAmount += sellFeeAmount;
toBurnAmount += netAmount;
Origin: verified AM token source.
The exploit transaction used that ordering in four stages:
18,000,000 USDT from PancakeSwap V3, 18,000,000 USDT from Moolah, 361710.322174577155227694 WBNB from Moolah, and then 100423811.362260943217052972 USDT from Venus after minting vWBNB collateral and entering the market.5062.546976007744828487 AM into the pair. That sell first consumed the preexisting pending fee and pending burn, then _handleSell created a fresh toBurnAmount of 4303.164929606583104214 AM for the next trigger.pairBalance - toBurnAmount = 3513745391167029175653781 AM to address(0). Because the pair transferred AM to the zero address, the zero-address return path bypassed _handleBuy, so the staking-only buy restriction did not execute.0.999999999999999993 AM then paid out almost the entire remaining USDT balance.The incident trace captures the key reserve-positioning and bypass steps:
swapExactTokensForTokensSupportingFeeOnTransferTokens(
5062546976007744828487,
0,
[AM, USDT],
0x11ab0c24fbc359a585587397d270b5fed2c85fd4,
...
)
...
emit Transfer(
from: 0x11ab0c24fbc359a585587397d270b5fed2c85fd4,
to: PancakePair,
value: 4303164929606583104214
)
...
swapTokensForExactTokens(
3513745391167029175653781,
...,
[USDT, AM],
0x0000000000000000000000000000000000000000,
...
)
...
AM::transfer(0x0000000000000000000000000000000000000000, 3513745391167029175653781)
emit Sync(reserve0: 4303164929606583104214, reserve1: 109178165585855602984878694)
Origin: incident execution trace and selected runtime observations.
The zero-address transfer is the decisive buy-bypass evidence: it shows the pair transferring AM to address(0) while the post-swap sync leaves exactly the pending-burn amount on the AM side of the pair.
The closing state confirms the drain:
{
"pair_usdt_before": "133376108623827763982079",
"pair_usdt_after": "150756072",
"pair_am_before": "3515634091167029175653781",
"pair_am_after": "849999999999999996",
"profit_to_origin_eoa_usdt": "131572533272569998886060"
}
Origin: incident balance diff plus selected runtime observations.
The adversary cluster has two relevant accounts:
0x0b9a1391269e95162bfec8785e663258c209333b, which originated the exploit transaction and received the final USDT payout.0x11ab0c24fbc359a585587397d270b5fed2c85fd4, which executed the flash-loan callbacks, Venus interactions, AM trades, and final repayment flow.The on-chain flow is fully contained in one transaction:
0x11ab...fd4 receives the PancakeSwap V3 USDT flash loan.100423811.362260943217052972 USDT.5062.546976007744828487 AM to consume the public pending values and create a new pending burn.3513745391167029175653781 AM to address(0) so the pair holds only the new pending burn.0.849999999999999996 AM.131572.533272569998886060 USDT to the originating EOA.The capital path is permissionless throughout, which is why the case is ACT:
No private key, privileged role, or attacker-only artifact is required to realize the exploit once the public prestate exposes a nonzero toBurnAmount.
The measurable impact was the destruction of the AM/USDT pair's liquidity and price integrity. The pair lost 133376108623827613226007 raw USDT units (18 decimals), falling from 133376.108623827763982079 USDT to 150.756072 USDT. Its AM side collapsed from 3515634.091167029175653781 AM to 0.849999999999999996 AM.
The attacker-origin EOA gained 131572533272569998886060 raw USDT units, or 131572.533272569998886060 USDT. The only adverse EOA-side asset movement was the consumed AM seed inventory of 5063.546976007744828487 AM, which the root-cause artifact values at approximately 192.100819931939023100742161093643799939949365034815755189445 USDT-equivalent using the pre-attack pool price. On that basis, the value delta is approximately 131380.432452638059862959257838906356200060050634965184244811 USDT-equivalent before gas. Gas paid was 91503550000000 wei of BNB, which is immaterial relative to the extracted USDT value.
The directly affected public protocol components are:
0x27f9787dbdca43f92ccc499892a082494c23213f.0x84858eff9f49d93039a09d392df2ec6e46d2ea07.0xd0d13179645985eae599c029574e866d79b286fbea395b66504f87f31629f859 at block 86066209.0x27f9787dbdca43f92ccc499892a082494c23213f, especially AMBase._update, _handleBuy, _handleSell, and _autoSwap.toBurnAmount, toFeeAmount, first sell amount, buy-to-zero amount, dust transfers, and final profit transfer.86066208, confirming the public prestate values for toBurnAmount, toFeeAmount, and the AM/USDT pair balances before the exploit transaction.