NEM Token Exploit: A Skim-and-Burn Loop Drained $822K | PandaSecurity
Web3 Security

NEM Token Exploit: A Skim-and-Burn Loop Drained $822K

PandaSecurity Team •
#Web3 Security#DeFi Exploit#BNB Chain#Smart Contract Audit
Diagram of the NEM exploit loop: transfer into the pair, skim it back, burn pair reserves, sync, repeat

On 21 October 2026 at 03:28 UTC, a single transaction on BNB Chain took about $822K out of the PancakeSwap USDT/NEM pair. The attacker did not steal a key or find a bug in PancakeSwap. They used the NEM token’s own tokenomics: a “sell recycle” feature that burns NEM straight out of the liquidity pool, driven by a counter that anyone could inflate for almost nothing.

Most security firms flagged the incident on 22 October, but the block timestamp of the attack transaction is 21 October. This post walks through how the feature was supposed to work, why the counter could be faked, how the attack was sequenced, and what token developers and auditors should take from it.

TL;DR

What the Sell-Recycle Feature Did

Fee-on-transfer and “deflationary” tokens on BNB Chain often try to support the price by burning part of the supply whenever someone sells. NEM’s version, as described by BlockSec Phalcon and SlowMist, worked in two steps:

  1. When NEM arrived at the LP pair, the token treated it as a sell and added to a running total, pendingLpSellRecycle.
  2. Later, flushLpSellRecycle() (an external function) called the internal _flushLpSellRecycle(), which took about 70% of the pending amount, capped by the pair’s balance, burned that much NEM directly from the pair’s balance, and then called sync() on the pair so its stored reserves matched the new, lower balance.

Burning tokens out of a pool and syncing is a well-known way to push a price up. Fewer NEM in the pool against the same USDT means each NEM is worth more USDT. That is already a dangerous power for a token contract to hold. NEM made it far worse by letting the size of the burn be decided by a number that did not reflect real sells.

Root Cause: A Counter That Trusted Where Tokens Landed

SlowMist summed it up precisely: NemTokenV2 credited transfers into the LP pair as sell volume without verifying swap settlement. In a Uniswap V2 style pair such as PancakeSwap’s, tokens arriving at the pair do not prove a sale. Anyone can send tokens to the pair address. Until someone calls swap(), those tokens are just excess balance above the stored reserves, and skim() lets any caller withdraw that excess to an address of their choice.

So the attacker could:

CertiK’s alert described the same thing as a stale record: the pending amount was “created when tokens enter pool, remained after they left”. The burn was based on a number that described tokens no longer there.

The sketch below is a simplified illustration of the pattern based on the public analyses. It is not NEM’s verified source code:

// Simplified illustration of the vulnerable pattern (not verbatim NemTokenV2 code)
uint256 public pendingLpSellRecycle;

function _transfer(address from, address to, uint256 amount) internal override {
    if (to == lpPair && from != address(this)) {
        // Any transfer to the pair is assumed to be a sell
        pendingLpSellRecycle += amount;
    }
    super._transfer(from, to, amount);
}

function flushLpSellRecycle() external {          // callable by anyone
    _flushLpSellRecycle();
}

function _flushLpSellRecycle() internal {
    uint256 burnAmount = pendingLpSellRecycle * 70 / 100;
    uint256 pairBal = balanceOf(lpPair);
    if (burnAmount > pairBal / 2) burnAmount = pairBal / 2; // some cap
    pendingLpSellRecycle = 0;
    _burnFrom(lpPair, burnAmount);                 // burns LPs' NEM
    IPancakePair(lpPair).sync();                   // reserves shrink, price jumps
}

Each piece looks reasonable to a developer in isolation. Together they give any caller a lever that destroys pool reserves in proportion to a number the caller controls.

The Attack, Step by Step

The flow below combines the BlockSec Phalcon, SlowMist and HashDit write-ups with our own read of the transaction receipt from a public BNB Chain RPC. Figures are rounded.

  1. Borrow. The attack contract pulled 235,785,000 USDT of flash liquidity from 21 different sources, plus some WBNB.

  2. Buy NEM. It sent the full 235.785M USDT into the USDT/NEM pair and bought NEM.

  3. Loop 42 times:

    • transfer NEM directly to the pair, increasing pendingLpSellRecycle;
    • call skim() to send the excess back out;
    • call flushLpSellRecycle(), which burned NEM from the pair and called sync().

    The receipt shows 42 burn transfers from the pair to the zero address and 46 Sync events, consistent with the 42 rounds SlowMist reported.

  4. Sell. With the pair’s NEM reserve heavily reduced, the attacker sold NEM back into the pool and received 236,606,920 USDT, about 821,920 USDT more than it had put in.

  5. Repay and exit. The contract repaid the 21 lenders a total of about 235,880,613 USDT (around 95.6K USDT in fees) and sent 726,307 USDT to the attacker’s wallet. Reports citing the alerts say the proceeds were later bridged to Ethereum.

The two headline numbers you will see, about $822K and about $725K, are both correct but measure different things. The first is what the pool’s liquidity providers lost. The second is what the attacker kept after paying for the flash loans.

Why Token-Level Burns From the Pair Keep Failing

This is not the first time a token’s own logic has been turned against its liquidity pool. We covered a related accounting bug in Inside the Likwid Exploit: One Missing pairDelta, and the funding model is the same one described in our guide to flash loan attack prevention. The NEM case is a clean example of three recurring mistakes:

Impact

Mitigations and Audit Lessons

For token developers

  1. Never burn from the LP pair. If you want sell-side deflation, take the fee from the seller’s transfer amount before it reaches the pair, and burn tokens the contract itself holds.
  2. If you must track sells, track settled sells. Measure what the pair actually received in a completed swap, for example by comparing reserves before and after inside your own router, rather than counting every incoming transfer.
  3. Do not let the counter outlive the tokens. If tokens leave the pair through skim(), any credit they created must be reversed. The simplest way to guarantee this is not to create the credit at transfer time at all.
  4. Restrict and rate-limit flush logic. Make it callable only by a keeper or owner, at most once per block, and cap the reserve change per call.

A safer shape for a burn-on-sell feature (again illustrative) looks like this:

function _transfer(address from, address to, uint256 amount) internal override {
    if (to == lpPair && !isExcluded[from]) {
        uint256 fee = amount * sellBurnBps / 10_000;
        super._transfer(from, DEAD, fee);       // burn the seller's tokens, not the pool's
        amount -= fee;
    }
    super._transfer(from, to, amount);
}
// No function in this contract changes balanceOf(lpPair) or calls sync().

For auditors

Our smart contract security checklist covers these AMM integration checks for tokens with transfer hooks.

Key Addresses

Sources

Closing Thoughts

NEM’s burn feature was meant to reward holders on every sell. Because it trusted where tokens landed instead of what actually traded, it handed anyone with a flash loan a button that destroyed the pool’s reserves. If your token has transfer hooks, burns, reflections or any logic that touches the LP pair, that logic is part of your AMM’s security model.

Launching a token with custom tokenomics on BNB Chain? Contact Panda Security for a review before liquidity goes live, or check our audit pricing.

Related articles

← Back to Blog