Перейти к содержанию

Коллизия хранилища прокси: как обновление повреждает состояние контракта

Прокси сохраняет состояние, пока код реализации меняется. Узнайте, как несовместимые схемы перезаписывают балансы, владельцев и управление обновлением и как безопасно проверить обновление.

Обновлено

Материал предназначен только для образовательных целей и не является инвестиционной рекомендацией. Инвестиции могут привести к убыткам.

Прямой ответ

Коллизия хранилища прокси возникает, когда код реализации читает или записывает слот прокси не в том смысле, который определяла схема, создавшая существующее состояние. После обновления баланс может восприниматься как адрес, владелец — обнулиться, базовый слот mapping — повредиться, а данные управления обновлением — оказаться перезаписанными.

При delegatecall байткод реализации выполняется в контексте прокси: хранилище, баланс и address(this) принадлежат прокси. Имена переменных не хранятся в блокчейне, поэтому EVM использует только слот и смещение в байтах, рассчитанные новым кодом.

Следовательно, обновление должно сохранять развернутую схему, а не просто успешно компилироваться или предоставлять те же функции. До авторизации сравните схемы, созданные компилятором, с точно той версией, которая развернута.

Как это работает

Обычно Solidity размещает переменные состояния начиная со слота 0 в порядке объявления после C3-линеаризации наследования. Значения меньше 32 байт могут делить слот; для структур и массивов действуют дополнительные правила, а mapping и динамические массивы вычисляют положение данных из базового слота. Сдвиг базового слота меняет и положение производных данных.

Проверять нужно четыре разные границы коллизии:

  • Прокси и реализация: собственные поля прокси, включая адрес реализации и администратора, не должны занимать слоты состояния приложения. ERC-1967 задает стандартные слоты вне обычного распределения компилятора для реализации, beacon и администратора.
  • Старая и новая реализации: существующие переменные должны сохранять совместимые слоты, смещения и типы. Добавление в конец может быть безопасным, а вставка, перестановка, удаление или смена типа переосмысливает существующие слова.
  • Наследование: добавление состояния в базовый контракт или изменение порядка наследования может сдвинуть хранилище потомков, даже если их исходный код не менялся.
  • Зарезервированное или изолированное хранилище: корректное использование storage gap резервирует место для базового контракта, а пространства имен в стиле ERC-7201 изолируют схемы. Ни один способ не разрешает произвольные изменения внутри существующей схемы.

Пример

Пусть версия 1 имеет такую схему:

uint256 totalAssets; // slot 0
address owner;       // slot 1

Версия 2 ошибочно вставляет переменную в начало:

bool paused;         // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner;       // slot 2

После обновления paused читает младший байт старого totalAssets, новый totalAssets трактует слово старого owner как целое число, а owner читает прежнее содержимое слота 2, часто ноль. Исходные слова остаются в хранилище, но новый код придает им другой смысл. Транзакция может завершиться успешно, применив авторизацию или учет к искаженному состоянию.

Риски и проверки обновления

  • Получите схему хранилища обеих реализаций и сравните слот, смещение, тип и наследование с фактически развернутым эталонным контрактом.
  • В обычной линейной схеме добавляйте переменные только в конец. Без доказательства совместимости не меняйте порядок или тип полей, не удаляйте и не используйте их повторно, не изменяйте базовые контракты.
  • При использовании storage gap уменьшайте его ровно на число занятых резервных слотов. Для пространств имен сохраняйте уникальные идентификаторы и проверяйте изменения в каждом существующем пространстве.
  • Не считайте ERC-1967 полной защитой. Он отделяет метаданные прокси от слотов приложения, выделяемых компилятором, но не делает две схемы реализации совместимыми.
  • Тестируйте обновление и повторную инициализацию на форке или снимке состояния. До и после выполнения проверяйте владельцев, роли, балансы, разрешения, элементы mapping, паузу, слоты реализации и средства отката или аварийного управления.

Если обновление уже могло вызвать коллизию, при возможности управления остановите дальнейшие обновления и вызовы, меняющие состояние. Сохраните номер блока и байткод до обновления, сравните сырое содержимое затронутых слотов и поручите квалифицированным инженерам спроектировать миграцию с независимой проверкой. Повторные обновления без подтвержденной карты хранилища могут уничтожить еще больше восстановимого состояния.

Распространенные заблуждения

  • «Имена переменных не изменились, значит схема безопасна». Положение определяют не имена, а типы, порядок, упаковка, наследование и правила пространств имен.
  • «Удаление переменной освобождает слот». Хранилище прокси сохраняется. Повторное использование придает старому слову новый смысл, если проверенная миграция не очистит или не преобразует его.
  • «Успешная тестовая транзакция доказывает совместимость». Она может затронуть лишь несколько слотов. Проверка схемы и различий состояния должна охватывать привилегированные поля, упакованные значения, mapping, массивы и унаследованное хранилище.

Связанные темы

Источники

Навигация

Поиск по вики...