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

Обновление Solidity-контракта: storage и права релиза

Последний этап курса — отдельная версия UpgradeableCourseCatalogV2. Она добавляет редакционный epoch и разрешённый способ его увеличить. Старое поле courses, структура Course и логика publish/deactivate сохраняются. Эта небольшая добавка позволяет обсудить реальное изменение storage, не скрывая его за общим названием «обновление контракта».

Наш исходный CourseCatalog всё ещё обычный и неизменённый. V2 относится только к отдельной upgradeable-модели прошлого урока. Если приложение подключено к прежнему реестру без прокси, инструкция upgradeToAndCall к нему не подходит. Сначала определяют фактический тип экземпляра, а потом допустимый маршрут.

Добавка к сохранённой V1

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {UpgradeableCourseCatalogV1} from "./UpgradeableCourseCatalogV1.sol";

contract UpgradeableCourseCatalogV2 is UpgradeableCourseCatalogV1 {
    error InvalidEpoch();
    uint256 public editorialEpoch;
    event EpochChanged(uint256 epoch);

    function initializeV2(uint256 initialEpoch) external reinitializer(2) onlyOwner {
        if (initialEpoch == 0) revert InvalidEpoch();
        editorialEpoch = initialEpoch;
        emit EpochChanged(initialEpoch);
    }
    function advanceEpoch() external onlyOwner {
        editorialEpoch += 1;
        emit EpochChanged(editorialEpoch);
    }
}

Это полный исходник V2 при наличии полного файла V1 предыдущего урока. Он добавляет поле в производной части и не переопределяет publish. Epoch является дополнительным редакционным счётчиком, а не автоматически номером каждого выпуска. Вызов publish не увеличивает его в показанной модели.

Новая реализация наследует constructor V1, который блокирует её собственные initializers. При выполнении через уже настроенную P родительский initialize не запускается заново. Новый шаг reinitializer(2) относится к версии настройки состояния P, а не к метке компилятора Solidity.

Настройка при upgrade

Owner P может вызвать upgradeToAndCall с адресом V2 и ABI-байтами initializeV2(1). Ниже предметная форма будущего вызова, не выполненная транзакция и не отдельный полный скрипт.

bytes memory step = abi.encodeCall(UpgradeableCourseCatalogV2.initializeV2, (uint256(1)));
UpgradeableCourseCatalogV1(address(proxy)).upgradeToAndCall(address(nextImplementation), step);

Объявления proxy и nextImplementation относятся к подготовленной среде. У выбранного UUPS интерфейс версии 5.0.0 предоставляет upgradeToAndCall. Не копируем старую инструкцию upgradeTo из другого профиля без чтения конкретного ABI закреплённой реализации.

Внутри upgrade проверяется полномочие owner, затем форма UUPS новой реализации, меняется служебная ссылка и вызывается настройка. Если initializeV2 отклонится, транзакция откатывает изменение implementation. Поэтому атомарный маршрут не должен оставлять новую реализацию с недействующим шагом настройки после этой ошибки.

Почему нужен onlyOwner у reinitializer

reinitializer(2) ограничивает повторное применение версии, но не устанавливает нужного вызывающего субъекта. Без onlyOwner первый посторонний вызов мог бы потребить шаг с выбранным им значением, если прокси уже доступна с V2 и шаг не был выполнен. Однократность не равна авторизации.

Состав модификаторов также требует анализа отката. Если толькоOwner отклоняет вызывающего, более ранняя подготовка version в reinitializer не остаётся успешной потреблённой настройкой: revert возвращает состояние вызова. Это ожидаемая атомарная семантика, а не основание считать неавторизованный запрос полезным способом «заблокировать версию».

После успешного шага повтор initializeV2 отклоняется. Дополнительный advanceEpoch остаётся разрешённым owner. Если продукт не хочет изменений epoch после первоначальной настройки, такой метод не нужен; это отдельное требование. Мы включили его явно, поэтому читатель не должен считать поле навсегда фиксированным.

Старые поля и новые интерпретации

В storage прокси уже могут находиться course17=(H1,true) и course18=(H2,false). V2 должна читать эти записи с прежним смыслом. Нельзя вставить новую переменную перед courses в прежней V1 и предположить, что компилятор переместит записанные данные. Solidity определяет расположение, а не миграцию по именам.

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

Также нельзя переставлять или переосмысливать поля Course. Новое название поля не является доказательством совместимости. В этой V2 структура не меняется. Если требуются новые свойства выпуска, можно добавить отдельный mapping в новой части, после анализа его связи с прежними записями и значениями по умолчанию.

OpenZeppelin Upgradeable использует собственные namespaces для ряда родительских модулей. Это уменьшает некоторые столкновения родительского storage, но не разрешает произвольную перестановку наших обычных полей. Совместимость пользовательской части всё ещё нуждается в осмысленном рассмотрении и последующей технической проверке.

Власть следующей реализации

Сохранённый owner имеет право выбрать V2, а в текущей модели и следующую подходящую реализацию. Библиотечная проверка UUPS не гарантирует, что новый код оставит onlyOwner, прежние правила публикации или возможность дальнейшего upgrade. Авторизованный субъект может выбрать несовместимое либо нежелательное поведение.

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

Откат на прежнюю implementation тоже не является универсальной кнопкой исправления. После новых записей V2 старый код может не понимать добавленное состояние или его последствия. Нужно отдельно решить, допустим ли возврат и какой договор данных сохраняется. Название rollback не отменяет возникшие записи истории.

Итоговый учебный отчёт

Сохраните старую и новую implementation, адрес P, owner, step calldata и ожидаемые values courses и epoch после операции. Отдельно укажите, что изменилось: появился счётчик и новый шаг настройки. Что сохранилось: смысл курса, разрешённый owner и текущие записи. Такой отчёт позволяет оценить конкретный релиз без пересказа всей переписки.

Для упражнения рассмотрите успешный initializeV2(1), нулевой initialEpoch, постороннего вызывающего и повторный шаг. Затем мысленно переместите courses перед новым полем и объясните, почему это другой риск. Код не исполняется и проверки не запускаются; техническое подтверждение совместимости остаётся будущей отдельной работой перед любой публикацией.

Значение по умолчанию до нового шага

Если owner сменит implementation на V2 с пустым data, editorialEpoch первоначально останется нулевым в прежнем storage P. Функция advanceEpoch уже доступна по новому коду и может сделать его единицей. Это отличается от объявленного атомарного initializeV2(1), хотя итоговое число иногда совпадёт. Для процедуры релиза важен маршрут настройки и сохранённая версия, а не только случайное равенство одного getter.

Поэтому план заранее определяет, какие вызовы должны быть доступны до потребления нового шага. Показанная небольшая V2 демонстрирует этот вопрос явно; для иной бизнес-логики могут понадобиться дополнительные условия готовности.

Для самостоятельного уточнения: UUPSUpgradeable.sol (5.4.0), Initializable.sol (5.4.0), layout_in_storage.html, writing-upgradeable.

Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.

Оглавление серии