Розробка системи пропозицій та голосування
Якщо DAO вже існує, а система голосування потребує доробки або побудови з нуля — потрібна ясність щодо того, що ми будуємо: повний on-chain Governor, легковісний off-chain механізм або гібрид. Вибір залежить від кількості активних учасників, розміру казни й вимог децентралізації.
Тут — детально про механіку пропозицій та голосування, включаючи паттерни не очевидні з документації.
Життєвий цикл пропозиції
Створення та снапшот
Пропозиція створюється викликом governor.propose(). При створенні фіксується proposalSnapshot — номер блоку, де буде враховуватися голосувальна сила. Це критично: якщо снапшот збігається з поточним блоком, зловмисник може купити токени в одному блоці й проголосувати.
Тому votingDelay — блоки/секунди між створенням пропозиції й початком голосування — повинен бути ненульовим. 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. Пропозиція проходить якщо: (1) кворум набраний (голосів не менше порога), (2) FOR > AGAINST.
Дробове голосування — більш продвинуте: делегат може розподілити голосувальну силу дробово між варіантами. Корисна коли делегат хоче виразити позиції виборців, де вони не погоджуються. Реалізується через користувацький GovernorCountingFractional (форк від a16z існує).
Квадратичне голосування — голосувальна сила = sqrt(токени). Вирівнює вплив великих і малих емітентів. Важко реалізувати on-chain чесно через Sybil: адреса з 10000 токенів може розділитися на 100 адрес по 100 токенів кожна, отримавши в 10 разів більше впливу. Вимагає верифікації особистості (Worldcoin, Gitcoin Passport) для Sybil-стійкості.
Кворум та розрахунок
Кворум — мінімум голосів (FOR + AGAINST + ABSTAIN) для валідності. GovernorVotesQuorumFraction рахує кворум як відсоток від загального у момент снапшоту.
Проблема: якщо вестинг поступово розблоковує токени, загальна кількість росте — кворум в абсолютних числах теж. При високому зростанні пропозиції ранні пройшли з нижчим кворумом. getPastTotalSupply(proposalSnapshot) вирішує це — кворум рахується від пропозиції на знапшоту, не поточної.
function quorum(uint256 timepoint) public view override returns (uint256) {
return token.getPastTotalSupply(timepoint) * quorumNumerator(timepoint) / quorumDenominator();
}
Механізм делегування
Рідке делегування
ERC20Votes дозволяє змінювати делегата в будь-який час. Зміна вступає в силу одразу для майбутніх голосувань, але не ретроактивно — для відкритих пропозицій снапшот вже фіксований.
Це створює цікаву динаміку: перед важливим голосуванням активні учасники агресивно збирають делегування. Компанії-делегати (Gauntlet, команда a16z governance, Blockchain@UMich) публічно декларують позиції щодо кожного питання, привертаючи пасивних емітентів.
Піддельгування
Стандартний ERC20Votes не підтримує піддельгування: якщо A делегував B, B не може далі делегувати C (B використовує власні токени + делеговані разом, але не піддельгування). Compound v3 Governor запровадив часткове делегування та піддельгування через окремий механізм.
Для складного управління з ієрархіями делегатів — потрібно користувацьке розширення поверх ERC20Votes.
Типи пропозицій та механіка
Однодійні пропозиції
Найпростіше: одна дія — змінити параметр. Приклад: змінити процентну ставку в протоколі лендингу:
targets = [address(lendingPool)];
values = [0];
calldatas = [abi.encodeWithSelector(ILendingPool.setInterestRate.selector, newRate)];
Мультидійні пропозиції (групові)
Governor підтримує масиви targets/values/calldatas — всі дії виконуються атомарно. Корисна для пов'язаних змін: оновити реалізацію контракту І оновити параметри в одній пропозиції. Якщо будь-яка дія реверт — всі реверт.
Скасування пропозиції
Творець може скасувати пропозицію перед початком голосування. Захищає від помилок (неправильні calldatas). Після початку голосування — тільки 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);
}
Розширення запобігання пізньому кворуму
Класична атака: великий емітент чекає останніх хвилин голосування, коли результат здається вирішеним, потім змінює результат одним голосом. Опоненти не мають часу реагувати.
GovernorPreventLateQuorum — розширення розширює період голосування, якщо кворум набраний в останніх N блоках перед дедлайном:
function _castVote(...) internal override returns (uint256) {
uint256 result = super._castVote(...);
uint256 deadline = proposalDeadline(proposalId);
if (deadline - block.number < voteExtension && _quorumReached(proposalId)) {
// розширити дедлайн
_extendedDeadlines[proposalId] = block.number + voteExtension;
emit ProposalExtended(proposalId, block.number + voteExtension);
}
return result;
}
Compound Governance використовує аналогічний механізм. Важливо для справедливості, особливо на ранній стадії з кількома активними учасниками.
Фронтенд та UX
Tally та Boardroom як готові рішення
Tally.xyz та Boardroom.io — готові UI управління. Підтримують OpenZeppelin Governor та Governor Bravo. Безкоштовні для публічних DAO. Надають: списки пропозицій, інтерфейс голосування, директорію делегатів, перегляд казни.
Мінус: брендинг та налаштування обмежені. Для вбудованого управління у власному dApp — потрібен користувацький UI.
Користувацький UI управління
Ключові дані фронтенду:
- Список пропозицій:
ProposalCreatedподій через The Graph subgraph - Голосувальна сила користувача:
token.getVotes(address)для поточної,token.getPastVotes(address, blockNumber)для снапшоту - Інформація делегата: Tally API або власний індексер
wagmi гаки для голосування:
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'],
})
}
Графік і обсяг
Базова система пропозицій та голосування на OpenZeppelin Governor — 1-2 тижні розробки контракту плюс 1 тиждень тестування. Користувацька механіка голосування (дробова, квадратична) — +1-2 тижні. Фронтенд з повним UI управління — 3-4 тижні.
Аудит обов'язковий, якщо Governor керує реальними коштами. Типовий обсяг аудиту контракту управління — 1-2 тижні від кваліфікованої команди.







