Розробка контрактів голосування (governance)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка контрактів голосування (governance)
Середній
~3-5 днів
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1309
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1222
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    922
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1151
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    614
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    887

Розробка контрактів голосування управління

Контракт управління — це не просто механізм голосування. Це система, яка керує протоколом з казначейством вартістю мільйони доларів. Помилки в управлінні призводять до реальних втрат: атака флеш-кредиту на Beanstalk у 2022 р. експлуатувала маніпуляцію голосуванням, щоб вилучити 182 млн доларів. Правильна архітектура управління — це баланс між безпекою та участю громади.

OpenZeppelin Governor: Базова архітектура

Стандарт де факто для управління в ланцюгу — фреймворк OpenZeppelin Governor. Він реалізує сумісний з Governor Bravo інтерфейс (сумісний з Tally, Boardroom, Snapshot).

Компоненти системи:

  • Governor: ядро, керує життєвим циклом пропозалів
  • GovernorSettings: налаштування (затримка голосування, період голосування, поріг пропозалу)
  • GovernorCountingSimple: підрахунок голосів (За/Проти/Утримання)
  • GovernorVotes: інтеграція з токеном ERC20Votes або ERC721Votes
  • GovernorTimelockControl: обов'язкова затримка перед виконанням
// 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,    // затримка голосування: ~1 день на Ethereum (12с/блок)
            50400,   // період голосування: ~7 днів
            100000e18 // поріг пропозалу: 100k токенів
        )
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4) // 4% кворум від обігу
        GovernorTimelockControl(_timelock)
    {}

    // Обов'язкові перевизначення для вирішення конфліктів між розширеннями
    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(); }
}

Токен управління: ERC20Votes

Голосова влада походить з історії контрольних точок токена. ERC20Votes зберігає знімки залишків на кожному блоці — запобігаючи маніпуляціям через покупку токенів перед голосуванням.

Критично важливо: користувачі повинні делегувати (принаймні собі) для облікування їх голосової влади. Це частий момент плутанини — токен існує, але голосування неможливе без делегування.

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)) для активації голосової влади
}

TimelockController: Обов'язковий компонент

Timelock — це буфер між схвальним голосуванням та виконанням. Дає громаді час помітити шкідливі пропозали та вийти з протоколу.

// Розгортання TimelockController
TimelockController timelock = new TimelockController(
    2 days,        // minDelay: мінімум 2 дні між queue та execute
    proposers,     // тільки Governor може ставити в чергу
    executors,     // будь-хто може виконати (після затримки)
    admin          // тимчасовий адмін, потім передати самому timelock
);

// Governor повинен мати PROPOSER_ROLE
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
// Будь-хто може виконувати
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Скасувати адмін у deployer
timelock.revokeRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);

Мінімальна затримка для протоколів з TVL > 10 млн доларів — 48 годин. Для критичних параметрів (оновлення, зміни комісії) — 7 днів.

Захист від атак

Атаки флеш-кредиту: зловмисник позичає токени флеш-кредиту, створює пропозал і голосує в одній транзакції. Захист — votingDelay > 0. Знімок контрольної точки беруться на блоці створення пропозалу, а не голосування. ERC20Votes зберігає історію — залишок на блоці контрольної точки, а не поточний.

Спам пропозалів: без proposalThreshold будь-хто може спамити пропозали. 100k токенів — розумний поріг для середніх протоколів. Для малих DAO — достатньо 1-5% пропозиції.

Маніпуляція кворумом: при низькій явці достатньо невеликої кількості токенів для прохідження. GovernorVotesQuorumFraction підраховує кворум як % від token.getPastTotalSupply() — правильний підхід, кворум прив'язаний до обігу, а не до абсолютного числа.

Параметри для різних типів DAO

Параметр Малий DAO Середній протокол Великий протокол
Затримка голосування 1 день 2 дні 2 дні
Період голосування 3 дні 5 днів 7 днів
Кворум 4% 4% 10%
Timelock 1 день 2 дні 7 днів
Поріг пропозалу 0.1% обігу 0.5% обігу 1% обігу

Делеговане голосування та безгазові підписи

Більшість власників токенів не голосуватимуть напрямку — газ дорогий, процес складний. Рішення:

Делегування: власник делегує голосову владу іншій адресі (делегату). Делегат голосує від імені багатьох власників. Використовується в Compound, Uniswap.

Безгазове голосування EIP-712: castVoteBySig() дозволяє підписати голос поза ланцюгом (через Snapshot або Tally) і відправити на ланцюг через релейера. Користувач не платить газ.

// Голосування через підпис — релейер платить газ
function castVoteBySig(
    uint256 proposalId,
    uint8 support,
    address voter,
    bytes memory signature
) public returns (uint256 weight) {
    // Верифікація EIP-712 підпису виборця
    // ...
    return _castVote(proposalId, voter, support, "", "");
}

Управління — це жива система. Параметри потребують переглядання по мірі зростання DAO: кворум, що працював при 10k власників, може бути недосяжним при 500k власників з низькою явкою.