Unstructured Storage

Storage pattern using keccak256 hash-derived slots to prevent collisions between proxy and implementation contract variables.

Unstructured storage is a technique used in proxy contracts to store critical variables (like implementation address and admin) in pseudo-random storage slots, preventing accidental collisions with implementation contract storage. Instead of using sequential storage slots, variables are stored at slots derived from keccak256 hashes.

The Storage Collision Problem

Traditional Sequential Storage

Solidity normally assigns storage slots sequentially:

1contract SequentialStorage {
2 address public owner; // Slot 0
3 uint256 public balance; // Slot 1
4 bool public paused; // Slot 2
5 mapping(address => uint256) balances; // Slot 3
6}

Collision in Proxy Patterns

When proxy and implementation use sequential storage, collisions occur:

1// Proxy contract
2contract VulnerableProxy {
3 address public implementation; // Slot 0
4 address public admin; // Slot 1
5}
6
7// Implementation contract
8contract Implementation {
9 address public owner; // Slot 0 - COLLISION with implementation!
10 uint256 public totalSupply; // Slot 1 - COLLISION with admin!
11}
12
13// When proxy delegates to implementation:
14// - Implementation thinks owner is at slot 0
15// - But slot 0 contains implementation address
16// - Implementation thinks totalSupply is at slot 1
17// - But slot 1 contains admin address
18// Result: Data corruption and unpredictable behavior

Unstructured Storage Solution

Hash-Based Slot Calculation

Use keccak256 to derive storage slots from unique strings:

1contract UnstructuredProxy {
2 // EIP-1967 standard slots
3 bytes32 private constant IMPLEMENTATION_SLOT =
4 keccak256("eip1967.proxy.implementation");
5 bytes32 private constant ADMIN_SLOT =
6 keccak256("eip1967.proxy.admin");
7
8 function _getImplementation() internal view returns (address impl) {
9 bytes32 slot = IMPLEMENTATION_SLOT;
10 assembly {
11 impl := sload(slot)
12 }
13 }
14
15 function _setImplementation(address newImplementation) internal {
16 bytes32 slot = IMPLEMENTATION_SLOT;
17 assembly {
18 sstore(slot, newImplementation)
19 }
20 }
21
22 function _getAdmin() internal view returns (address admin) {
23 bytes32 slot = ADMIN_SLOT;
24 assembly {
25 admin := sload(slot)
26 }
27 }
28}

Slot Calculation Example

1// Calculate actual slot values:
2IMPLEMENTATION_SLOT = keccak256("eip1967.proxy.implementation")
3 = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
4
5ADMIN_SLOT = keccak256("eip1967.proxy.admin")
6 = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

These slots are effectively random and extremely unlikely to collide with implementation storage.

EIP-1967 Standard

Standard Slot Definitions

EIP-1967 defines standard unstructured storage slots for proxy patterns:

1// Implementation slot
2bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
3= 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
4
5// Admin slot
6bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
7= 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
8
9// Beacon slot (for beacon proxy pattern)
10bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)
11= 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50

Why Subtract 1?

Subtracting 1 ensures the slot is never 0, which has special meaning in Solidity storage (uninitialized state).

Implementation Patterns

Basic Unstructured Storage

1contract UnstructuredStorageProxy {
2 bytes32 private constant IMPLEMENTATION_SLOT =
3 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
4
5 constructor(address _logic, bytes memory _data) {
6 _setImplementation(_logic);
7 if (_data.length > 0) {
8 Address.functionDelegateCall(_logic, _data);
9 }
10 }
11
12 function _implementation() internal view returns (address impl) {
13 bytes32 slot = IMPLEMENTATION_SLOT;
14 assembly {
15 impl := sload(slot)
16 }
17 }
18
19 function _setImplementation(address newImplementation) private {
20 require(
21 Address.isContract(newImplementation),
22 "Implementation not a contract"
23 );
24
25 bytes32 slot = IMPLEMENTATION_SLOT;
26 assembly {
27 sstore(slot, newImplementation)
28 }
29
30 emit Upgraded(newImplementation);
31 }
32}

Advanced Pattern with Multiple Variables

1contract AdvancedUnstructuredProxy {
2 // Custom slots for additional variables
3 bytes32 private constant VERSION_SLOT =
4 keccak256("zealynx.proxy.version");
5 bytes32 private constant UPGRADE_TIMESTAMP_SLOT =
6 keccak256("zealynx.proxy.upgradeTimestamp");
7 bytes32 private constant EMERGENCY_ADMIN_SLOT =
8 keccak256("zealynx.proxy.emergencyAdmin");
9
10 function _getVersion() internal view returns (uint256 version) {
11 bytes32 slot = VERSION_SLOT;
12 assembly {
13 version := sload(slot)
14 }
15 }
16
17 function _setVersion(uint256 newVersion) internal {
18 bytes32 slot = VERSION_SLOT;
19 assembly {
20 sstore(slot, newVersion)
21 }
22 }
23
24 function _getUpgradeTimestamp() internal view returns (uint256 timestamp) {
25 bytes32 slot = UPGRADE_TIMESTAMP_SLOT;
26 assembly {
27 timestamp := sload(slot)
28 }
29 }
30
31 function _recordUpgrade() internal {
32 bytes32 slot = UPGRADE_TIMESTAMP_SLOT;
33 assembly {
34 sstore(slot, timestamp())
35 }
36 }
37}

Security Benefits

1. Collision Prevention

Unstructured storage eliminates accidental storage collisions:

1// Safe: Implementation can use any storage layout
2contract SafeImplementation {
3 address public owner; // Slot 0 - No collision
4 uint256 public totalSupply; // Slot 1 - No collision
5 bool public paused; // Slot 2 - No collision
6
7 // Proxy variables are in completely different slots:
8 // implementation: slot 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
9 // admin: slot 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
10}

2. Future-Proof Storage

Implementation contracts can add variables without worrying about proxy storage:

1// Implementation V1
2contract ImplV1 {
3 address public owner;
4 uint256 public balance;
5}
6
7// Implementation V2 - Safe to add variables
8contract ImplV2 {
9 address public owner; // Same slot as V1
10 uint256 public balance; // Same slot as V1
11 bool public newFeature; // New slot - no proxy collision
12 mapping(address => bool) public whitelist; // New slot - no proxy collision
13}

3. Cross-Implementation Compatibility

Multiple implementation contracts can coexist safely:

1// TokenImplementation
2contract TokenImpl {
3 string public name;
4 string public symbol;
5 mapping(address => uint256) public balances;
6}
7
8// GovernanceImplementation
9contract GovernanceImpl {
10 uint256 public votingDelay;
11 mapping(uint256 => Proposal) public proposals;
12}
13
14// Both can be used with same proxy without storage conflicts

Common Mistakes

1. Hardcoding Wrong Slot Values

Using incorrect slot calculations:

1// WRONG: Manual calculation error
2bytes32 private constant WRONG_SLOT =
3 0x1234567890abcdef; // Random value, not derived from hash
4
5// CORRECT: Use proper keccak256 calculation
6bytes32 private constant CORRECT_SLOT =
7 keccak256("eip1967.proxy.implementation");

2. Not Following EIP-1967 Standard

Using custom slot derivation instead of standard:

1// AVOID: Custom derivation (not compatible with tooling)
2bytes32 private constant CUSTOM_SLOT =
3 keccak256("myproxy.implementation");
4
5// PREFERRED: EIP-1967 standard (compatible with tools/explorers)
6bytes32 private constant STANDARD_SLOT =
7 keccak256("eip1967.proxy.implementation");

3. Assembly Usage Errors

Incorrect assembly for storage operations:

1// WRONG: Using wrong assembly operations
2function badGet() internal view returns (address) {
3 bytes32 slot = IMPLEMENTATION_SLOT;
4 assembly {
5 return(slot, 0x20) // WRONG: return instead of sload
6 }
7}
8
9// CORRECT: Proper assembly usage
10function goodGet() internal view returns (address impl) {
11 bytes32 slot = IMPLEMENTATION_SLOT;
12 assembly {
13 impl := sload(slot) // CORRECT: sload to read storage
14 }
15}

Testing Unstructured Storage

1describe("Unstructured Storage", function() {
2 it("should store implementation in correct slot", async function() {
3 const expectedSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
4
5 // Deploy proxy with implementation
6 const proxy = await deployProxy(Implementation, []);
7
8 // Read storage at unstructured slot
9 const implementationAddress = await ethers.provider.getStorageAt(
10 proxy.address,
11 expectedSlot
12 );
13
14 // Convert to address format and verify
15 const actualImpl = ethers.utils.getAddress(
16 ethers.utils.hexDataSlice(implementationAddress, 12)
17 );
18
19 expect(actualImpl).to.equal(implementation.address);
20 });
21
22 it("should not conflict with implementation storage", async function() {
23 // Deploy proxy and set implementation variables
24 await proxy.setOwner(owner.address);
25 await proxy.setBalance(1000);
26
27 // Verify proxy admin and implementation are separate
28 const proxyAdmin = await proxy.admin();
29 const implOwner = await proxy.owner();
30
31 expect(proxyAdmin).to.not.equal(implOwner);
32 });
33
34 it("should survive implementation upgrades", async function() {
35 const originalAdmin = await proxy.admin();
36
37 // Upgrade implementation
38 await proxy.upgradeTo(newImplementation.address);
39
40 // Admin should be unchanged (different storage slot)
41 const afterUpgradeAdmin = await proxy.admin();
42 expect(afterUpgradeAdmin).to.equal(originalAdmin);
43 });
44});

Advanced Usage Patterns

Storage Slot Libraries

Create reusable libraries for common unstructured storage patterns:

1library UnstructuredStorage {
2 function getAddressSlot(bytes32 slot) internal pure returns (StorageSlot.AddressSlot storage) {
3 return StorageSlot.getAddressSlot(slot);
4 }
5
6 function getUint256Slot(bytes32 slot) internal pure returns (StorageSlot.Uint256Slot storage) {
7 return StorageSlot.getUint256Slot(slot);
8 }
9
10 function getBooleanSlot(bytes32 slot) internal pure returns (StorageSlot.BooleanSlot storage) {
11 return StorageSlot.getBooleanSlot(slot);
12 }
13}
14
15contract ModernProxy {
16 bytes32 private constant IMPLEMENTATION_SLOT =
17 keccak256("eip1967.proxy.implementation");
18
19 function _getImplementation() internal view returns (address) {
20 return UnstructuredStorage.getAddressSlot(IMPLEMENTATION_SLOT).value;
21 }
22
23 function _setImplementation(address impl) internal {
24 UnstructuredStorage.getAddressSlot(IMPLEMENTATION_SLOT).value = impl;
25 }
26}

Multiple Storage Namespaces

Organize related variables in storage namespaces:

1contract NamespacedProxy {
2 // Core proxy variables
3 bytes32 private constant PROXY_NAMESPACE = keccak256("proxy");
4 bytes32 private constant IMPLEMENTATION_SLOT =
5 keccak256(abi.encode(PROXY_NAMESPACE, "implementation"));
6 bytes32 private constant ADMIN_SLOT =
7 keccak256(abi.encode(PROXY_NAMESPACE, "admin"));
8
9 // Security variables
10 bytes32 private constant SECURITY_NAMESPACE = keccak256("security");
11 bytes32 private constant EMERGENCY_ADMIN_SLOT =
12 keccak256(abi.encode(SECURITY_NAMESPACE, "emergencyAdmin"));
13 bytes32 private constant PAUSED_SLOT =
14 keccak256(abi.encode(SECURITY_NAMESPACE, "paused"));
15
16 // Upgrade variables
17 bytes32 private constant UPGRADE_NAMESPACE = keccak256("upgrade");
18 bytes32 private constant TIMELOCK_SLOT =
19 keccak256(abi.encode(UPGRADE_NAMESPACE, "timelock"));
20 bytes32 private constant PENDING_UPGRADE_SLOT =
21 keccak256(abi.encode(UPGRADE_NAMESPACE, "pendingUpgrade"));
22}

Unstructured storage is fundamental to secure proxy patterns. By using hash-derived storage slots, proxy contracts can safely coexist with any implementation contract storage layout, preventing dangerous collisions that could corrupt data or create security vulnerabilities.

Need expert guidance on Unstructured Storage?

Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.

Get a Quote

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx