# FHToken: premature sync() in sell tax drains USDT pair

FHToken's sell path burned pool FH and called pair.sync() before crediting the seller, shrinking reserves so PancakeSwap overpaid USDT on each sell.

- Payout: Real-world hack
- Severity: critical
- Program: FHToken
- Credited: DeFiHackLabs
- Disclosed: 2026-08-25
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-08/FHToken_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260825-fhtoken
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained technique:source-review vuln:business-logic vuln:flash-loan vuln:oracle-manipulation

**Target:** BEP-20 token sell-tax logic and PancakeSwap V2 FH/USDT pair
**Root cause:** In FHToken._transfer's sell branch, the 80% burn and 20% treasury transfer are taken from the pair's own FH balance and pair.sync() is called before the seller's net FH is credited to the pair, latching reserve1 far below the true post-transfer balance.
**Steps:**
1. Take fee-free flash loan of 25,999.35 USDT from Moolah (0x8F73b65B...) via flashLoan with onMoolahFlashLoan callback
2. Approve PancakeRouterV2 and FH
3. Buy FH with USDT via `swapExactTokensForTokensSupportingFeeOnTransferTokens` routing output to whitelisted SwapRouter 0x1b81D678 (buy path is recipient-whitelist gated)
4. Pull FH back with `SWEEP_ROUTER.sweepToken(FH, 0, attacker)`
5. Sell all FH into the pair via router; sell branch burns pool FH and calls sync() before crediting, so getAmountOut uses shrunken reserve1 and overpays USDT
6. Repeat 25 buy/sell cycles with decreasing buy sizes to compound pool USDT inflation
7. Repay flash loan via approve to Moolah; net ~19,999.02 USDT
**How found:** Reconstruction from verified FHToken source and on-chain tx: noticed sell branch removes burn/treasury from the pair's own balance and calls sync() before the seller's net FH is credited, and that the buy path's recipient whitelist could be bypassed via a hardcoded-whitelisted router's public sweepToken().
**Exploitation:** Fee-free flash loan funds the buy leg; buy output is routed through a constructor-whitelisted SwapRouter and swept back with its public sweepToken() to bypass the buy whitelist. Each sell triggers the premature sync() inside _transfer, so the router prices the sell against an artificially low FH reserve and pays excess USDT. Looping 25 cycles drains ~$20K.
**Escalation:** Amplification: each buy inflates the pool's USDT side, so the following sell's overpay compounds across 25 cycles.
**Insight:** When a token's transfer hook calls pair.sync() inside the transfer, check ordering: if sync() runs before the incoming amount is credited (or after removing tokens from the pair's own balance), reserves desync from real balances and any AMM pricing on that pair overpays. Also check whether a buy-recipient whitelist can be satisfied by routing output to a whitelisted router that exposes a public sweep function.
