Initializer в Solidity: настройка upgradeable-контракта
Теперь создадим конкретную модель для прокси: UpgradeableCourseCatalogV1. Она сохраняет знакомые поля contentHash и active, но заменяет immutable publisher на административный owner и разрешённый путь обновления. Это явно новый договор. Исходный CourseCatalog не меняется и не становится обновляемым после публикации нового файла.
Используем две согласованные зависимости OpenZeppelin 5.4.0: Contracts для ERC1967Proxy и Contracts Upgradeable для initializer-модулей. Конструктор реализации блокирует её собственную инициализацию. Настройка owner прокси выполняется initialize через delegatecall, поэтому живёт в другом состоянии.
Полная первоначальная реализация
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract UpgradeableCourseCatalogV1 is OwnableUpgradeable, UUPSUpgradeable {
error InvalidCourse();
error NotActive();
struct Course { bytes32 contentHash; bool active; }
mapping(uint256 => Course) public courses;
event Published(uint256 indexed courseId, bytes32 contentHash);
event Deactivated(uint256 indexed courseId);
constructor() { _disableInitializers(); }
function initialize(address admin) external initializer {
__Ownable_init(admin);
__UUPSUpgradeable_init();
}
function publish(uint256 id, bytes32 digest) external onlyOwner {
if (id == 0 || digest == bytes32(0)) revert InvalidCourse();
courses[id] = Course(digest, true);
emit Published(id, digest);
}
function deactivate(uint256 id) external onlyOwner {
if (!courses[id].active) revert NotActive();
courses[id].active = false;
emit Deactivated(id);
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
Это полный учебный исходник реализации. В выбранном UUPSUpgradeable функции __UUPSUpgradeable_init имеются; их конкретное поведение берётся из закреплённого исходника, а не из старого чужого урока. Листинг не компилировался, зависимости не устанавливались, прокси не создавалась.
Отдельное создание прокси
Для будущего маршрута конструктор ERC1967Proxy получает адрес V1 и ABI-вызов initialize(admin). Следующий фрагмент показывает выражение, которое можно использовать в отдельно подготовленном фабричном коде. Это не полный исполняемый сценарий и не операция в текущем окружении.
bytes memory setup = abi.encodeCall(UpgradeableCourseCatalogV1.initialize, (admin));
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), setup);
Импорты и объявления admin, implementation и ERC1967Proxy должны быть определены в таком будущем файле явно. Важен атомарный смысл: создание и подготовка состояния выполняются в одной транзакции. Если initialize отклонится, успешной наполовину настроенной прокси из этого конструктора не получится.
Пустой initData оставил бы состояние P ненастроенным. В показанном initialize нет отдельной внешней авторизации на первую настройку: его безопасность опирается на корректное атомарное создание с нужными параметрами и однократный механизм. Поэтому нельзя разворачивать пустую прокси и надеяться позднее быстро вызвать initialize первым.
Два состояния Initializable
_disableInitializers в конструкторе работает с собственным storage implementation I. Через делегированный initialize используется storage прокси P, где первоначальная версия ещё не потреблена. Блокировка I не мешает предусмотренной первоначальной настройке P, потому что это разные адреса состояния.
После успешного initialize обычное повторное initialize через P отклоняется. Нельзя сменить owner, ещё раз отправив тот же первоначальный метод. Для административной передачи используется унаследованный transferOwnership с его проверками, а будущая миграция новых полей потребует отдельного согласованного reinitializer.
Выбранный initializer допускает специальный контекст первоначального конструирования, описанный библиотекой. Поэтому формулируем гарантию точно: обычная повторная внешняя настройка уже созданной прокси не допускается данным механизмом. Не заменяем чтение исходника чрезмерным универсальным утверждением о любых возможных вложенных вызовах.
Настройка родительских модулей
__Ownable_init(admin) устанавливает ненулевого owner для состояния P. Это важный шаг, который нельзя опустить из-за того, что родительский класс уже указан в inheritance. Наследование предоставляет код, но не выполняет автоматически каждый нужный initializer, как обычная конструкция базовых конструкторов.
onlyInitializing ограничивает внутренние родительские процедуры подходящим периодом настройки. Оно не является общим публичным правом любой будущей операции. Корректный порядок вызовов и отсутствие лишних повторений остаются обязанностью автора композиции.
Если позже добавится ещё один upgradeable-модуль, необходимо изучить его initializer и storage. Нельзя просто вызвать все функции с похожим окончанием init без анализа двойных цепочек родителей. Иначе общий родитель может получить повторное неидемпотентное обновление, а ожидаемая первоначальная политика изменится.
Полномочие обновления
_authorizeUpgrade защищён onlyOwner. Следовательно, тот же субъект может публиковать курс, отзывать его и выбирать следующую реализацию. Последнее полномочие шире первых двух: новая логика способна изменить многие будущие правила. Такое совмещение является нашей явной учебной политикой, не минимально необходимой властью редактора.
Передача owner изменяет и полномочие обновления. Отказ от owner прекращает обычный onlyOwner-маршрут публикации и upgrade в показанной реализации. Он не доказывает, что любые ранее выбранные будущие версии имеют ту же политику: анализ всегда относится к активному коду и доступным маршрутам.
Прямая попытка upgradeToAndCall у implementation не проходит onlyProxy предусмотренного UUPS. Это другой защитный механизм, чем заблокированная initialize. Первая проверка относится к контексту обновления, вторая — к версии настройки состояния. Их нельзя объяснить одним общим словом «закрыто».
Подтверждение подготовленного договора
В локальном плане сохраните I, P, admin и полные setup-байты. После будущего выполнения owner() следует читать через P, courses тоже через P, а активную реализацию определять отдельно. На странице отчёта не следует показывать пустое состояние I как результат настройки пользовательского реестра.
Для самостоятельного разбора объясните три ситуации: initialize по I после constructor; первый initialize через P с ненулевым admin; второй обычный initialize через P. Затем рассмотрите нулевой admin в атомарном конструкторном вызове и ожидаемый откат создания. Проверки пока не запускаются, но причины результата уже определены конкретным договором исходника.
Владелец выбран заранее
В initialize передаётся admin, а не неявно msg.sender. При создании через фабрику непосредственным caller делегированного setup может быть сама фабрика. Если бы owner назначался без анализа этим адресом, управление получилось бы у иного субъекта, чем ожидал оператор. Явный параметр требует проверки полных реквизитов, но делает намерение понятным.
Если admin является Timelock, его методы должны позволять будущие публикации и обновления по нужному маршруту. Ненулевой адрес ещё не доказывает способность действовать. Подобная ошибка не исправляется initializer, потому что он уже потреблён. Следующий допустимый административный переход должен проходить доступную политику реального owner.
При чтении setup сохраните также отсутствие платы: initialize в этом коде не payable. Отправка value по неподходящему маршруту может отклонить создание, а не внести депозит. Реестр не содержит обязательств хранения ETH, и добавление прокси не создаёт их автоматически.
Наконец, потреблённая версия Initializable является внутренним состоянием настройки. Она не заменяет внешнюю редакционную метку статьи или semantic version приложения. Для отчёта используйте отдельные понятные имена, чтобы цифра 1 не воспринималась одновременно как номер курса, версия компилятора и номер шага initialize.
Для самостоятельного уточнения: OwnableUpgradeable.sol (5.4.0), Initializable.sol (5.4.0), UUPSUpgradeable.sol (5.4.0), ERC1967Proxy.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.