Skip to content

blockful/ranked-choice-voting

Repository files navigation

ranked-choice-voting

Onchain ranked-choice voting for DAOs with Copeland and Schulze methods.

CI License: MIT Solidity Built with Foundry

Overview

Two standalone Solidity contracts that run pairwise-Condorcet ranked-choice elections fully onchain. Voters submit ranked ballots weighted by IVotes (ERC20Votes / ERC721Votes) snapshots. CopelandVoting and SchulzeVoting both implement the common IRankedChoiceVoting interface — integrate once, swap methods with a single constructor change. Each contract outputs a deterministic strict total order over all candidates; downstream consumers (Governors, council selectors, budget allocators) interpret that ordering however they need.

Aimed at DAOs and onchain organizations that need a Condorcet-method election — council selection, grant allocation, prioritized lists, multi-winner committees — where the canonical "winner-take-all" Governor pattern doesn't fit and an ordered ranking is the natural output.

Explicitly not: an audited contract, an instant-runoff (IRV) implementation, or an OpenZeppelin Governor replacement. It produces a ranking; combining that ranking with proposal execution is the integrator's job.

Features

  • Two methods, one interface — swap CopelandVoting <-> SchulzeVoting with no integration changes.
  • Standalone — no coupling to OpenZeppelin Governor or any DAO framework.
  • Generic — any IVotes token works (ERC20Votes, ERC721Votes, custom checkpointed tokens).
  • Permissionless — anyone can create an election, cast, tally, or finalize.
  • Replaceable ballots — voters can recast at any time before the deadline.
  • Lazy batched tally — pay gas only when you need the result; spread across multiple transactions.
  • Live preview — getCurrentResult(id) returns the ranking the contract would produce if finalize() were called right now, at any lifecycle phase.
  • Deterministic ordering — always a strict total order, with method-specific tiebreaks (Copeland: Minimax then index; Schulze: strongest-path then index).
  • Partial rankings — voters rank any subset of candidates; unranked candidates contribute nothing.

Methods

  • Copeland — each candidate's score is (head-to-head wins − losses); Minimax breaks ties, then candidate index. Cheaper to finalize (O(C²)) and easier to explain. Default choice unless you specifically expect cyclical preferences.
  • Schulze — Floyd-Warshall over the widest-path semiring produces strongest-path scores; candidate index breaks ties. Resolves asymmetric Condorcet cycles decisively at the cost of O(C³) finalize.

Full methodology, worked examples and tiebreak rules: docs/voting-methods.md.

Quick start

forge install
forge build
forge test

Requires Foundry.

Usage

import {IRankedChoiceVoting} from "src/interfaces/IRankedChoiceVoting.sol";
import {CopelandVoting} from "src/CopelandVoting.sol";
// or: import {SchulzeVoting} from "src/SchulzeVoting.sol";

// Integrate against the common interface — swap methods freely.
IRankedChoiceVoting voting = IRankedChoiceVoting(address(new CopelandVoting()));

// 1. Create an election
bytes32[] memory candidates = new bytes32[](3);
candidates[0] = keccak256("Alice");
candidates[1] = keccak256("Bob");
candidates[2] = keccak256("Carol");

uint256 electionId = voting.createElection(IRankedChoiceVoting.ElectionConfig({
    candidates: candidates,
    votingToken: ensToken,
    snapshotBlock: block.number - 1,
    startTime: uint64(block.timestamp),
    endTime: uint64(block.timestamp + 7 days),
    metadataURI: bytes32(0)
}));

// 2. Voters cast ranked ballots
uint8[] memory ranking = new uint8[](3);
ranking[0] = 2; // Carol first
ranking[1] = 0; // Alice second
ranking[2] = 1; // Bob third
voting.castBallot(electionId, ranking);

// 3. Live preview at any time — no need to wait for finalize.
uint8[] memory provisional = voting.getCurrentResult(electionId);

// 4. After endTime, anyone tallies and finalizes
while (!voting.tallyBallots(electionId, 50)) {} // batched
voting.finalize(electionId);

// 5. Read the final result
uint8[] memory orderedWinners = voting.getRanking(electionId);
// orderedWinners[0] is the top-ranked candidate index

Limits: maximum 64 candidates per election; voter weights bounded by int256 max.

Deployments

Not yet deployed to mainnet. Run via the deploy scripts in script/ (DeployCopelandVoting.s.sol, DeploySchulzeVoting.s.sol).

Documentation

Contributing

Contributions welcome. See CONTRIBUTING.md for setup, conventions, and PR process.

Security

Report vulnerabilities per SECURITY.md. These contracts have not been independently audited — production deployments should commission an audit first.

License

MIT.

About

Onchain ranked-choice voting for DAOs with Copeland and Schulze methods

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors