# Migration will be impossible in case token has vesting recipients

Doppler's Airlock assumes it holds all unsold tokens, but vesting mints tokens to the token contract, so migrate() reverts and funds are stuck.

- Payout: Contest reward
- Severity: high
- Program: Doppler
- Credited: deadrosesxyz
- Disclosed: 2025-02-18
- Write-up quality: 7/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_doppler.pdf#3.1.2
- Library page: https://bugbountylibrary.com/r/cantina%3Adoppler%3A3.1.2
- Tags: domain:web3 impact:dos impact:funds-loss kind:contest platform:cantina sev:high tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** Airlock migration contract / token factory with vesting
**Root cause:** Token creation mints vested tokens to the token contract itself, but Airlock.migrate assumes it holds totalSupply - numTokensToSell and transfers that amount, exceeding its actual balance.
**Steps:**
1. Create token with vesting recipients (e.g. users 1337/1338, 1e25 each) via tokenFactoryData
2. Set numTokensToSell = initialSupply - vestedTokens
3. Buy tokens via router until DEFAULT_ENDING_TIME
4. Warp to DEFAULT_ENDING_TIME and call `airlock.migrate(asset)`
5. Observe revert `TRANSFER_FAILED` from safeTransfer of total0/total1 exceeding balance
**How found:** Code review comparing token minting logic (vestedTokens minted to address(this)) against migrate()'s assumption that Airlock holds totalSupply - numTokensToSell.
**Exploitation:** Any creator specifying vesting recipients causes migrate() to revert on safeTransfer to liquidityMigrator; migration is permanently bricked, tokens worthless, numeraire stuck.
**Insight:** When auditing token launch/migration contracts, reconcile every mint destination against balance assumptions in later transfer paths — vesting/airdrop mints to the token contract break 'contract holds all unsold supply' invariants.
