Originally published at NEM Token Exploit: A Skim-and-Burn Loop Drained $822K
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
-
Target:
NemTokenV2(NEM) and its PancakeSwap USDT/NEM pair on BNB Chain. - Loss: the pair lost 821,920 USDT in net terms. The attacker's wallet received 726,307 USDT; the remaining ~95.6K USDT went to flash-loan fees.
-
Root cause: transfers into the pair were counted as "sell volume" in
pendingLpSellRecyclewithout checking that a real swap happened. The counter stayed intact after the tokens were pulled back out withskim(), andflushLpSellRecycle()then burned NEM that already belonged to the pool and calledsync(). - Method: transfer, skim, flush, repeated 42 times, then a large NEM-to-USDT sell into the distorted pool.
- Funding: about 235.8M USDT of flash liquidity borrowed from 21 sources inside the same transaction.
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:
- When NEM arrived at the LP pair, the token treated it as a sell and added to a running total,
pendingLpSellRecycle. - 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 calledsync()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:
- send NEM to the pair, which raised
pendingLpSellRecycle, - call
skim()to take most of those tokens back, which did not lower the counter, - call
flushLpSellRecycle(), which burned NEM that belonged to the pool's liquidity providers, not the attacker's tokens.
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.
- Borrow. The attack contract pulled 235,785,000 USDT of flash liquidity from 21 different sources, plus some WBNB.
- Buy NEM. It sent the full 235.785M USDT into the USDT/NEM pair and bought NEM.
-
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 calledsync().
- transfer NEM directly to the pair, increasing
The receipt shows 42 burn transfers from the pair to the zero address and 46 Sync events, consistent with the 42 rounds SlowMist reported.
- 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.
- 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:
-
Inferring intent from balances. "Tokens arrived at the pair, so this must be a sell" is false in any AMM that allows direct transfers and
skim(). A sell is only real onceswap()settles. -
Letting a token contract move the pool's balance. A token that can burn from the pair and then call
sync()has, in effect, an admin-free price lever. If anyone can pull it, flash loans make it cheap to pull at full size. -
Permissionless maintenance with no rate limit.
flushLpSellRecycle()was callable by anyone, any number of times in the same transaction.
Impact
- Direct loss: about 821,920 USDT from the USDT/NEM pair, borne by its liquidity providers, according to SlowMist and matching the on-chain flows.
- Attacker profit: about 726,307 USDT after flash-loan fees.
- Remediation: at the time of writing we found no public fix or post-mortem from the NEM team. 0xCERT's advisory lists no fix available and advises avoiding the pool until the project remediates.
Mitigations and Audit Lessons
For token developers
- 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.
- 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.
-
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. - 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
-
Search for
sync()calls and any write to the pair's balance inside token contracts. Each one is a price lever. Ask who can trigger it, how often, and how large the move can be. -
Treat
skim()as part of the threat model. For every piece of state updated on "transfer to pair", ask what happens if the tokens are skimmed straight back out. - Write the loop test. A Foundry test that flash-borrows, runs transfer, skim and flush in a loop and checks the pair's USDT balance would have exposed this in minutes.
Our smart contract security checklist covers these AMM integration checks for tokens with transfer hooks.
Key Addresses
- Attack transaction:
0xfb093c96c0094cbcca10801f97ed2e5ed3d96550ce64b544df5a349545723f91 - Attacker:
0xfd929b0f270dfea6c4827262b4e2bf6ea50fe9b6 - Attack contract:
0x5e6d1f40be466dd2facc94a26818948ecd3b1ca2 - Vulnerable contract (NemTokenV2):
0x9cf0e3ad4efa7ab602d7b5a38c55795cfa777777 - Victim pair (PancakeSwap USDT/NEM):
0x840e19dcca541ee0b55a3c67e900a39d523b12b8
Sources
- SlowMist TI Alert on X: loss figure, root cause and addresses
-
BlockSec Phalcon on X:
_flushLpSellRecycle()mechanics and attack steps -
CertiK Alert on X: stale
pendingLpSellRecycleexplanation - HashDit Alert on X: 42-round loop and attacker profit estimate
- 0xCERT advisory 0xCERT-2026-0263 and Crypto Briefing coverage
- Transaction receipt and block timestamp, read by us from a public BNB Chain RPC endpoint
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.
Top comments (0)