Solidity: история голосов и снимки состояния
Предположим, R отдал тысячу единиц представителю S, а после начала голосования передал их Q. Если считать голоса по текущему балансу в момент каждого нажатия, одни и те же единицы могут участвовать через меняющееся владение. Исторический снимок задаёт определённую точку, по состоянию которой определяется вес предложения.
Продолжаем LibraryVotes без изменения исходника. Его ERC20Votes хранит checkpoints делегированного веса и выпуска. Публичный getPastVotes(account,timepoint) читает значение в прошлом, а getPastTotalSupply(timepoint) — исторический общий объём голосующих единиц. Это не произвольный вызов современного balanceOf с дополнительным номером.
Шкала времени
В нашем закреплённом Votes clock() по умолчанию возвращает номер блока, а CLOCK_MODE описывает режим blocknumber. Значит, timepoint 100 означает блок 100, а не сотую секунду и не Unix timestamp. Обе величины могут иметь целочисленный тип, но их предметный смысл различается.
GovernorVotes согласует свои часы с токеном, если тот поддерживает нужный интерфейс. Поэтому длительность голосования в настройке нужно читать в единицах выбранного clock. Сто блоков не равно заранее гарантированному числу секунд, особенно при переносе между сетями и их режимами создания блоков.
Для собственного другого токена можно переопределить timestamp-часы вместе с соответствующим CLOCK_MODE. Нельзя изменить только функцию clock, оставив старое описание режима. Такое несоответствие нарушает договор клиента и может отклоняться самой библиотечной проверкой. Мы не выполняем это изменение в LibraryVotes.
Прошедшая точка
Выбранная реализация требует timepoint<clock(). Запрос текущей точки или будущей отклоняется. Причина в том, что текущий блок ещё может содержать изменения веса, а исторический результат должен быть определён завершённой точкой. Для получения нынешнего значения имеется getVotes.
clock() = 105
getPastVotes(S, 104) — допустимый исторический запрос
getPastVotes(S, 105) — отклоняется как не прошедшая точка
getVotes(S) — текущее значение без такого требования
Это схема ожидаемого чтения, не выполненный вывод узла. В лабораторном окружении управление блоками может дать удобный разбор границы, но в текущей задаче узел, приложение и проверки не запускаются. Мы фиксируем условие непосредственно по исходнику закреплённого Votes.
Изменения внутри одного блока
Checkpoint для одной точки времени представляет итоговое значение на этой точке, а не полный журнал каждого внутреннего действия. Если R несколько раз сменил делегата в одном блоке, чтение веса на этом блоке после его завершения соответствует результату данного блока. Для последовательности отдельных действий используются события и receipt.
Поэтому numCheckpoints(S) не является числом всех транзакций делегирования. Несколько обновлений в одной точке могут объединяться в последнюю запись. Интерфейс исторического участия и журнал действий должны использовать разные способы представления, чтобы не обещать лишних подробностей из одного массива checkpoints.
Также значение до первой записи ожидаемо нулевое для представителя. Это не обязательно означает ошибку загрузки. Адрес мог ещё не получить делегирование. Приложению нужно отличать корректный ноль исторического веса от недоступного RPC и неверного адреса токена.
Один снимок, разные последующие балансы
Пусть в блоке 100 S имеет вес 1000 LV от R. В блоке 101 R передаёт всё Q, который выбирает T. При clock больше 101 getPastVotes(S,100) остаётся 1000 LV, а getVotes(S) может быть нулём. Историческое значение не переписывается из-за поздней передачи.
Это обеспечивает стабильный источник веса для предложения, использующего snapshot 100. Однако сам механизм не создаёт отметку «этот представитель уже голосовал». Запрет повторного голоса по одному предложению реализуется отдельным подсчётом Governor. Исторический источник и учёт участия решают разные части задачи.
Снимок также не доказывает справедливость распределения или независимость участников. Один субъект может контролировать несколько адресов, а большой объём может быть сосредоточен у одного держателя. Техническое правило предотвращает определённый повторный учёт, но не устанавливает автоматически общественное качество управления.
Исторический выпуск и кворум
getPastTotalSupply включает все доступные голосующие единицы, в том числе те, которые тогда не были делегированы. Это отличается от суммы веса участвовавших представителей. Если будущий Governor считает четыре процента от выпуска, отсутствие делегирования большинства держателей не уменьшает знаменатель само по себе.
Для тысячи LV четыре процента составляют 40 LV в базовых единицах. Если делегированы только 20 LV, такой порог не достигнуть их голосами при неизменном snapshot. Пользовательский экран должен объяснить источник порога, чтобы нулевая активность не выглядела неизвестной ошибкой контракта.
Наш токен не имеет внешнего mint и burn, но исторический учёт выпуска всё равно существует. В иной модели их позднее появление потребовало бы рассмотреть, на какую точку берётся supply и как изменяется доля. Использование текущего totalSupply вместо исторического изменило бы требования старого предложения.
История в интерфейсе
Храните рядом с proposalId адрес источника голосов, режим часов и snapshot. Число snapshot без этого контекста невозможно правильно интерпретировать после смены сети или подключения другого токена. Для постоянной ссылки на голосование нужны также адрес Governor и сеть.
Клиент может показывать текущий вес как справочную величину, но кнопка голосования должна объяснять, какой вес будет использован для конкретного предложения. Надпись «у вас 100 LV» недостаточна, если исторический делегированный вес равен нулю. Повторное делегирование после snapshot не гарантирует исправления этого предложения.
Для самостоятельного задания разместите mint, delegate, transfer и смену представителя на четырёх разных номерах блоков. Запишите ожидаемый getPastVotes для S и T на каждой прошедшей точке, затем отдельно текущий getVotes. Второй вариант разместите в одном блоке и объясните, какие подробности сохранят checkpoints, а какие придётся восстанавливать из событий. Этот разбор подготавливает работу с реальным состоянием предложения в следующем уроке.
При получении исторического результата фиксируйте также выбранную цепочку и договор подтверждения. Номер блока сам по себе не является глобальным временем. Если приложение поддерживает несколько сетей, snapshot 100 в одной не совпадает с snapshot 100 в другой. Это ещё одна причина хранить контекст предложения рядом с его числовыми полями.
Для самостоятельного уточнения: Votes.sol (5.4.0), eip-6372, eip-5805.
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.