Ownable2Step в Solidity: двухэтапная передача владельца
В простой Ownable-модели неверный ненулевой адрес сразу получает право. Для редакции полезнее передача, которую новый субъект подтверждает сам. OpenZeppelin предоставляет Ownable2Step: сначала назначается кандидат, затем кандидат принимает полномочия отдельным вызовом.
В этом уроке создадим явный вариант TwoStepCourseCatalog на базе знакомого OwnedCourseCatalog. Тип, экземпляр и интерфейс меняются осознанно. Базовый CourseCatalog с immutable publisher остаётся отдельным исходником.
Два значения вместо одного
owner() сообщает действующего владельца. pendingOwner() сообщает адрес кандидата. После начала передачи эти значения не становятся равными автоматически. Предыдущий владелец продолжает выполнять свои методы, пока новый не вызовет acceptOwnership() согласно правилам библиотеки.
Исходно: owner=P, pendingOwner=0
P вызывает transferOwnership(N)
После: owner=P, pendingOwner=N
N вызывает acceptOwnership()
После: owner=N, pendingOwner=0
Это последовательность ожидаемых результатов. В ней обе подписи имеют самостоятельный смысл: P предлагает передачу, N принимает. Присутствие кандидата в поле не делает его владельцем, поэтому интерфейс не должен преждевременно включать ему публикацию.
Изменение исходного файла
Возьмите полный OwnedCourseCatalog из предыдущего урока. В новом файле замените импорт и объявление наследования, сохранив тела publish и deactivate без изменений:
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
contract TwoStepCourseCatalog is Ownable2Step {
// Course, courses, ошибки, события и оба метода —
// те же определения из полного OwnedCourseCatalog.
constructor(address initialOwner) Ownable(initialOwner) {}
}
Это явно обозначенная замена части файла, а не полный контракт. Все необходимые собственные определения находятся на предыдущей странице. Импорт Ownable нужен для явного вызова его конструктора. Ownable2Step наследует этот механизм, но не добавляет собственного аргумента первоначального владельца вместо него.
Такой способ показывает, что двухэтапность относится к передаче, а не к другой модели курсов. Публикация остаётся onlyOwner, поэтому на промежуточном этапе P может обновлять hash, N ещё не может. После принятия роли меняются, записи сохраняются.
Кто вправе принять
Текущий кандидат N вызывает acceptOwnership() со своего субъекта. Другой адрес R не может принять предложение, даже если видит pendingOwner и знает все реквизиты. Подписание произвольного сообщения вне предусмотренного маршрута также не меняет право в этом контракте автоматически.
Если N является контрактом, у него должна быть возможность выполнить нужный вызов. Двухэтапность помогает обнаружить такую способность до потери прежнего владельца: пока принятие не прошло, P сохраняется. Однако это не гарантирует, что управление N останется доступным навсегда или что его внутренняя модель безопасна.
Поэтому проверка включает не только успешное принятие, но и понимание нового субъекта. Для мультиподписи — порог и процедуры изменения подписантов. Для timelock — предложение, задержка и исполнение. Реестр видит итоговый адрес, а его владельцы отвечают за собственную эксплуатацию.
Отмена и замена кандидата
В закреплённом исходнике Ownable2Step 5.4.0 transferOwnership допускает нулевой pending-адрес для отмены незавершённого предложения. Это отличается от прямой transferOwnership Ownable, где нулевой новый владелец отклоняется. Нулевой кандидат здесь не является отказом от действующего owner.
P может также начать передачу другому кандидату M, заменив прежнее pendingOwner=N. После этого N не должен принять старое предложение: проверяется текущее значение. Такая возможность полезна при ошибке реквизитов, но интерфейс должен показывать актуальное предложение, а не сохранённый скриншот формы.
| Действие | Owner | Pending | Значение |
|---|---|---|---|
| Начать N | P | N | Предложение |
| Заменить на M | P | M | Старое предложение не актуально |
| Отменить нулём | P | 0 | Право P сохранено |
| Принять текущим M | M | 0 | Передача завершена |
Что остаётся опасным
Двухэтапность не запрещает действующему владельцу отказаться от владения через предусмотренную наследуемую операцию. Она также не вводит обязательную задержку между предложением и принятием. Если нужны эти ограничения, выбирается отдельная модель, а не предполагается, что слово two-step их включает.
Нельзя считать принятие проверкой качества адреса в юридическом смысле. N доказал возможность выполнить действие как субъект. Связь с редакцией устанавливается организационно. Даже адрес, который успешно принял роль, может принадлежать не тому участнику, если реквизиты были получены из неверного канала.
Полезно также заранее решить, как сайт показывает переход. На промежуточном этапе он может сообщать «передача предложена N, действующий владелец P». После результата — «владелец N». Сокращённая надпись «новый владелец N» до принятия вводит человека в заблуждение.
Восстановление интерфейса
Если страница закрылась после начала передачи, повторное открытие должно прочитать оба getter. Они показывают текущее состояние независимо от локальной памяти браузера. Если неизвестен результат принятия, не нужно автоматически повторять назначение: сначала узнают owner и pendingOwner, затем связывают их с результатами действий.
Для упражнения предположите, что P публикует H2 между предложением и принятием N. Выпуск меняется при действующем P, а затем роль переходит. N получает реестр уже с H2. Передача полномочий не переносит состояние назад к моменту предложения. Это ещё одно различие времени намерения и времени исполнения.
Мы получили безопаснее устроенную процедуру подтверждения адреса, сохранив её реальные границы. Следующий шаг — разделить права публикации и отзыва через роли. Исходник Ownable2Step 5.4.0 фиксирует выбранное поведение отмены и принятия.
Передача между контрактами управления
Пусть будущим N выбран timelock. У него должна существовать процедура, которая в нужный момент вызывает acceptOwnership. Простое заполнение pendingOwner не доказывает, что команда подготовила такой маршрут. До принятия прежний P сохраняет возможность исправить предложение, и это главное преимущество промежуточного состояния.
Если пользователь видит две ожидающие транзакции в кошельке, нужно выяснить их назначение. Первая предлагает N, вторая может предлагать M вместо него. Принятие старым N после успешной замены уже не проходит. Порядок исполнения важнее порядка кликов в разных вкладках.
Сохраните в отчёте действующего owner, текущий pendingOwner и конкретное действие. Не называйте N владельцем по одному подтверждённому началу передачи. Также не считайте отсутствие pending после неизвестного результата достаточным объяснением: предложение могло быть принято, отменено или заменено другой операцией. Текущие getter и история передачи вместе позволяют восстановить последовательность.
Этот случай показывает, что двухэтапный процесс состоит не только из двух кнопок. У каждой кнопки известный субъект и актуальная предпосылка.
Для самостоятельного уточнения: Ownable2Step.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.