Разработка контрактов DAO
DAO на бумаге выглядит просто: токены = голоса, большинство решает. На практике это одна из наиболее сложных областей смарт-контрактов — не потому что код особенно трудный, а потому что цена ошибки в governance механике катастрофически высока. Proposal прошёл с ошибкой в логике кворума — и злоумышленник слил treasury. Так случилось с Beanstalk в апреле 2022: flash loan governance атака вывела $182M. Или Mango Markets в октябре того же года. Разработка контрактов DAO — это проектирование экономически-безопасной системы принятия решений.
Архитектурные компоненты
Governor + Timelock
Стандарт де-факто — OpenZeppelin Governor с TimelockController. Governor управляет lifecycle proposal-ов (создание, голосование, постановка в очередь), Timelock добавляет задержку между принятием proposal и его исполнением.
// 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(
1, // votingDelay: 1 блок (~12 сек на Ethereum)
50400, // votingPeriod: ~7 дней в блоках
100_000e18 // proposalThreshold: минимум 100K токенов для proposal
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(10) // 10% от total supply = кворум
GovernorTimelockControl(_timelock)
{}
// Обязательные override-ы при множественном наследовании
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 _execute(uint256 proposalId, address[] memory targets,
uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
internal override(Governor, GovernorTimelockControl)
{
super._execute(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();
}
}
TimelockController настройка
Timelock — критически важный компонент. Он даёт сообществу время среагировать на принятый proposal до его исполнения. Минимальная задержка для treasury операций — 48 часов, для обновления контрактов — 72 часа.
// Деплой TimelockController
// minDelay: 172800 (48 часов в секундах)
// proposers: [address(governor)]
// executors: [address(0)] — любой может исполнить после задержки
// admin: address(0) — нет суперадмина, только через Timelock
TimelockController timelock = new TimelockController(
172800,
proposers, // только Governor может ставить в очередь
executors, // address(0) = любой может исполнить
address(0) // admin revoked
);
executors: [address(0)] означает что исполнить операцию после задержки может любой — это правильно. Иначе создаётся centralization point, где конкретный адрес должен нажать «execute».
Proposal lifecycle
Pending → Active → Succeeded/Defeated → Queued → Executed
↓
Canceled
Pending: proposal создан, ждёт votingDelay блоков. Snapshot голосов берётся в последнем блоке перед переходом в Active.
Active: открыто голосование на votingPeriod блоков. Голосование: For, Against, Abstain. Abstain считается в кворуме, но не влияет на результат.
Succeeded: кворум достигнут, For > Against. Proposal ждёт queue в Timelock.
Queued: в Timelock очереди. Ждёт minDelay. В этот период Guardian (если есть) может отменить.
Executed: исполнен on-chain.
Flash loan атаки: защита
Beanstalk-сценарий: злоумышленник берёт flash loan на огромную сумму, получает временный governance контроль (delegateVote для governance веса), создаёт и немедленно проводит malicious proposal, возвращает loan. Всё в одной транзакции.
Ключевая защита — snapshot timing. GovernorVotes использует getPastVotes(account, proposalSnapshot) — вес голоса определяется на блоке snapshot, а не на блоке голосования. votingDelay = 1 (один блок) уже ломает flash loan атаку: нельзя занять токены и использовать их в том же блоке для snapshot.
Дополнительные меры:
Timelock задержка: даже если злоумышленник прошёл голосование, 48-72 часа задержки дают время сообществу заметить и Guardian может отменить.
Proposal threshold: минимальный баланс для создания proposal (100K токенов в примере выше). Снижает спам и требует реального владения токенами.
QuorumFraction: кворум привязан к total supply на момент snapshot. Если supply 10M токенов, кворум 10% = 1M токенов. Мелкие proposals не проходят без широкой поддержки.
Delegation tracking: в базовом ERC20Votes баланс != voting power без explicit делегирования. Пользователь должен delegate(address(self)) или делегировать другому. Это фича, а не баг: снижает effective supply voting power.
Voting модели: что выбрать
Token-weighted voting (стандарт)
1 токен = 1 голос. Просто, предсказуемо, но plutocratic: крупные холдеры доминируют.
Quadratic voting
Стоимость N голосов = N². Пользователь с 100 токенами может дать proposal максимум 10 голосов (√100). Снижает доминирование крупных холдеров.
Проблема: требует Sybil resistance (иначе 1 крупный кошелёк делится на 100 маленьких). Gitcoin Passport, Worldcoin ID, NFT membership — возможные решения.
function _getVotes(address account, uint256 blockNumber, bytes memory)
internal view override returns (uint256)
{
uint256 balance = token.getPastVotes(account, blockNumber);
// Квадратный корень (в fixed-point арифметике)
return Math.sqrt(balance);
}
Optimistic governance
Proposal исполняется автоматически через delay, если не было достаточного veto. Обратная модель: по умолчанию «да», нужно активное veto для блокировки. Снижает voter apathy проблему (многие не голосуют за рутинные proposals).
Используется в Optimism Collective: Citizens' House имеет veto право на токен-weighted decisions.
Multi-sig как Guardian
Полностью on-chain governance уязвимо на ранних стадиях: мало токенов в обращении, низкая явка, легко манипулировать. Стандартная практика — Guardian мультисиг (Gnosis Safe 3/5 или 4/7), который может отменить queued proposals, но не может их инициировать или исполнять.
// В TimelockController: CANCELLER_ROLE для Guardian Safe
timelock.grantRole(timelock.CANCELLER_ROLE(), guardianSafe);
Guardian не должен иметь PROPOSER_ROLE или EXECUTOR_ROLE — только CANCELLER. Это создаёт asymmetric protection: Guardian защищает от атак, но не контролирует governance.
По мере роста сообщества Guardian постепенно выводится: сначала снижается threshold, затем полностью удаляется роль.
Sub-DAO и специализированные комитеты
Монолитный Governor для всех решений — антипаттерн. Ончейн голосование за каждую операцию убивает скорость. Типичная многоуровневая структура:
Core DAO Governor: крупные решения — изменение протокольных параметров, крупные treasury движения (> $100K), изменение контрактов. Долгий votingPeriod (7-14 дней).
Treasury Committee: Gnosis Safe 4/7 из выбранных DAO представителей. Быстрые treasury операции < $100K без on-chain голосования. Регулярный отчёт DAO.
Technical Committee: аудит контрактов, emergency pause. Отдельный Gnosis Safe с PAUSER_ROLE на критичных контрактах.
Grants Committee: небольшой grants budget ($5-20K), быстрые решения без full governance. Часто Snapshot (off-chain) голосование.
// Пример: pause механизм для emergency
contract ProtocolCore is Pausable {
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
// Timelock имеет DEFAULT_ADMIN_ROLE
// Technical Committee Safe имеет PAUSER_ROLE
// Только Timelock может назначать/снимать PAUSER_ROLE
}
Gasless voting через EIP-712
Основная проблема on-chain голосования — gas cost. На Ethereum mainnet один голос стоит $5-20. При 1000 активных участников это $5K-20K на один proposal. Большинство DAO решает это через off-chain voting (Snapshot) с on-chain execution.
Hybrid подход: голосование в Snapshot (бесплатно, подпись EIP-712), результат реализуется через SafeSnap — Gnosis Safe модуль, который автоматически ставит в очередь Timelock операции по итогам Snapshot proposal.
Для on-chain голосования на L2 (Arbitrum, Optimism, Base) gas уже приемлем — $0.05-0.50 за транзакцию.
Токен делегирование и voter apathy
Voter apathy — главная проблема работающих DAO. Uniswap governance: кворум 40M UNI при total supply 1B — реально участвует 4%. Часто proposal не проходит просто потому что никто не проголосовал.
Решения:
Delegation UX: при первом взаимодействии с протоколом или при получении токенов — prompt для делегирования. Многие передают голоса активным delegates (аналог представительной демократии).
Optimistic default: если кворум не достигнут за votingPeriod — proposal не провалился, а перенесён. Опционально.
Participation rewards: небольшое вознаграждение за голосование. Спорная механика — может привлечь бездумные голоса, но увеличивает явку.
Delegate dashboard: публичные профили delegates, их позиции по прошлым proposals, их аргументы — снижает информационный барьер для делегирования.
Стек и инструменты
| Компонент | Инструмент |
|---|---|
| Governor контракт | OpenZeppelin Governor 5.x |
| Timelock | OpenZeppelin TimelockController |
| Governance token | ERC20Votes + ERC20Permit |
| Off-chain voting | Snapshot.org |
| On-chain execution | SafeSnap / Reality.eth |
| Frontend | Tally.xyz (готовое) или кастомное |
| Multi-sig | Gnosis Safe |
| Testnet | Hardhat fork + Foundry |
Tally.xyz — готовый UI для OpenZeppelin Governor-совместимых DAO. Если нет специфических требований к frontend — экономит 2-4 недели разработки.
Процесс работы
Дизайн governance (1-2 недели). Voting model, параметры (delay, period, threshold, quorum), структура committees, Guardian схема, migration plan от мультисига к полному on-chain governance.
Разработка контрактов (2-3 недели). Governor + Timelock + Governance token (если нет) + тесты атаки сценариев.
Безопасность (1-2 недели). Внутренний review всех proposal lifecycle сценариев, flash loan тесты, timelock операции.
Аудит (2-3 недели). Внешний аудит обязателен — governance контракты управляют всем протоколом.
Frontend и интеграция (2-3 недели). Tally.xyz интеграция или кастомный UI, Snapshot space настройка, SafeSnap настройка.
Постепенный launch. Начало с Guardian мультисигом, снижение централизации по roadmap.
Полный цикл: 3-4 месяца. Стоимость зависит от complexity governance модели и наличия существующего токена.







