# QTNToken: cooldown bypass via skim-split contracts drains pair

QTN token's buy-time limit and fee-on-transfer logic let an attacker split funds across 40 contracts via Pair.skim to bypass the 5-minute cooldown and dump for profit.

- Payout: Real-world hack
- Severity: medium
- Program: - QTNToken
- Analysis by: DeFiHackLabs
- Disclosed: 2023-01-18
- Write-up quality: 6/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2023-01/QTN_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20230118-qtn
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:medium tech:evm tech:solidity technique:chained vuln:business-logic

**Target:** ERC20 token with Uniswap V2 pair and fee-on-transfer/cooldown logic
**Root cause:** Token enforces a per-holder buy cooldown (_timeLimitFromLastBuy 5 minutes) and fee-on-transfer accounting, but Pair.skim lets an attacker credit fresh contracts without triggering the buy path, so each new contract holds tokens free of the cooldown.
**Steps:**
1. Fork mainnet at block 16430212; wrap 2 ETH to WETH.
2. Swap WETH->QTN via Uni Router V2 (fee-on-transfer variant).
3. Warp +500s to clear _timeLimitFromLastBuy.
4. Loop 40 times: deploy QTNContract, transfer QTN.balanceOf(this)/40 to the Pair, then Pair.skim(QTNcontract) to credit the new contract.
5. Warp +500s again.
6. Call transferBack() on all 40 contracts to pull QTN back to attacker.
7. Swap all QTN->WETH via router; log WETH balance.
**How found:** BlockSec observed the on-chain txs and identified that skim() credits recipients without going through the token's buy hook, defeating the per-holder cooldown.
**Exploitation:** Attacker bought QTN, waited out the cooldown, then used Pair.skim to distribute tokens into 40 fresh contracts each bypassing the buy-time limit, waited again, consolidated via transferBack(), and sold into the pair for ~2 ETH profit.
**Insight:** When a token gates transfers/sells by per-address state (cooldown, last-buy timestamp), check whether Uniswap V2 skim()/sync() can seed fresh addresses with balance without invoking that state — each new contract becomes a clean slate.

