Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

Setting Up a Token-Based Voting System from Scratch

This guide provides a technical walkthrough for implementing an on-chain voting system using smart contracts. It covers proposal creation, voting power calculation based on token balance, vote tallying, and execution. The guide includes considerations for gas optimization, security against common attacks, and integration with a frontend governance portal.
Chainscore © 2026
introduction
GOVERNANCE

Introduction to On-Chain Token Voting

This guide explains how to build a secure, on-chain voting system where voting power is determined by token ownership, a core mechanism for DAOs and decentralized governance.

On-chain token voting is a governance model where decisions are recorded directly on a blockchain. Each governance token represents one vote, and a user's voting power is typically proportional to their token balance. This system is fundamental for Decentralized Autonomous Organizations (DAOs) like Uniswap, Compound, and Aave, allowing token holders to propose and vote on protocol upgrades, treasury allocations, and parameter changes. The primary benefits are transparency, as all votes are publicly verifiable, and immutability, as passed proposals can be executed autonomously via smart contracts.

The core architecture involves three key smart contracts: a Governance Token (e.g., an ERC-20 or ERC-1155), a Governor contract that manages proposal lifecycle, and a Timelock contract for secure execution. The Governor contract defines rules for proposal submission, voting periods, and quorum. A common standard is OpenZeppelin's Governor, which provides a modular, audited foundation. Votes are usually weighted by token balance at a specific block number (snapshot), preventing manipulation by buying tokens mid-vote. Delegation features allow users to assign their voting power to others without transferring tokens.

To set up a basic system, start by deploying a governance token. Using Solidity and OpenZeppelin contracts, you can extend ERC20Votes, which includes snapshot functionality. Next, deploy a Governor contract, such as GovernorCompatibilityBravo, configuring parameters like votingDelay (blocks before voting starts), votingPeriod (blocks voting is open), and quorumPercentage. Finally, deploy a Timelock controller to queue and execute successful proposals after a delay, a critical security measure. The Governor must be granted proposer and executor roles on the Timelock.

A proposal lifecycle follows specific steps: 1) A proposer submits a transaction list (targets, values, calldata) to the Governor, requiring a minimum token threshold. 2) After the voting delay, token holders cast votes using castVote. 3) If the vote succeeds (meets quorum and majority), the proposal is queued in the Timelock. 4) After the timelock delay, anyone can execute the proposal. This process ensures deliberate, secure upgrades. Tools like Tally and Snapshot (for off-chain signaling) provide user interfaces for these on-chain actions.

Security considerations are paramount. Use a Timelock to give users time to exit if a malicious proposal passes. Implement proposal thresholds to prevent spam. Carefully set quorum and voting period to balance efficiency and decentralization. For complex governance, consider vote delegation (like ERC-5805) and gasless voting via signatures (ERC-1271 & EIP-712) to improve participation. Always audit contracts and consider battle-tested implementations from Compound or OpenZeppelin before writing custom logic from scratch.

Testing and deployment are final steps. Use a framework like Hardhat or Foundry to simulate proposal lifecycles. Write tests that verify: token snapshots work correctly, quorum rules are enforced, and only successful proposals can execute. For mainnet deployment, consider a gradual rollout: first deploy on a testnet, then use a multisig for initial control, and finally transition full authority to the Governor contract through a final governance vote. This establishes a fully on-chain, self-governing system controlled by its token holders.

prerequisites
TOKEN VOTING

Prerequisites and Setup

Before deploying a token-based voting system, you need a foundational development environment and a clear understanding of the core components.

A token-based voting system requires a secure and functional development stack. You will need Node.js (v18 or later) and npm or yarn installed. The primary tool is a development framework for writing and testing smart contracts. We recommend Hardhat or Foundry for their robust testing environments and local blockchain simulation. You'll also need a code editor like VS Code and the MetaMask browser extension to interact with your deployed contracts. Finally, ensure you have access to an Ethereum testnet like Sepolia or Goerli for deployment, which requires test ETH from a faucet.

The system's architecture is built on three core smart contracts. First, an ERC-20 token contract defines the voting power; each token equals one vote. You can use OpenZeppelin's audited ERC20 implementation. Second, a governance contract manages the proposal lifecycle: creation, voting, and execution. This contract will hold the token's snapshot and tally votes. Third, a timelock contract is a critical security component. It queues and executes successful proposals after a mandatory delay, preventing malicious or rushed changes to the protocol.

Start by initializing your project. Using Hardhat, run npx hardhat init in an empty directory and select the TypeScript template for type safety. Next, install the OpenZeppelin Contracts library, which provides the secure, standard building blocks: npm install @openzeppelin/contracts. For Foundry, use forge init and then forge install openzeppelin/openzeppelin-contracts. This setup gives you access to the ERC20, Governor, and TimelockController contracts, which we will extend and customize in the following steps.

core-contract-structure
CORE CONTRACT STRUCTURE

Setting Up a Token-Based Voting System from Scratch

This guide details the essential smart contract architecture for a secure, on-chain governance system where voting power is proportional to token holdings.

A token-based voting system requires a foundational data model to track proposals, votes, and voter power. At its core, you need three primary structures: a Proposal struct to define the voting item, a Vote struct to record individual choices, and a mechanism to calculate voting weight. The Proposal typically includes fields like id, description, voteStart, voteEnd, forVotes, and againstVotes. Voting power is derived from an ERC-20 token balance, often using a snapshot of balances taken at the proposal creation block to prevent manipulation.

The contract's state variables are critical for security and functionality. You will store proposals in a mapping, such as mapping(uint256 => Proposal) public proposals, and track votes with a nested mapping like mapping(uint256 => mapping(address => Vote)) public votes. A crucial security pattern is to implement a snapshot mechanism. Instead of using the user's live balance, you record token balances at a specific block number using a library like OpenZeppelin's ERC20Snapshot or by calling the balanceOfAt function from a snapshot-enabled token. This prevents users from borrowing tokens to increase voting power.

Key functions include createProposal(string description), castVote(uint256 proposalId, bool support), and executeProposal(uint256 proposalId). The castVote function must check that the vote period is active, that the voter has not already voted, and calculate the voter's weight from the historical snapshot. Always use the Checks-Effects-Interactions pattern: validate inputs, update state, then emit events. An event like VoteCast(address indexed voter, uint256 proposalId, bool support, uint256 weight) is essential for off-chain indexing.

Consider advanced features like vote delegation, where users can delegate their voting power to another address, similar to Compound's Governor Bravo. This requires a separate delegates mapping and logic to transfer voting power. For gas efficiency, especially with many proposals, you might store vote data in packed storage or use bitmaps for tracking voter participation. Always include a timelock contract between proposal passage and execution to give users time to react to governance decisions.

Testing is non-negotiable. Write comprehensive unit tests using Foundry or Hardhat that cover edge cases: voting after a snapshot, double voting, voting with zero weight, and proposal execution logic. Use forked mainnet tests to simulate real token distributions. The final contract should inherit from established libraries like OpenZeppelin's Governor contracts for a production-ready foundation, but understanding the underlying structure is key to customization and security auditing.

proposal-lifecycle
IMPLEMENTING THE PROPOSAL LIFECYCLE

Setting Up a Token-Based Voting System from Scratch

A step-by-step guide to building a secure, on-chain governance system where token holders can create, vote on, and execute proposals.

A token-based voting system is the cornerstone of decentralized governance for DAOs and DeFi protocols. It allows stakeholders to collectively decide on protocol upgrades, treasury management, and parameter changes. The core lifecycle consists of three phases: proposal creation, voting, and execution. This guide will implement this lifecycle using a Solidity smart contract, focusing on security patterns like timelocks and vote delegation. We'll assume the use of an ERC-20 or ERC-721 token for voting power, with one token equaling one vote.

The first step is to define the smart contract's state and data structures. You need a Proposal struct to store the proposal's details: a unique ID, proposer address, description, target contract, calldata for execution, and vote tallies. Critical state variables include a proposalCount, a mapping from proposal ID to the Proposal struct, and a mapping to track which addresses have voted on which proposals to prevent double-voting. You must also set a votingDelay (time before voting starts) and votingPeriod (duration of the vote), typically expressed in block numbers.

Proposal creation should be permissioned, often requiring the proposer to hold a minimum number of tokens. The createProposal function will take the target address and calldata as arguments, create a new Proposal struct, and store it. It should emit an event for off-chain indexing. Before voting begins, implement a timelock period; this is a delay between a proposal's creation and the start of voting, giving the community time to review. This is a critical security measure against rushed or malicious proposals.

The voting logic is implemented in a castVote function. It should check that the voter has not already voted, that the proposal is in the active voting period, and calculate the voter's power. Voting power is typically sourced from an external token contract via the balanceOf function, often using a snapshot of balances from a past block to prevent manipulation. Support options are usually For, Against, and Abstain. Tally the votes and update the proposal state. Consider implementing vote delegation, where users can delegate their voting power to another address, similar to Compound's Governor system.

After the voting period ends, any account can call a queue function for successful proposals (e.g., those with more For than Against votes and meeting a quorum). This function should move the proposal to a queued state, often interacting with a TimelockController contract (like OpenZeppelin's) which will enforce a delay before execution. Finally, the execute function can be called after the timelock delay expires. It will send the stored calldata to the target contract. Always include comprehensive event emission at each stage (ProposalCreated, VoteCast, ProposalQueued, ProposalExecuted) for full transparency.

Key security considerations include: setting a sensible proposal threshold and quorum to prevent spam and ensure meaningful participation, using a timelock for all treasury and parameter-changing actions, and thoroughly auditing the contract's interaction with the token. For production, consider using battle-tested frameworks like OpenZeppelin's Governor contracts, which abstract much of this logic. You can extend the base contract to customize the voting token, timelock, and voting settings. The complete code and further details are available in the OpenZeppelin Governor documentation.

voting-mechanism
GOVERNANCE

Setting Up a Token-Based Voting System from Scratch

A practical guide to implementing a secure and transparent on-chain voting mechanism using token-weighted power calculation.

A token-based voting system is a foundational governance primitive where voting power is directly proportional to a user's token holdings. This model, used by protocols like Compound and Uniswap, aligns incentives by giving more influence to stakeholders with greater economic skin in the game. The core components are a voting smart contract, a snapshot mechanism to record token balances at a specific block, and a power calculation function. Setting this up requires careful consideration of security, gas efficiency, and Sybil resistance from the outset.

The first step is to define the voting power calculation. The simplest method is a one-token-one-vote system, where power equals the holder's balance. However, this can be extended. Many protocols implement delegation, allowing users to delegate their voting power to other addresses without transferring tokens, as seen in OpenZeppelin's Governor contracts. For quadratic voting or time-weighted models, the calculation becomes more complex, requiring logic to compute power based on a formula like sqrt(balance) or balance * lockupDuration. This logic is executed in the contract's getVotes function.

Here is a minimal Solidity example for a snapshot-based, token-weighted voting contract using the OpenZeppelin Governor library:

solidity
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
contract TokenGovernor is Governor, GovernorVotes {
    constructor(IVotes _token)
        Governor("TokenGovernor")
        GovernorVotes(_token)
    {}
    // The voting delay (in blocks) allows time for users to delegate after a proposal is submitted.
    function votingDelay() public pure override returns (uint256) { return 1; }
    // The voting period defines how long the vote remains active.
    function votingPeriod() public pure override returns (uint256) { return 45818; } // ~1 week
    // Quorum is the minimum voting power required for a proposal to pass.
    function quorum(uint256 blockNumber) public pure override returns (uint256) { return 1000e18; }
}

This contract uses GovernorVotes which automatically handles power calculation from the ERC20Votes token snapshot.

Critical security considerations include preventing double voting and flash loan attacks. Using a block snapshot (like ERC20Votes) prevents users from borrowing tokens to manipulate a live vote. The votingDelay parameter is essential here, as it sets a period between proposal creation and the snapshot block, giving delegates time to react. Furthermore, setting a proposal threshold (e.g., 1% of total supply) prevents spam. Always audit the token's getPastVotes function and ensure the governance contract has no privileged upgrade path that could overturn vote results.

To finalize the system, you must integrate the frontend and define proposal lifecycle. The typical flow is: 1) A proposer submits a transaction calldata target, 2) After the votingDelay, a snapshot of votes is taken, 3) Token holders cast votes for, against, or abstain, 4) After the votingPeriod, the proposal is queued for execution if quorum and majority are met. Tools like Tally or building a custom UI with wagmi and ConnectKit can facilitate voter interaction. Remember to test thoroughly on a testnet like Sepolia, simulating various token distribution and delegation scenarios.

VOTE WEIGHTING

Comparison of Voting Power Strategies

Methods for calculating a user's influence in a token-based governance system.

MechanismOne Token, One VoteQuadratic VotingTime-Locked VotingDelegated Voting

Core Principle

Linear power based on token balance

Power = sqrt(tokens committed)

Power multiplies with lockup duration

Tokens delegated to a representative

Sybil Resistance

Whale Mitigation

Voter Commitment

None required

Tokens temporarily locked

Tokens locked for set period (e.g., 4 years)

None for delegator

Implementation Complexity

Low

Medium

Medium

Medium-High

Gas Cost per Vote

Low

Medium

Low (after lock)

Low (for delegator)

Used By

Uniswap, Compound

Gitcoin Grants

Curve Finance, veTokens

MakerDAO, ENS

Key Trade-off

Simplicity vs. centralization risk

Fairness vs. computational overhead

Long-term alignment vs. liquidity sacrifice

Efficiency vs. voter apathy

security-gas-optimization
SECURITY AND GAS OPTIMIZATION

Setting Up a Token-Based Voting System from Scratch

A technical guide to implementing a secure and gas-efficient on-chain governance contract using Solidity, focusing on common vulnerabilities and optimization patterns.

A token-based voting system, or on-chain governance, allows token holders to propose and vote on protocol changes. The core contract must manage three key states: proposal creation, voting with token-weighted power, and execution of passed proposals. Security is paramount, as these contracts often control treasury funds or critical parameters. Common vulnerabilities include vote manipulation through token delegation flaws, reentrancy during proposal execution, and improper access controls. Using established standards like OpenZeppelin's governance contracts provides a secure foundation, but custom logic requires careful auditing.

Gas optimization is critical for voter participation. A naive implementation where users vote directly can become prohibitively expensive. The standard pattern is to use a snapshot mechanism. Instead of locking or transferring tokens, the contract records each voter's token balance at a specific block number (e.g., the proposal creation block) using block.number. This allows users to vote with their past balance without moving funds, drastically reducing gas costs. Libraries like OpenZeppelin's Snapshot abstract this logic.

Here's a simplified example of a vote function using a snapshot. The proposalSnapshot is set during proposal creation.

solidity
function castVote(uint256 proposalId, uint8 support) external returns (uint256) {
    address voter = msg.sender;
    uint256 votingPower = getVotes(voter, proposals[proposalId].snapshotBlock);
    require(votingPower > 0, "No voting power");
    require(!hasVoted[proposalId][voter], "Already voted");

    hasVoted[proposalId][voter] = true;
    if (support == 1) {
        proposals[proposalId].forVotes += votingPower;
    } else {
        proposals[proposalId].againstVotes += votingPower;
    }
    emit VoteCast(voter, proposalId, support, votingPower);
    return votingPower;
}

The getVotes function would query a token contract's getPastVotes method, which is part of the EIP-5805 standard.

To prevent flash loan attacks, where an attacker borrows a large number of tokens to manipulate a vote, the snapshot block must be in the past. A best practice is to set the snapshot block at least one block before the proposal goes live, making it impossible to use tokens acquired after the snapshot for voting. Furthermore, implement a timelock contract for executing passed proposals. This adds a mandatory delay between a vote's success and its execution, giving the community time to react to any malicious proposal that may have slipped through.

For further gas savings, consider using gasless voting via signature delegation (EIP-712). Voters can sign their vote off-chain, and a relayer submits the signed transaction, paying the gas fee. This removes the gas barrier for small holders. The contract must verify the EIP-712 signature and then process the vote as if it came from the signer. Always use the nonces pattern to prevent signature replay attacks across different proposals or chains.

TOKEN VOTING

Common Implementation Mistakes

Building a secure and efficient on-chain voting system requires careful attention to contract architecture and edge cases. This guide addresses frequent pitfalls developers encounter.

Double voting typically stems from a flawed state-tracking mechanism. The most common mistake is not properly recording a vote to prevent the same token from being used again.

Critical Checks:

  • Store a mapping like hasVoted[proposalId][voter] = true upon casting a vote.
  • Ensure the check and state update happen in a single transaction to prevent reentrancy.
  • For snapshot-based voting, you must record the voter's token balance at a specific block number. A frequent error is using balanceOf(voter) at the time of voting, which allows users to transfer tokens after the snapshot to vote again elsewhere.

Example Fix:

solidity
function castVote(uint proposalId, uint8 support) external {
    require(!hasVoted[proposalId][msg.sender], "Already voted");
    // ... voting logic
    hasVoted[proposalId][msg.sender] = true;
}
frontend-integration
TUTORIAL

Frontend Integration with a Governance Portal

A step-by-step guide to building a token-based voting interface using popular Web3 libraries and smart contract interactions.

A token-based governance portal allows token holders to create and vote on proposals, directly translating on-chain ownership into decision-making power. The frontend's primary role is to serve as a secure and intuitive bridge between the user and the governance smart contract. This involves several key functions: reading user token balances and voting power, fetching active proposals, submitting votes, and displaying real-time results. Modern libraries like wagmi, viem, and RainbowKit streamline these interactions by abstracting wallet connection and contract call complexities.

The first step is setting up the development environment and connecting to the blockchain. Using a framework like Next.js or Vite, install the necessary dependencies: wagmi, viem, @rainbow-me/rainbowkit, and the specific Ethers.js or viem adapter for your governance contract's chain. Configure the Wagmi client with your RPC provider (like Alchemy or Infura) and the contract's ABI (Application Binary Interface). This ABI, generated when you compile the Solidity contract, defines the functions your frontend can call, such as getProposals, castVote, and getVotes.

User authentication is handled via wallet connection. RainbowKit provides a pre-built button that supports multiple wallets (MetaMask, Coinbase Wallet, WalletConnect). Upon connection, you obtain the user's address, which is essential for querying their governance token balance—a prerequisite for voting. You'll call the token contract's balanceOf function or the governance contract's getVotingPower function. It's critical to handle network switching; ensure your app prompts users to switch to the correct chain (e.g., Ethereum Mainnet, Arbitrum, Optimism) if they are connected to an unsupported network.

Fetching and displaying proposals requires reading data from the governance contract. A typical proposal struct includes an id, description, voteCountFor, voteCountAgainst, and an endBlock. Use Wagmi's useContractRead hook to call a view function like getAllProposals. Map this data to a UI component, clearly showing the proposal status (active, passed, defeated) and time remaining. For voting, implement buttons that trigger a transaction via useContractWrite. The call will be to castVote(proposalId, support), where support is a boolean. Always estimate gas and display the transaction confirmation status.

After a vote is cast, the UI must update to reflect the new state. Implement real-time updates by listening for contract events using useContractEvent or by refetching data after a transaction is confirmed. For a better user experience, cache proposal data with React Query (TanStack Query) to minimize RPC calls. Security considerations are paramount: always verify proposal data on-chain, never rely solely on off-chain summaries, and use CSP headers to prevent injection attacks. The final interface should empower users with clear information, transparent transaction feedback, and secure, verifiable interactions with the governance protocol.

TOKEN VOTING

Frequently Asked Questions

Common technical questions and solutions for developers building on-chain governance systems.

A basic on-chain token voting system requires three primary smart contracts:

  1. Governance Token (ERC-20/ERC-1155): The fungible token that represents voting power. It must implement a getPastVotes function for snapshot-based voting, often using OpenZeppelin's ERC20Votes extension.
  2. Governor Contract: The core logic contract (e.g., OpenZeppelin's Governor). It manages proposal lifecycle (create, vote, execute), defines voting delay/period, and integrates with the token contract for vote counting.
  3. Timelock Controller: An optional but critical security module that queues and delays successful proposal execution. This prevents last-minute malicious proposals from executing immediately and gives token holders time to exit if a harmful proposal passes.

Using a battle-tested framework like OpenZeppelin Governor reduces audit surface area and provides modular extensions for custom logic.

conclusion
IMPLEMENTATION SUMMARY

Conclusion and Next Steps

You have now built a foundational on-chain voting system. This guide covered the core components: a token contract, a governance contract for proposal lifecycle management, and a basic frontend for interaction.

The system you've implemented demonstrates the essential mechanics of token-based governance: using ERC20Votes for snapshotting, creating proposals with executable calldata, and casting weighted votes. However, this is a starting point. For a production system, you must address critical security and scalability considerations. Key upgrades include implementing a timelock controller (like OpenZeppelin's) to queue and delay executed transactions, adding a governor compatibility layer for gas-efficient voting with signatures, and thoroughly auditing the contract logic, especially the proposal state machine and vote tallying.

To extend functionality, consider integrating with existing frameworks. The OpenZeppelin Governor contract provides a robust, audited base with modules for timelocks, vote delegation, and different voting strategies (e.g., ERC721 voting). For gas optimization, explore EIP-712 typed structured data signing so users can vote without sending an on-chain transaction until the final tally. You should also implement proposal thresholds and quorum requirements to prevent spam and ensure sufficient participation. Testing is paramount; use forked mainnet simulations with tools like Tenderly or Foundry's cheatcodes to simulate complex voting scenarios and attack vectors.

Your next practical steps should be: 1) Deploy the upgraded contracts to a testnet (like Sepolia or Goerli) and run through the entire proposal flow. 2) Use a block explorer to verify the contract source code. 3) Integrate a frontend library like wagmi or web3.js to connect user wallets and display proposal data dynamically. 4) Consider the user experience for delegation, which is crucial for voter participation; platforms like Snapshot offer off-chain signaling that can inform on-chain execution. Finally, join developer communities in the Ethereum Magicians forum or OpenZeppelin Discord to discuss governance design patterns and security best practices.