# Migration writes without existence checks

Migration or follow paths that write state without first checking the record already exists

_Area: DeFi accounting and math bugs. Seen in 3 disclosed reports; latest 2025-07-21. Updated 2026-10-30._

## How it works
A migration or follow path writes new state (a follow record, a withdrawal status, an effective balance) without first confirming that the same logical record was not already created by the normal path. The normal path has a guard; the migration path does not, or the guard is keyed on a value that legacy entries never set. The assumption that breaks is "migration only runs for records the normal path has not touched." When both paths can run for the same record, the write is applied twice or overwrites, and counters, balances or status flags drift from reality.

## Where it shows up
- New mapping state introduced in an upgrade, defaulting to zero for legacy entries, so normal-path guards pass for them [E2](https://bugbountylibrary.com/r/c4%3A2023-07-lens%3AM-02.md)
- Migration functions that write counters or records without reading the existing entry first [E2](https://bugbountylibrary.com/r/c4%3A2023-07-lens%3AM-02.md)
- Legacy-status checks gated on a boundary value (batch number, epoch, version) that some records fall outside [E3](https://bugbountylibrary.com/r/c4%3A2024-03-zksync%3AM-04.md)
- Spec steps reordered for a single-pass optimization, so a stateful update with thresholds runs twice [E1](https://bugbountylibrary.com/r/cantina%3Aethereum_pectra_lighthouse%3A3.1.1.md)
- Any write that increments a count or sets a flag where the read-before-write is absent

## How to review
For every migration/follow write, find the corresponding normal-path guard and confirm the migration path enforces the same invariant: the record must not already exist (or must be in the expected prior state) before writing. Check that legacy entries cannot satisfy the normal guard by default. For boundary-gated checks, enumerate records on both sides of the boundary and confirm each is checked exactly once. For reordered spec steps, confirm stateful updates with hysteresis are idempotent or run once.

## Variants seen
- Missing already-following check in tryMigrate(), double-counting followerCount [E2](https://bugbountylibrary.com/r/c4%3A2023-07-lens%3AM-02.md)
- Legacy withdrawal check skipped for the upgrade batch, allowing a second finalization [E3](https://bugbountylibrary.com/r/c4%3A2024-03-zksync%3AM-04.md)
- Effective balance updated twice per epoch, breaking hysteresis (63 vs 64 ETH) [E1](https://bugbountylibrary.com/r/cantina%3Aethereum_pectra_lighthouse%3A3.1.1.md)

## Impact and severity
Corrupted counters and follow data [E2](https://bugbountylibrary.com/r/c4%3A2023-07-lens%3AM-02.md), double withdrawal of bridged funds [E3](https://bugbountylibrary.com/r/c4%3A2024-03-zksync%3AM-04.md), and consensus divergence that can chain-split [E1](https://bugbountylibrary.com/r/cantina%3Aethereum_pectra_lighthouse%3A3.1.1.md). Severity rises when the drift feeds consensus or fund accounting, and when the boundary window is wide or attacker-triggerable.

## Evidence
- E1: https://bugbountylibrary.com/r/cantina%3Aethereum_pectra_lighthouse%3A3.1.1.md
- E2: https://bugbountylibrary.com/r/c4%3A2023-07-lens%3AM-02.md
- E3: https://bugbountylibrary.com/r/c4%3A2024-03-zksync%3AM-04.md
