Эта страница — практическая шпаргалка по storage layout в Solidity. Теория и подробные примеры разобраны на основной странице про storage layout. Здесь — сжатые правила и чек-лист перед апгрейдом контрактов.
Базовые принципы (что помнить всегда)
- Storage в EVM — это map slot → 32 байта.
- Нумерация слотов начинается с 0.
- Порядок объявления переменных в контракте определяет порядок слотов.
- Маленькие типы (< 32 байт) упаковываются в один слот по порядку:
- сначала идёт более «широкий» тип (например, uint128),
- затем более узкие (uint64, bool и т.д.) до заполнения 32 байт.
- Любое изменение порядка/типа переменных в апгрейдируемых контрактах может сломать состояние.
Быстрый план размещения переменных
Рекомендованный порядок:
- Сначала крупные «ядровые» переменные:
- адреса админов, ролей;
- конфигурация протокола;
- критичные счётчики и лимиты.
- Затем маппинги и динамические массивы.
- В конце — вспомогательные флаги, мелкие типы и gap’ы.
Паттерн упаковки:
- Старайтесь группировать мелкие типы:
- uint128, uint128 → 1 слот
- uint64, uint64, uint64, uint64 → 1 слот
- bool, uint8, uint8 → 1 слот (часто с отступами под будущее)
- Избегайте хаотичного чередования разных размеров — это усложняет анализ.
Mapping, массивы и struct: что держать в голове
- mapping:
- «пустой» слот с индексом p — это только base slot;
- значение mapping[key] лежит по адресу keccak256(encode(key), encode(p));
- порядок вставки элементов не влияет на layout.
- Динамические массивы (uint[], bytes, string):
- в base slot хранится длина;
- элементы начинаются с keccak256(p) и далее по порядку.
- struct:
- занимает подряд несколько слотов;
- внутри действуют те же правила упаковки;
- если struct — поле контракта, он «забирает» диапазон слотов (учитывайте это при апгрейдах).
Апгрейды через прокси: что строго запрещено
Если контракт живёт за прокси (см. DELEGATECALL и прокси-контракты):
- НЕЛЬЗЯ:
- менять порядок уже существующих переменных;
- менять тип уже существующих переменных (например, uint256 → uint128);
- удалять переменные;
- вставлять новые переменные в середину списка.
- МОЖНО:
- добавлять новые переменные только в конец списка;
- расширять struct’ы, но только в конец и с аккуратной проверкой;
- использовать зарезервированные gap-массивы для будущих полей.
Если нужно «удалить» переменную — перестаньте её использовать в логике, но оставьте слот нетронутым.
Gap-поля: зачем и как их использовать
Стандартный паттерн для апгрейдируемых контрактов:
- В базовом контракте добавляем:
uint256[50] private __gap;
- __gap занимает 50 слотов в storage.
- В будущих версиях эти слоты можно «разрезать» под новые переменные, не трогая уже существующие.
Правила:
- не переиспользуйте один и тот же слот для двух разных значений в разных версиях;
- ведите документацию: какие индексы gap уже заняты и чем.
Мини-чек-лист перед деплоем/апгрейдом
Перед деплоем первой версии:
- Проверить, что:
- порядок переменных осознанно выбран;
- мелкие типы упакованы разумно;
- заложены хотя бы один-два gap-массива в базовых контрактах.
Перед апгрейдом реализации в прокси:
- Сравнить storage layout старой и новой версии:
- нет ли изменённых типов;
- нет ли перестановок полей;
- добавлены ли новые поля только в конец.
- Прогнать тесты миграции / апгрейда:
- задать состояние в старой версии;
- обновить реализацию;
- проверить инварианты (балансы, роли, параметры).
- При сомнениях — явно задокументировать layout слотов в комментариях или отдельном файле.
Кому и когда полезно копать глубже
Глубокое понимание storage layout особенно важно, если вы:
- пишете смарт-контракты, которые нужно апгрейдить без потери состояния;
- работаете с низкоуровневым кодом на Yul или используете прямой доступ к storage;
- занимаетесь аудитом, реинжинирингом или анализом чужих протоколов;
- строите аналитические панели, читающие данные напрямую из storage по слотам.
Для повседневной разработки достаточно запомнить простое правило: «не трогай существующие слоты, добавляй только в конец и оставляй gap для будущего».
