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

Solidity: делегирование и сила голоса

Наличие ERC-20 не создаёт голосование автоматически. Библиотеке нужен источник веса, который хранит историю и позволяет владельцам делегировать участие. Создадим отдельный LibraryVotes на ERC20Votes OpenZeppelin 5.4.0. Это новый токен, а не переключатель, который превратит уже выпущенный LibraryCredit в управленческую систему.

Сначала разберём два числа. balanceOf(R) показывает, сколько единиц принадлежит R. getVotes(R) показывает текущий делегированный вес, который R может использовать как представитель. Они могут совпасть, различаться или иметь нулевое значение независимо друг от друга. Такая разница является предусмотренной моделью, а не ошибкой отображения.

Небольшой источник голосующих единиц

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

contract LibraryVotes is ERC20Votes {
    constructor(address initialHolder)
        ERC20("LibraryVotes", "LV") EIP712("LibraryVotes", "1")
    {
        _mint(initialHolder, 1000 * 10 ** 18);
    }
}

Листинг полный для выбранной задачи. Мы наследуем ERC20Votes как одну ветвь, поэтому не добавляем конфликтующий второй независимый путь обновления ERC20. EIP712 получает имя и версию домена для предусмотренных подписанных операций делегирования. Поддержка permit из урока 14 сюда не добавлена.

decimals остаётся стандартным значением 18 выбранного ERC20. Тысяча отображаемых единиц соответствует 1000×10¹⁸ базовых. Объём фиксирован конструктором, внешнего mint нет. Владение токеном не является правом редактора CourseCatalog, поскольку реестр проверяет своего publisher независимо от баланса.

Почему новый баланс ещё не голосует

После первоначального mint R получает тысячу единиц, но до делегирования его getVotes ожидаемо равен нулю. ERC20Votes не активирует самостоятельный вес каждого владельца автоматически. Это позволяет не вести отдельную историю представителя для человека, который не выбирал участие.

Чтобы участвовать самостоятельно, R вызывает delegate(R). После ожидаемого успеха delegates(R) равен R, а getVotes(R) отражает тысячу отображаемых единиц в базовом масштабе. Токены остаются на балансе R. Делегирование не является transfer и не отправляет представителю право собственности.

до delegate: balanceOf(R)=1000 LV; getVotes(R)=0
R выбирает delegate(R)
после delegate: balanceOf(R)=1000 LV; getVotes(R)=1000 LV

Здесь LV используется только как понятное отображение базовых целых. ABI возвращает большие uint256. Клиент голосования не должен сравнивать строку «1000» с необработанным числом 1000×10¹⁸. Порог предложения также выражается в том же масштабе источника голосов.

Представитель другого адреса

R может вызвать delegate(S). Его собственность не меняется, но вес переносится от прежнего представителя к S. Если S уже представлял Q, getVotes(S) становится суммой соответствующих делегированных единиц. Наличие собственного нулевого баланса у S не препятствует такому представительству.

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

Если R передаст часть токенов Q, вес представителя R уменьшится на переданное количество. Получатель Q направит новые единицы своему текущему выбранному представителю. Если Q никого не выбирал, они не появятся как текущий вес произвольного адреса только из факта передачи.

Рассмотрим R с 1000 LV, делегированными S. R передаёт Q 200 LV. У S остаётся вес 800, если других делегаторов нет. Если Q заранее делегировал T, T получает увеличение 200. Если Q не делегировал, текущий делегированный суммарный вес уменьшается, хотя totalSupply всё ещё тысяча.

Отказ от представительства

Делегирование нулевому адресу в этой логике убирает активный делегированный вес владельца из представителей. Это не сжигает токены и не уменьшает общий выпуск. Владелец может позднее выбрать представителя снова. Поэтому текст кнопки должен объяснять изменение участия, а не обещать уничтожение баланса.

Сумма getVotes по всем представителям может быть меньше totalSupply. В частности, неделегированные единицы входят в общий выпуск, но не участвуют в текущем весе адресов. Дальше это станет важным для кворума: доля от выпуска и доля от реально делегированного веса являются разными требованиями.

Не создавайте ручной второй mapping голосов рядом с ERC20Votes без необходимости. Если он обновляется только при transfer, то забудет delegate, mint или burn другого будущего расширения. Библиотечный механизм связывает перечисленные изменения через свои внутренние пути, а собственное расширение должно сохранять этот договор.

Подписанное делегирование

delegateBySig позволяет передать намерение через подпись в предусмотренном домене с nonce и expiry. Это другой тип сообщения, чем ERC20 permit: оно выбирает представителя, а не устанавливает allowance spender. Нельзя принимать одинаковую подпись для разных предметных операций по совпадению адреса.

В закреплённом Votes используется восстановление ECDSA для этого маршрута. Наличие контрактного аккаунта среди держателей не означает автоматическую поддержку его произвольной подписи через данный метод. Прямой вызов delegate контрактом и обработка подписанного сообщения являются разными способами управления.

Завершите разбор таблицей для R, S, Q и T после первоначального mint, delegate(S), transfer(Q,200 LV) и изменения представителя Q. Отдельно укажите balance, delegates и getVotes. Затем сформулируйте, почему getVotes(S)>balanceOf(S) не является нарушением учёта. Мы не исполняем операции; ожидаемые значения следуют из описанного договора и закреплённого исходника.

Делегирование и распоряжение токеном

S с весом от R не получает через delegate разрешение transferFrom. Для движения собственности ERC-20 применяется собственный allowance, который может быть нулевым при большом getVotes(S). И наоборот, spender с разрешением на передачу не становится представителем только из этого факта. Нельзя показывать обе операции как одинаковое «разрешение доверенному адресу».

Если R и Q выбрали одного S, перевод единиц между R и Q не меняет суммарный вес S. Балансы держателей меняются, но представитель остаётся тем же для перемещённых единиц. Такой случай полезен для проверки понимания: событие Transfer не обязательно означает изменение текущего getVotes конкретного представителя.

В выбранной версии checkpoints используют ограниченную ширину для веса. ERC20Votes устанавливает соответствующий безопасный верхний предел выпуска. Наш небольшой фиксированный объём находится далеко ниже него. При разработке собственного огромного mint нельзя игнорировать предел лишь потому, что обычный баланс представлен uint256. Исторический механизм имеет свои требования представления.

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

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

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