Разработка системы предложений и голосований DAO
Если DAO уже существует, а система голосований должна быть доработана или построена с нуля — значит нужно чётко понимать что именно строим: полноценный on-chain Governor, легковесный off-chain механизм, или гибрид. Выбор зависит от числа активных участников, размера treasury и требований к decentralization.
Здесь — детально про механику proposals и voting, включая паттерны, которые не очевидны из документации.
Жизненный цикл proposal
Создание и snapshot
Proposal создаётся вызовом governor.propose(). В момент создания фиксируется proposalSnapshot — номер блока, на котором будет считаться voting power. Это критично: если snapshot совпадает с текущим блоком, злоумышленник может в том же блоке купить токены и проголосовать ими.
Поэтому votingDelay — количество блоков/секунд между созданием proposal и началом голосования — должен быть ненулевым. Compound использует 1 день (6570 блоков на Ethereum mainnet), Aave — 1 день.
// GovernorSettings параметры
uint48 public constant VOTING_DELAY = 1 days; // задержка до начала голосования
uint32 public constant VOTING_PERIOD = 7 days; // длительность голосования
uint256 public constant PROPOSAL_THRESHOLD = 100_000e18; // минимум токенов для создания
Голосование: простое vs взвешенное
GovernorCountingSimple — стандарт: FOR, AGAINST, ABSTAIN. Proposal проходит если: (1) quorum набран (голосов не меньше порога), (2) FOR > AGAINST.
Fractional voting — более продвинутый паттерн: делегат может распределить voting power между вариантами дробно. Полезно когда делегат хочет выразить позицию своих доверителей, а они расходятся во мнениях. Реализуется через custom GovernorCountingFractional (есть fork от a16z).
Quadratic voting — voting power = sqrt(токенов). Выравнивает влияние крупных и мелких держателей. Сложно реализовать on-chain честно из-за Sybil: адрес с 10000 токенами может разбить их на 100 адресов по 100 токенов, получив в 10 раз больше влияния. Требует identity verification (Worldcoin, Gitcoin Passport) для Sybil resistance.
Quorum и его расчёт
Quorum — минимальное количество голосов (FOR + AGAINST + ABSTAIN) для валидности голосования. GovernorVotesQuorumFraction считает quorum как процент от total supply на момент snapshot.
Проблема: если вестинг постепенно разблокирует токены, total supply растёт — quorum в абсолютных числах тоже растёт. При высоком росте supply ранние proposals проходили с меньшим quorum. getPastTotalSupply(proposalSnapshot) решает это — quorum считается от supply на момент snapshot, не текущего.
function quorum(uint256 timepoint) public view override returns (uint256) {
return token.getPastTotalSupply(timepoint) * quorumNumerator(timepoint) / quorumDenominator();
}
Delegation механизм
Fluid delegation
ERC20Votes позволяет менять делегата в любой момент. Изменение вступает в силу сразу для будущих голосований, но не retroactively — для открытых proposals уже зафиксирован snapshot.
Это создаёт интересную динамику: перед важным голосованием активные участники агрессивно собирают делегирования. Компании-делегаты (Gauntlet, a16z governance team, Blockchain@UMich) публично декларируют позицию по каждому вопросу, привлекая пассивных держателей.
Subdelegation
Стандартный ERC20Votes не поддерживает subdelegation: если A делегировал B, B не может делегировать дальше C (B использует свои собственные токены + делегированные вместе, но не может subdelegation). Compound v3 Governor ввёл partial delegation и subdelegation через отдельный механизм.
Для сложных governance систем с delegate hierarchies — нужен кастомный extension поверх ERC20Votes.
Типы proposals и их механика
Single-action proposals
Простейший случай: одно действие — изменить параметр. Например, поменять процентную ставку в lending протоколе:
targets = [address(lendingPool)];
values = [0];
calldatas = [abi.encodeWithSelector(ILendingPool.setInterestRate.selector, newRate)];
Multi-action proposals (batched)
Governor поддерживает массивы targets/values/calldatas — все действия исполняются атомарно. Полезно для связанных изменений: например, обновить реализацию контракта И обновить параметры в одном proposal. Если любое действие ревертится — всё ревертится.
Proposal cancellation
Создатель proposal может отменить его до начала голосования. Это защита от ошибок (неверный calldata). После начала голосования — только Guardian с CANCELLER ролью в TimelockController.
function cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public returns (uint256) {
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
require(
_msgSender() == proposalProposer(proposalId),
"Only proposer can cancel"
);
return _cancel(targets, values, calldatas, descriptionHash);
}
Prevent Late Quorum extension
Классическая атака: большой держатель ждёт последних минут голосования, когда результат кажется предрешённым, и меняет исход одним голосом. У оппонентов нет времени среагировать.
GovernorPreventLateQuorum — расширение, которое продлевает voting period если quorum набран в последние N блоков перед дедлайном:
function _castVote(...) internal override returns (uint256) {
uint256 result = super._castVote(...);
uint256 deadline = proposalDeadline(proposalId);
if (deadline - block.number < voteExtension && _quorumReached(proposalId)) {
// продлить deadline
_extendedDeadlines[proposalId] = block.number + voteExtension;
emit ProposalExtended(proposalId, block.number + voteExtension);
}
return result;
}
Compound Governance использует аналогичный механизм. Это важно для fairness, особенно на ранней стадии с малым числом активных участников.
Proposal description и metadata
Description в proposal — произвольная строка. Стандарт: первая строка — заголовок, остальное — Markdown описание. Индексаторы (Tally, Boardroom) парсят это для отображения.
Deskription hash: keccak256(bytes(description)) входит в proposalId расчёт. Это позволяет верифицировать что описание не менялось после создания proposal.
IPFS для больших описаний. Если proposal включает длинный текст (specification, audit report ссылки) — description хранит только CID: ipfs://Qm.... IPFS контент immutable — гарантия что условия proposal не изменятся.
Frontend и UX
Tally и Boardroom как готовые решения
Tally.xyz и Boardroom.io — готовые governance UI. Поддерживают OpenZeppelin Governor и Governor Bravo. Бесплатны для public DAO. Дают: список proposals, voting interface, delegate directory, treasury view.
Минус: брендинг и кастомизация ограничены. Для embedded governance в собственном dApp — нужен custom UI.
Custom governance UI
Ключевые данные для frontend:
- Список proposals:
ProposalCreatedсобытия через The Graph субграф - Voting power пользователя:
token.getVotes(address)для текущей,token.getPastVotes(address, blockNumber)для snapshot - Delegate информация: Tally API или собственный индексер
wagmi hooks для голосования:
const { writeContract } = useWriteContract()
function castVote(proposalId: bigint, support: 0 | 1 | 2) {
writeContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'castVoteWithReason',
args: [proposalId, support, 'My reasoning here'],
})
}
Сроки и scope
Базовая система proposals и голосований на OpenZeppelin Governor — 1-2 недели разработки контракта + 1 неделя тестов. Custom voting механика (fractional, quadratic) — +1-2 недели. Frontend с полным governance UI — 3-4 недели.
Аудит обязателен если Governor управляет реальными средствами. Типичный объём аудита governance контракта — 1-2 недели у квалифицированной команды.







