Security·9 min का पठन·Solingo द्वारा

Upgradeable Contracts में Storage Slot Collisions

Upgradeable contracts proxy के साथ storage share करते हैं। Layout गलत = funds lost।

# Upgradeable Contracts में Storage Slot Collisions

Proxy patterns powerful हैं — लेकिन storage layout गलती से millions lost हो सकते हैं।

यहाँ समझें क्यों, और कैसे बचें।

Problem: Shared Storage Space

Normal Contract

contract Token {

mapping(address => uint) public balances; // Slot 0

uint public totalSupply; // Slot 1

}

Storage clear hai — हर variable अपनी slot में।

Proxy Pattern

// Proxy

contract Proxy {

address public implementation; // Slot 0 ❌

fallback() external payable {

// delegatecall to implementation

}

}

// Implementation

contract TokenV1 {

mapping(address => uint) public balances; // Slot 0 ❌❌

uint public totalSupply; // Slot 1

}

Collision: implementation और balances दोनों slot 0 में!

delegatecall executes in PROXY storage

→ balances[user] overwrites implementation address

→ Proxy bricked 💥

Real Incident

Parity Multisig Hack (2017):

// Library (implementation)

contract WalletLibrary {

address[] public owners; // Slot 0

function initWallet(address[] _owners) public {

owners = _owners; // ❌ Overwrites proxy's implementation!

}

}

Attacker called initWallet directly on library → became owner → killed contract।

Loss: $150M+ frozen।

Solution 1: EIP-1967 Standard Slots

Use high random slots for proxy storage।

contract EIP1967Proxy {

// bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)

bytes32 private constant IMPLEMENTATION_SLOT =

0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

function implementation() public view returns (address impl) {

assembly {

impl := sload(IMPLEMENTATION_SLOT)

}

}

function upgradeTo(address newImpl) internal {

assembly {

sstore(IMPLEMENTATION_SLOT, newImpl)

}

}

}

Why high slot?

Normal variables start at slot 0 → collision unlikely with slot 0x360894a13...

Implementation Contract

contract TokenV1 {

// Normal storage, starts at slot 0

mapping(address => uint) public balances; // Slot 0 ✅

uint public totalSupply; // Slot 1 ✅

// No collision with proxy (proxy uses high slot)

}

Solution 2: Unstructured Storage

OpenZeppelin pattern:

abstract contract Proxy {

function _implementation() internal view virtual returns (address impl) {

bytes32 slot = _IMPLEMENTATION_SLOT;

assembly {

impl := sload(slot)

}

}

function _setImplementation(address newImpl) internal {

bytes32 slot = _IMPLEMENTATION_SLOT;

assembly {

sstore(slot, newImpl)

}

}

}

Solution 3: Diamond Storage (ERC-7201)

Multiple implementations के लिए — isolated namespaces।

library TokenStorage {

// keccak256("myprotocol.token.storage")

bytes32 constant STORAGE_SLOT =

0x1234567890abcdef...;

struct Layout {

mapping(address => uint) balances;

uint totalSupply;

}

function layout() internal pure returns (Layout storage l) {

bytes32 slot = STORAGE_SLOT;

assembly {

l.slot := slot

}

}

}

contract TokenV1 {

using TokenStorage for *;

function balanceOf(address user) public view returns (uint) {

return TokenStorage.layout().balances[user];

}

function mint(address to, uint amount) public {

TokenStorage.Layout storage s = TokenStorage.layout();

s.balances[to] += amount;

s.totalSupply += amount;

}

}

Benefits:

  • Multiple facets (Diamond pattern)
  • No collision possible
  • Isolated upgrades

Collision Detection

Tool 1: Slither

slither . --detect proxy-collision

Output:

ProxyAdmin.implementation (slot 0) collides with Token.balances (slot 0)

Tool 2: Foundry vm.load

Test storage layout:

contract StorageTest is Test {

Proxy proxy;

function testNoCollision() public {

proxy = new Proxy();

// Implementation should be in high slot

bytes32 implSlot = bytes32(uint(keccak256("eip1967.proxy.implementation")) - 1);

address impl = address(uint160(uint(vm.load(address(proxy), implSlot))));

assertEq(impl, expectedImpl);

}

}

Tool 3: OpenZeppelin Storage Layout Hash

npx hardhat storage-layout

Generates JSON:

{

"storage": [

{ "label": "balances", "slot": "0", "type": "t_mapping" },

{ "label": "totalSupply", "slot": "1", "type": "t_uint256" }

]

}

Compare before/after upgrade — slot changes = danger।

Upgrade Safety Checklist

❌ Unsafe

// V1

contract TokenV1 {

uint public totalSupply; // Slot 0

mapping(address => uint) balances; // Slot 1

}

// V2

contract TokenV2 {

mapping(address => uint) balances; // Slot 0 ❌ Moved!

uint public totalSupply; // Slot 1 ❌ Swapped!

}

Result: totalSupply now reads garbage from balances slot।

✅ Safe

// V1

contract TokenV1 {

uint public totalSupply; // Slot 0

mapping(address => uint) balances; // Slot 1

}

// V2

contract TokenV2 {

uint public totalSupply; // Slot 0 ✅ Same

mapping(address => uint) balances; // Slot 1 ✅ Same

uint public newFeatureFlag; // Slot 2 ✅ Appended

}

Rules:

  • Never reorder existing variables
  • Never change types (uint256 → uint128)
  • Never delete variables (leave gap)
  • Only append new variables
  • Gap Pattern

    Reserve slots for future upgrades:

    contract TokenV1 {
    

    uint public totalSupply;

    mapping(address => uint) balances;

    // Reserve 50 slots for future variables

    uint[48] private __gap;

    }

    V2 can add 48 more variables safely:

    contract TokenV2 {
    

    uint public totalSupply;

    mapping(address => uint) balances;

    uint public newVar1; // Uses gap slot 0

    uint public newVar2; // Uses gap slot 1

    uint[46] private __gap; // Reduced by 2

    }

    Real Incidents

    1. Compound cToken (2020)

    Wrong storage layout in upgrade → markets corrupted।

    Fix: Emergency pause, rollback।

    2. Audius (2022)

    Storage collision allowed attacker to change governance।

    Loss: $6M drained।

    Root cause: Missing gap, wrong inheritance order।

    Testing Storage Layout

    contract UpgradeTest is Test {
    

    ProxyAdmin admin;

    TransparentProxy proxy;

    TokenV1 v1;

    TokenV2 v2;

    function testUpgradeSafety() public {

    // Deploy V1

    v1 = new TokenV1();

    proxy = new TransparentProxy(address(v1), address(admin), "");

    TokenV1 token = TokenV1(address(proxy));

    token.mint(alice, 1000 ether);

    // Snapshot storage

    uint aliceBalBefore = token.balanceOf(alice);

    uint supplyBefore = token.totalSupply();

    // Upgrade to V2

    v2 = new TokenV2();

    admin.upgrade(proxy, address(v2));

    // Check storage preserved

    TokenV2 tokenV2 = TokenV2(address(proxy));

    assertEq(tokenV2.balanceOf(alice), aliceBalBefore, "Balance corrupted");

    assertEq(tokenV2.totalSupply(), supplyBefore, "Supply corrupted");

    }

    }

    Prevention Stack

    | Layer | Tool | Purpose |

    |-------|------|---------|

    | Design | EIP-1967 | Standard high slots |

    | Code | Diamond storage | Namespace isolation |

    | Test | Foundry vm.load | Storage assertions |

    | CI | Slither | Auto-detect collisions |

    | Audit | Manual review | Inheritance order |

    Conclusion

    Storage collisions silent killers हैं — no compile error, no warning।

    Best practices:

  • Use EIP-1967 compliant proxies (OpenZeppelin)
  • Never reorder/delete storage variables
  • Use __gap patterns
  • Test upgrades with storage assertions
  • Run Slither on every upgrade
  • 2026 में tools better हैं, लेकिन developer awareness critical है।

    एक गलत upgrade = protocol dead। Take storage layout seriously। 🔒

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें