Incident Report: AM Token toBurnAmount Reserve Manipulation Attack
Summary
On March 12, 2026 (BSC block 86066209), attacker EOA 0x0b9a1391269e95162bfec8785e663258c209333b exploited a combination of the AM token’s fee-on-transfer burn mechanism and Moolah lending protocol’s collateralized borrowing to extract approximately 131,572 USDT in profit.
The root cause is a design flaw in the AM token (0x27f9787dbdca43f92ccc499892a082494c23213f) where an accumulated toBurnAmount storage variable causes a deferred, unbounded burn of AM tokens from the PancakeSwap pair on the next sell transaction. By leveraging large flash-borrowed capital to inflate the pair’s USDT reserves before triggering this deferred burn, the attacker sold AM tokens at a drastically inflated effective price.
Transaction Details
| Field | Value |
|---|---|
| Transaction Hash | 0xd0d13179645985eae599c029574e866d79b286fbea395b66504f87f31629f859 |
| Chain | BSC (BNB Chain, chain ID 56) |
| Block | 86066209 |
| Attacker EOA | 0x0b9a1391269e95162bfec8785e663258c209333b |
| Exploit Contract | 0x11ab0c24fbc359a585587397d270b5fed2c85fd4 |
| Profit | ~131,572 USDT net |
Contracts Involved
| Label | Address | Role |
|---|---|---|
| AM Token | 0x27f9787dbdca43f92ccc499892a082494c23213f | Vulnerable token with deferred burn |
| AM/USDT PancakePair | 0x84858eff9f49d93039a09d392df2ec6e46d2ea07 | PancakeSwap V2 pool (victim) |
| PancakeSwap V3 FlashPool | 0x92b7807bf19b7dddf89b706143896d05228f3121 | Primary flash loan source (18M USDT) |
| Moolah FlashLoan Proxy | 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c | Flash loan provider (USDT + WBNB) |
| Moolah vWBNB cToken | 0x6bca74586218db34cdb402295796b79663d816e9 | Collateral minting market |
| Moolah vUSDT cToken | 0xfd5840cd36d94d7229439859c0112a4185bc0255 | Borrow market (victim) |
| Moolah Comptroller | 0xfd36e2c2a6789db23113685031d7f16329158384 | Diamond proxy for borrow checks |
| PancakeSwap V2 Router | 0x10ed43c718714eb63d5aa57b78b54704e256024e | Swap router |
| ResilientOracle Proxy | 0x6592b5de802159f3e74b2486b091d11a8256ab8a | Moolah price oracle |
Root Cause Analysis
Vulnerability in AMBase.sol: Deferred toBurnAmount Mechanism
The AM token contract (AMBase.sol) implements an unusual fee-on-transfer mechanism where sell-transaction fees are not burned immediately, but instead deferred via a storage variable toBurnAmount. The critical logic resides in two functions:
_handleSell (line 364-371 of AMBase.sol):
function _handleSell(address from, address to, uint256 amount) private {
uint256 sellFeeAmount = (amount * sellFee) / 100;
uint256 netAmount = amount - sellFeeAmount;
super._update(from, address(this), sellFeeAmount);
super._update(from, to, netAmount);
toFeeAmount += sellFeeAmount;
toBurnAmount += netAmount; // net proceeds accumulate in state
}
After every sell, toBurnAmount grows by netAmount (85% of the sold tokens after the 15% fee). This value is never reset within _handleSell.
_update for sell path (lines 245-258 of AMBase.sol):
if (is_sell) {
if (toFeeAmount > 0) {
_autoSwap();
}
if (toBurnAmount > 0) {
super._update(
address(uniswapV2Pair),
DEAD_ADDRESS,
toBurnAmount
); // burns accumulated amount FROM THE PAIR
uniswapV2Pair.sync(); // updates pair reserves
toBurnAmount = 0;
}
_handleSell(from, to, value); // then adds current sell to toBurnAmount
}
The execution order is:
- Burn all previously accumulated
toBurnAmounttokens directly from the PancakeSwap pair’s balance - Call
pair.sync()to update the pair’s recorded reserves (reflecting the burn) - Process the current sell and accumulate new net amount into
toBurnAmount
This design means any accumulated toBurnAmount from previous user sells can be burned all at once during the next sell transaction. The attacker exploits the time gap during which this value accumulates to a large amount, then triggers the burn at a strategically chosen moment when the pair’s USDT reserves have been artificially inflated.
Attack Mechanism: Reserve Inflation + Deferred Burn
The attack proceeds in two main phases:
Phase 1: Capital Acquisition
- Flash borrow 18,000,000 USDT from PancakeSwap V3 pool (
0x92b7807b) - Flash borrow 9,265,120 USDT from Moolah via
flashLoan(USDT, 9265119886...) - Flash borrow 361,710 WBNB from Moolah via
flashLoan(WBNB, 361710322...) - Deposit the 361,710 WBNB into Moolah’s WBNB market (
minton0x6bca...), receiving ~359,921 vWBNB tokens - Enter the WBNB market as collateral via
enterMarkets([0x6bca...]) - Borrow 100,423,811 USDT from Moolah’s USDT market (
borrowon0xfd5840cd) using the vWBNB as collateral
At this point the attacker’s exploit contract holds approximately 127 million USDT (18M + 9.26M + 100.4M) and 5,063 AM tokens transferred from the EOA.
Phase 2: Pool Manipulation and Profit Extraction
Step 1 - Pre-manipulation sells: The attacker sells a small amount of AM (via swapExactTokensForTokensSupportingFeeOnTransferTokens at index 209) through the AM contract’s _handleSell. This triggers the existing accumulated toBurnAmount (from prior legitimate user sells) to be burned from the pair, and the current sell’s net amount is appended to toBurnAmount. Multiple sync() calls are interspersed to settle the pair state.
Step 2 - Buy AM tokens: The attacker calls swapTokensForExactTokens (index 251) with USDT, buying a precise amount of AM tokens from the pair. The USDT reserve of the pair decreases and AM reserve increases slightly.
Step 3 - Direct USDT injection: The attacker executes a direct transfer of AM tokens to the AM contract (not a swap), followed by transferring 109,044,955 USDT directly to the pair contract (transfer index 18 at log_index 46), then calls pair.sync() directly (index 279 and 291). This dramatically inflates the pair’s USDT reserve without changing the AM reserve via the standard AMM mechanism.
After the direct sync(), the pair records:
- USDT reserves: elevated by ~109M USDT
- AM reserves: unchanged (the large
toBurnAmountis still pending)
Step 4 - Final sell with burn trigger: The attacker calls swapExactTokensForTokensSupportingFeeOnTransferTokens (index 296) to sell the accumulated AM tokens. When the AM token’s _update function processes this sell:
- It first burns the accumulated
toBurnAmount(~4,303 AM) from the pair and callspair.sync()— this reduces the pair’s AM reserve - Then
_handleSellprocesses the sell (85% net amount flows into the pair)
Step 5 - The swap execution: The swap() call on the pair (index 306) transfers USDT out to the attacker. Because the pair’s USDT reserve was massively inflated and AM reserve was reduced by the deferred burn, the AMM calculation returns significantly more USDT than the market rate would justify.
Phase 3: Cleanup and Repayment
- Repay the Moolah USDT borrow: 100,423,811 USDT (index 313)
- Exit the WBNB market
- Redeem vWBNB → get WBNB back (index 408)
- Repay Moolah WBNB flash loan (361,710 WBNB)
- Repay Moolah USDT flash loan (9,265,120 USDT)
- Repay PancakeSwap V3 USDT flash loan (18,001,800 USDT including fee)
- Transfer remaining USDT profit to EOA: 131,572 USDT (transfer index 37)
Oracle Analysis
The Moolah protocol’s ResilientOracle (0x6592b5de... / 0x90d840f4...) uses Chainlink price feeds and Binance oracle as pivot for WBNB pricing. The oracle does NOT read spot reserves from the AM/USDT PancakeSwap pool. The oracle pricing for WBNB uses:
- Main: Chainlink BNB/USD aggregator (
0x0567f232...) - Pivot: Binance oracle (
0x8dd2d85c...) backed by0x97c19d3adata provider - Bound validation via
validatePriceWithAnchorPrice(0x6e332ff0...)
This means the Moolah borrow (Phase 1) is based on a legitimate oracle price for BNB. The attacker is not manipulating the lending protocol’s oracle. The borrow of 100.4M USDT against 361K WBNB at BNB price $620 provides adequate collateral coverage ($224M collateral vs $100M borrow at 75% LTV).
The actual exploitation is purely through the AM token’s defective pool-reserve manipulation, not through oracle price inflation.
Why toBurnAmount Enables Profitable Pool Drain
The AM token’s toBurnAmount design creates the following exploitable invariant:
toBurnAmountaccumulates from all user sells but is only triggered to burn during the NEXT sell transaction.The burn happens BEFORE the current sell is processed, and burns tokens directly from the pair’s token balance.
After calling
pair.sync(), the pair’s internalreserve1(AM reserve) is reduced.The attacker can independently inflate the USDT reserve by sending USDT directly to the pair and calling
sync().When the final sell occurs and the accumulated burn fires, the combination of:
- High USDT reserve (from direct injection)
- Reduced AM reserve (from burn)
- The sell proceeding at the post-burn, post-inflate state
allows the attacker to receive disproportionate USDT for their AM tokens.
The key is that the deferred burn is trustlessly triggered by anyone who sells AM — it is not gated by access control. The attacker accumulates AM tokens cheaply (having bought them earlier at normal price), then orchestrates the burn timing to coincide with a favorable reserve state they have artificially created.
Attack Flow Diagram
EOA (0x0b9a)
└─> AttackerContract.0x07adecd7(arg) [entry point]
└─> PancakeV3FlashPool.flash(USDT=18M) [0x92b7807b]
└─> pancakeV3FlashCallback
└─> Moolah.flashLoan(USDT=9.26M) [0x8f73b65b]
└─> onMoolahFlashLoan(round=0)
└─> Moolah.flashLoan(WBNB=361,710) [0x8f73b65b]
└─> onMoolahFlashLoan(round=1)
├─ vWBNB.mint(WBNB=361,710)
├─ Comptroller.enterMarkets([vWBNB])
├─ vUSDT.borrow(100,423,811 USDT)
├─ AM.transferFrom(EOA, 5,063 AM)
├─ [Phase 2: pool manipulation]
│ ├─ sell small AM → trigger existing toBurnAmount burn
│ ├─ swapTokensForExactTokens(USDT→AM)
│ ├─ AM.transfer(to pair) [+AM to pool]
│ ├─ USDT.transfer(to pair, 109M) [direct inflate]
│ ├─ PancakePair.sync() × 2
│ └─ sell AM (final) → burn 4,303 AM from pair, extract USDT
├─ vUSDT.repayBorrow(100,423,811 USDT)
├─ Comptroller.exitMarket(vWBNB)
└─ vWBNB.redeemUnderlying(WBNB)
[repay Moolah WBNB flash + Moolah USDT flash]
[repay PancakeV3 USDT flash loan + fee]
└─> EOA receives 131,572 USDT profit
Funds Flow
| Transfer | From | To | Token | Amount |
|---|---|---|---|---|
| PancakeV3 flash in | FlashPool | Exploit | USDT | 18,000,000 |
| Moolah flash loan 1 | Moolah | Exploit | USDT | 9,265,120 |
| Moolah flash loan 2 | Moolah | Exploit | WBNB | 361,710 |
| vWBNB mint | Exploit | Moolah vWBNB | WBNB | 361,710 |
| Moolah borrow | Moolah vUSDT | Exploit | USDT | 100,423,811 |
| EOA seeds AM | EOA | Exploit | AM | 5,063 |
| Pool USDT drain (net) | AM/USDT Pair | Exploit | USDT | ~127,822,303 |
| Exploit → Pair direct | Exploit | AM/USDT Pair | USDT | ~127,689,094 |
| Repay Moolah USDT borrow | Exploit | Moolah vUSDT | USDT | 100,423,811 |
| Moolah redeem WBNB | Moolah vWBNB | Exploit | WBNB | 361,710 |
| Repay Moolah WBNB flash | Exploit | Moolah | WBNB | 361,710 |
| Repay Moolah USDT flash | Exploit | Moolah | USDT | 9,265,120 |
| Repay PancakeV3 flash | Exploit | FlashPool | USDT | 18,001,800 |
| Profit to EOA | Exploit | EOA | USDT | 131,572 |
Net losses:
- AM/USDT PancakeSwap pair: ~133,376 USDT and ~3.5M AM tokens burned
- Moolah lending protocol: ~0 USDT (borrow fully repaid)
- Chainlink/oracle: not affected (oracle not manipulated)
Net gains:
- Attacker EOA: +131,572 USDT (minus gas costs; EOA also spent 5,063 AM tokens)
- PancakeSwap V3 FlashPool: +1,800 USDT (0.01% flash loan fee on 18M USDT)
Vulnerable Code
The root cause code is in AMBase.sol of the AM token contract at 0x27f9787dbdca43f92ccc499892a082494c23213f:
File: contracts/bsc_am/AM/AMBase.sol
// _update (sell branch, lines 245-258):
if (is_sell) {
if (toFeeAmount > 0) {
_autoSwap();
}
if (toBurnAmount > 0) {
// VULNERABILITY: Burns accumulated toBurnAmount from pair's own balance
// before processing the current sell. This amount is not bounded
// and can be triggered by any sell transaction.
super._update(
address(uniswapV2Pair),
DEAD_ADDRESS,
toBurnAmount
);
uniswapV2Pair.sync(); // Updates reserves after burn
toBurnAmount = 0;
}
_handleSell(from, to, value);
}
// _handleSell (lines 364-371):
function _handleSell(address from, address to, uint256 amount) private {
uint256 sellFeeAmount = (amount * sellFee) / 100;
uint256 netAmount = amount - sellFeeAmount;
super._update(from, address(this), sellFeeAmount);
super._update(from, to, netAmount);
toFeeAmount += sellFeeAmount;
toBurnAmount += netAmount; // Accumulates indefinitely across all user sells
}
Impact Assessment
| Metric | Value |
|---|---|
| Direct USDT loss (from AM/USDT pair) | ~133,376 USDT |
| AM tokens burned from pair | ~6,475 AM (to 0xdead) + ~3.5M AM (to 0x0) |
| Attacker net profit | ~131,572 USDT |
| Flash loan capital deployed | ~127.7M USDT equivalent peak |
| Protocols affected | AM Token (AMBase.sol), AM/USDT PancakeSwap pair |
| Protocols NOT affected | Moolah (borrow repaid in full), Chainlink oracles |
The attack is self-contained within one transaction. The Moolah protocol is used as a temporary capital amplifier but suffers no lasting loss. The primary victim is the AM/USDT liquidity pool, whose USDT reserves are drained by ~133K USDT.
Root Cause Classification
Primary: Logic flaw in fee-on-transfer token design — deferred burn from pool reserves
Specific flaw: The AM token accumulates sell-transaction fees in toBurnAmount as a global storage variable, and burns them from the PancakeSwap pair’s actual token balance on the next sell. This creates a griefing/manipulation surface where:
- The burn timing is fully controllable by any caller who initiates a sell
- The burn amount is unbounded and grows with all prior user activity
- The burn directly removes tokens from the pair’s balance and updates reserves via
sync() - An attacker can front-run or time the trigger after inflating the pair’s USDT reserves
Secondary contributing factor: The pair’s sync() function, which is callable by anyone (standard PancakeSwap design), allows arbitrary reserve manipulation by directly transferring one token to the pair and syncing. This is used to inflate the USDT reserve without going through swap().
Recommendations
For the AM Token Contract:
- Burn tokens at time of sell (immediate burn): Replace the
toBurnAmountaccumulator with an immediate burn in_handleSell. Do not defer burns across transactions. - Do not burn from pair’s balance: If a deferred burn is needed for gas optimization, accumulate and burn from a separate treasury account, not from the DEX pair’s token balance. Burns from the pair change spot reserves and create oracle/price manipulation surfaces.
- Bound the burn trigger: If
toBurnAmountis retained, cap it at a maximum fraction of pair reserves per transaction to limit the potential impact of any single manipulation.
For the Moolah Protocol:
- Flash loan same-block re-entrancy control: The Moolah flash loan allowed the borrowed WBNB to be deposited as collateral within the same flash loan callback. Protocols should consider whether allowing the flash-borrowed asset to be used as collateral within the same loan callback is intentional.
General DeFi Design Principles:
- Fee-on-transfer tokens that directly modify DEX pool reserves are inherently dangerous when used in lending protocols or as collateral.
- The
toBurnAmountpattern (accumulating deferred state that anyone can trigger) is an anti-pattern in adversarial environments.
Confidence
| Aspect | Confidence | Notes |
|---|---|---|
| Root cause identification | HIGH | Full source code reviewed; exact vulnerable code lines identified |
| Attack sequence | HIGH | Decoded call trace and fund flows corroborate each step |
| Profit figures | HIGH | Confirmed from funds_flow.json net_changes |
| Oracle not manipulated | HIGH | Oracle calls use Chainlink + Binance feeds, not spot pool price |
| Moolah not the victim | HIGH | Borrow fully repaid, net change = 0 for Moolah |