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

OpenZeppelin Governor: предложение и подсчёт голосов

Источник голосов ещё не является системой предложений. Нужны предмет решения, время голосования, правило участия и исполнение. Соберём учебный LibraryGovernor из модулей OpenZeppelin Contracts 5.4.0. Его задача — показать последовательность proposal и различие между порогом создания, кворумом и перевесом голосов.

Используем LibraryVotes предыдущих уроков. Часы токена работают в номерах блоков. Настроим задержку один блок, период сто блоков, порог создания десять LV и кворум четыре процента исторического выпуска. Это учебные величины для разбора, не готовая политика управления реальной организацией.

Композиция модулей

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {Governor} from "@openzeppelin/contracts/governance/Governor.sol";
import {GovernorSettings} from "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import {GovernorCountingSimple} from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotes} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import {GovernorVotesQuorumFraction} from "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";

contract LibraryGovernor is Governor, GovernorSettings, GovernorCountingSimple,
    GovernorVotes, GovernorVotesQuorumFraction
{
    constructor(IVotes votes)
        Governor("LibraryGovernor") GovernorSettings(1, 100, 10 * 10 ** 18)
        GovernorVotes(votes) GovernorVotesQuorumFraction(4) {}
    function votingDelay() public view override(Governor, GovernorSettings)
        returns (uint256) { return super.votingDelay(); }
    function votingPeriod() public view override(Governor, GovernorSettings)
        returns (uint256) { return super.votingPeriod(); }
    function proposalThreshold() public view override(Governor, GovernorSettings)
        returns (uint256) { return super.proposalThreshold(); }
    function quorum(uint256 t) public view
        override(Governor, GovernorVotesQuorumFraction)
        returns (uint256) { return super.quorum(t); }
    function clock() public view override(Governor, GovernorVotes)
        returns (uint48) { return super.clock(); }
    function CLOCK_MODE() public view override(Governor, GovernorVotes)
        returns (string memory) { return super.CLOCK_MODE(); }
}

Листинг полный для Governor без Timelock. Повторные override явно разрешают унаследованные определения. Мы не запускаем компиляцию, поэтому не называем его собранным артефактом. Версия, исходные файлы и ожидаемая композиция сохранены для последующей технической проверки.

Что именно предлагается

Предложение содержит одинаково упорядоченные массивы target addresses, values и calldatas, а также описание. Для реестра это может быть один target CourseCatalog, value ноль и calldata publish(17,digest). digest в согласованном веб-примере является SHA-256 точных сохранённых байтов манифеста, но Governor не проверяет их содержание.

Описание не заменяет фактические параметры вызова. Красивый текст «обновить урок» может сопровождать другую цель или другие байты. Перед голосованием интерфейс должен декодировать известный ABI и показывать полный адрес, курс и digest. Для неизвестной цели нельзя притворяться, что смысл надёжно установлен по названию.

Идентификатор связан с набором параметров и хешем описания. В default hashProposal он не включает автоматически сеть или адрес Governor. Поэтому приложение хранит эти контексты отдельно. Совпадение proposalId в двух экземплярах не означает одно общее предложение и общий результат.

Порог создания

Десять LV — требование к историческому делегированному весу proposer на предшествующей точке clock, когда применяется положительный threshold. Оно не означает, что для принятия достаточно десяти голосов. Человек с большим балансом, но без подходящего делегирования, может не пройти порог.

После создания proposal сначала находится в Pending. При текущей точке, равной snapshot, выбранный Governor ещё считает его ожидающим. Active начинается позже snapshot; после deadline голосование завершается. Поэтому границы нужно читать из конкретной реализации, а не предполагать универсальное включение момента начала.

Наш период измеряется блоками. Если приложение подпишет его как сто секунд, оно сообщит неверный договор. Для оценочного времени можно показать отдельный расчёт с неопределённостью, но фактический статус определяется clock и сохранёнными границами предложения.

Три варианта голоса

GovernorCountingSimple принимает Against=0, For=1 и Abstain=2. Вес берётся из исторического снимка, а участие данного account отмечается для данного proposalId. Повторное castVote тем же account отклоняется. Перенос текущих токенов после snapshot не даёт этому же снимку новый независимый объём.

Для кворума данной композиции считаются For плюс Abstain. Against не добавляется в эту сумму. Для успеха дополнительно требуется For>Against. Поэтому кворум и перевес являются двумя отдельными условиями, а не одной надписью «большинство проголосовало».

Если исторический supply равен тысяче LV, кворум составляет 40 LV. Сценарий For=30, Abstain=10, Against=20 проходит оба условия. Сценарий For=30, Abstain=0, Against=20 имеет перевес, но не кворум. Сценарий For=20, Abstain=20, Against=20 достигает кворума, но не строгого перевеса.

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

Результат не исполняется сам

Succeeded означает, что голосование удовлетворило правилам. Это ещё не receipt вызова publish в целевом реестре. Исполнитель должен вызвать предусмотренный execute с теми же параметрами. Если target отклоняет вызов, завершённого изменения курса не возникает.

В Governor без Timelock непосредственным вызывающим для target будет Governor. Поэтому CourseCatalog должен назначить именно его publisher при создании отдельного подходящего реестра. Старый immutable publisher невозможно просто заменить голосованием по желанию интерфейса. Композиция полномочий планируется заранее.

Настройки GovernorSettings могут изменяться через governance, а не свободным setter произвольного администратора. Это не отменяет необходимости анализировать, кто контролирует достаточный вес и какие другие маршруты исполнения доступны. Подключение библиотечных модулей не доказывает независимость участников.

Для задания подготовьте одно предложение о публикации, выпишите snapshot, deadline, threshold и quorum, затем рассмотрите три комбинации голосов выше. Отдельно укажите фактический publisher целевого реестра. В следующей главе добавим задержанное исполнение и увидим, почему его субъектом становится другой контракт.

Неизвестное предложение и отсутствие участия

state для неизвестного proposalId в выбранном Governor отклоняет запрос, а не выдаёт успешный статус Pending для любой числовой строки. Интерфейс должен отличать отсутствующий объект от известного ожидающего предложения. Ошибка ABI или неверный адрес тоже может дать иной сбой чтения и не должна автоматически превращаться в надпись «голосование ещё не началось».

Нулевой threshold у другой композиции означает отсутствие положительного порога создания в данной проверке. Это может облегчить создание множества предложений. Наличие кворума не запрещает сам факт их подготовки. Поэтому обнаружение, отображение и организационная политика предложений требуют отдельного рассмотрения, даже если исполнение случайных действий ограничено голосами.

Если R пытается голосовать без исторического веса, сам вызов не создаёт ему вес из будущего баланса. В простом подсчёте участие account может быть отмечено и с нулевым вкладом. Последующее увеличение токенов не является предусмотренным способом повторно изменить тот же голос. Пользователь должен видеть вес перед отправкой и понимать окончательность выбранного участия в конкретной модели.

Также строка reason служит объяснению позиции, но не определяет результат подсчёта. Длинный убедительный текст не добавляет числового веса. На странице решения полезно показывать оба слоя отдельно: аргументы участников и точные totals из состояния.

Для самостоятельного уточнения: Governor.sol (5.4.0), GovernorCountingSimple.sol (5.4.0), GovernorVotesQuorumFraction.sol (5.4.0), GovernorSettings.sol (5.4.0).

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

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