Foundational principles of how smart contracts persistently store data on the Ethereum Virtual Machine.
Understanding EVM Storage Collisions
Core Concepts of EVM Storage
Storage Slots
The storage slot is the fundamental 256-bit (32-byte) unit of storage in the EVM. Each smart contract has a contiguous, sparse array of 2^256 slots, indexed from 0.
- Data is stored in slots via
SSTOREand retrieved viaSLOAD. - State variables are mapped to specific slots based on declaration order and type.
- Understanding slot mapping is critical for predicting storage layout and avoiding collisions.
State Variable Layout
The state variable layout defines how high-level Solidity variables are packed into 32-byte storage slots.
- Static-sized types (like
uint256) occupy one full slot. - Smaller types (like
uint8) may be packed together in a single slot if contiguous. - Dynamic arrays and mappings have a more complex, keccak256-based derivation scheme.
- Incorrect assumptions about layout are a primary cause of storage collisions.
Storage Collisions
A storage collision occurs when two distinct pieces of data are unintentionally mapped to the same storage slot, causing one to overwrite the other.
- Can happen during contract upgrades if new variables are appended incorrectly.
- Can occur in proxy patterns or delegatecall contexts where storage pointers mismatch.
- Results in critical, often silent, data corruption and security vulnerabilities.
Inheritance & Storage
Inheritance in Solidity affects storage layout by concatenating the storage variables of parent contracts.
- Variables from the most base contract occupy the lowest-numbered slots.
- The order of inheritance in the
isclause determines the layout of parent contracts. - Developers must account for this linearized layout to prevent collisions when extending contracts.
Mappings & Dynamic Arrays
Mappings and dynamic arrays do not store their data in linearly allocated slots. Their storage locations are computed using cryptographic hashes.
- A mapping's data for key
kis stored atkeccak256(h(k) . p)wherepis the slot of the mapping. - The slot for a dynamic array holds its length; element
iis atkeccak256(p) + i. - This non-linear addressing is a common source of confusion and collision risk.
Gas Costs & Optimization
Gas costs for storage operations are significant, incentivizing efficient layout and access patterns.
- An
SSTOREto a zero-valued slot costs 22,100 gas; changing a non-zero value costs 5,000 gas. - Packing multiple small variables into one slot reduces deployment and write costs.
- Understanding these costs is essential for writing gas-efficient contracts and analyzing transaction patterns.
How Storage Collisions Occur
Process overview of storage slot mapping conflicts in the EVM.
Understand EVM Storage Layout
Learn how state variables are mapped to 32-byte storage slots.
Detailed Instructions
The EVM stores contract state in a key-value store where each key is a 32-byte storage slot and each value is a 32-byte word. Variables are assigned slots sequentially based on their declaration order and size. For example, a uint256 consumes one full slot, while multiple smaller variables (like uint128) can be packed into a single slot if declared consecutively. The slot index for a simple variable is its zero-based position in the contract. This deterministic mapping is crucial; if two different contracts or logic paths calculate the same slot for different data, a collision occurs.
- Sub-step 1: Declare state variables in a sample contract.
- Sub-step 2: Compile the contract and inspect the storage layout via
solc --storage-layout. - Sub-step 3: Note the assigned slot number for each variable.
solidity// Example showing sequential slot assignment contract StorageExample { uint256 public a; // Slot 0 uint128 public b; // Slot 1 (first half) uint128 public c; // Slot 1 (second half) - packed with b uint256 public d; // Slot 2 }
Tip: Use
web3.eth.getStorageAt(contractAddress, slotIndex)to inspect live contract storage.
Identify Mappings and Dynamic Arrays
Examine how complex types use keccak256 hashing for slot derivation.
Detailed Instructions
For dynamic types like mapping and dynamic arrays, storage slots are not sequential. Their data is stored starting at a slot computed via a keccak256 hash. For a mapping mapping(key => value), the slot for value is keccak256(abi.encode(key, baseSlot)), where baseSlot is the slot index of the mapping variable itself. For a dynamic array, the array data starts at keccak256(abi.encode(baseSlot)), and its length is stored at baseSlot. This hashing can lead to probabilistic collisions if the computed hash for one data structure overlaps with the slot range of another.
- Sub-step 1: Write a contract with a mapping and a dynamic array.
- Sub-step 2: Manually calculate the first storage slot for a mapping entry using a known key.
- Sub-step 3: Calculate the starting slot for the array's data.
soliditycontract ComplexStorage { mapping(address => uint256) public balances; // Mapping at slot 0 uint256[] public dataArray; // Array length at slot 1 // balances[addr] slot = keccak256(abi.encode(addr, 0)) // dataArray[0] slot = keccak256(abi.encode(1)) }
Tip: Be aware that hashed slots can theoretically collide with statically assigned slots if the 256-bit hash falls within a small, used range.
Analyze Inheritance and Proxy Patterns
See how parent contracts and delegatecall proxies create collision risks.
Detailed Instructions
Inheritance concatenates storage layouts of parent contracts. Variables from the most base contract occupy the lowest slots. A collision occurs if a parent and child contract, or two parent contracts, are compiled separately with mismatched layouts. Proxy patterns using delegatecall are a major risk area. The logic contract's storage layout must be exactly compatible with the proxy's storage. If a proxy has its own state variable at slot 0 and an upgraded logic contract expects a different variable at slot 0, a catastrophic collision will overwrite critical data.
- Sub-step 1: Create a parent contract with a variable at slot 0 and a child that adds another.
- Sub-step 2: Deploy and check the child's storage layout to see the concatenation.
- Sub-step 3: Write a simple proxy contract that uses
delegatecallto a logic contract with a different variable order.
solidity// Parent contract contract Base { uint256 public baseVar; // Slot 0 in Base, Slot 0 in Final } // Child contract - layout is Base's slots, then its own. contract Final is Base { uint256 public finalVar; // Slot 1 in Final } // A proxy's storage at slot 0 might clash with the logic contract's slot 0.
Tip: Use unstructured storage proxies or EIP-1967 to isolate proxy administration slots in a dedicated, hashed location to avoid collisions.
Trigger a Manual Storage Overwrite
Demonstrate a collision by writing to a calculated storage slot.
Detailed Instructions
The most direct proof is to force an overwrite. Using inline assembly or a carefully crafted external call, you can write to a specific storage slot. If that slot is already used by another variable, the original data will be corrupted. For instance, if a contract has a public owner at slot 0, an attacker could exploit a function that writes to a mapping at a calculated slot of 0. This requires the attacker to find a key k such that keccak256(abi.encode(k, mappingBaseSlot)) == 0, which is computationally infeasible but illustrates the principle. More realistic are selector collisions in upgradeable contracts.
- Sub-step 1: Deploy a contract with an
address ownerat slot 0. - Sub-step 2: Write a function that uses
sstore(slot, value)to write to slot 0. - Sub-step 3: Call the function and then check the
ownervariable to confirm it changed.
soliditycontract Vulnerable { address public owner = 0x1234...; // Stored at slot 0 function overwriteSlot(uint256 slot, address newValue) public { assembly { sstore(slot, newValue) } } // Calling overwriteSlot(0, attackerAddress) will change the owner. }
Tip: This demonstrates why uncontrolled write access to arbitrary slots is extremely dangerous and should be prevented.
Review Historical Exploit Cases
Study real-world incidents caused by storage collisions.
Detailed Instructions
Analyzing past exploits provides concrete understanding. A classic case is the Parity Multi-Sig Wallet hack (2017), where an uninitialized proxy contract had a public function that became callable due to a storage layout mismatch, allowing an attacker to become the owner. Another example is DELEGATECALL vulnerabilities in custom proxy implementations where the logic contract's first variable collided with the proxy's stored logic address. Review the transaction hashes and contract code from these events to trace the slot miscalculation. For instance, in the Parity case, the walletLibrary address and owner address occupied conflicting slots after a specific function call.
- Sub-step 1: Find the Parity wallet contract addresses (e.g., 0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4) on Etherscan.
- Sub-step 2: Examine the transaction that triggered the exploit, noting the storage changes.
- Sub-step 3: Recreate the flawed storage layout in a test environment to verify the collision.
Tip: Always use established, audited upgradeability standards like OpenZeppelin's Upgrades Plugins, which include storage layout compatibility checks.
Historical Incidents and Case Studies
Analysis of notable EVM storage collision vulnerabilities and their outcomes.
| Incident / Contract | Vulnerability Type | Financial Impact | Root Cause | Mitigation Applied |
|---|---|---|---|---|
Parity Multi-Sig Wallet (2017) | Delegatecall Proxy Collision | $30M+ frozen | Library self-destruct via delegatecall in fallback | Wallet logic permanently frozen; funds unrecoverable |
Uniswap V1 (2018) - Initial Bug | Storage Layout Mismatch | Potential loss prevented | Incorrect storage variable ordering in migration | Contract redeployment with corrected storage layout |
MakerDAO MCD Migration (2019) | Storage Slot Collision Risk | System-wide risk averted | Potential collision between legacy and new system storage | Comprehensive storage layout audit and dedicated migration module |
SushiSwap MasterChef V1 | Reward Calculation Corruption | Incorrect reward distribution | Collision between reward and user info structs in mapping | Emergency migration to MasterChef V2 |
Various Proxy Patterns | Uninitialized Proxy Storage | Variable hijacking & logic manipulation | Proxy storage slots overlapping with implementation slots | Use of EIP-1967 standard storage slots |
Aragon Network Vote Proxy | Governance Storage Overwrite | Voting power manipulation | Collision between proxy admin and voting data storage | Implementation of transparent proxy pattern (EIP-1967) |
Early ERC-721 Enumerable | Token Index Corruption | Broken enumeration & metadata | Collision between | Adoption of standardized, non-overlapping storage layouts |
Prevention and Mitigation Strategies
Core Vulnerability Explained
An EVM storage collision occurs when two different variables in a smart contract unintentionally occupy the same storage slot. This happens due to the deterministic way the EVM calculates storage positions based on variable declaration order and types within contracts and their inheritance hierarchy. A collision can cause a variable's value to be overwritten by another, leading to critical bugs, fund loss, or contract hijacking.
Key Attack Vectors
- Inheritance Misalignment: A parent contract upgrade that changes variable order can collide with child contract storage.
- Unstructured Storage: Using low-level
delegatecallwith libraries or proxy patterns without proper slot management. - Packed Variables: Overwriting parts of a packed
bytes32slot when updating adjacent variables.
Real-World Impact
In the 2017 Parity multi-sig wallet hack, a vulnerability related to library initialization led to a storage collision that allowed an attacker to become the owner of the library contract, subsequently freezing over 500,000 ETH.
Auditing for Storage Vulnerabilities
A systematic process for identifying and mitigating storage collision risks in smart contracts.
Map the Contract's Storage Layout
Analyze all state variable declarations and their positions.
Detailed Instructions
Begin by creating a complete map of the contract's storage slots. State variables are stored sequentially starting from slot 0, with packing rules applying for variables under 32 bytes. For each contract and its inherited parents, list every variable, its type, and its calculated storage slot.
- Sub-step 1: Use
solc --storage-layoutto generate a formal layout JSON for the compiled contract. - Sub-step 2: Manually verify the layout, paying special attention to structs and arrays which have complex slot calculation rules.
- Sub-step 3: Cross-reference this map against any delegatecall targets or proxy implementations to identify potential slot overlaps.
solidity// Example of storage layout for a simple contract contract Vulnerable { address owner; // slot 0 uint256 balance; // slot 1 mapping(address => uint256) public allowances; // slot 2 (keccak256 hash) }
Tip: For upgradeable proxies, compare the storage layout of the logic contract with the proxy's reserved slots to prevent collisions during upgrades.
Identify Dynamic Data Structures
Locate mappings, dynamic arrays, and nested structs that use keccak256 hashing.
Detailed Instructions
Dynamic types like mappings and dynamic arrays do not store their data in linearly allocated slots. Instead, their data is stored at a keccak256 hash of the concatenated slot key and index. This is a primary source of non-obvious collisions.
- Sub-step 1: List all
mappingandbytes[]oruint256[]declarations. Note their base storage slot. - Sub-step 2: Calculate the starting storage slot for a mapping's first entry:
keccak256(abi.encodePacked(key, baseSlot)). - Sub-step 3: For dynamic arrays, the length is stored at the base slot, and elements start at
keccak256(abi.encodePacked(baseSlot)).
solidity// Calculating a mapping slot contract Example { mapping(address => uint256) public scores; // Base slot: 0 // score for address 0x123... is stored at: // bytes32 slot = keccak256(abi.encodePacked(uint256(0x123...), uint256(0))); }
Tip: Use Foundry's
vm.load()in a test to directly read calculated slots and verify your assumptions about storage locations.
Analyze Delegatecall and Proxy Patterns
Check for storage collisions in upgradeable contracts and modular designs.
Detailed Instructions
Delegatecall executes code from another contract in the context of the caller's storage. If storage layouts are misaligned, the called contract will read/write to incorrect slots, causing critical vulnerabilities.
- Sub-step 1: For any
delegatecallinstruction, audit the storage layout of both the caller and the target contract. They must be identical or use explicit storage pointers. - Sub-step 2: In UUPS or Transparent Proxy patterns, verify the logic contract's layout does not overwrite the proxy's admin or implementation pointer slots (e.g.,
_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103). - Sub-step 3: Use structured storage libraries like OpenZeppelin's
StorageSlotto mitigate collision risks by using pseudorandom slot generation.
solidity// Dangerous delegatecall with mismatched layout contract Library { uint256 public storedData; // Intends to write to slot 0 } contract Proxy { address public implementation; // Occupies slot 0 function delegate() public { (bool success, ) = implementation.delegatecall(...); // Library will overwrite the implementation address! } }
Tip: Explicitly define storage gaps in base contracts reserved for future upgrades to prevent child contract collisions.
Review Inheritance and Contract Ordering
Verify storage slot assignments across linearized inheritance chains.
Detailed Instructions
Solidity uses C3 Linearization to determine the order of inherited contracts. State variables are allocated slots based on this order, not the order of declaration in a single file. A parent contract's variables are placed before the child's.
- Sub-step 1: Generate the inheritance graph and linearized order using
solcor tools likeslither. Command:slither . --print inheritance-graph. - Sub-step 2: Manually trace slot allocation through the linearized chain, starting from the most base contract.
- Sub-step 3: Check for collisions where a child contract's variable unintentionally occupies the same slot as an unused but reserved slot in a parent (e.g., a gap left for future use).
soliditycontract BaseA { uint256 baseVar; } // slot 0 contract BaseB { address baseAddr; } // slot 1 contract Child is BaseA, BaseB { uint256 childVar; // slot 2 } // Linearized order: BaseA, BaseB, Child // Slots: 0=baseVar, 1=baseAddr, 2=childVar
Tip: Changing the order of
isdeclarations in a contract will reshuffle the entire storage layout, a common source of upgrade errors.
Simulate and Test Edge Cases
Use static analysis and fuzzing to uncover hidden collision paths.
Detailed Instructions
Theoretical analysis must be validated with concrete execution. Use a combination of static analysis tools and dynamic testing to probe the storage model.
- Sub-step 1: Run static analysis with Slither or Mythril to flag potential storage collisions automatically. Command:
slither . --detect storage-array. - Sub-step 2: Write differential fuzzing tests using Foundry. Deploy two contract versions with minor layout changes and compare storage slots after identical transaction sequences.
- Sub-step 3: Craft edge case transactions that write to a calculated mapping slot, then attempt to read the value as a different variable type from the same slot to test for type confusion.
solidity// Foundry test to verify a slot is unused function test_StorageSlotIsEmpty() public { VulnerableContract v = new VulnerableContract(); // Read a slot that should be reserved for future use bytes32 value = vm.load(address(v), bytes32(uint256(100))); assertEq(value, 0, "Reserved storage slot is not empty"); }
Tip: Integrate storage collision checks into your CI/CD pipeline by asserting the hash of the storage layout JSON matches a known-good snapshot after each change.
Frequently Asked Questions
Further Reading and Tools
Ready to Start Building?
Let's bring your Web3 vision to life.
From concept to deployment, ChainScore helps you architect, build, and scale secure blockchain solutions.