Ownable в Solidity: первоначальный владелец контракта
В первом курсе издатель выбирался конструктором и не менялся. Теперь редакции нужна передача управления другому субъекту. Это новое требование, поэтому создадим отдельный OwnedCourseCatalog. Исходный CourseCatalog остаётся прежним: его immutable publisher не начинает автоматически передаваться.
Для новой модели используем OpenZeppelin Contracts 5.4.0 и Solidity 0.8.30. Точная версия библиотеки важна: в 5.x Ownable получает первоначального владельца аргументом конструктора. Старый урок с неявным назначением разворачивающего аккаунта нельзя копировать без изменения. Зависимость пока не устанавливается, листинг не компилируется.
Владелец как субъект полномочия
Ownable предоставляет owner(), onlyOwner, передачу права и отказ от владения. Это простой механизм одного управляющего адреса, который может быть внешним аккаунтом либо контрактом управления. Назначение адреса не раскрывает личность человека и не проверяет внутреннюю безопасность его кошелька.
В нашем варианте владелец выполняет публикацию и деактивацию. Поля курса сохраняют знакомые значения: hash и active. Публикация по-прежнему заменяет текущий выпуск и активирует его, отзыв сохраняет hash. Новое свойство относится только к субъекту этих операций.
Полный отдельный реестр
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract OwnedCourseCatalog is Ownable {
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(address initialOwner) Ownable(initialOwner) {}
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);
}
}
Путь импорта относится к зафиксированной библиотеке. Конструктор передаёт initialOwner в базовый Ownable. Нулевой первоначальный адрес отвергается библиотекой. В самом реестре нет отдельного поля publisher: текущее право читается через owner(). Это другой ABI управления, хотя методы курсов похожи на первоначальные.
Заменять getter publisher на owner в старом приложении без объявления новой модели нельзя. Клиент должен знать адрес нового экземпляра и его интерфейс. Прежний реестр продолжает жить отдельно, даже если сайт переключится на новую карточку управления.
Передача и изменение права
Пусть owner=P. Публикация курса 17 от P разрешена. После успешной transferOwnership(N) текущим владельцем становится N. Следующая попытка P изменить курс отклоняется; N получает оба административных метода. В простой Ownable-модели право передаётся непосредственно, без обязательного принятия новым адресом.
Это удобно, но создаёт важный ошибочный случай. Если P передал неверный ненулевой адрес, проверка формата не исправляет ошибку. Прежний владелец уже не может обычным образом вернуть право. Поэтому передача требует проверки полных реквизитов, сети и способности нового субъекта выполнять вызовы.
При выборе контракта управления нужно изучить его маршрут исполнения. Адрес может существовать, но не поддерживать нужную операцию. Ownable сравнит непосредственный субъект, однако не построит ему механизм отправки транзакции. Следующая страница введёт двухэтапную передачу, которая уменьшает риск непринятого назначения.
Отказ от владения
renounceOwnership() оставляет владельца нулевым. В показанном коде это отключает обычную возможность пользоваться методами onlyOwner. Такая операция не превращает реестр в демократическую систему, где любой может публиковать. Напротив, разрешённый субъект исчезает.
Если библиотека должна оставаться редактируемой, добровольный отказ может нарушить её эксплуатационные требования. В иной модели отказ используется для намеренного окончательного прекращения административного управления. Требование следует определить заранее. Сам факт наличия метода в библиотеке не означает, что пользователь обязательно должен его выполнять.
| Состояние управления | Кто меняет выпуск | Текущие записи |
|---|---|---|
| owner=P | P | Сохраняются |
| После передачи N | N | Сохраняются |
| После renounce | Обычного владельца нет | Сохраняются |
Права управления и данные курсов независимы. Передача не меняет hash автоматически. Отказ не стирает историю. Поэтому интерфейс должен сообщать изменение субъекта отдельно от содержания выпуска.
Разделение создания и владения
Технический аккаунт D может развернуть реестр с initialOwner=P. После этого D не получает право только из факта размещения. Это позволяет не делать временный технический ключ постоянным редакционным владельцем. Но процедура выпуска должна сохранить аргумент конструктора и подтвердить результат owner().
Для последующего отчёта важны сеть, адрес, текущий владелец и события передачи. Устаревшая серверная сессия не возвращает право прежнему субъекту. Предварительная проверка аккаунта в интерфейсе помогает человеку, а окончательная проверка по-прежнему происходит в контракте.
Также необходимо учитывать изменение права между чтением и исполнением. P мог открыть форму, затем право перешло N. Подготовленная публикация P теперь отклоняется. Это ожидаемое поведение текущей модели, а не неисправность кнопки. Клиент должен обновить данные и объяснить актуального владельца.
Цена новой возможности
Передаваемый владелец решает одну проблему immutable-модели: позволяет сменить субъект штатным маршрутом. Но добавляет операции, которые сами требуют защиты и понимания. Ошибочная передача способна остановить редакцию, скомпрометированный владелец может назначить другой адрес, а renounce может стать необратимым отказом.
Поэтому будущие способы управления будут выбирать исходя из требований: двухэтапное принятие, роли, задержка и мультиподпись. Они не появляются от одного наследования Ownable. Полезно сначала сохранить маленькую модель и прочитать её ограничения целиком. Документация доступа и исходник Ownable 5.4.0 помогают сверить этот конкретный договор.
Два независимых выпуска управления
Если новый OwnedCourseCatalog создаётся рядом с прежним CourseCatalog, передача владельца относится только к новому адресу. Адрес P в прежнем immutable-реестре не меняется. Сайт, который поддерживает оба источника, должен показывать, для какого экземпляра выполняется административное действие.
Для упражнения назначьте новому владельцу N, но оставьте в клиенте старый адрес C1. Чтение publisher старого контракта продолжает возвращать P, а методы управления Ownable по этому адресу отсутствуют. Ошибка не исправляется обновлением названия кнопки: нужны согласованные сеть, адрес и ABI.
Есть и менее заметный вариант. Клиент переключился на новый экземпляр, но показывает кэш прежнего owner. P видит доступную кнопку и получает отказ. Правильный путь — обновить данные и объяснить смену субъекта, а не убирать onlyOwner для удобства формы. Окончательная проверка поддерживает реальную модель несмотря на устаревшее отображение.
Таким образом, передаваемое право расширяет жизненный цикл конкретного реестра. Оно не мигрирует содержимое и не соединяет все одноимённые контракты библиотеки в одну общую администрацию.
Для самостоятельного уточнения: Ownable.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.