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

How to Implement a Delegated Voting System

This guide provides a technical framework for building a delegated voting (liquid democracy) system on Ethereum. It covers smart contract architecture for delegation, vote execution, and strategies to mitigate centralization risks.
Chainscore © 2026
introduction
DEVELOPER TUTORIAL

How to Implement a Delegated Voting System

A technical guide to building a smart contract-based delegated voting system, covering delegation logic, vote tallying, and security considerations.

A delegated voting system allows token holders to delegate their voting power to a representative who votes on their behalf. This mechanism, used by protocols like Compound and Uniswap, improves governance participation by reducing voter apathy. At its core, the system tracks two key mappings: the delegate a user has chosen and the voting weight (often based on token balance) each delegate commands. Implementing this requires a smart contract that securely manages delegation state changes and ensures votes are tallied from the delegate's aggregated weight, not the individual delegators.

The primary contract functions are delegate(address to), castVote(uint proposalId, uint8 support), and internal methods for vote tallying. When a user calls delegate, the contract must transfer the user's voting power from their previous delegate (if any) to the new one. This involves updating the delegate mapping and adjusting the delegate's vote count. A critical optimization is to use a checkpointing system (like OpenZeppelin's Checkpoints library) to record historical voting power, which is essential for correctly tallying votes based on a user's balance at the time a proposal was created, not when the vote is cast.

Here is a simplified Solidity snippet for the delegation logic:

solidity
mapping(address => address) public delegates;
mapping(address => uint256) public delegatedVotes;

function delegate(address delegatee) public {
    address currentDelegate = delegates[msg.sender];
    uint256 senderBalance = balanceOf(msg.sender);
    
    delegates[msg.sender] = delegatee;
    
    // Subtract votes from old delegate
    _moveDelegates(currentDelegate, delegatee, senderBalance);
}

function _moveDelegates(address src, address dst, uint256 amount) internal {
    if (src != dst && amount > 0) {
        if (src != address(0)) {
            delegatedVotes[src] -= amount;
        }
        if (dst != address(0)) {
            delegatedVotes[dst] += amount;
        }
    }
}

For vote casting, the castVote function must calculate the delegate's voting power at the correct historical block. If Alice delegated to Bob, and a proposal was created at block #100, the contract should tally Bob's voting power based on the sum of balances from all his delegators at block #100. This prevents manipulation where users delegate large sums after a proposal is made. Using a library like Checkpoints from OpenZeppelin v4.9 or later provides a gas-efficient way to store and look up this historical data with binary search.

Key security considerations include protecting against double voting (a user voting both directly and through a delegate) and ensuring delegation updates cannot affect already-concluded votes. The contract should use the Checks-Effects-Interactions pattern and consider implementing a timelock or warm-up period for new delegations to prevent flash loan attacks, where an attacker borrows tokens, delegates them to sway a vote, and repays the loan within a single transaction. Always audit the final contract and consider using established governance frameworks like OpenZeppelin Governor as a foundation.

To test your implementation, write comprehensive unit tests covering edge cases: delegation to zero address, chained delegation (A delegates to B who delegates to C), and voting power snapshots. Tools like Hardhat or Foundry are ideal for this. For production deployment, you'll integrate the voting contract with a proposal factory and timelock executor. Remember, the goal is a transparent, secure system that accurately reflects the will of the token-holding community, making on-chain governance both accessible and robust.

prerequisites
PREREQUISITES AND SYSTEM DESIGN GOALS

How to Implement a Delegated Voting System

This guide outlines the core requirements and architectural decisions for building a secure and efficient on-chain delegated voting system, such as those used by DAOs and governance protocols.

Before writing any code, you must define the system's design goals. A robust delegated voting system must achieve several objectives: security against manipulation, decentralization of voting power, gas efficiency for user participation, and transparency in vote tallying and delegation. Common implementations include the ERC-20 Votes standard for token-weighted voting and custom logic for quadratic voting or conviction voting. The choice of blockchain (e.g., Ethereum, L2s like Arbitrum or Optimism) will significantly impact cost and finality speed, directly affecting voter turnout.

Key prerequisites include a foundational understanding of smart contract development with Solidity, familiarity with OpenZeppelin contracts for secure base implementations, and knowledge of frontend integration using libraries like ethers.js or viem. You'll need a token contract that implements ERC20Votes (EIP-5805), which provides snapshots of token balances to prevent manipulation via token transfers during active voting periods. Setting up a local development environment with Hardhat or Foundry is essential for testing complex governance logic and simulating attack vectors before deployment.

The core system architecture revolves around three main contracts: the Voting Token, the Governor Contract, and the Timelock Controller. The token holds the voting power. The governor contract, often extending OpenZeppelin's Governor base, manages proposal lifecycle—creation, voting, and execution. The timelock contract queues and executes successful proposals after a delay, providing a safety mechanism for reverting malicious transactions. This separation of concerns enhances security and modularity.

Critical design decisions involve setting governance parameters. You must define the voting delay (time between proposal submission and voting start), voting period (duration of the vote), proposal threshold (minimum tokens required to submit a proposal), and quorum (minimum voting power participation for a vote to be valid). These values require careful calibration; a quorum too high can lead to governance paralysis, while a threshold too low may enable spam. Analyze existing systems like Compound Governance or Uniswap Governance for real-world benchmarks.

Finally, plan for vote delegation and sybil resistance. The ERC20Votes standard allows token holders to delegate their voting power to another address (or to themselves) without transferring tokens. This is crucial for participation, as users can delegate to knowledgeable community members. Your system must efficiently track these delegations and calculate voting power from historical snapshots, preventing double-voting and ensuring each token is counted only once per proposal.

key-concepts
DELEGATED VOTING

Core Concepts for Implementation

Key technical components and design patterns for building a secure, gas-efficient, and user-friendly delegated voting system on-chain.

contract-architecture
SMART CONTRACT ARCHITECTURE AND DATA STRUCTURES

How to Implement a Delegated Voting System

A step-by-step guide to building a gas-efficient, on-chain delegated voting system using Solidity, covering core data structures, delegation logic, and vote tallying.

A delegated voting system allows token holders to delegate their voting power to representatives, enabling scalable governance for DAOs and protocols. The core architecture requires three primary data structures: a mapping for token balances (balances), a mapping linking delegates to delegators (delegates), and a struct to store proposal and vote data. Efficient storage is critical, as vote tallying can become gas-intensive. This guide implements a system where delegation is transitive, meaning a delegate's voting power includes the tokens of all users who delegated to them, directly or indirectly.

The delegation mechanism is managed by a delegate function. When a user calls delegate(address to), the contract must update the delegation chain. First, it sets the caller's delegate (delegates[msg.sender] = to). Then, it must move the caller's voting power from their previous delegate (if any) to the new one. This requires tracking a votes mapping for each delegate at a given block number. A common pattern is to use a checkpoint system, storing historical vote balances in an array of Checkpoint structs to allow lookups for past votes, which is essential for proposals that snapshot votes at a specific block.

Proposals are created with a propose function, storing a Proposal struct containing the description, vote counts for/against/abstain, and the snapshot block number. When a user votes via a castVote function, the contract calculates their voting power by reading from the delegate's vote checkpoint at the proposal's snapshot block. The vote is then added to the proposal's tally. To prevent double-voting, a mapping of hasVoted[proposalId][voter] is used. This structure ensures that even if a user's token balance changes after the snapshot, their voting power for that specific proposal remains locked and accurate.

Here is a simplified code snippet for the core delegation and vote tallying logic, excluding checkpoint details for brevity:

solidity
mapping(address => address) public delegates;
mapping(address => uint256) public balances;
mapping(uint256 => Proposal) public proposals;

function delegate(address to) external {
    address currentDelegate = delegates[msg.sender];
    // Move votes from old delegate to new delegate
    _moveDelegates(currentDelegate, to, balances[msg.sender]);
    delegates[msg.sender] = to;
}

function castVote(uint256 proposalId, uint8 support) external {
    Proposal storage proposal = proposals[proposalId];
    require(block.number <= proposal.endBlock, "Voting ended");
    address voter = getDelegate(msg.sender, proposal.snapshotBlock);
    uint256 votes = getVotes(voter, proposal.snapshotBlock);
    proposal.forVotes += votes;
}

Key security considerations include preventing delegation loops, which can be mitigated by checking that to != msg.sender and implementing a check for circular delegation in _moveDelegates. Gas optimization is achieved by using the checkpoint pattern instead of updating all delegate totals on every transfer. For production use, integrate with existing standards like OpenZeppelin's ERC20Votes or Compound's Governor, which provide audited, gas-efficient implementations of these patterns. Always conduct thorough testing, especially for edge cases in delegate transfer and vote power calculation at historical blocks.

delegation-logic
SMART CONTRACT DEVELOPMENT

Implementing Delegation and Undelegation Logic

This guide explains how to build a delegated voting system using Solidity, covering core concepts like vote weight aggregation, delegation chains, and gas optimization.

Delegated voting allows token holders to transfer their voting power to another address, called a delegate. This is a core mechanism in governance systems like Compound and Uniswap, enabling participants who lack expertise or time to still influence protocol decisions. In a smart contract, this requires tracking two key mappings: one for each address's token balance and another for its chosen delegate. The contract's logic must ensure that delegated votes are counted correctly during proposals, even if the original delegator's balance changes after delegation.

The primary challenge is calculating an account's total voting power, which is the sum of its own tokens plus all tokens delegated to it. A naive approach that iterates through all delegators for each vote is prohibitively expensive. The efficient solution, used by OpenZeppelin's Votes library, is to maintain a history of checkpoints. When a user delegates, the contract records the delegate's new vote power at that block number. To get a user's voting power at any past block (e.g., when a proposal was created), the contract performs a binary search on this checkpoint history. This makes the lookup gas-efficient and secure against manipulation.

Here is a simplified core of the delegation logic. The contract must update vote power checkpoints when tokens are transferred, minted, burned, or when delegation changes.

solidity
function _delegate(address delegator, address delegatee) internal {
    address currentDelegate = delegates[delegator];
    uint256 delegatorBalance = balances[delegator];
    delegates[delegator] = delegatee;

    // Move votes from old delegate to new delegate
    _moveVotingPower(currentDelegate, delegatee, delegatorBalance);
}

function _moveVotingPower(address src, address dst, uint256 amount) internal {
    if (src != dst && amount > 0) {
        if (src != address(0)) {
            // Write a new checkpoint subtracting power from source
            _writeCheckpoint(_checkpoints[src], _subtract, amount);
        }
        if (dst != address(0)) {
            // Write a new checkpoint adding power to destination
            _writeCheckpoint(_checkpoints[dst], _add, amount);
        }
    }
}

Undelegation is simply a special case of delegation where the delegatee is set to the delegator's own address (or a zero address, depending on the design). The _moveVotingPower function handles the reversal of vote weight. A critical security consideration is preventing delegation loops, which could be exploited to double-count votes. The standard practice is to enforce that a delegate cannot delegate to themselves in a way that creates a cycle; the _delegate function should revert if delegatee == delegator or if it would form a chain longer than a defined limit.

When integrating this system, you must also handle state changes from token transfers. Every _beforeTokenTransfer hook must call _moveVotingPower to adjust the checkpoints for the sender's and receiver's delegates. For snapshot voting, you use the getVotes function with a past block number, which reads from the checkpoint history. This design ensures the voting power for a historical proposal remains immutable and verifiable, a property known as vote checkpointing. Always audit delegation logic thoroughly, as flaws here can lead to governance attacks.

vote-casting-execution
DELEGATION MECHANICS

How to Implement a Delegated Voting System

A technical guide to building a smart contract system for delegated voting, covering vote casting, power calculation, and delegation logic.

A delegated voting system allows token holders to delegate their voting power to representatives, or delegates, who vote on their behalf. This model is fundamental to governance in protocols like Compound and Uniswap. The core smart contract must manage three primary states: a mapping of token balances, a mapping of delegate assignments, and a record of votes cast. When a user delegates, their voting power is transferred to the delegate's address for all future proposals until the delegation is changed. This design separates the act of holding tokens from the act of participating in governance.

Voting power calculation is typically snapshot-based to prevent manipulation. The standard approach is to use a checkpoint system, where a user's voting power for a given proposal is determined by their token balance at a specific block number, defined before voting begins. A contract stores a history of balance checkpoints for each address. The getPriorVotes(address account, uint256 blockNumber) function, popularized by OpenZeppelin's ERC20Votes standard, performs a binary search on this history to find the correct balance. The delegate's voting power is the sum of all balances delegated to them at that historical block.

Implementing vote casting requires enforcing delegation logic. Only the current delegate for an address at the proposal's snapshot block should be authorized to cast votes using that address's power. The contract function castVote(uint256 proposalId, uint8 support) should: 1) verify the proposal is active, 2) fetch the caller's voting power via getPriorVotes, 3) record the vote and the power used, and 4) ensure the same delegate cannot vote twice for the same proposal. The state change should emit an event like VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 votes) for off-chain tracking.

Here is a simplified code snippet for a core voting contract using a checkpoint system:

solidity
function delegate(address delegatee) public {
    _delegate(msg.sender, delegatee);
}
function _delegate(address delegator, address delegatee) internal {
    address currentDelegate = delegates[delegator];
    uint256 delegatorBalance = balanceOf(delegator);
    delegates[delegator] = delegatee;
    emit DelegateChanged(delegator, currentDelegate, delegatee);
    _moveDelegates(currentDelegate, delegatee, delegatorBalance);
}
function _moveDelegates(address src, address dst, uint256 amount) internal {
    if (src != dst && amount > 0) {
        if (src != address(0)) {
            uint256 srcOld = _writeCheckpoint(_checkpoints[src], _subtract, amount);
            emit DelegateVotesChanged(src, srcOld, srcOld - amount);
        }
        if (dst != address(0)) {
            uint256 dstOld = _writeCheckpoint(_checkpoints[dst], _add, amount);
            emit DelegateVotesChanged(dst, dstOld, dstOld + amount);
        }
    }
}

Key security considerations include protecting against double voting, ensuring delegation cannot be front-run to steal voting power, and correctly handling token transfers. When tokens are transferred, the voting power associated with them must be moved from the sender's delegate to the receiver's delegate automatically. This is handled in the _beforeTokenTransfer hook in an ERC20 implementation. Additionally, consider implementing a timelock or voting delay to prevent last-minute delegation swings from affecting active proposals. Always audit the binary search logic in getPriorVotes to prevent out-of-bounds errors.

For production use, integrate established libraries like OpenZeppelin's ERC20Votes and Governor contracts. These provide audited, modular components for checkpoints, vote casting, and proposal lifecycle management. The Governor contract uses the IVotes interface to query voting power, making it compatible with any checkpoint token standard. When designing the system, decide on critical parameters: voting period length, proposal threshold, quorum requirements, and whether to allow delegation to the zero address (which effectively revokes voting power). Test thoroughly with forked mainnet state to simulate real delegation behavior.

GOVERNANCE ARCHITECTURE

Delegated Voting vs. Direct Voting: Trade-offs

A comparison of core design choices for on-chain governance, detailing the operational and security implications of each model.

Feature / MetricDelegated VotingDirect Voting

Voter Participation Requirement

Low (Delegates vote)

High (All token holders vote)

Typical Voter Turnout

40-70% of voting power

5-15% of token holders

Gas Cost for Average Voter

~$0 (Delegation only)

$10-50+ per proposal

Decision-Making Speed

Fast (Delegate consensus)

Slow (Mass coordination)

Barrier to Informed Voting

Low (Relies on experts)

High (Requires self-research)

Risk of Voter Apathy

High (Power centralization)

Mitigated (Direct stake)

Sybil Attack Resistance

Strong (Stake-weighted)

Weak (1-token-1-vote common)

Implementation Complexity

High (Slashing, delegation logic)

Low (Simple tally)

mitigation-strategies
GOVERNANCE DESIGN

Strategies to Prevent Delegate Cartels

Technical mechanisms and incentive structures to decentralize voting power and mitigate the formation of dominant delegate blocs.

03

Enforce Delegation Caps

Protocols can implement hard or soft caps on the percentage of total voting power any single delegate can wield. Compound Governance uses a proposal threshold (e.g., 100,000 COMP) to limit proposal creation to a broad base. A soft cap could progressively reduce voting weight for delegates exceeding a certain stake, similar to curve decay models in tokenomics.

05

Design Sybil-Resistant Identity

Prevent vote-buying and fake accounts by integrating sybil-resistant identity layers. Solutions include:

  • Proof of Personhood (Worldcoin, BrightID)
  • Social graph analysis (Gitcoin Passport, ENS)
  • Non-transferable soulbound tokens (SBTs) These systems ensure one-human-one-vote principles, breaking cartels that rely on accumulating anonymous wallets.
testing-auditing
TESTING, SECURITY, AND AUDIT CONSIDERATIONS

How to Implement a Delegated Voting System

A secure delegated voting system requires rigorous testing, formal verification, and comprehensive audits to protect user funds and ensure governance integrity.

Implementing a delegated voting system begins with a robust testing strategy. Unit tests should verify the core logic of the Vote and Delegate functions, while integration tests simulate complex user journeys like delegation chaining and vote execution. Use a forked mainnet environment (e.g., Foundry's forge create --fork-url) to test interactions with live token contracts. Property-based testing with tools like Foundry's fuzzing is critical for uncovering edge cases in vote weight calculations and delegation state transitions. For example, fuzz invariants should assert that the total voting power never exceeds the total token supply.

Security considerations are paramount. The system must prevent common vulnerabilities: - Double voting: Ensure a token holder's voting power is counted only once, whether they vote directly or through a delegate. - Reentrancy attacks: Use the Checks-Effects-Interactions pattern when executing proposals that transfer funds. - Front-running delegation: Consider implementing a time-lock or snapshot mechanism to prevent last-minute delegation changes that could manipulate vote outcomes. Implement access controls with OpenZeppelin's Ownable or AccessControl to restrict critical functions like proposal creation to authorized governance modules.

For high-assurance systems, formal verification with tools like Certora or Halmos provides mathematical proof that the contract logic adheres to its specification. Key properties to verify include: the conservation of voting power (sum(delegatedVotes) <= totalSupply), the transitivity of delegation (if A delegates to B and B delegates to C, then A's votes go to C), and the inability to vote with undelegated tokens. Writing clear specifications for these properties is often as valuable as the verification process itself, as it forces a rigorous definition of correct behavior.

Before mainnet deployment, engage multiple professional audit firms. A thorough audit report will cover: - Business logic flaws: Can delegates vote on expired proposals? Can voting power be frozen? - Centralization risks: Are there admin keys that can unilaterally alter votes or proposals? - Gas optimization: Are vote tallying functions, which may be called repeatedly, gas-efficient? Share your comprehensive test suite and formal verification reports with auditors to streamline their review. Post-audit, create a detailed remediation report and conduct a testnet deployment to verify all fixes before the final launch.

Monitoring and incident response planning are ongoing requirements. Use on-chain monitoring tools like OpenZeppelin Defender or Tenderly to set up alerts for suspicious events, such as a large, unexpected delegation shift. Prepare a pause mechanism or timelock-controlled upgrade path for the voting contract to respond to discovered vulnerabilities. Document all governance parameters—proposal thresholds, voting periods, quorum requirements—clearly for users. A transparent and tested security process is the foundation of a trusted decentralized governance system.

DELEGATED VOTING

Frequently Asked Questions

Common technical questions and solutions for developers implementing on-chain delegated voting systems.

Delegated voting is a governance model where token holders can delegate their voting power to representatives, called delegates. On-chain, this is implemented using smart contracts that manage delegation logic. A typical system has three core functions:

  1. Delegation: A user calls a function like delegate(address to) to assign their voting weight to another address.
  2. Vote Casting: Delegates (or self-delegated users) call a function like castVote(uint proposalId, uint8 support) to vote on proposals.
  3. Vote Tallying: The contract calculates a user's voting power at a specific block (often when a proposal is created) to prevent manipulation. The delegate's vote is then weighted by the sum of all tokens delegated to them at that snapshot.

Protocols like Compound Governor Bravo and OpenZeppelin's Governor are standard implementations. The key on-chain invariant is that voting power is calculated from a historical snapshot, not live balances.

conclusion
IMPLEMENTATION SUMMARY

Conclusion and Next Steps

This guide has covered the core components for building a secure and functional delegated voting system on-chain. Here's a summary of the key takeaways and resources for further development.

You have now implemented the foundational logic for a delegated voting system. The core smart contract should include functions for: registering voters and delegates, submitting and executing delegation, creating proposals with a snapshot mechanism, casting delegated votes, and tallying results. Remember that on-chain delegation is often implemented using a pull-over-push pattern for gas efficiency, where delegates must actively claim voting power rather than having it automatically transferred. This prevents state bloat and reduces gas costs for the delegator.

For production deployment, several critical enhancements are necessary. Security audits are non-negotiable; consider engaging firms like OpenZeppelin or Trail of Bits. Implement comprehensive access controls using libraries like OpenZeppelin's Ownable or role-based systems. To prevent governance attacks, integrate a timelock contract (e.g., TimelockController) to delay proposal execution. For frontend integration, use libraries like wagmi and viem to interact with your contract, and consider indexing delegation events with The Graph for efficient querying of voter histories and delegate relationships.

To explore more advanced patterns, study existing implementations. The Compound Governance system is a canonical reference for delegation and vote-weighting. For gasless voting, review Snapshot's off-chain signing mechanism paired with an on-chain executor. To manage complex delegation strategies, look into ERC-20 vote tokens with permit functionality for signature-based approvals. Your next steps should involve forking and testing these systems on a testnet, starting with a simplified version before adding complexity like quadratic voting or conviction voting modules.

How to Implement a Delegated Voting System (Liquid Democracy) | ChainScore Guides