Storage layout в Solidity: шпаргалка по слотам и апгрейдам контрактов

Эта страница — практическая шпаргалка по storage layout в Solidity. Теория и подробные примеры разобраны на основной странице про storage layout. Здесь — сжатые правила и чек-лист перед апгрейдом контрактов.

Storage layout в Solidity: шпаргалка по слотам и апгрейдам контрактов

Базовые принципы (что помнить всегда)

  • Storage в EVM — это map slot → 32 байта.
  • Нумерация слотов начинается с 0.
  • Порядок объявления переменных в контракте определяет порядок слотов.
  • Маленькие типы (< 32 байт) упаковываются в один слот по порядку:
    • сначала идёт более «широкий» тип (например, uint128),
    • затем более узкие (uint64, bool и т.д.) до заполнения 32 байт.
  • Любое изменение порядка/типа переменных в апгрейдируемых контрактах может сломать состояние.

Быстрый план размещения переменных

Рекомендованный порядок:

  1. Сначала крупные «ядровые» переменные:
    1. адреса админов, ролей;
    2. конфигурация протокола;
    3. критичные счётчики и лимиты.
  2. Затем маппинги и динамические массивы.
  3. В конце — вспомогательные флаги, мелкие типы и gap’ы.

Паттерн упаковки:

  • Старайтесь группировать мелкие типы:
  1. uint128, uint128 → 1 слот
  2. uint64, uint64, uint64, uint64 → 1 слот
  3. 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 и прокси-контракты):

  • НЕЛЬЗЯ:
    1. менять порядок уже существующих переменных;
    2. менять тип уже существующих переменных (например, uint256uint128);
    3. удалять переменные;
    4. вставлять новые переменные в середину списка.
  • МОЖНО:
    1. добавлять новые переменные только в конец списка;
    2. расширять struct’ы, но только в конец и с аккуратной проверкой;
    3. использовать зарезервированные gap-массивы для будущих полей.

Если нужно «удалить» переменную — перестаньте её использовать в логике, но оставьте слот нетронутым.

Gap-поля: зачем и как их использовать

Стандартный паттерн для апгрейдируемых контрактов:

  • В базовом контракте добавляем:

uint256[50] private __gap;

  • __gap занимает 50 слотов в storage.
  • В будущих версиях эти слоты можно «разрезать» под новые переменные, не трогая уже существующие.

Правила:

  • не переиспользуйте один и тот же слот для двух разных значений в разных версиях;
  • ведите документацию: какие индексы gap уже заняты и чем.

Мини-чек-лист перед деплоем/апгрейдом

Перед деплоем первой версии:

  • Проверить, что:
    1. порядок переменных осознанно выбран;
    2. мелкие типы упакованы разумно;
    3. заложены хотя бы один-два gap-массива в базовых контрактах.

Перед апгрейдом реализации в прокси:

  • Сравнить storage layout старой и новой версии:
    1. нет ли изменённых типов;
    2. нет ли перестановок полей;
    3. добавлены ли новые поля только в конец.
  • Прогнать тесты миграции / апгрейда:
    1. задать состояние в старой версии;
    2. обновить реализацию;
    3. проверить инварианты (балансы, роли, параметры).
  • При сомнениях — явно задокументировать layout слотов в комментариях или отдельном файле.

Кому и когда полезно копать глубже

Глубокое понимание storage layout особенно важно, если вы:

  • пишете смарт-контракты, которые нужно апгрейдить без потери состояния;
  • работаете с низкоуровневым кодом на Yul или используете прямой доступ к storage;
  • занимаетесь аудитом, реинжинирингом или анализом чужих протоколов;
  • строите аналитические панели, читающие данные напрямую из storage по слотам.

Для повседневной разработки достаточно запомнить простое правило: «не трогай существующие слоты, добавляй только в конец и оставляй gap для будущего».

См. также

Task Runner