Розробка контрактів голосування управління
Контракт управління — це не просто механізм голосування. Це система, яка керує протоколом з казначейством вартістю мільйони доларів. Помилки в управлінні призводять до реальних втрат: атака флеш-кредиту на Beanstalk у 2022 р. експлуатувала маніпуляцію голосуванням, щоб вилучити 182 млн доларів. Правильна архітектура управління — це баланс між безпекою та участю громади.
OpenZeppelin Governor: Базова архітектура
Стандарт де факто для управління в ланцюгу — фреймворк OpenZeppelin Governor. Він реалізує сумісний з Governor Bravo інтерфейс (сумісний з Tally, Boardroom, Snapshot).
Компоненти системи:
- Governor: ядро, керує життєвим циклом пропозалів
- GovernorSettings: налаштування (затримка голосування, період голосування, поріг пропозалу)
- GovernorCountingSimple: підрахунок голосів (За/Проти/Утримання)
- GovernorVotes: інтеграція з токеном ERC20Votes або ERC721Votes
- GovernorTimelockControl: обов'язкова затримка перед виконанням
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
contract DAOGovernor is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
)
Governor("DAO Governor")
GovernorSettings(
7200, // затримка голосування: ~1 день на Ethereum (12с/блок)
50400, // період голосування: ~7 днів
100000e18 // поріг пропозалу: 100k токенів
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4) // 4% кворум від обігу
GovernorTimelockControl(_timelock)
{}
// Обов'язкові перевизначення для вирішення конфліктів між розширеннями
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 quorum(uint256 blockNumber)
public view override(Governor, GovernorVotesQuorumFraction)
returns (uint256) { return super.quorum(blockNumber); }
function state(uint256 proposalId)
public view override(Governor, GovernorTimelockControl)
returns (ProposalState) { return super.state(proposalId); }
function proposalNeedsQueuing(uint256 proposalId)
public view override(Governor, GovernorTimelockControl)
returns (bool) { return super.proposalNeedsQueuing(proposalId); }
function _queueOperations(
uint256 proposalId, address[] memory targets,
uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
}
function _executeOperations(
uint256 proposalId, address[] memory targets,
uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
}
function _cancel(
address[] memory targets, uint256[] memory values,
bytes[] memory calldatas, bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor() internal view override(Governor, GovernorTimelockControl)
returns (address) { return super._executor(); }
}
Токен управління: ERC20Votes
Голосова влада походить з історії контрольних точок токена. ERC20Votes зберігає знімки залишків на кожному блоці — запобігаючи маніпуляціям через покупку токенів перед голосуванням.
Критично важливо: користувачі повинні делегувати (принаймні собі) для облікування їх голосової влади. Це частий момент плутанини — токен існує, але голосування неможливе без делегування.
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
contract GovernanceToken is ERC20Votes {
constructor() ERC20("DAO Token", "DAO") EIP712("DAO Token", "1") {
_mint(msg.sender, 10_000_000e18);
}
// Користувачі викликають delegate(address(self)) для активації голосової влади
}
TimelockController: Обов'язковий компонент
Timelock — це буфер між схвальним голосуванням та виконанням. Дає громаді час помітити шкідливі пропозали та вийти з протоколу.
// Розгортання TimelockController
TimelockController timelock = new TimelockController(
2 days, // minDelay: мінімум 2 дні між queue та execute
proposers, // тільки Governor може ставити в чергу
executors, // будь-хто може виконати (після затримки)
admin // тимчасовий адмін, потім передати самому timelock
);
// Governor повинен мати PROPOSER_ROLE
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
// Будь-хто може виконувати
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Скасувати адмін у deployer
timelock.revokeRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);
Мінімальна затримка для протоколів з TVL > 10 млн доларів — 48 годин. Для критичних параметрів (оновлення, зміни комісії) — 7 днів.
Захист від атак
Атаки флеш-кредиту: зловмисник позичає токени флеш-кредиту, створює пропозал і голосує в одній транзакції. Захист — votingDelay > 0. Знімок контрольної точки беруться на блоці створення пропозалу, а не голосування. ERC20Votes зберігає історію — залишок на блоці контрольної точки, а не поточний.
Спам пропозалів: без proposalThreshold будь-хто може спамити пропозали. 100k токенів — розумний поріг для середніх протоколів. Для малих DAO — достатньо 1-5% пропозиції.
Маніпуляція кворумом: при низькій явці достатньо невеликої кількості токенів для прохідження. GovernorVotesQuorumFraction підраховує кворум як % від token.getPastTotalSupply() — правильний підхід, кворум прив'язаний до обігу, а не до абсолютного числа.
Параметри для різних типів DAO
| Параметр | Малий DAO | Середній протокол | Великий протокол |
|---|---|---|---|
| Затримка голосування | 1 день | 2 дні | 2 дні |
| Період голосування | 3 дні | 5 днів | 7 днів |
| Кворум | 4% | 4% | 10% |
| Timelock | 1 день | 2 дні | 7 днів |
| Поріг пропозалу | 0.1% обігу | 0.5% обігу | 1% обігу |
Делеговане голосування та безгазові підписи
Більшість власників токенів не голосуватимуть напрямку — газ дорогий, процес складний. Рішення:
Делегування: власник делегує голосову владу іншій адресі (делегату). Делегат голосує від імені багатьох власників. Використовується в Compound, Uniswap.
Безгазове голосування EIP-712: castVoteBySig() дозволяє підписати голос поза ланцюгом (через Snapshot або Tally) і відправити на ланцюг через релейера. Користувач не платить газ.
// Голосування через підпис — релейер платить газ
function castVoteBySig(
uint256 proposalId,
uint8 support,
address voter,
bytes memory signature
) public returns (uint256 weight) {
// Верифікація EIP-712 підпису виборця
// ...
return _castVote(proposalId, voter, support, "", "");
}
Управління — це жива система. Параметри потребують переглядання по мірі зростання DAO: кворум, що працював при 10k власників, може бути недосяжним при 500k власників з низькою явкою.







