This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0xbfb9b3b8a0d3c589a02f06c516b5c7b7569739edd00f9836645080f2148aefc70x9601313572ecd84b6b42dbc3e47bc54f8177558eBSC0x94bb269518ad17f1c10c85e600bde481d4999bffBSCOn BNB Chain block 39289513, transaction 0xbfb9b3b8a0d3c589a02f06c516b5c7b7569739edd00f9836645080f2148aefc7 used a public DODO flash loan to buy NCD, route the purchased balance through a pre-aged helper cluster, mint additional NCD through self-transfers, and then liquidate the inflated inventory through repeated per-address sells into the NCD/USDT Pancake pair. The attacker EOA 0xd52f125085b70f7f52bd112500a9c334b7246984 finished with a 1942626839723871306033 raw-unit USDT gain, while the pair lost 1932626839723879276033 raw-unit USDT.
The root cause is two-part and deterministic. First, NCD rewards are minted from the holder's current balance at claim time rather than from the balance that actually aged. Second, the anti-dump guard limits only the current sender, so an adversary can rotate inventory across many helpers and repeatedly sell up to 5% per helper.
NCD is a taxed transfer token paired against USDT at 0x94bb269518ad17f1c10c85e600bde481d4999bff. On buys, the token initializes mineStartTime[recipient] when tokens come from the pair. On later non-pair transfers, it calls doReward(sender), which can mint additional NCD to the sender. On sells to the pair, it enforces a per-address cap using sellmaxrate, initialized to 5.
The exploit depends on a helper cluster whose mineStartTime values are already older than one rewardPeriod. Once such helpers exist, newly acquired NCD can be moved into them and immediately rewarded even though those fresh tokens did not remain in the address for the elapsed period. Public DODO flash liquidity and public Pancake pair liquidity make the sequence permissionless.
The incident is an ACT attack against flawed token accounting. The reward path treats the sender's entire current balance as eligible yield whenever a qualifying transfer occurs after at least one elapsed reward period. That lets fresh inventory inherit historical age it never earned. The sell-control path fails independently: it checks only amount <= _balances[sender] * sellmaxrate / 100 and tracks lastSellTime by sender, so moving the balance to a new helper resets the practical limit. Together these flaws convert time-based rewards and anti-dump controls into attacker-controlled amplification tools.
The key invariant is that rewards should accrue only on balance actually held during the elapsed period, and any sell throttle meant to constrain dumping should apply to the adversary's aggregate inventory rather than a single address. NCD breaks both invariants at code level in doReward and _transfer.
The verified NCD source shows the reward and sell logic directly:
function doReward(address _sender) internal {
if (mineStartTime[_sender] == 0) return;
uint256 dayss = (block.timestamp.sub(mineStartTime[_sender])).div(rewardPeriod);
if (dayss > 0) {
uint256 reward = _balances[_sender].mul(15).div(1000).mul(dayss);
_balances[_sender] += reward;
_totalSupply += reward;
mineStartTime[_sender] = block.timestamp;
}
}
if (uniswapV2Pair == recipient) {
require(amount <= _balances[sender].mul(sellmaxrate).div(100), "amount exceed limit");
if (lastSellTime[sender] > 0) {
require(block.timestamp.sub(lastSellTime[sender]) > rewardPeriod, "one time a day only");
}
lastSellTime[sender] = block.timestamp;
}
This is the concrete breakpoint. reward is computed from _balances[_sender] at claim time, not from a time-weighted or snapshotted balance. The sell gate is checked only for the current sender.
The seed trace matches that mechanism. The flash-loan executor receives 10000000000000000000000 USDT, buys NCD from the pair, then triggers a self-transfer on the first helper:
0x6098...::flashLoan(..., 10000000000000000000000, 0xb83F..., 0x00)
PancakePair::swap(0, 23165495498822353576596979, 0xCd4594..., 0x)
NCD::transferFrom(0xCd4594..., 0xCd4594..., 1)
emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0xCd4594..., value: 326633486533395185534808)
That mint event on self-transfer proves the helper received reward on the balance it had just been sent. The same pattern then repeats across helpers as the enlarged balance is transferred forward and each new aged helper performs another reward-triggering self-transfer.
Later in the same trace, helpers sell to the pair in sequence. Each sell stays within the per-helper 5% cap, but the aggregate inventory is dumped across many helper addresses:
NCD::transferFrom(0x5f7382..., PancakePair, 3205688339269786284228062)
PancakePair::swap(1466427557302307061, 0, 0xb83F..., 0x)
...
NCD::transferFrom(0x0D7ace..., PancakePair, 3045403922306296970016659)
PancakePair::swap(1369913577220730357, 0, 0xb83F..., 0x)
The balance diff confirms the economic result. The pair lost 1932626839723879276033 USDT raw units, the attacker EOA gained 1942626839723871306033 USDT raw units, and the pair's NCD balance increased by 347970725885974436363618960 raw units as inflated inventory was dumped back into liquidity.
The adversary cluster consisted of EOA 0xd52f125085b70f7f52bd112500a9c334b7246984, the direct transaction target 0xfad2a0642a44a68606c2295e69d383700643be68, and the flash-loan/orchestration contract 0xb83f991621d42d5fc4832609b125312f0a3f0d1d. The full exploit completed in one adversary-crafted transaction.
Execution flow:
1. Borrow 10,000 USDT from DODO flash loan pool 0x6098...
2. Buy NCD from Pancake pair 0x94bb...
3. Move bought NCD into aged helpers and self-transfer 1 token per helper
4. Compound minted NCD across the helper chain
5. Sell up to 5% from each helper into the pair
6. Transfer remainders to the next helper and repeat
7. Repay DODO principal and forward residual USDT to the EOA
The trace and metadata show this was permissionless. No privileged contract role, private key, or hidden state was needed. The only prerequisites were pre-aged helper addresses, public token logic, and available public liquidity.
The directly measurable loss in the paired reference asset was USDT drained from the NCD/USDT pool:
1932626839723879276033 raw units (1932.626839723879276033 USDT at 18 decimals)347970725885974436363618960 raw unitsThe impact is broader than the USDT delta. The reward system minted unbacked supply onto balances that did not age, and the anti-dump logic failed to constrain the adversary's aggregate exit path. That combination invalidated the token's intended reward-accounting and sell-throttling controls.
0xbfb9b3b8a0d3c589a02f06c516b5c7b7569739edd00f9836645080f2148aefc70xd52f125085b70f7f52bd112500a9c334b72469840x9601313572ecd84b6b42dbc3e47bc54f8177558e0x94bb269518ad17f1c10c85e600bde481d4999bff0x6098a5638d8d7e9ed2f952d35b2b67c34ec6b476/workspace/session/artifacts/collector/seed/56/0x9601313572ecd84b6b42dbc3e47bc54f8177558e/src/Contract.sol/workspace/session/artifacts/collector/seed/56/0xbfb9b3b8a0d3c589a02f06c516b5c7b7569739edd00f9836645080f2148aefc7/trace.cast.log/workspace/session/artifacts/collector/seed/56/0xbfb9b3b8a0d3c589a02f06c516b5c7b7569739edd00f9836645080f2148aefc7/balance_diff.json