# Upgradeable storage layout collision

New or shifted state variables in upgradeable contracts overwriting live slots or leaving config uninitialized

_Area: Smart contract access control and control flow. Seen in 5 disclosed reports; latest 2025-03-30. Updated 2026-10-23._

## How it works
Upgradeable contracts keep state at fixed storage slots. The proxy reads and writes slots by position, not by name. If a new implementation declares a variable before existing ones, or a parent mixin lacks a trailing gap, every later variable shifts to a different slot. Old data is then read as the wrong variable, and writes land on live state. The same break happens when a migration copies only part of the old state, leaving new slots at zero.

## Where it shows up
- A mapping or variable inserted mid-layout between already-occupied slots [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-perennial-v2-update-3%3AM-8.md)
- Inherited mixin contracts that define storage but no trailing `__gap` [E4](https://bugbountylibrary.com/r/sherlock%3A2022-09-notional%3AM-16.md)
- Upgrade/migration loops that copy some per-chain config but skip others [E3](https://bugbountylibrary.com/r/c4%3A2023-11-zetachain%3AM-28.md)
- Critical addresses or roles stored in low slots reachable by user-influenced writes [E1](https://bugbountylibrary.com/r/dhl%3A20250330-leveragesir.md)
- New implementation whose storage diff against the deployed one is not checked

## How to review
The invariant: for every slot, the deployed and new layouts must agree, and every state variable the old contract exposed must be re-initialized after upgrade.
- Diff the full storage layout (names, types, order, mapping positions, immutables vs slots) between deployed and new implementation; any insertion before existing vars is unsafe [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-perennial-v2-update-3%3AM-8.md)
- Walk the whole inheritance chain; every parent with storage needs a trailing `__gap` sized to leave room [E4](https://bugbountylibrary.com/r/sherlock%3A2022-09-notional%3AM-16.md)
- Enumerate every state variable in the old contract and confirm the migration sets each one [E3](https://bugbountylibrary.com/r/c4%3A2023-11-zetachain%3AM-28.md)
- For low-slot addresses, check whether any user-controlled write (mint amount, balance, allowance) can reach that slot [E1](https://bugbountylibrary.com/r/dhl%3A20250330-leveragesir.md)

## Variants seen
- Mapping inserted mid-layout corrupting operators, markets, referral fees [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-perennial-v2-update-3%3AM-8.md)
- Missing `__gap` in mixins so future additions corrupt child storage [E4](https://bugbountylibrary.com/r/sherlock%3A2022-09-notional%3AM-16.md)
- Migration omitting `gasPriceByChainId`, leaving it zero [E3](https://bugbountylibrary.com/r/c4%3A2023-11-zetachain%3AM-28.md)
- Low-slot address overwritten via colliding token mapping, spoofing a callback [E1](https://bugbountylibrary.com/r/dhl%3A20250330-leveragesir.md)

## Impact and severity
High: corrupted state froze assets [E4](https://bugbountylibrary.com/r/sherlock%3A2022-09-notional%3AM-16.md), shifted slots broke core config [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-perennial-v2-update-3%3AM-8.md), zero gas price halted withdrawals [E3](https://bugbountylibrary.com/r/c4%3A2023-11-zetachain%3AM-28.md), and a spoofed callback drained multiple tokens [E1](https://bugbountylibrary.com/r/dhl%3A20250330-leveragesir.md). Severity rises when the corrupted slot gates fund movement or callback trust.

## Evidence
- E1: https://bugbountylibrary.com/r/dhl%3A20250330-leveragesir.md
- E2: https://bugbountylibrary.com/r/sherlock%3A2024-08-perennial-v2-update-3%3AM-8.md
- E3: https://bugbountylibrary.com/r/c4%3A2023-11-zetachain%3AM-28.md
- E4: https://bugbountylibrary.com/r/sherlock%3A2022-09-notional%3AM-16.md
