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

ERC-7201 Namespaced Storage: सुरक्षित upgradeable contracts के लिए

Storage collisions proxy upgrades के silent killer हैं। ERC-7201 namespaced storage हर module को अपना collision-resistant slot region देता है, और यहाँ बिल्कुल समझाया गया है कि formula कैसे काम करता है।

# ERC-7201 Namespaced Storage: सुरक्षित upgradeable contracts के लिए

Upgradeable contracts logic को state से अलग करते हैं। Proxy storage रखता है, implementation code रखता है, और हर upgrade code को बदलता है जबकि वही storage रखता है। यह separation शक्तिशाली है, लेकिन यह एक खतरा लाता है जिसने असली contracts को brick कर दिया है: storage collisions। ERC-7201 ("Namespaced Storage Layout") इसका standard जवाब है। यह article दिखाता है कि collisions कहाँ से आती हैं, ERC-7201 base slot कैसे compute होता है, और इसे सही तरीके से कैसे इस्तेमाल करें, OpenZeppelin convention सहित।

Proxy upgrades में collision क्यों होती है

EVM में, contract का state 32-byte slots के एक flat key-value store में रहता है। Solidity state variables को declaration order में sequential slots देता है: पहला variable slot 0 पर जाता है, अगला slot 1 पर, और इसी तरह (छोटे types के लिए packing के साथ)। Mappings और dynamic arrays अपना slot एक hash से derive करते हैं, लेकिन उनकी length और base pointer फिर भी एक sequential slot घेरते हैं।

Proxy के साथ, implementation contract इन slots को पढ़ता और लिखता है, लेकिन data भौतिक रूप से proxy में रहता है। इसलिए दोनों contracts को byte-दर-byte सहमत होना होगा कि slot 0, slot 1 आदि पर क्या रहता है। समस्याएं दो classic रूपों में आती हैं:

  • Inheritance reordering। आप एक नया base contract जोड़ते हैं, या मौजूदा को reorder करते हैं। Slot 3 पहले totalSupply था; बदलाव के बाद वह owner है। पहले से stored value अब एक अलग variable के रूप में reinterpret हो जाती है।
  • Module overlap। आप कई स्वतंत्र modules जोड़ते हैं (एक ERC20, एक access-control module, एक pausable module)। हर एक भोलेपन से slot 0 से गिनना शुरू करता है, इसलिए वे सब अलग data के लिए slot 0 चाहते हैं।
  • पुराना उपाय storage gaps था: हर base contract के अंत में uint256[50] private __gap; declare करना ताकि future variables के लिए जगह reserve हो। Gaps काम करते हैं, लेकिन वे नाजुक हैं। जब आप variable जोड़ते हैं तो आपको gap को manually छोटा करना पड़ता है, और एक भी miscount चुपचाप state को corrupt कर देता है। ERC-7201 इस अनुमान-लगाने को हटा देता है।

    मुख्य विचार: हर namespace को अपना region दो

    पूरे state को 0 से शुरू होने वाले sequential slots में packing करने के बजाय, ERC-7201 variables के हर logical group को एक struct में रखता है, और उस struct को एक human-readable namespace id से derive किए गए pseudo-random base slot पर anchor करता है। चूँकि base slots एक hash द्वारा 256-bit slot space में फैले होते हैं, दो अलग namespaces व्यावहारिक रूप से कभी overlap नहीं करते, और वे slot 0 पर sequential layout के साथ कभी collide नहीं करते।

    एक namespace id एक dotted string है जैसे myapp.storage.Vault। OpenZeppelin द्वारा इस्तेमाल की जाने वाली convention .storage. है, उदाहरण के लिए openzeppelin.storage.ERC20

    Formula

    एक namespace id N के लिए ERC-7201 base slot है:

    bytes32 slot = keccak256(abi.encode(uint256(keccak256(bytes(N))) - 1)) & ~bytes32(uint256(0xff));

    इसे अंदर से बाहर पढ़ें:

  • keccak256(bytes(N)) namespace string को hash करता है।
  • uint256 में cast करें और 1 घटाएं। यह - 1 step किसी को भी ऐसी preimage खोजने से रोकता है जो एक mapping या array element को namespace slot पर map कर दे, क्योंकि mapping slots स्वयं keccak256 के outputs हैं और यह offset उस alignment को तोड़ देता है।
  • abi.encode(...) value को बाईं ओर 32 bytes तक pad करता है, फिर keccak256 फिर से hash करता है। यह double hash परिणाम को Solidity के standard slot derivation के खिलाफ collision-resistant बनाता है।
  • & ~bytes32(uint256(0xff)) अंतिम byte को mask कर देता है (उसे zero कर देता है)। यह base slot को 256-slot boundary पर align करता है, जगह छोड़ता है ताकि multi-slot structs और per-variable offsets एक साफ region के अंदर रहें और storage-access optimizations के अनुकूल बने रहें।
  • परिणाम deterministic है। वही id हमेशा वही slot देता है, किसी भी chain पर, किसी भी compiler version में।

    Command line से verify करना

    आप slot को offline reproduce कर सकते हैं। Foundry के cast के साथ:

    cast index-erc7201 "openzeppelin.storage.ERC20"
    

    # 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00

    अंतिम 00 पर ध्यान दें: वह masked अंतिम byte है। आपका contract जो value इस्तेमाल करता है उसे हमेशा एक स्वतंत्र computation के खिलाफ cross-check करें। Namespace string में एक typo पूरी तरह से अलग, valid-दिखने वाला slot बनाता है, और bug तब तक अदृश्य रहता है जब तक दो contracts असहमत न हों।

    व्यवहार में struct-based storage

    यहाँ एक self-contained pattern है जिसे आप बिना किसी library के लागू कर सकते हैं। हर module एक struct, एक annotated constant, और एक private accessor define करता है जो inline assembly का उपयोग करके struct को उसके fixed slot से load करता है।

    // SPDX-License-Identifier: MIT
    

    pragma solidity ^0.8.20;

    contract VaultUpgradeable {

    /// @custom:storage-location erc7201:myapp.storage.Vault

    struct VaultStorage {

    mapping(address => uint256) balances;

    uint256 totalDeposits;

    address asset;

    }

    // keccak256(abi.encode(uint256(keccak256("myapp.storage.Vault")) - 1)) & ~bytes32(uint256(0xff))

    bytes32 private constant VAULT_STORAGE_LOCATION =

    0x590a9a5de9ed35bc0349b8615141727573f4e89701ca33d602fbe9de39aaa900;

    function _vault() private pure returns (VaultStorage storage $) {

    assembly {

    $.slot := VAULT_STORAGE_LOCATION

    }

    }

    function deposit(uint256 amount) external {

    VaultStorage storage $ = _vault();

    $.balances[msg.sender] += amount;

    $.totalDeposits += amount;

    }

    function balanceOf(address account) external view returns (uint256) {

    return _vault().balances[account];

    }

    }

    तीन details इसे सुरक्षित और idiomatic बनाते हैं:

    • @custom:storage-location erc7201:... NatSpec tag Solidity compiler को एक storage layout emit करने देता है जिसे tooling (और compiler के अपने collision checks) verify कर सकते हैं। इसके बिना, struct बस एक struct है।
    • $ variable name namespaced storage pointer के लिए community convention है। यह accessors को छोटा और पहचानने योग्य रखता है।
    • Struct एक बार anchor होती है। Struct के अंदर, mapping और दोनों scalars को base slot के सापेक्ष अपने offsets मिलते हैं, बिल्कुल वैसे ही जैसे normal Solidity layout उन्हें assign करता, लेकिन इस namespace के अंदर isolated।

    चूँकि हर variable अब एक namespaced struct के अंदर रहता है, आप future upgrade में VaultStorage के अंत में एक नया field जोड़ सकते हैं बिना किसी अन्य module को छुए, और modules को स्वतंत्र रूप से reorder कर सकते हैं। sync रखने के लिए कोई shared sequential counter नहीं है।

    OpenZeppelin usage

    OpenZeppelin Upgradeable contracts हर जगह ERC-7201 अपनाते हैं। हर base contract अपनी namespaced struct और accessor define करता है। उदाहरण के लिए, OwnableUpgradeable namespace openzeppelin.storage.Ownable का उपयोग करता है, और ERC20Upgradeable openzeppelin.storage.ERC20 का। आप इन्हें बिल्कुल किसी भी अन्य contract की तरह inherit करते हैं:

    // SPDX-License-Identifier: MIT
    

    pragma solidity ^0.8.20;

    import {ERC20Upgradeable} from

    "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

    import {OwnableUpgradeable} from

    "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

    import {Initializable} from

    "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

    contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable {

    /// @custom:oz-upgrades-unsafe-allow constructor

    constructor() {

    _disableInitializers();

    }

    function initialize(address owner_) external initializer {

    __ERC20_init("MyToken", "MTK");

    __Ownable_init(owner_);

    }

    function mint(address to, uint256 amount) external onlyOwner {

    _mint(to, amount);

    }

    }

    चूँकि हर parent अपना state एक अलग namespace में रखता है, ERC20Upgradeable और OwnableUpgradeable का inheritance order अब storage layout को प्रभावित नहीं करता। यही व्यावहारिक फायदा है: आप inheritance graph में slot arithmetic के बारे में सोचना बंद कर देते हैं।

    कुछ नियम अभी भी लागू होते हैं:

    • Initial state set करने के लिए initializer और __X_init functions का उपयोग करें, constructors का नहीं, क्योंकि constructor code proxy context में कभी नहीं चलता।
    • Implementation constructor में _disableInitializers() call करें ताकि implementation को सीधे initialize न किया जा सके।
    • OpenZeppelin Upgrades plugins @custom:storage-location annotations पढ़ते हैं ताकि validate करें कि एक upgrade किसी namespaced variable को move या remove नहीं करता। Plugin को अपने CI में रखें।

    एक छोटी checklist

    • संबंधित state को हर module के लिए एक struct में group करें, @custom:storage-location erc7201: के साथ tag करें।
    • Base slot को सटीक formula से compute करें, और deploy करने से पहले एक स्वतंत्र tool से verify करें।
    • Upgrades में namespaced struct के अंत में हमेशा केवल fields append करें।
    • एक storage-layout validator (OpenZeppelin Upgrades plugin) CI में रखें।

    इसका अभ्यास करें

    Formula पढ़ना एक बात है; एक collision को अपने state को corrupt करते हुए महसूस करना और फिर उसे namespace से ठीक करना ही वह है जो इसे स्थायी बनाता है। Solingo पर आप एक upgradeable contract लिख सकते हैं, एक proxy में जानबूझकर storage collision trigger कर सकते हैं, फिर उसे ERC-7201 में refactor करके slots को align होते देख सकते हैं। app.solingo-blockchain.xyz पर upgradeable-storage exercises आज़माएं और answer reveal होने से पहले खुद एक base slot compute करें।

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

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

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