Governance Voting Contract Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Governance Voting Contract Development
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1217
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1046
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Development of Governance Voting Contracts

A governance contract is not just a voting mechanism. It's a system that manages a protocol with a treasury worth millions of dollars. Errors in governance lead to real losses: the 2022 flash loan attack on Beanstalk exploited voting manipulation to extract $182M. Proper governance architecture is a balance between security and community participation.

OpenZeppelin Governor: Basic Architecture

The de facto standard for on-chain governance is the OpenZeppelin Governor framework. It implements a Governor Bravo compatible interface (compatible with Tally, Boardroom, Snapshot).

System components:

  • Governor: core, manages proposal lifecycle
  • GovernorSettings: settings (voting delay, voting period, proposal threshold)
  • GovernorCountingSimple: vote counting (For/Against/Abstain)
  • GovernorVotes: integration with ERC20Votes or ERC721Votes token
  • GovernorTimelockControl: mandatory timelock between approval and execution
// 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,    // voting delay: ~1 day on Ethereum (12s/block)
            50400,   // voting period: ~7 days
            100000e18 // proposal threshold: 100k tokens
        )
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4) // 4% quorum from circulating supply
        GovernorTimelockControl(_timelock)
    {}

    // Mandatory overrides to resolve conflicts between extensions
    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(); }
}

Governance Token: ERC20Votes

Voting power comes from checkpoint history of the token. ERC20Votes stores balance snapshots at each block — preventing manipulation through token purchases immediately before voting.

Critically important: users must delegate (at least to themselves) for their voting power to be counted. This is a frequent point of confusion — token exists, but voting is impossible without delegation.

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);
    }
    // Users call delegate(address(self)) to activate voting power
}

TimelockController: Mandatory Component

Timelock is a buffer between approved voting and execution. Gives the community time to notice malicious proposals and exit the protocol.

// Deploy TimelockController
TimelockController timelock = new TimelockController(
    2 days,        // minDelay: minimum 2 days between queue and execute
    proposers,     // only Governor can queue
    executors,     // anyone can execute (after delay)
    admin          // temporary admin, then transfer to timelock itself
);

// Governor must have PROPOSER_ROLE
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
// Anyone can execute
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Revoke admin from deployer
timelock.revokeRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);

Minimum delay for protocols with TVL > $10M — 48 hours. For critical parameters (upgrades, fee changes) — 7 days.

Protection Against Attacks

Flash loan attacks: attacker borrows flash loan tokens, creates proposal and votes in single transaction. Protection — votingDelay > 0. Checkpoint snapshot is taken at proposal creation block, not voting block. ERC20Votes stores history — balance at snapshot block, not current.

Proposal spam: without proposalThreshold anyone can spam proposals. 100k tokens — reasonable threshold for medium protocols. For small DAOs — sufficient 1-5% of supply.

Quorum gaming: at low turnout, small amount of tokens suffice for passage. GovernorVotesQuorumFraction counts quorum as % of token.getPastTotalSupply() — correct approach, quorum tied to circulating supply, not absolute number.

Parameters for Different DAO Types

Parameter Small DAO Medium Protocol Large Protocol
Voting delay 1 day 2 days 2 days
Voting period 3 days 5 days 7 days
Quorum 4% 4% 10%
Timelock 1 day 2 days 7 days
Proposal threshold 0.1% supply 0.5% supply 1% supply

Delegated Voting and Gasless Signatures

Most token holders won't vote directly — gas is expensive, process is complex. Solutions:

Delegation: holder delegates voting power to another address (delegate). Delegate votes on behalf of multiple holders. Used in Compound, Uniswap.

EIP-712 gasless vote: castVoteBySig() allows signing vote off-chain (via Snapshot or Tally) and submitting on-chain through relayer. User doesn't pay gas.

// Voting via signature — relayer pays gas
function castVoteBySig(
    uint256 proposalId,
    uint8 support,
    address voter,
    bytes memory signature
) public returns (uint256 weight) {
    // EIP-712 signature verification of voter
    // ...
    return _castVote(proposalId, voter, support, "", "");
}

Governance is a living system. Parameters need review as DAO grows: quorum that worked at 10k holders may be unachievable at 500k holders with low turnout.