Перейти к содержанию
Learning Platform
Средний
30 минут
ERC20Votes Delegation Checkpoints ERC20Permit IERC6372

Требуемые знания:

  • 01-dao-concepts

Governance Tokens

Зачем это блокчейн-разработчику?

У вас 1 миллион governance токенов. Сколько у вас голосов? Ноль. Пока вы не вызовете delegate(). Это самая частая ошибка новичков в governance — и мы объясним, почему так сделано.

ERC20Votes — расширение стандартного ERC20, которое добавляет checkpoints (историческое отслеживание балансов), delegation (делегирование голосов), и voting power queries. Без понимания этого расширения невозможно работать с governance.

ERC20 + ERC20Votes

ERC20Votes — extension к стандартному ERC20. Добавляет:

ФункцияНазначение
delegate(address)Активирует voting power (самоделегирование или представительское)
getVotes(address)Текущая voting power адреса
getPastVotes(address, timepoint)Voting power на определенный момент в прошлом
delegates(address)Кому делегированы голоса
delegateBySig(...)Gasless delegation через подпись (EIP-712)

Import paths (OpenZeppelin v5):

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

CRITICAL: Delegation Requirement

Balance vs Voting Power: зачем нужна делегация
Token Amount
500,000
Delegated?
Balance (ERC20)
balanceOf(user)
500,000
Voting Power (ERC20Votes)
getVotes(user)
0
delegation required!
Активация voting power:
token.delegate(address(this));
CRITICALERC20Votes НЕ отслеживает voting power автоматически. balanceOf() может быть 1M, а getVotes() будет 0. Вызовите delegate(self) для активации!

Voting power — НЕ автоматическая. balanceOf(user) может быть 1M, а getVotes(user) будет 0.

Почему так сделано? Gas optimization. Отслеживание checkpoints при каждом transfer стоит gas. Без delegation обычные ERC20 transfers не создают checkpoints — дешевле для пользователей, которые не участвуют в governance.

Два паттерна делегирования:

// Self-delegation: активирует собственную voting power
token.delegate(address(this));
// или из вызова пользователя:
token.delegate(msg.sender);

// Representative delegation: делегирует голоса представителю
token.delegate(representative);

Gasless delegation (delegateBySig):

// Пользователь подписывает EIP-712 сообщение off-chain
// Кто угодно может отправить транзакцию с подписью
token.delegateBySig(delegatee, nonce, expiry, v, r, s);

Делегирование: пошаговый flow

Делегирование: от токенов к голосам
1
2
3
4
5
Step 1: mint()
mint() создает 1M токенов для Alice
Alice
balanceOf1,000,000
getVotes0
Bob
balanceOf0
getVotes0
Токены созданы, но voting power = 0!

Ключевые моменты:

  1. mint() создает токены, но НЕ voting power
  2. delegate(self) активирует voting power и создает checkpoint
  3. transfer() перемещает токены, но получатель должен делегировать отдельно
  4. delegate(representative) перенаправляет voting power другому адресу
  5. Proposal snapshot фиксирует voting power на момент создания proposal

Checkpoints

Checkpoints — система исторического отслеживания балансов. При каждом изменении delegation создается запись: {timestamp, votes}.

// Governor запрашивает voting power на момент создания proposal
uint256 votes = token.getPastVotes(voter, proposalSnapshot);

Зачем это нужно? Предотвращение flash loan governance атак. Если атакующий занимает токены ПОСЛЕ создания proposal, его voting power на момент snapshot = 0.

Clock Modes (IERC6372)

OpenZeppelin v5 поддерживает два режима:

Режимclock()votingDelayvotingPeriod
Timestampblock.timestamp1 days1 weeks
Block numberblock.number7200 (~1 day)50400 (~1 week)

Мы используем timestamp mode — более интуитивный для разработчиков:

// В GovernanceToken.sol:
function clock() public view override returns (uint48) {
    return uint48(block.timestamp);
}

function CLOCK_MODE() public pure override returns (string memory) {
    return "mode=timestamp";
}

Governor автоматически определяет clock mode токена и использует соответствующие единицы.

ERC20Permit

ERC20Permit (EIP-2612) — gasless approvals через подписи. Включен в GovernanceToken для удобства:

// Вместо двух транзакций (approve + transferFrom):
// 1. Пользователь подписывает permit off-chain
// 2. Контракт вызывает permit() + transferFrom() в одной tx
token.permit(owner, spender, value, deadline, v, r, s);

Не специфичен для governance, но bundled в OZ v5 pattern вместе с ERC20Votes.

GovernanceToken.sol: полный код

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";

contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes {
    constructor()
        ERC20("Governance Token", "GOV")
        ERC20Permit("Governance Token")
    {
        _mint(msg.sender, 1_000_000 * 1e18);
    }

    // _update: chains ERC20 base + ERC20Votes checkpoint
    function _update(address from, address to, uint256 value)
        internal override(ERC20, ERC20Votes)
    {
        super._update(from, to, value);
    }

    // nonces: resolves ERC20Permit + Nonces conflict
    function nonces(address owner)
        public view override(ERC20Permit, Nonces)
        returns (uint256)
    {
        return super.nonces(owner);
    }

    // Timestamp-based clock
    function clock() public view override returns (uint48) {
        return uint48(block.timestamp);
    }

    function CLOCK_MODE() public pure override returns (string memory) {
        return "mode=timestamp";
    }
}

Ключевые override:

  • _update() — chains ERC20 base _update и ERC20Votes checkpoint update через C3 linearization
  • nonces() — разрешает конфликт между ERC20Permit и Nonces (оба наследуют Nonces)
  • clock() / CLOCK_MODE() — устанавливает timestamp mode для governance

Алгоритмический уровень

Checkpoint data structure:

// Внутренняя структура ERC20Votes:
mapping(address => Checkpoint[]) private _checkpoints;

struct Checkpoint {
    uint48 _key;     // timestamp
    uint208 _value;  // voting power
}

// getPastVotes использует binary search:
function getPastVotes(address account, uint256 timepoint):
    checkpoints = _checkpoints[account]
    // Binary search: найти последний checkpoint с timestamp <= timepoint
    return upperLookupRecent(checkpoints, timepoint)

Binary search по массиву checkpoints — O(log n) lookup для исторической voting power.

Математический уровень

Voting power как функция:

V(a,t)=bD1(a,t)balance(b,t)V(a, t) = \sum_{b \in D^{-1}(a, t)} \text{balance}(b, t)

где D1(a,t)D^{-1}(a, t) — множество адресов, делегировавших голоса адресу aa на момент tt.

Свойство сохранения:

aAV(a,t)=totalSupply(t)\sum_{a \in A} V(a, t) = \text{totalSupply}(t)

Сумма всех voting power равна totalSupply — голоса не создаются и не уничтожаются, только перераспределяются через delegation.

Delegation как направленный граф:

Пусть D:AAD: A \to A — функция делегирования, где D(b)=aD(b) = a означает “b делегировал a”. Тогда voting power:

V(a,t)={b:D(b)=a}balance(b,t)V(a, t) = \sum_{\{b : D(b) = a\}} \text{balance}(b, t)

Самоделегирование: D(a)=aD(a) = a.

Итоги

Что мы узнали:

  1. ERC20Votes — расширение ERC20 для governance: checkpoints + delegation + voting power
  2. Delegation requiredbalanceOf() > 0 НЕ означает наличие voting power
  3. Checkpoints — историческое отслеживание, binary search для getPastVotes()
  4. Timestamp clockclock() override для человекочитаемых delays (1 days, 1 weeks)
  5. GovernanceToken.sol — полный контракт с 4 override: _update, nonces, clock, CLOCK_MODE

Что дальше: В GOV-03 — механизмы голосования, state machine предложений, и самое важное — governance attacks. Как в 2022 году один человек за одну транзакцию украл $182M через flash loan governance.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс