Root Cause
SKYDAO's tokenomics include a 35% sell tax routed to a PoolController. When a sell transfer targets the PancakeSwap V2 pair (to == pair), the controller:
- Sells the tax portion into the pair.
- Burns the entire SKY balance currently held by the pair.
- Calls
pair.sync()to checkpoint the pair's reserves.
This sequence happens before the original transfer's net 65% is credited to the pair. At the moment sync() runs, the pair's recorded SKY reserve drops to near-zero. When the net tokens eventually arrive, the pair's actual balance is far higher than its recorded reserve0, enabling a direct swap() call to drain USDT against the stale (under-reported) reserve at a massively favourable rate.
// Simplified controller logic (pseudocode)
function controlburn(address pair) internal {
uint256 pairBalance = ISKY(token).balanceOf(pair);
ISKY(token).burn(pair, pairBalance); // burns pair's entire SKY balance
IPair(pair).sync(); // locks in near-zero SKY reserve
// *** transfer's net tokens haven't arrived yet ***
}
Attack Steps
| Step | Action | Amount |
|---|---|---|
| 1 | Flash borrow USDT | 2,656,932.60 USDT from Moolah |
| 2 | Buy SKY via PancakeSwap V2 | Spend 435,367.43 USDT (35% buy tax applied) |
| 3 | Initiate sell to pair | Triggers controller: tax-sell → burn pair SKY → sync() while pair SKY ≈ 0 |
| 4 | Direct pair.swap() | Drain USDT against stale near-zero SKY reserve |
| 5 | Repay flash loan | Return 2,656,932.60 USDT |
| 6 | Net profit | ~59,914.12 USDT; pair lost 183,482.88 USDT |
Impact
- $183,483 total drained from the SKYDAO/USDT PancakeSwap V2 pair.
- Attacker net gain: ~59,914 USDT after repaying the flash loan.
- Chain: BNB Smart Chain. Date: September 30, 2026.
- No admin functions or privileged keys required; the attack path is fully permissionless.
Lessons for Auditors
- Never call
sync()mid-transfer:pair.sync()checkpoints reserves; calling it before a transfer completes permanently corrupts reserve accounting for that transaction. - Deflationary tax + AMM pairs: Any contract that burns from a pair balance and then calls
sync()(orskim()) creates a window where reserves diverge from balances, enabling sandwich or drain attacks. - Test the full sell path atomically: The vulnerable sequence requires tracing through the transfer hook → controller → back to the pair. Standard unit tests on individual functions miss this cross-contract ordering.
- Flash-loan amplification: Even a small reserve discrepancy can be amplified to a full pair drain when flash loans allow arbitrary capital for the buy step.