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

Solidity Governor: голосование, очередь и исполнение

В предыдущем уроке успешное предложение ещё требовало execute. Добавим дополнительную задержку между решением и целевым вызовом. GovernorTimelockControl связывает Governor с TimelockController. Теперь Succeeded, Queued и Executed обозначают разные этапы, которые нельзя объединять в одну зелёную отметку на странице.

Начнём с главного изменения полномочия. Через эту композицию фактическую публикацию выполняет Timelock. Следовательно, publisher нового CourseCatalog должен быть адресом Timelock, а не Governor и не адресом голосующего человека. Базовый реестр остаётся прежним; изменяется выбранный при его создании субъект.

Расширение исходника Governor

Следующий блок является точной схемой дополнения предыдущего LibraryGovernor, а не самостоятельным компилируемым файлом. Для полной модели создайте отдельный LibraryGovernorDelayed, сохраните прежние настройки и override часов, добавьте импорт и базовый класс GovernorTimelockControl, а в конструктор передайте TimelockController.

import {GovernorTimelockControl} from
    "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import {TimelockController} from
    "@openzeppelin/contracts/governance/TimelockController.sol";

// Дополнительный базовый конструктор:
// GovernorTimelockControl(clockContract)

function state(uint256 id) public view
    override(Governor, GovernorTimelockControl) returns (ProposalState)
{ return super.state(id); }
function proposalNeedsQueuing(uint256 id) public view
    override(Governor, GovernorTimelockControl) returns (bool)
{ return super.proposalNeedsQueuing(id); }
function _queueOperations(uint256 id, address[] memory targets, uint256[] memory values,
    bytes[] memory calls, bytes32 descriptionHash)
    internal override(Governor, GovernorTimelockControl) returns (uint48)
{ return super._queueOperations(id, targets, values, calls, descriptionHash); }
function _executeOperations(uint256 id, address[] memory targets, uint256[] memory values,
    bytes[] memory calls, bytes32 descriptionHash)
    internal override(Governor, GovernorTimelockControl)
{ super._executeOperations(id, targets, values, calls, descriptionHash); }
function _cancel(address[] memory targets, uint256[] memory values,
    bytes[] memory calls, bytes32 descriptionHash)
    internal override(Governor, GovernorTimelockControl) returns (uint256)
{ return super._cancel(targets, values, calls, descriptionHash); }
function _executor() internal view override(Governor, GovernorTimelockControl)
    returns (address)
{ return super._executor(); }

Явные override разрешают несколько определений общих внутренних точек. Сами по себе строки import ничего не связывают: необходимы наследование, базовый конструктор и правильные адреса ролей. Мы сохраняем форму интеграции как учебный фрагмент и не выдаём её за собранный новый контракт.

Настройка маршрута

Governor должен иметь PROPOSER_ROLE Timelock, чтобы ставить операции в очередь. Ему также предоставляется предусмотренное право исполнения и, по выбранной политике, отмены. У Timelock остаётся собственное управление ролями. Если bootstrap-администратор временно использовался, порядок прекращения его прав документируется отдельно.

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

Лишний canceller может препятствовать исполнению одобренных действий, отменяя pending-операции. Это другой вид влияния. Система может намеренно иметь аварийного отменяющего субъекта, но тогда интерфейс и описание не должны обещать, что только голосование определяет судьбу предложения.

Два вида времени

Задержка и период голосования нашей модели используют blocknumber-часы токена. Минимальная задержка Timelock измеряется секундами относительно block.timestamp. Равные целые значения в настройке не означают одинаковую продолжительность. На схеме показывайте обе шкалы и соответствующие getter.

После Succeeded вызывается queue с теми же targets, values, calldatas и descriptionHash. Расширение планирует batch в Timelock с его текущей минимальной задержкой. Governor хранит связь proposalId с идентификатором queued-операции и показывает соответствующий статус.

Когда timestamp достигает готовности, операция ещё не выполняется самостоятельно. Кто-то с разрешённым маршрутом должен отправить execute. Ожидающий исполнитель платит gas своей транзакции по правилам сети. Отсутствие автоматического фонового процесса следует объяснять заранее, чтобы очередь не воспринималась как зависшая публикация.

Те же байты при исполнении

Параметры должны совпасть с согласованным предложением. Если заменить digest H1 на H2, это другой набор calldata и другой идентификатор операции. Название предложения не даёт права незаметно исправить байты «по смыслу». Для нового выпуска требуется новая допустимая операция по правилам управления.

При успешном execute Timelock вызывает CourseCatalog.publish. Реестр видит msg.sender=Timelock, проверяет ненулевые id и hash, записывает текущий выпуск и активирует курс. Он не читает количество голосов: эта проверка выполнена в другом слое композиции.

Если реестр настроен с publisher=Governor, тот же маршрут отклонится Unauthorized. Голосование могло пройти совершенно корректно, но целевое полномочие не соответствует фактическому caller. Это конкретная ошибка сборки экземпляров, которую нельзя исправить повторным нажатием execute без изменения доступного договора.

Отклонение и отмена

Ошибка target откатывает вызов исполнения. Не появляется частичная публикация с ложным статусом успешного завершения. Для batch один отклонённый элемент может отменить весь набор. Поэтому каждую цель и зависимость параметров рассматривают заранее, даже если голосование содержит красивую общую программу действий.

cancel pending-операции не отменяет уже состоявшийся publish. После успешного изменения требуется новое действие, например публикация исправленного hash либо deactivate в подходящей модели полномочий. Задержка предоставляет время до исполнения, но не редактирует прошлую историю сети.

В закреплённом расширении прямое исполнение queued-операции через Timelock отражается в state Governor как Executed, а отмена через Timelock — как Canceled. Клиент должен читать согласованную модель состояния вместо собственной вечной отметки Queued после первой отправки queue.

Завершённый маршрут

Для учебного отчёта сохраните сеть, адрес токена, Governor, Timelock и CourseCatalog, параметры предложения и operation ID. Эти адреса выполняют разные функции. Одной ссылки на Governor недостаточно, чтобы проверить фактический current hash курса после исполнения.

Завершите мысленным сценарием: предложение прошло голосование, очередь готова, посторонний без executor пробует execute, затем разрешённый субъект исполняет правильные байты. Второй вариант задайте с неверным publisher target. Объясните статусы и отсутствие изменения курса после ошибки. Сейчас мы документируем ожидаемый маршрут и не запускаем контракты, узлы или проверки.

Идентификатор очереди и повторное чтение

Governor proposalId и Timelock operation ID являются разными ключами. Первый относится к предложению и его правилам голосования, второй к запланированному исполнению. Нельзя отправлять один getter с другим числом только потому, что оба выглядят как 32-байтовое значение. Храните явную подпись типа рядом с каждым идентификатором.

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

Если внешний отменяющий субъект удалил pending-операцию, это не означает, что голоса For исчезли. Изменяется судьба маршрута исполнения, а историческое голосование сохраняет свои данные. Поэтому страница отчёта может показывать одобрение и последующую отмену одновременно, без противоречивого заявления об отсутствии решения.

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

Для самостоятельного уточнения: GovernorTimelockControl.sol (5.4.0), TimelockController.sol (5.4.0).

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

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