Crypto exploits

SKYDAO exploit analysis

SKYDAO's sell path charged a 35% tax, ran a controller-driven controlburn + premature sync() of the SKYDAO/USDT PancakeSwap V2 pair's SKY reserve, and only afterwards credited the seller's net SKY, so a direct pair.swap against the freshly-synced ~0 reserve drained the pair's entire 183,482.88 USDT for 59,914.12 USDT.

David

Security ·

Reported loss
59,914.12 USDT net to the attacker; pair lost its entire 183,482.88 USDT side, per DeFiHackLabs
Occurred
Logged
Chain
BNB Chain

Logged as “2026-09-30: Sell Path Controller Burns The Pairs Skydao And Syncs Befo”.

Analysis3 min read

SKYDAO's _transfer treats a transfer whose recipient is the pair as a sell. On a sell it charges 35% tax, hands the gross amount to its pool controller, lets the controller sell the tax and call controlburn(gross) which burns the pair's full SKY balance to 0x..dEaD and calls pair.sync() - all BEFORE the seller's net SKY is credited back to the pair. The pair's stored reserve1 (SKY) is therefore latched at ~0 right before swap() checks its K invariant. The seller's net 65% SKY lands after the sync, so the pair's real SKY balance is now far above its freshly-synced stored reserve. On 30 September 2026, a single attacker transaction on BNB Chain used one PancakeSwap V2 swap() against the stale reserve to drain the pair's entire USDT side for 59,914.12 USDT net, with the pair losing its full 183,482.88 USDT side.

The vulnerable contracts are the verified SKYDAO token at 0x7eBa33c7a0e555D115277BA4Af04DFbB4F4Fa70c and its unverified pool controller at 0xEe5fDff6364dDe0A3C66dD38A4303cDd3D10730c. The pair at 0x096e08ddA1E18625fFdfBae4BB65a414Aa7eC2c8 is a stock PancakeSwap V2 pair; nothing about the pair is non-standard. The bug is that the sell path in SKYDAO runs pair.sync() before the seller's net tokens have been credited, so the pair's stored reserves temporarily diverge from its real balances.

The on-chain sequence

The exploit is a single contract-creation transaction (0x8e30166...e440eb, block 124,921,242, BNB Chain). The attacker's constructor ran the entire exploit. It opened with a fee-free Moolah flash loan of 2,656,932.60 USDT (all the USDT Moolah held), then:

  • Bought SKYDAO through the PancakeSwap V2 router with 435,367.43 USDT, taking the 35% buy tax (~3.9015e27 net SKY remained).
  • Transferred the entire SKY balance to the pair (SKY.transfer(pair, balance)). The pair was now the to of a SKYDAO transfer, so SKYDAO's _transfer entered its sell branch: it taxed 35% to the controller, the controller sold the taxed SKY, called controlburn(gross) which burned the pair's existing SKY to 0x..dEaD and called pair.sync(). pair.reserve1 (SKY) was now near zero in storage, but the seller's net 65% had not yet been credited to the pair's actual balance.
  • Called pair.swap(reserve0 - 1, 0, self, "") directly. The pair checked K = reserve0 * reserve1 against the stale ~0 SKY reserve and authorized paying out essentially all of its USDT side to the caller.
  • Pulled ~495,281.56 USDT out of the pair in that single swap, repaying the Moolah flash, and keeping 59,914.12 USDT as the attacker's net.

Per-USD T math reconciles exactly: pair USDT in = 435,367.43 (buy) + 92,676.57 (controller's tax-sell refill) = 528,044.00; pair USDT out = 216,245.33 (tax sell) + 495,281.56 (final drain) = 711,486.89; net -183,482.88 = the pair's entire starting USDT reserve, all of which left the pair in a single transaction.

The PoC reproduces the sequence against a BNB Chain fork at block 124,921,241, replays the typed calls (no calldata copy-paste), and asserts both the attacker's 59,914.12 USDT net and the pair's 183,482.88 USDT loss to 1% tolerance. The PoC must run with --evm-version cancun because Moolah's flash uses transient storage; the --evm-version shanghai default in the repo's foundry.toml will fail on that step.

What was missing

Two clean fixes. Either the controller's controlburn should NOT call pair.sync() (so the pair's stored reserves stay aligned with the real balances after the tax burn); or the seller's net credit to the pair should happen BEFORE the controller is allowed to sync. The first is smaller and correct: the pair already re-syncs on every swap when needed, and the explicit sync() is only useful when something OTHER than a swap has changed the balances, which here it has not. The seller's taxAmount = (amount * 35) / 100 math itself is fine; only the ordering is wrong.

What it does NOT involve

No private-key compromise, no privileged signer, no governance vote. The 35% sell tax is the only gating; the controller path it opens is the bug. A buy to an ordinary address goes through once totalHolder >= 200000, which the trigger transfer itself satisfies.

Sources read (3)

Other exploits

Query every incident yourself — free, no signup

Updated 11 Nov 2026. Figures are as reported by the sources listed above and are not independently verified.