Разработка контрактов голосования (governance)
Governance контракт — это не просто голосовалка. Это система, которая управляет протоколом с treasury в миллионы долларов. Ошибки в governance приводят к реальным потерям: flash loan атака на Beanstalk в 2022 вывела $182M именно через манипуляцию голосованием. Правильная архитектура governance — это баланс между security и участием сообщества.
OpenZeppelin Governor: базовая архитектура
Стандарт де-факто для on-chain governance — OpenZeppelin Governor framework. Он реализует Governor Bravo совместимый интерфейс (совместим с Tally, Boardroom, Snapshot).
Компоненты системы:
- Governor: ядро, управляет жизненным циклом proposals
- GovernorSettings: настройки (voting delay, voting period, proposal threshold)
- GovernorCountingSimple: подсчёт голосов (For/Against/Abstain)
- GovernorVotes: интеграция с ERC20Votes или ERC721Votes токеном
- GovernorTimelockControl: обязательный timelock между принятием и исполнением
// 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, // voting delay: ~1 день на Ethereum (12s/block)
50400, // voting period: ~7 дней
100000e18 // proposal threshold: 100k токенов
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4) // 4% quorum от circulating supply
GovernorTimelockControl(_timelock)
{}
// Обязательные override для разрешения конфликтов между extensions
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(); }
}
Governance токен: ERC20Votes
Voting power берётся из checkpoint-истории токена. ERC20Votes хранит snapshot балансов на каждом блоке — это предотвращает манипуляцию через покупку токенов непосредственно перед голосованием.
Критически важно: пользователи должны сделать delegate (хотя бы самому себе) чтобы их voting power учитывалась. Это частая точка путаницы — токен есть, но голосовать нельзя.
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)) для активации voting power
}
TimelockController: обязательный элемент
Timelock — это буфер между принятым голосованием и исполнением. Даёт сообществу время заметить вредоносный proposal и выйти из протокола.
// Деплой TimelockController
TimelockController timelock = new TimelockController(
2 days, // minDelay: минимум 2 дня между queue и execute
proposers, // только Governor может ставить в очередь
executors, // любой может выполнить (после задержки)
admin // временный admin, потом передать самому timelock
);
// Governor должен быть PROPOSER_ROLE
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
// Любой может execute
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Отзываем admin у deployer
timelock.revokeRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);
Минимальный delay для протоколов с TVL > $10M — 48 часов. Для критических параметров (upgrade, fee changes) — 7 дней.
Защита от атак
Flash loan атака: злоумышленник берёт flash loan токенов, делает proposal и голосует в одной транзакции. Защита — votingDelay > 0. Checkpoint snapshot берётся на блоке создания proposal, а не голосования. ERC20Votes хранит историю — баланс на момент snapshot блока, а не текущий.
Proposal spam: без proposalThreshold любой может спамить proposals. 100k токенов — разумный порог для средних протоколов. Для малых DAO — достаточно 1-5% supply.
Quorum gaming: при низкой явке достаточно небольшого количества токенов для прохождения. GovernorVotesQuorumFraction считает quorum как % от token.getPastTotalSupply() — это правильно, quorum привязан к circulating supply, а не к абсолютному числу.
Параметры для разных типов DAO
| Параметр | Маленький DAO | Средний протокол | Крупный протокол |
|---|---|---|---|
| Voting delay | 1 день | 2 дня | 2 дня |
| Voting period | 3 дня | 5 дней | 7 дней |
| Quorum | 4% | 4% | 10% |
| Timelock | 1 день | 2 дня | 7 дней |
| Proposal threshold | 0.1% supply | 0.5% supply | 1% supply |
Delegated voting и gasless signatures
Большинство токен-холдеров не будут голосовать напрямую — gas дорогой, процесс сложный. Решения:
Delegation: holder делегирует voting power другому адресу (делегату). Делегат голосует от имени множества холдеров. Используют Compound, Uniswap.
EIP-712 gasless vote: castVoteBySig() позволяет подписать голос off-chain (через Snapshot или Tally) и отправить on-chain через relayer. Пользователь не платит gas.
// Голосование через подпись — relayer платит gas
function castVoteBySig(
uint256 proposalId,
uint8 support,
address voter,
bytes memory signature
) public returns (uint256 weight) {
// EIP-712 верификация подписи voter
// ...
return _castVote(proposalId, voter, support, "", "");
}
Governance — живая система. Параметры нужно пересматривать по мере роста DAO: quorum, который работал при 10k holders, может быть недостижим при 500k holders с низкой явкой.







