Разработка системы голосования DAO

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка системы голосования DAO
Сложная
~1-2 недели
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка смарт-контрактов DAO

DAO — это не просто контракт с голосованием. Это набор смарт-контрактов, который реализует governance жизненный цикл: proposal creation → voting → timelock → execution. Добавьте к этому treasury management, delegation механику, quorum расчёты и upgrade логику — и получите систему, которая при неверном дизайне становится либо слишком централизованной (всё решает multisig), либо недееспособной (quorum не набирается никогда).

Стандарты и фреймворки

Прежде чем писать что-то с нуля — нужно понять, что уже есть.

OpenZeppelin Governor — де-факто стандарт для EVM DAO. Модульная архитектура: Governor (базовый контракт) расширяется миксинами GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl. Используется Compound, Uniswap, Gitcoin, ENS DAO.

Compound Governor Bravo — более старый стандарт, от которого отпочковался OpenZeppelin Governor. До сих пор используется в форках Compound. Менее гибкий, но battle-tested.

Aragon — high-level фреймворк с plugin архитектурой. Подходит когда нужна кастомная governance логика через plugins, не трогая core контракты.

Zodiac (Gnosis) — набор паттернов для расширения Gnosis Safe через modules. Позволяет превратить multisig в DAO с on-chain голосованием.

Архитектура OpenZeppelin Governor

Минимальная сборка

// 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 MyDAO is
    Governor,
    GovernorSettings,
    GovernorCountingSimple,
    GovernorVotes,
    GovernorVotesQuorumFraction,
    GovernorTimelockControl
{
    constructor(
        IVotes _token,
        TimelockController _timelock
    )
        Governor("MyDAO")
        GovernorSettings(
            1 days,    // voting delay: сколько ждать после proposal до начала голосования
            1 weeks,   // voting period: длительность голосования
            100_000e18 // proposal threshold: минимум токенов для создания proposal
        )
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4)  // 4% от total supply для кворума
        GovernorTimelockControl(_timelock)
    {}
    
    // Обязательные overrides для разрешения конфликтов миксинов
    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 _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(); }
    
    function supportsInterface(bytes4 interfaceId)
        public view override(Governor, GovernorTimelockControl) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

TimelockController — критический компонент

Timelock — это задержка между моментом, когда proposal прошёл голосование, и моментом, когда он может быть выполнен. Это даёт сообществу время на реакцию, если proposal оказался вредоносным.

// Деплой TimelockController
TimelockController timelock = new TimelockController(
    2 days,                    // minDelay: минимальная задержка
    proposers,                 // кто может ставить в очередь (обычно Governor)
    executors,                 // кто может выполнять (address(0) = anyone)
    admin                      // admin (обычно address(0) после setup)
);

// После деплоя — Governor должен быть PROPOSER и CANCELLER
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
timelock.grantRole(timelock.CANCELLER_ROLE(), address(governor));

// EXECUTOR_ROLE — address(0) означает что любой может выполнить прошедший proposal
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));

// Отозвать admin права у deployer!
timelock.revokeRole(timelock.TIMELOCK_ADMIN_ROLE(), deployer);

Последний шаг критически важен: если deployer остаётся admin Timelock, он может обойти governance. Большинство взломов через governance строится именно на оставленных admin правах.

ERC-20 Votes: governance токен

Токен для голосования должен реализовывать IVotes интерфейс. OpenZeppelin ERC20Votes хранит checkpoint историю балансов для snapshot-based voting.

contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes {
    constructor(address initialHolder)
        ERC20("MyDAO Token", "MDT")
        ERC20Permit("MyDAO Token")
    {
        _mint(initialHolder, 10_000_000e18);
    }
    
    // ERC20Votes требует explicit delegation
    // Новые получатели токенов должны вызвать delegate(self) для активации voting power
    
    function _afterTokenTransfer(address from, address to, uint256 amount)
        internal override(ERC20, ERC20Votes) {
        super._afterTokenTransfer(from, to, amount);
    }
    
    function _mint(address to, uint256 amount)
        internal override(ERC20, ERC20Votes) {
        super._mint(to, amount);
    }
    
    function _burn(address account, uint256 amount)
        internal override(ERC20, ERC20Votes) {
        super._burn(account, amount);
    }
}

Важный нюанс с delegation: в ERC20Votes токены не имеют voting power пока владелец не вызвал delegate(address). Обычно delegate(msg.sender) — самоделегирование. Это неочевидно для новых пользователей и требует явного onboarding. Многие DAO решают это через автоматическую self-delegation при первом transfer.

Delegation для неактивных холдеров

Проблема participation: большинство холдеров пассивны. Delegated voting позволяет передать voting power специализированным участникам (delegates) без передачи токенов.

// Пользователь делегирует своё voting power другому адресу
governanceToken.delegate(trustedDelegate);

// Теперь trustedDelegate голосует весом всех, кто на него делегировал
// Сами токены остаются у владельца

// Revoke delegation — вернуть себе
governanceToken.delegate(msg.sender);

Compound ввёл фреймворк публичного делегирования: delegates публикуют свои позиции, аргументируют решения, участники выбирают delegate по взглядам. ENS DAO, Gitcoin, Uniswap активно используют delegate ecosystem.

Lifecycle proposal

Pending → Active → (Defeated | Succeeded) → Queued → Executed
                                          ↘ Canceled
                                          ↗ Expired (не выполнен за MAX_TIMELOCK)

Создание proposal

// Proposal = набор транзакций которые будут выполнены при принятии
address[] memory targets = new address[](1);
uint256[] memory values = new uint256[](1);
bytes[] memory calldatas = new bytes[](1);

// Пример: изменить параметр в протоколе
targets[0] = address(protocol);
values[0] = 0;
calldatas[0] = abi.encodeWithSignature(
    "setFeeRate(uint256)",
    500  // новый fee rate: 5%
);

uint256 proposalId = governor.propose(
    targets,
    values,
    calldatas,
    "# Proposal: Update fee rate\n\nPropose changing fee rate from 3% to 5%..."
);

Description хранится off-chain (обычно IPFS), on-chain хранится только keccak256(description) через descriptionHash.

Голосование с reason

// Три варианта: 0 = Against, 1 = For, 2 = Abstain
governor.castVote(proposalId, 1);  // за

// С аргументацией (хранится в event)
governor.castVoteWithReason(proposalId, 1, "This fee increase is necessary for protocol sustainability");

// Через meta-transaction (gasless voting через EIP-712 подпись)
governor.castVoteBySig(proposalId, 1, v, r, s);

Gasless voting через castVoteBySig критически важен для участия — если пользователь должен платить gas за голосование, participation резко падает. Relayer берёт на себя gas, пользователь подписывает EIP-712 message.

Treasury management

DAO treasury — это средства, которыми управляет Timelock контракт (и через него Governor). Никто не может потратить treasury без прошедшего proposal.

Multisig как дополнительный защитный слой

Для emergency situations (критическая уязвимость, когда нет времени на governance cycle) практика — отдельный 5/9 Guardian multisig с правом экстренного pause.

contract DAOTreasury {
    address public immutable governor;     // только Governor может тратить
    address public immutable guardian;    // Guardian может pause при угрозе
    
    bool public paused;
    
    modifier onlyGovernance() {
        require(msg.sender == governor, "Only governance");
        _;
    }
    
    modifier whenNotPaused() {
        require(!paused, "Treasury paused");
        _;
    }
    
    // Выплата грантов, финансирование работ, инвестиции
    function transfer(address token, address recipient, uint256 amount)
        external onlyGovernance whenNotPaused
    {
        IERC20(token).safeTransfer(recipient, amount);
        emit Transfer(token, recipient, amount);
    }
    
    // Guardian может только pause, не тратить
    function pause() external {
        require(msg.sender == guardian, "Only guardian");
        paused = true;
    }
    
    // Unpause — только через governance
    function unpause() external onlyGovernance {
        paused = false;
    }
}

Кастомные voting mechanisms

Quadratic voting

Quadratic voting снижает влияние китов: voting power = sqrt(token balance). Whale с 1M токенов имеет voting power 1000, а не 1M.

function _getVotes(
    address account,
    uint256 blockNumber,
    bytes memory /*params*/
) internal view virtual override returns (uint256) {
    uint256 balance = token.getPastVotes(account, blockNumber);
    // Квадратный корень через Babylonian method
    return _sqrt(balance);
}

function _sqrt(uint256 x) internal pure returns (uint256 y) {
    if (x == 0) return 0;
    uint256 z = (x + 1) / 2;
    y = x;
    while (z < y) {
        y = z;
        z = (x / z + z) / 2;
    }
}

Проблема quadratic voting: Sybil-атаки. Один адрес с 1M токенов vs 1000 адресов по 1000 токенов — во втором случае суммарный voting power равен 1000 * sqrt(1000) ≈ 31623 против sqrt(1M) = 1000. Это делает дробление выгодным. Quadratic voting работает только в связке с Sybil-resistance (Proof of Humanity, Worldcoin).

Conviction voting

Conviction voting (использует Gardens/1Hive) накапливает voting power со временем: чем дольше держишь голос за proposal, тем больший вес он получает. Снятие голоса обнуляет conviction. Хорошо для continuous funding (treasury spending без отдельных proposals для каждой маленькой выплаты).

struct ProposalConviction {
    uint256 stakedTokens;
    uint256 lastConviction;   // значение conviction в момент последнего обновления
    uint256 lastTimestamp;
}

// conviction = stakedTokens * (1 - alpha^timePassed) / (1 - alpha)
// alpha = decay rate (например 0.9 per day)
function calculateConviction(
    ProposalConviction storage p,
    uint256 currentTime
) internal view returns (uint256) {
    uint256 timePassed = currentTime - p.lastTimestamp;
    // Упрощённая целочисленная версия с decay factor
    uint256 decayFactor = DECAY_PRECISION - (DECAY_RATE * timePassed);
    return (p.lastConviction * decayFactor / DECAY_PRECISION) + p.stakedTokens;
}

Upgrade механизм Governor

Governor контракты рекомендуется деплоить за UUPS proxy — это позволяет обновлять логику через governance proposal.

contract UpgradeableGovernor is Governor, UUPSUpgradeable {
    function _authorizeUpgrade(address newImplementation)
        internal override onlyGovernance {}
    
    // onlyGovernance modifier — только через прошедший proposal
    modifier onlyGovernance() {
        require(msg.sender == address(this), "Only governance can upgrade");
        _;
    }
}

Upgrade через self-call: proposal вызывает upgradeTo(newImplementation) на самом Governor. Это означает что апгрейд прошёл полный governance cycle включая Timelock задержку.

Распространённые ошибки при разработке

Flash loan governance attacks. Атакующий берёт flash loan → получает огромный voting power → создаёт и сразу же принимает proposal → возвращает loan. Защита: voting delay (задержка между proposal и началом голосования) + snapshot-based voting (голосование считается на момент snapshot, не текущие балансы).

Low quorum trap. Quorum 4% выглядит разумно, но если большой holder делегирует кому-то — реально активных голосов может не хватить. Calibrate quorum под реальную voter turnout статистику.

Proposal spam. Без proposalThreshold любой может создавать proposal. С низким порогом — тоже. Устанавливайте threshold достаточно высоким чтобы spam был дорогим, но не настолько высоким чтобы блокировать реальных участников.

Короткий timelock. 24 часа Timelock — слишком мало для DeFi протокола. Стандарт: 48–72 часа minimum для production. Для major upgrades — 7 дней.

Параметры по умолчанию для разных типов DAO

Параметр Small Community DAO DeFi Protocol DAO Treasury DAO
Voting Delay 1 день 2 дня 1 день
Voting Period 5 дней 7 дней 7 дней
Timelock 24 часа 48–72 часа 48 часов
Quorum 10% 4% 5%
Proposal Threshold 1% total supply 0.25% 0.5%

Процесс разработки

Фаза Содержание Срок
Дизайн механики Выбор voting model, параметры, treasury политика 1–2 нед
Governance токен ERC20Votes + distribution механика 2–3 нед
Governor + Timelock Сборка из OZ модулей + кастомизация 2–3 нед
Treasury контракт Управление активами, emergency pause 1–2 нед
Тесты Fork tests, simulation голосований, attack scenarios 2–3 нед
Frontend Proposal UI, voting interface, delegate directory 3–5 нед
Аудит 3–4 нед

Governance смарт-контракты — одна из немногих областей, где аудит двумя независимыми командами оправдан. Уязвимость в Governor может позволить атакующему вывести весь treasury одним proposal.