# Checkpoints are incorrectly cleared during transferFrom

In VotingEscrow.transferFrom, moveTokenDelegates clears source checkpoints because ownerOf returns address(0) after _removeTokenFrom, corrupting delegation history.

- Payout: Contest reward
- Severity: medium
- Program: Blackhole
- Credited: rashmor
- Disclosed: 2025-07-03
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2025-05-blackhole#m-14
- Library page: https://bugbountylibrary.com/r/c4%3A2025-05-blackhole%3AM-14
- Tags: domain:web3 impact:integrity kind:contest platform:code4rena sev:medium tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** VotingEscrow / VotingDelegationLib.moveTokenDelegates
**Root cause:** transferFrom calls _removeTokenFrom (setting owner to address(0)) before moveTokenDelegates; the cleanup loop then sees ownerOfFn(tId) != srcRep for the transferred token and pops entries in-place without incrementing i, wiping checkpoints.
**Steps:**
1. Set up a checkpoint for srcRep with tokenIds [1,2,3].
2. Call transferFrom for token 1; _removeTokenFrom sets ownerOf(1)=address(0).
3. moveTokenDelegates runs with _isCheckpointInNewBlock=false and iterates srcRepNew.
4. ownerOfFn(1)!=srcRep triggers `srcRepNew[i]=srcRepNew[length-1]; srcRepNew.pop(); length--;` without i++.
5. Repeat for token 2; remaining checkpoint for srcRep loses token 3 as well.
**How found:** Code-reading of moveTokenDelegates cleanup loop combined with tracing transferFrom ordering: noticing _removeTokenFrom zeroes owner before delegation update, making the inequality always true for the moved token.
**Insight:** When auditing delegation/checkpoint libraries, check the ordering of ownership mutation vs. delegation bookkeeping; any ownerOf lookup after burn/zeroing will misclassify the token and in-place array compaction without index increment can silently drop unrelated entries.
