In the first eight days of March 2026, two deflationary tokens on BNB Chain — BUBU2 and LEDS — were exploited using variations of the same fundamental attack pattern. The combined damage was modest by DeFi standards ($84K), but the vulnerability class they exposed is far more dangerous than the dollar figures suggest. Every AMM pair with a deflationary token is a potential time bomb, and most DeFi developers don't know how to spot the fuse.
This article dissects both exploits, reconstructs the attack mechanics, and provides concrete Solidity patterns to detect and prevent this class of vulnerability.
The Core Problem: When Burns Hit the Wrong Balance
Deflationary tokens reduce supply over time by burning tokens on every transfer. The idea is simple: scarcity drives value. The implementation, however, creates a fundamental tension with AMM pool mechanics.
AMMs like PancakeSwap maintain reserves of two tokens and use the constant product formula (x * y = k) to price swaps. The formula assumes that the tokens in the pool stay in the pool unless explicitly swapped out. But deflationary tokens violate this assumption — every transfer into or within the pool burns some tokens, silently eroding reserves.
When the burn mechanism targets AMM pair reserves directly (not just transfer amounts), the consequences are catastrophic: an attacker can trigger enough burns to collapse one side of the pool to near-zero, then swap the inflated token for the paired asset at a wildly favorable rate.
Exploit #1: BUBU2 — The Time-Accumulated Burn Bomb (March 1, 2026)
Loss: ~$19,700
Chain: BNB Chain
Root Cause: Unbounded accumulated burn rounds triggered in a single transaction
The Vulnerable Mechanism
The BUBU2 token contract included a _triggerDailyBurnAndMint() function designed to periodically burn tokens from the AMM pair's reserves. The burn amount was proportional to the time elapsed since the last trigger:
// Simplified vulnerable pattern (reconstructed)
function _triggerDailyBurnAndMint() internal {
uint256 elapsed = block.timestamp - lastTriggerTime;
uint256 rounds = elapsed / TRIGGER_INTERVAL;
for (uint256 i = 0; i < rounds; i++) {
uint256 burnAmount = pairBalance * BURN_RATE / 10000;
_burn(pair, burnAmount);
}
lastTriggerTime = block.timestamp;
}
The Attack
The contract owner (or an attacker who compromised the owner key) set TRIGGER_INTERVAL to an absurdly small value — potentially a single second. This meant that even a few hours of inactivity would accumulate hundreds of burn rounds.
When the next transfer triggered _triggerDailyBurnAndMint(), all accumulated rounds executed in a single transaction:
- Accumulate: Wait for rounds to build up with the tiny interval
- Trigger: Execute any transfer involving the token to invoke the burn function
- Drain: Hundreds of sequential burns compound against the pair's reserves, each burn reducing the base for the next burn calculation
- Swap: With BUBU2 reserves decimated, swap a tiny amount of BUBU2 for a disproportionate amount of WBNB
The Math of Compound Burns
If each burn round removes 1% of the pair's BUBU2 balance, after N rounds:
remaining = initialBalance * (0.99)^N
After 100 rounds: ~36.6% remains
After 300 rounds: ~4.9% remains
After 500 rounds: ~0.66% remains
The attacker doesn't need to drain it to zero — even a 90% reduction makes the subsequent swap enormously profitable.
Exploit #2: LEDS — The Multi-Path Burn Chain (March 8, 2026)
Loss: ~$64,000
Chain: BNB Chain
Root Cause: Multiple unprotected burn mechanisms with no access control or cooldowns
The Vulnerable Pattern
The LEDS token contract was worse than BUBU2 because it contained multiple independent functions that could burn tokens from the AMM pair, and none of them had:
- Access control (anyone could call them)
- Cooldown periods
- Per-transaction burn limits
// Vulnerable pattern: multiple unguarded burn paths
function burnFromPair_A() external {
uint256 amount = _calculateBurnA();
_burn(pair, amount);
}
function burnFromPair_B() external {
uint256 amount = _calculateBurnB();
_burn(pair, amount);
}
function burnFromPair_C() external {
uint256 amount = _calculateBurnC();
_burn(pair, amount);
}
// No access control. No cooldowns. No caps.
The Attack
The attacker simply called all burn functions sequentially in a single transaction:
- Flash loan WBNB from PancakeSwap
-
Call
burnFromPair_A()→ burns X% from pair reserves -
Call
burnFromPair_B()→ burns Y% from (already reduced) reserves -
Call
burnFromPair_C()→ burns Z% from (further reduced) reserves -
Sync the pair to update reserves:
IPancakePair(pair).sync() - Swap minimal LEDS for maximum WBNB through the now-imbalanced pool
- Repay flash loan + fee, pocket the difference
The compounding effect of chaining multiple burn paths meant the LEDS reserve in the pair was essentially zeroed out, while the WBNB reserve remained untouched.
Why This Attack Class Is More Dangerous Than It Looks
Scale of Exposure
A quick scan of BNB Chain (and Ethereum) reveals hundreds of deflationary tokens paired in AMM pools. Most were launched with minimal or no audit. The common patterns include:
- Reflection tokens (SafeMoon clones) that redistribute fees
- Auto-burn tokens that destroy tokens on each transfer
- Elastic supply tokens with rebase mechanisms affecting pair balances
The Rug-Pull Gray Zone
The BUBU2 exploit required the contract owner to change TRIGGER_INTERVAL. This sits in a gray zone between exploit and rug-pull — the owner deliberately configured the contract to enable the attack. Many deflationary tokens have similar owner-controlled parameters that can be weaponized:
- Adjustable burn rates
- Configurable burn targets (which address gets burned)
- Toggleable burn mechanisms
- Mutable fee structures
Detection: How to Spot Vulnerable Deflationary Tokens
Red Flags in Solidity Code
// 🚩 RED FLAG 1: Direct burns from pair address
_burn(pairAddress, amount);
// or
_balances[pairAddress] -= amount;
// 🚩 RED FLAG 2: Unbounded loop based on elapsed time
uint256 rounds = elapsed / interval;
for (uint256 i = 0; i < rounds; i++) { ... }
// 🚩 RED FLAG 3: Public burn functions without access control
function triggerBurn() external { // no onlyOwner, no modifier
_burn(pair, calculateBurn());
}
// 🚩 RED FLAG 4: Owner-adjustable burn parameters without bounds
function setTriggerInterval(uint256 _interval) external onlyOwner {
TRIGGER_INTERVAL = _interval; // No minimum! Could be set to 1
}
// 🚩 RED FLAG 5: Multiple independent burn paths
function burnA() external { _burn(pair, amountA); }
function burnB() external { _burn(pair, amountB); }
Automated Detection with Slither
You can write a custom Slither detector for this pattern:
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.declarations import Function
class DeflationaryPairBurn(AbstractDetector):
ARGUMENT = "deflationary-pair-burn"
HELP = "Detects direct burns from AMM pair addresses"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://example.com/deflationary-burn"
def _detect(self):
results = []
for contract in self.compilation_unit.contracts_derived:
pair_vars = [v for v in contract.state_variables
if "pair" in v.name.lower()]
burn_fns = [f for f in contract.functions
if any("burn" in n.name.lower()
for n in f.internal_calls)]
for fn in burn_fns:
for pair_var in pair_vars:
if pair_var in fn.state_variables_read:
info = [
f"Function {fn.name} burns tokens from pair address ",
pair_var, "\n"
]
results.append(self.generate_result(info))
return results
Prevention: Building Safe Deflationary Tokens
Pattern 1: Cap Accumulated Rounds
uint256 constant MAX_ROUNDS_PER_TX = 5;
function _triggerBurn() internal {
uint256 elapsed = block.timestamp - lastTriggerTime;
uint256 rounds = elapsed / TRIGGER_INTERVAL;
// Cap to prevent compound drain
if (rounds > MAX_ROUNDS_PER_TX) {
rounds = MAX_ROUNDS_PER_TX;
}
for (uint256 i = 0; i < rounds; i++) {
uint256 burnAmount = pairBalance * BURN_RATE / 10000;
_burn(pair, burnAmount);
}
lastTriggerTime = block.timestamp;
}
Pattern 2: Enforce Minimum Intervals with Bounds
uint256 constant MIN_TRIGGER_INTERVAL = 1 hours;
uint256 constant MAX_TRIGGER_INTERVAL = 7 days;
function setTriggerInterval(uint256 _interval) external onlyOwner {
require(
_interval >= MIN_TRIGGER_INTERVAL &&
_interval <= MAX_TRIGGER_INTERVAL,
"Interval out of bounds"
);
// Add timelock for extra safety
pendingInterval = _interval;
intervalChangeTime = block.timestamp + 24 hours;
}
function applyIntervalChange() external {
require(block.timestamp >= intervalChangeTime, "Timelock active");
TRIGGER_INTERVAL = pendingInterval;
}
Pattern 3: Per-Block Burn Budget
uint256 constant MAX_BURN_PER_BLOCK_BPS = 100; // 1% max per block
mapping(uint256 => uint256) public burnedInBlock;
function _safeBurn(address from, uint256 amount) internal {
uint256 maxBurn = _balances[from] * MAX_BURN_PER_BLOCK_BPS / 10000;
uint256 alreadyBurned = burnedInBlock[block.number];
uint256 actualBurn = amount;
if (alreadyBurned + amount > maxBurn) {
actualBurn = maxBurn > alreadyBurned ? maxBurn - alreadyBurned : 0;
}
if (actualBurn > 0) {
burnedInBlock[block.number] = alreadyBurned + actualBurn;
_burn(from, actualBurn);
}
}
Pattern 4: Never Burn Directly From Pair
The safest approach: never burn tokens directly from the AMM pair address. Instead, accumulate burn amounts in a separate tracking variable and execute burns only during user-initiated swaps, proportional to the swap amount:
uint256 public pendingBurnFromPair;
function _accumulateBurn(uint256 amount) internal {
pendingBurnFromPair += amount;
}
// Execute only during swaps, not as standalone function
function _executePendingBurn() internal {
if (pendingBurnFromPair > 0) {
uint256 pairBal = _balances[pair];
uint256 maxBurn = pairBal * MAX_BURN_PER_BLOCK_BPS / 10000;
uint256 burnNow = pendingBurnFromPair > maxBurn ? maxBurn : pendingBurnFromPair;
pendingBurnFromPair -= burnNow;
_burn(pair, burnNow);
IPancakePair(pair).sync();
}
}
For AMM Developers: Protecting Against External Deflation
If you're building an AMM or integrating deflationary tokens, use balance-delta accounting instead of trusting transfer amounts:
function swap(address tokenIn, uint256 amountIn, ...) external {
uint256 balanceBefore = IERC20(tokenIn).balanceOf(address(this));
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
uint256 actualAmountIn = IERC20(tokenIn).balanceOf(address(this)) - balanceBefore;
// Use actualAmountIn, not amountIn, for all calculations
// This catches any burn-on-transfer deflation
}
Key Takeaways
- Deflationary burns targeting AMM pair reserves are a ticking time bomb — the attacker just needs to trigger enough burns to collapse one side of the pool
- Multiple unguarded burn paths compound the risk — LEDS had three independent burn functions, all publicly callable
- Owner-controlled parameters are a rug-pull vector — BUBU2's tiny trigger interval was set deliberately
- Cap everything: maximum rounds per transaction, minimum intervals with bounds, per-block burn budgets
- Never trust transfer amounts in AMMs — always use balance-delta accounting to handle fee-on-transfer and deflationary tokens
- Audit deflationary tokenomics as attack surface, not just as an economic feature
The deflationary token exploit pattern is one of the oldest in DeFi (remember Balancer/STA in 2020?), yet it keeps recurring because token developers don't treat their burn mechanics as security-critical code paths. In March 2026, the damage was $84K. The next deflationary token paired against a deep liquidity pool could lose millions.
Security researcher focused on DeFi exploit analysis, smart contract auditing, and blockchain security tooling. Follow for weekly deep-dives into real-world exploits and practical defense patterns.
Tags: #security #blockchain #defi #smartcontracts
Top comments (0)