BurnAddress / MONA - Deferred LP Burn Lets Attacker Zero the MONA Reserve and Drain USDT

On 2026-04-14 at 05:18:27 UTC (BNB Chain block 92,429,268), the attacker used Moolah flash liquidity plus a same-tx Venus USDT borrow to fund a reserve-accounting exploit against the MONA token’s BurnAddress mechanism. The attacker first farmed 10,000 MONA through 25 freshly created accounts, then sold 9,900 MONA to create a deferred burn credit of 7,641.130933404251852809 MONA, bought out the pair’s original MONA inventory, triggered the delayed burn with a zero-value MONA transferFrom, and finally sold only 100 MONA back into a pool that now held almost all of its USDT but almost none of its MONA. The final realized profit was 60,950.308123921915843825 USDT sent to 0x7eeec499e501293f6e589d550046375a2ad0b4c3.

The core bug is not in the flash-loan path, Venus, or the referral setup. The loss comes from the MONA / BurnAddress design itself: a sell pays out USDT immediately, but the MONA side of that sell can be removed from the LP later, on an attacker-chosen transfer, by directly mutating the pair balance and calling sync(). That breaks the AMM invariant and lets the attacker separate the USDT proceeds of a sell from the MONA inventory that should have remained in the pool.


Root Cause

Vulnerable Contracts

  • Primary target: BurnAddress at 0xd7ab8cc95eab59bab429242ec176feebaea88da3
  • Coupled vulnerable token logic: monaToken at 0x311838c073a865e8249f5c35e4cb2a5f815a36e8
  • Drained pair: MONA/USDT Pancake pair 0x4dfb65e12f331c58380c55d7f288fe8fb22d3ea7
  • Not root cause, but setup path: NodeSubscriptionLisa at 0xb9d8f078043dbf3297416735a84ab87324190fec, ReferralRegistryLisa at 0x651c11eb567df9dcc5a3385f9f204ccbeee9e002, and unverified farming contract 0xaea6e5ca6c1feeabbd3a114bcbca30a21424f76b

Vulnerable Functions

  • monaToken._update(address,address,uint256) - contracts/mona/monaToken.sol, lines 137-153
  • monaToken._handleSell(address,address,uint256) - contracts/mona/monaToken.sol, lines 181-207
  • BurnAddress.burn() - contracts/mona/burnAddress.sol, lines 44-65
  • monaToken.burnsellMona(uint256) - contracts/mona/monaToken.sol, lines 284-295

Vulnerable Code

The bug is easier to see as a three-step pipeline. First, the sell path pays USDT out immediately, but also records a delayed LP-burn claim that is not settled inline with the swap.

// contracts/mona/monaToken.sol
// Verified source, lines 145-152 and 181-207

function _update(address from, address to, uint256 amount) internal override {
    require(!isBlacklisted[from] && !isBlacklisted[to], "Blacklisted");
    if (isExcludedFromTransfer[from] || isExcludedFromTransfer[to]) {
        super._update(from, to, amount);
        return;
    }

    if (from == lpPairAddress) {
        _handleBuy(from, to, amount);
    } else if (to == lpPairAddress) {
        _handleSell(from, to, amount);
    } else {
        super._update(from, to, amount);
        IburnAddressInterface(burnAddress).burn();
        // VULNERABILITY: any non-pair MONA transfer can settle old sell credits later,
        // at attacker-chosen timing, even if this transfer moves 0 tokens.
    }
}

function _handleSell(address from, address to, uint256 amount) private {
    require(isOpenTrade, "Not open");
    require(block.timestamp - lastTradeTime[from] >= COOLDOWN_TIME, "Cooldown");

    // ... fees omitted ...

    uint256 remainingMona = amount - nodeFee - ecologyFee - remainingBurn;
    (uint256 monaReserve, uint256 usdtReserve, ) = _getPairReserves();
    uint256 amountUSDT = getAmountOut(remainingMona, monaReserve, usdtReserve);
    uint256 monaDeduction = _handleProfitDeduction(from, amountUSDT, monaReserve, usdtReserve);
    uint256 monaTransfer = remainingMona - monaDeduction;

    super._update(from, to, monaTransfer);
    _updateLastTradeTime(from);
    IburnAddressInterface(burnAddress).sell(monaTransfer);
    // VULNERABILITY: the pair pays USDT for this sell now, but `monaTransfer`
    // is also queued for a future burn from the LP itself.
}

This first snippet contains two linked flaws:

  • _handleSell() lets the AMM pay out USDT for monaTransfer immediately, then separately books that same monaTransfer into BurnAddress.sellMona.
  • _update() allows any later non-pair MONA transfer to trigger burnAddress.burn(), so the attacker can decide when those stale sell credits are settled.

Second, BurnAddress converts the accumulated historical sell credits into a fresh burn amount without verifying that the corresponding MONA is still supposed to remain in the pair at that moment.

// contracts/mona/burnAddress.sol
// Verified source, lines 44-58

function burn() external {
    if (msg.sender == monaTokenAddress) {
        uint256 amount = sellMona - burnedMona;
        if (amount == 0) {
            return;
        }

        uint256 currentBurned = monaToken.balanceOf(address(0xdead));
        uint256 burnLimit = monaToken.burnLimit();
        if (currentBurned < burnLimit) {
            uint256 needBurn = burnLimit - currentBurned;
            if (needBurn < amount) {
                amount = needBurn;
            }
            burnedMona += amount;
            monaToken.burnsellMona(amount);
            // VULNERABILITY: burns from the LP based on stale cumulative sell credits,
            // not as part of the sell that created those credits.
        }
    } else {
        tapcount++;
    }
}

Here the vulnerability is that burn() uses sellMona - burnedMona as a deferred claim on LP inventory. That amount is derived from prior sells, but it is executed later, on a different transfer context, after the attacker has already rearranged the pool reserves.

Third, once burn() decides how much to burn, monaToken.burnsellMona() directly removes those tokens from the pair and commits the manipulated reserve state with sync().

// contracts/mona/monaToken.sol
// Verified source, lines 284-295

function burnsellMona(uint256 amount) external {
    require(amount > 0, "Must be greater than 0");
    require(msg.sender == burnAddress || msg.sender == joinAddress, "Only burnAddress or joinAddress");

    super._update(lpPairAddress, address(0xdead), amount);
    // VULNERABILITY: directly mutates the pair's MONA balance after an unrelated trade.

    IPancakePair(lpPairAddress).sync();
    // VULNERABILITY: commits the manipulated balance to AMM reserves.

    emit extracTransfer(lpPairAddress, address(0xdead), amount);
}

This is the final reserve-breaking step: instead of charging the seller inline, the contract reaches back into lpPairAddress, transfers MONA out of the pool to 0xdead, and then calls sync(). That lets the attacker first get paid in USDT, then later erase the MONA side of the earlier sell from the AMM reserves.

Why It Is Vulnerable

Expected behavior: If a sell is taxed or burned, that burn must be settled inside the same swap path, before or during the AMM state transition, and it must come from the seller’s tokens or from a protocol fee bucket. After the seller receives USDT, the pair should still hold the MONA inventory that the swap actually delivered to it.

Actual behavior:

  1. _handleSell() transfers monaTransfer into the pair and immediately lets the seller receive USDT.
  2. The same monaTransfer is also recorded in BurnAddress.sellMona as pending future burn credit.
  3. A later, unrelated MONA transfer can trigger BurnAddress.burn().
  4. burn() calls burnsellMona(amount), which directly subtracts MONA from lpPairAddress and then calls sync().

So the seller gets paid once in USDT, and the pair can lose the MONA side of that exact sell later. That is equivalent to canceling the token side of the earlier swap after the USDT side has already settled. An attacker can therefore choose the moment when the LP inventory disappears.

The exploit becomes practical because the attacker can control when burn() runs. They do not call BurnAddress.burn() directly; that would only increment tapcount. Instead, they trigger it through the token hook in _update() by sending a normal non-pair MONA transfer. In this transaction, they used a zero-value transferFrom(0x000000000000000000000000000000000000deed, 0x000000000000000000000000000000000000deed, 0) to enter the non-pair branch and execute the burn without spending any MONA.

Normal Flow vs Attack Flow

StageNormal flowAttack flow
Sell settlementSeller transfers MONA into the pair and receives quoted USDT; the sold MONA remains in the pair.Seller helper sells 9,900 MONA, receives 51.249833102620958684 USDT, and queues 7,641.130933404251852809 MONA as delayed burn credit.
Burn accountingAny burn/tax should be applied inline to the same transfer, from seller inventory or a fee bucket.The MONA side of the earlier sell is deferred into sellMona, decoupled from the swap that already paid USDT out.
Burn triggerUnrelated token transfers should not rewrite LP reserves from previous sells.After buying almost all MONA out of the pool, the helper triggers burn() with a zero-value transferFrom, causing burnsellMona() to remove the last 7,641.130933404251852809 MONA from the pair.
Reserve stateReserves remain consistent with swap history.sync() locks in a near-zero MONA reserve while the pair still holds almost all of its USDT, so a tiny final sell can drain the USDT side.

Attack Execution

High-Level Flow

  1. The attacker EOA 0x7eeec499e501293f6e589d550046375a2ad0b4c3 deploys factory 0x865217406668c6750547fdf9cbd81dbff33bfefc, which deploys main helper 0x6ab3b009614cd4bbd9c58b1f1cd0ee6ae49eaa87 in the same transaction.

  2. 0x6ab3...aa87 takes five nested Moolah flash loans from 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c using flashLoan(address,uint256,bytes) / callback onMoolahFlashLoan(uint256,bytes), borrowing:

    • 408,880.587066566339821548 WBNB
    • 349.903711492409140667 BTCB
    • 270.828520979121253536 SolvBTC
    • 7,279,312.592405260060030054 USDT
    • 11,784,826.933714838602152410 lisUSD
  3. With the borrowed collateral in place, the helper borrows 94,212,209.464329208278107126 USDT from Venus vUSDT 0xfd5840cd36d94d7229439859c0112a4185bc0255 via borrow(uint256).

  4. The helper creates 25 fresh accounts. Each account:

    • binds 0x7eee...b4c3 as referrer through NodeSubscriptionLisa.bindReferrer(address) -> ReferralRegistryLisa.bind(address,address)
    • receives 220 USDT from the helper
    • pays that 220 USDT into unverified contract 0xaea6e5ca6c1feeabbd3a114bcbca30a21424f76b
    • receives 400 MONA back and forwards it to 0x6ab3...aa87

    This setup costs 5,500 USDT total and yields exactly 10,000 MONA to the helper.

  5. The helper sends 9,900 MONA to a freshly deployed sell-helper 0x9b9442e49acd08aa084bff9735fa2e76a1e75349. This isolates the initial sell from the main helper’s cooldown state.

  6. 0x9b94...5349 sells 9,900 MONA through Pancake Router using swapExactTokensForTokensSupportingFeeOnTransferTokens. Because of MONA’s sell-side deductions, only 7,641.130933404251852809 MONA actually reaches the pair and gets recorded in BurnAddress.sellMona; the main helper receives only 51.249833102620958684 USDT from this sell.

  7. The main helper then spends 86,026,351.586237271086020734 USDT through swapTokensForExactTokens to buy exactly 9,875,067.446160841960227167 MONA, sending the output to separate recipient 0xdd0215b556b08dcd7bad43a8116f89814b1545e0. This keeps the main helper off the token’s buy cooldown path.

  8. At that point the pair still holds exactly the previously queued 7,641.130933404251852809 MONA. The helper calls MONA.transferFrom(0x000000000000000000000000000000000000deed, 0x000000000000000000000000000000000000deed, 0), which triggers the non-pair _update() branch and therefore BurnAddress.burn().

  9. BurnAddress.burn() calls monaToken.burnsellMona(7,641.130933404251852809), which transfers the entire remaining MONA balance of the pair to 0xdead and calls sync(). The pair is now left with effectively zero MONA but still almost its full USDT reserve.

  10. The main helper, which deliberately retained 100 MONA, sells that final 100 MONA via swapExactTokensForTokensSupportingFeeOnTransferTokens. After fees, 93.999999998477809336 MONA reaches the pair, and the pair pays out 86,092,750.644528090380905875 USDT.

  11. The helper repays Venus (94,212,209.464329208278107126 USDT) and the USDT leg of the Moolah flash loans (7,279,312.592405260060030054 USDT), then forwards the remaining 60,950.308123921915843825 USDT to the attacker EOA.

Why the Attacker Used Multiple Addresses

The address split is deliberate and important:

  • 0x9b94...5349 performs the first 9,900 MONA sell so the main helper is not marked by lastTradeTime[from] on a sell.
  • 0xdd02...45e0 receives the huge MONA buy so the main helper is not marked by lastTradeTime[to] on a buy.
  • 0x6ab3...aa87 keeps only the final 100 MONA and performs the drain sell after the burn, with no cooldown conflict.

This is not the root bug, but it shows the attacker understood the token’s state machine and threaded around it to reach the real LP-burn vulnerability.

Detailed Call Trace

[39, 43, 47, 53, 57] 0x6ab3...aa87 -> 0x8f73...5d8c
    flashLoan(...) / onMoolahFlashLoan(...)
    Borrow WBNB, BTCB, SolvBTC, USDT, lisUSD

[186] 0x6ab3...aa87 -> vUSDT 0xfd58...0255
    borrow(94,212,209.464329208278107126 USDT)

[404..1227] 25 fresh accounts
    bindReferrer(0x7eee...b4c3)
    pay 220 USDT each into 0xaea6...f76b
    receive 400 MONA each and forward it to 0x6ab3...aa87

[1228] CREATE sell-helper 0x9b94...5349
[1229] MONA.transfer(0x9b94...5349, 9,900 MONA)

[1233] 0x9b94...5349 -> PancakeRouter
    swapExactTokensForTokensSupportingFeeOnTransferTokens(
        9,900 MONA,
        0,
        [MONA, USDT],
        0x6ab3...aa87,
        deadline
    )

[1239] MONA -> BurnAddress.sell(7,641.130933404251852809)
[1243] Pair -> 0x6ab3...aa87 transfers 51.249833102620958684 USDT

[1252] 0x6ab3...aa87 -> PancakeRouter
    swapTokensForExactTokens(
        9,875,067.446160841960227167 MONA,
        101,486,073.306567570959095864 USDT max,
        [USDT, MONA],
        0xdd02...45e0,
        deadline
    )

[1255] Pair -> 0xdd02...45e0 transfers 9,875,067.446160841960227167 MONA
[1254/604 log] Helper actually pays 86,026,351.586237271086020734 USDT into the pair

[1259] 0x6ab3...aa87 -> MONA
    transferFrom(0x000...deed, 0x000...deed, 0)
    This is the explicit delayed-burn trigger.

[1263] BurnAddress -> MONA
    burnsellMona(7,641.130933404251852809)

[1264] MONA -> Pair
    sync()

[1268] 0x6ab3...aa87 -> PancakeRouter
    swapExactTokensForTokensSupportingFeeOnTransferTokens(
        100 MONA,
        0,
        [MONA, USDT],
        0x6ab3...aa87,
        deadline
    )

[1274] MONA -> BurnAddress.sell(93.999999998477809336)
[1278] Pair -> 0x6ab3...aa87 transfers 86,092,750.644528090380905875 USDT

[1284] 0x6ab3...aa87 -> vUSDT.repayBorrow(94,212,209.464329208278107126)
[logs] 0x6ab3...aa87 -> Moolah repayment 7,279,312.592405260060030054 USDT
[logs] 0x6ab3...aa87 -> 0x7eee...b4c3 profit transfer 60,950.308123921915843825 USDT

Financial Impact

Pair-Level Loss vs Attacker Profit

ItemAmountNotes
Pair USDT before attack path was complete66,450.317305679570184169 USDTObserved around the exploit path and consistent with prior state
Pair USDT after exploit0.009181757654340344 USDTEnd-of-tx reserve on the MONA/USDT pair
Pair USDT loss66,450.308123921915843825 USDTEconomic damage to the MONA/USDT pool
MONA farming setup cost5,500 USDT25 * 220 USDT paid into 0xaea6...f76b
Attacker profit transferred out60,950.308123921915843825 USDTSent to 0x7eee...b4c3

Reserve Transition Around the Core Drain

StageMONA reserveUSDT reserve
After initial 9,900 MONA sell settles9,882,708.57709424621207997666,399.067472576949225485
After exact-token buy drains original pool MONA7,641.13093340425185280986,092,750.653709848035246219
After delayed burn executes086,092,750.653709848035246219
After final 100 MONA sell94.0000000084778093360.009181757654340344

Key Realized Amounts

StepAmount
MONA farmed from 25 accounts10,000 MONA
Initial MONA sold9,900 MONA
MONA actually credited to sellMona / pair7,641.130933404251852809 MONA
USDT from initial sell51.249833102620958684 USDT
USDT spent on exact-token buy86,026,351.586237271086020734 USDT
MONA bought out of pair9,875,067.446160841960227167 MONA
Final MONA sold100 MONA (93.999999998477809336 reaches pair)
USDT from final sell86,092,750.644528090380905875 USDT
Venus repay94,212,209.464329208278107126 USDT
Moolah USDT repay7,279,312.592405260060030054 USDT
Net profit60,950.308123921915843825 USDT

Evidence

ItemValue
Attack tx0x3a60e1b3a4b0736be4f31839bfd7abc8bfc53b93ddbd3702e77fbc64561a7ea4
Receipt status0x1
Block / timestamp92,429,268 / 2026-04-14 05:18:27 UTC
Tx index in block0x13
BurnAddress.burn() selector0x44df8e70
BurnAddress.sell(uint256) selector0xe4849b32
monaToken.burnsellMona(uint256) selector0xf1fef213
Initial sell router callswapExactTokensForTokensSupportingFeeOnTransferTokens at trace index 1233
Buy-back router callswapTokensForExactTokens at trace index 1252
Final drain router callswapExactTokensForTokensSupportingFeeOnTransferTokens at trace index 1268
Explicit delayed-burn triggertransferFrom(0x000...deed, 0x000...deed, 0) at trace index 1259
Burn amount moved from pair to 0xdead7,641.130933404251852809 MONA
Final USDT payout from pair86,092,750.644528090380905875 USDT
Profit transfer to attacker EOA60,950.308123921915843825 USDT
Pair end reserve94.000000008477809336 MONA / 0.009181757654340344 USDT

Secondary Observation: Referral Path Was Sybil-Friendly, but Not the Drain Root Cause

NodeSubscriptionLisa.bindReferrer() and ReferralRegistryLisa.bind() let freshly created accounts bind the attacker’s EOA as referrer through the whitelisted subscription contract, and nothing in that path prevented same-tx account farming. That setup mattered because it produced the initial 10,000 MONA used in the drain. However, even without that farming path, the actual loss of USDT still depends on the BurnAddress design flaw described above.

Residual Unknowns

The contract at 0xaea6e5ca6c1feeabbd3a114bcbca30a21424f76b is unverified. Its exact business logic is not required to establish the LP-drain root cause, because the decisive evidence is already on-chain in the verified MONA / BurnAddress code, the pair swap sequence, the burn trigger, and the reserve outcomes.