Розробка контрактів DAO
DAO на папері виглядає просто: токени = голоси, більшість вирішує. На практиці це одна з найскладніших областей смарт-контрактів — не тому що код особливо важкий, а тому що ціна помилки в governance механіці катастрофічно висока. Пропозиція пройшла з помилкою в логіці кворуму — і зловмисник злив treasury. Так сталось з Beanstalk у квітні 2022: flash loan governance атака вивела $182M. Або Mango Markets у жовтні того ж року. Розробка контрактів DAO — це проектування економічно-безпечної системи прийняття рішень.
Архітектурні компоненти
Governor + Timelock
Стандарт де-факто — OpenZeppelin Governor з TimelockController. Governor управляє життєвим циклом пропозицій (створення, голосування, постановка в чергу), 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(
1, // votingDelay: 1 блок (~12 сек на Ethereum)
50400, // votingPeriod: ~7 днів у блоках
100_000e18 // proposalThreshold: мінімум 100K токенів для пропозиції
)
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 — критично важливий компонент. Він дає спільноті час реагувати на прийняту пропозицію до її виконання. Мінімальна затримка для 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: пропозиція створена, чекає votingDelay блоків. Снімок голосів береться в останньому блоці перед переходом в Active.
Active: відкрито голосування на votingPeriod блоків. Голосування: For, Against, Abstain. Abstain вважається в кворумі, але не впливає на результат.
Succeeded: кворум досягнутий, For > Against. Пропозиція чекає queue в Timelock.
Queued: в Timelock черзі. Чекає minDelay. У цей період Guardian (якщо є) може скасувати.
Executed: виконаний on-chain.
Flash loan атаки: захист
Beanstalk-сценарій: зловмисник бере flash loan на величезну суму, отримує тимчасовий governance контроль (delegateVote для governance ваги), створює та негайно проводить malicious пропозицію, повертає loan. Все в одній транзакції.
Ключовий захист — snapshot timing. GovernorVotes використовує getPastVotes(account, proposalSnapshot) — вага голосу визначається на блоці snapshot, а не на блоці голосування. votingDelay = 1 (один блок) вже ламає flash loan атаку: неможливо зайняти токени та використати їх у тому ж блоці для snapshot.
Додаткові заходи:
Timelock затримка: навіть якщо зловмисник пройшов голосування, 48-72 години затримки дають час спільноті помітити та Guardian може скасувати.
Proposal threshold: мінімальний баланс для створення пропозиції (100K токенів у прикладі вище). Знижує спам та вимагає реального володіння токенами.
QuorumFraction: кворум прив'язаний до total supply на момент snapshot. Якщо supply 10M токенів, кворум 10% = 1M токенів. Малі пропозиції не проходять без широкої підтримки.
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 токенами може дати пропозиції максимум 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
Пропозиція виконується автоматично через delay, якщо не було достатнього veto. Зворотна модель: за замовчуванням «так», потрібно активне veto для блокування. Знижує voter apathy проблему (багато хто не голосує за рутинні пропозиції).
Використовується в Optimism Collective: Citizens' House має veto право на токен-weighted decisions.
Multi-sig як Guardian
Повністю on-chain governance уразливо на ранніх стадіях: мало токенів в обігу, низька явка, легко маніпулювати. Стандартна практика — Guardian мультисиг (Gnosis Safe 3/5 або 4/7), який може скасувати queued пропозиції, але не може їх ініціювати або виконувати.
// У TimelockController: CANCELLER_ROLE для Guardian Safe
timelock.grantRole(timelock.CANCELLER_ROLE(), guardianSafe);
Guardian не повинен мати PROPOSER_ROLE або EXECUTOR_ROLE — тільки CANCELLER. Це створює asymmetric захист: 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 бюджет ($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 на одну пропозицію. Більшість DAO вирішує це через off-chain voting (Snapshot) з on-chain виконанням.
Hybrid підхід: голосування в Snapshot (безплатно, підпис EIP-712), результат реалізується через SafeSnap — Gnosis Safe модуль, який автоматично ставить в чергу Timelock операції за результатами Snapshot пропозиції.
Для on-chain голосування на L2 (Arbitrum, Optimism, Base) gas уже прийнятний — $0.05-0.50 за транзакцію.
Токен делегування та voter apathy
Voter apathy — головна проблема працюючих DAO. Uniswap governance: кворум 40M UNI при total supply 1B — реально бере участь 4%. Часто пропозиція не проходить просто тому що ніхто не проголосував.
Рішення:
Delegation UX: при першій взаємодії з протоколом або при отриманні токенів — prompt для делегування. Багато хто передають голоси активним delegates (аналог представницької демократії).
Optimistic default: якщо кворум не досягнутий за votingPeriod — пропозиція не провалилась, а перенесена. Опціонально.
Participation rewards: невелика винагорода за голосування. Спірна механіка — може привабити бездумні голоси, але збільшує явку.
Delegate dashboard: публічні профілі delegates, їхні позиції щодо минулих пропозицій, їхні аргументи — знижує інформаційний бар'єр для делегування.
Стек та інструменти
| Компонент | Інструмент |
|---|---|
| Governor контракт | OpenZeppelin Governor 5.x |
| Timelock | OpenZeppelin TimelockController |
| Governance токен | ERC20Votes + ERC20Permit |
| Off-chain голосування | Snapshot.org |
| On-chain виконання | 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 модель, параметри (delay, period, threshold, quorum), структура комітетів, Guardian схема, migration plan від мультисига до повного on-chain governance.
Розробка контрактів (2-3 тижні). Governor + Timelock + Governance токен (якщо немає) + тести атак сценарієв.
Безпека (1-2 тижні). Внутрішній review всіх proposal lifecycle сценаріїв, flash loan тести, timelock операції.
Аудит (2-3 тижні). Зовнішній аудит обов'язковий — governance контракти управляють всім протоколом.
Frontend та інтеграція (2-3 тижні). Tally.xyz інтеграція або кастомний UI, Snapshot space налаштування, SafeSnap налаштування.
Поступовий launch. Початок з Guardian мультисигом, зниження централізації за дорожною картою.
Повний цикл: 3-4 місяці. Вартість залежить від складності governance моделі та наявності існуючого токена.







