# tryMigrate() doesn’t ensure that followerProfileId isn’t already following

Lens FollowNFT.tryMigrate() lacks an already-following check, letting pre-V2 followers call follow() then migrate, double-counting followerCount and corrupting follow data.

- Payout: Audit contest
- Severity: medium
- Program: Lens Protocol
- Found by: MiloTruck
- Disclosed: 2023-12-11
- Write-up quality: 7/10
- Original: https://code4rena.com/reports/2023-07-lens#m-02
- Library page: https://bugbountylibrary.com/r/c4%3A2023-07-lens%3AM-02
- Tags: area:web3 domain:web3 impact:integrity kind:contest platform:code4rena sev:medium tech:evm tech:solidity technique:race-window technique:source-review vuln:accounting-error vuln:business-logic

**Target:** FollowNFT.sol migration function on Lens Hub
**Root cause:** tryMigrate() never checks _followTokenIdByFollowerProfileId[followerProfileId] before writing, so a follower who already called follow() post-upgrade can still migrate their old V1 follow token, incrementing _followerCount twice and leaving stale _followDataByFollowTokenId entries.
**Steps:**
1. Deploy target's FollowNFT and simulate a V1 follow via helper that increments _lastFollowTokenId and mints a follow NFT to Alice.
2. After V2 upgrade, Alice calls `hub.follow(aliceProfileId, [targetProfileId], [0], [''])` — succeeds because _followTokenIdByFollowerProfileId is 0.
3. Alice calls `hub.batchMigrateFollows([aliceProfileId],[targetProfileId],[oldFollowTokenId])` — tryMigrate does not revert.
4. Observe getFollowerCount()==2 while only Alice follows; getFollowTokenId(aliceProfileId) points to oldFollowTokenId while new token's followerProfileId is Alice.
5. Wrap both tokens and call `followNFT.removeFollower(followTokenId)` then `removeFollower(oldFollowTokenId)` — both emit Unfollowed despite isFollowing()==false.
**How found:** Noticed _followTokenIdByFollowerProfileId is a new V2 state variable defaulting to 0 for legacy followers, so follow()'s AlreadyFollowing guard is bypassable before migration; then checked tryMigrate() and found no symmetric guard, confirmed via Foundry PoC.
**Exploitation:** A pre-V2 follower front-runs the protocol's migration transaction (or holds profile and follow NFT at different addresses so tryMigrate returns early) by calling follow() first, then migrates the old token; follower counts inflate and stale follow data lets them unfollow twice or invoke follower-gated functions when not actually following.
**Escalation:** Corrupted _followDataByFollowTokenId may allow followers to call functions gated on follow status when they should not be permitted.
**Insight:** When a contract upgrade introduces new mapping-based state, audit every function that reads/writes it for missing guards on legacy (zero-valued) entries — migration paths and normal paths must cross-check each other's invariants.

