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

FieldValue
Transaction Hash0xd0d13179645985eae599c029574e866d79b286fbea395b66504f87f31629f859
ChainBSC (BNB Chain, chain ID 56)
Block86066209
Attacker EOA0x0b9a1391269e95162bfec8785e663258c209333b
Exploit Contract0x11ab0c24fbc359a585587397d270b5fed2c85fd4
Profit~131,572 USDT net

Contracts Involved

LabelAddressRole
AM Token0x27f9787dbdca43f92ccc499892a082494c23213fVulnerable token with deferred burn
AM/USDT PancakePair0x84858eff9f49d93039a09d392df2ec6e46d2ea07PancakeSwap V2 pool (victim)
PancakeSwap V3 FlashPool0x92b7807bf19b7dddf89b706143896d05228f3121Primary flash loan source (18M USDT)
Moolah FlashLoan Proxy0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8cFlash loan provider (USDT + WBNB)
Moolah vWBNB cToken0x6bca74586218db34cdb402295796b79663d816e9Collateral minting market
Moolah vUSDT cToken0xfd5840cd36d94d7229439859c0112a4185bc0255Borrow market (victim)
Moolah Comptroller0xfd36e2c2a6789db23113685031d7f16329158384Diamond proxy for borrow checks
PancakeSwap V2 Router0x10ed43c718714eb63d5aa57b78b54704e256024eSwap router
ResilientOracle Proxy0x6592b5de802159f3e74b2486b091d11a8256ab8aMoolah 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:

  1. Burn all previously accumulated toBurnAmount tokens directly from the PancakeSwap pair’s balance
  2. Call pair.sync() to update the pair’s recorded reserves (reflecting the burn)
  3. 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 (mint on 0x6bca...), receiving ~359,921 vWBNB tokens
  • Enter the WBNB market as collateral via enterMarkets([0x6bca...])
  • Borrow 100,423,811 USDT from Moolah’s USDT market (borrow on 0xfd5840cd) 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 toBurnAmount is 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:

  1. It first burns the accumulated toBurnAmount (~4,303 AM) from the pair and calls pair.sync() — this reduces the pair’s AM reserve
  2. Then _handleSell processes 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 by 0x97c19d3a data 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:

  1. toBurnAmount accumulates from all user sells but is only triggered to burn during the NEXT sell transaction.

  2. The burn happens BEFORE the current sell is processed, and burns tokens directly from the pair’s token balance.

  3. After calling pair.sync(), the pair’s internal reserve1 (AM reserve) is reduced.

  4. The attacker can independently inflate the USDT reserve by sending USDT directly to the pair and calling sync().

  5. 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

TransferFromToTokenAmount
PancakeV3 flash inFlashPoolExploitUSDT18,000,000
Moolah flash loan 1MoolahExploitUSDT9,265,120
Moolah flash loan 2MoolahExploitWBNB361,710
vWBNB mintExploitMoolah vWBNBWBNB361,710
Moolah borrowMoolah vUSDTExploitUSDT100,423,811
EOA seeds AMEOAExploitAM5,063
Pool USDT drain (net)AM/USDT PairExploitUSDT~127,822,303
Exploit → Pair directExploitAM/USDT PairUSDT~127,689,094
Repay Moolah USDT borrowExploitMoolah vUSDTUSDT100,423,811
Moolah redeem WBNBMoolah vWBNBExploitWBNB361,710
Repay Moolah WBNB flashExploitMoolahWBNB361,710
Repay Moolah USDT flashExploitMoolahUSDT9,265,120
Repay PancakeV3 flashExploitFlashPoolUSDT18,001,800
Profit to EOAExploitEOAUSDT131,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

MetricValue
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 affectedAM Token (AMBase.sol), AM/USDT PancakeSwap pair
Protocols NOT affectedMoolah (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:

  1. The burn timing is fully controllable by any caller who initiates a sell
  2. The burn amount is unbounded and grows with all prior user activity
  3. The burn directly removes tokens from the pair’s balance and updates reserves via sync()
  4. 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:

  1. Burn tokens at time of sell (immediate burn): Replace the toBurnAmount accumulator with an immediate burn in _handleSell. Do not defer burns across transactions.
  2. 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.
  3. Bound the burn trigger: If toBurnAmount is 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:

  1. 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 toBurnAmount pattern (accumulating deferred state that anyone can trigger) is an anti-pattern in adversarial environments.

Confidence

AspectConfidenceNotes
Root cause identificationHIGHFull source code reviewed; exact vulnerable code lines identified
Attack sequenceHIGHDecoded call trace and fund flows corroborate each step
Profit figuresHIGHConfirmed from funds_flow.json net_changes
Oracle not manipulatedHIGHOracle calls use Chainlink + Binance feeds, not spot pool price
Moolah not the victimHIGHBorrow fully repaid, net change = 0 for Moolah