P12: DAO Governance System

P12: DAO Governance System

Project Overview

Attribute Value
Main Language Solidity
Alternative Languages Vyper, Rust (Solana)
Difficulty Advanced
Coolness Level Level 4: Hardcore Tech Flex
Business Potential Startup-Ready (DAOs are core Web3 infrastructure)
Knowledge Area Governance / DAOs
Main Book “Mastering Ethereum” by Andreas M. Antonopoulos & Gavin Wood

Learning Objectives

By completing this project, you will:

  1. Understand decentralized governance mechanisms including proposal lifecycles, voting strategies, and execution patterns
  2. Master vote weighting algorithms from simple token-weighted voting to quadratic voting and conviction voting
  3. Implement vote delegation enabling liquid democracy where users can delegate their voting power to representatives
  4. Build timelock controllers that enforce mandatory delays between proposal approval and execution for security
  5. Design quorum and threshold systems that determine when proposals are valid and pass
  6. Recognize governance attack vectors including flash loan governance attacks, vote buying, and plutocracy concerns

Deep Theoretical Foundation

The Problem: How Do Decentralized Organizations Make Decisions?

In traditional organizations, decisions flow from a hierarchical structure: executives decide, managers implement, employees execute. But what happens when there’s no CEO? No board of directors? No central authority at all?

This is the fundamental challenge of Decentralized Autonomous Organizations (DAOs). A DAO is an organization where:

  • Rules are encoded in smart contracts
  • Decisions are made through token holder voting
  • Execution happens automatically via code
  • No single party has unilateral control

The first major DAO, simply called “The DAO,” launched in 2016 and raised $150 million in ETH. It was hacked for $60 million due to a reentrancy vulnerability, leading to the Ethereum hard fork. This disaster taught the Web3 community that DAO governance isn’t just about voting—it’s about secure, manipulation-resistant decision-making.

Why DAOs Matter

DAOs represent the organizational primitive of Web3:

Traditional Organization DAO
Legal entity in jurisdiction Smart contracts on blockchain
Decisions by management Decisions by token vote
Bylaws and contracts Immutable code
Trust in institutions Trust in mathematics
Limited transparency Full on-chain transparency
Slow bureaucratic change Programmable governance evolution

Real-world DAOs today:

  • MakerDAO: Governs the DAI stablecoin ($4B+ in collateral)
  • Uniswap: Protocol upgrades and treasury ($2B+)
  • Compound: Interest rate parameters and risk management
  • ENS DAO: Ethereum Name Service governance
  • Gitcoin: Funding public goods through quadratic funding

The Governance Lifecycle

Every DAO governance action follows a lifecycle:

[Proposal Creation] → [Discussion Period] → [Voting Period] → [Timelock] → [Execution]
      ↓                      ↓                    ↓              ↓            ↓
   Who can propose?     Off-chain debate     Vote counting    Security      On-chain
   What's the cost?     Temperature check    Quorum check     delay        action

1. Proposal Creation

A proposal is a formalized suggestion for on-chain action. It typically includes:

  • Target contracts: Which contracts to call
  • Calldata: The encoded function calls to execute
  • Description: Human-readable explanation
  • Proposer: Address creating the proposal

Key design decisions:

  • Proposal threshold: Minimum tokens required to propose (prevents spam)
  • Proposal bond: Tokens locked during proposal (refunded if proposal passes)
  • Proposal types: Constitutional changes vs parameter updates vs treasury spending

2. Voting Mechanisms

This is the heart of DAO governance. Different mechanisms optimize for different properties:

Token-Weighted Voting (1 token = 1 vote)

votingPower(address) = tokenBalance(address)

Pros: Simple, Sybil-resistant Cons: Plutocratic (whales dominate), susceptible to vote buying

Quadratic Voting

votingPower(address) = sqrt(tokensSpent(address))

Pros: Reduces whale dominance, encourages broader participation Cons: Sybil-attackable without identity, complex cost calculation

Conviction Voting

conviction(address) = sum(tokens * time_staked)

Pros: Rewards long-term commitment, resistant to last-minute swings Cons: Slower decision-making, complex to implement

Vote Delegation

effectiveVotes(delegate) = ownTokens(delegate) + sum(delegatedTokens[delegators])

Pros: Enables liquid democracy, increases informed participation Cons: Can centralize power in popular delegates

3. Timelocks

A timelock enforces a mandatory delay between when a proposal passes and when it can be executed. This serves multiple purposes:

  1. Security window: Users who disagree can exit before changes take effect
  2. Bug discovery: Community can review the exact transactions
  3. Attack mitigation: Prevents flash loan governance attacks
  4. Trust building: Proves changes won’t be rug-pulled instantly

Standard timelock delays:

  • Emergency actions: 1-2 days
  • Parameter changes: 2-7 days
  • Core protocol upgrades: 7-30 days

4. Quorum and Thresholds

Quorum: Minimum participation required for a vote to be valid

valid = totalVotes >= quorumPercentage * totalSupply

Threshold: Minimum “for” votes required to pass

passed = forVotes > threshold * (forVotes + againstVotes)

Common configurations:

  • Simple majority: threshold = 50%, quorum = 4%
  • Supermajority: threshold = 66%, quorum = 10%
  • Constitutional change: threshold = 75%, quorum = 20%

The Mathematics of Voting

Token-Weighted Voting Analysis

In token-weighted voting, the probability of controlling a vote scales linearly with token holdings:

P(control) = tokens_owned / total_supply

This creates a cost of attack calculation:

attack_cost = token_price * (total_supply * quorum_percentage / 2 + 1)

For a $1B token with 4% quorum and 50% threshold:

attack_cost = $1B * 0.04 * 0.5 = $20M

An attacker needs $20M to control outcomes—assuming no other voters participate.

Quadratic Voting Mathematics

Quadratic voting addresses the “tyranny of the majority” by making additional votes increasingly expensive:

Cost to cast n votes = n^2 tokens
√tokens = votes

This means:

  • 1 token → 1 vote
  • 4 tokens → 2 votes
  • 9 tokens → 3 votes
  • 100 tokens → 10 votes
  • 10,000 tokens → 100 votes

A whale with 10,000x more tokens only gets 100x more votes, not 10,000x.

The Sybil Problem: Without identity verification, quadratic voting is broken. An attacker can split tokens across multiple wallets:

1 wallet with 100 tokens → 10 votes
100 wallets with 1 token each → 100 votes

Solutions include proof-of-humanity, soul-bound tokens, or social attestations.

Vote Delegation and Liquid Democracy

Traditional democracy: voters elect representatives for fixed terms. Liquid democracy: voters can delegate their vote to anyone, at any time, on any issue.

Delegation chains:

Alice delegates to Bob
Bob delegates to Carol
Carol votes FOR

Result: Alice's tokens count as FOR (through the delegation chain)

Key implementation questions:

  • Can delegates re-delegate? (Creates chains)
  • How deep can chains go? (Gas costs, circular prevention)
  • Can users vote directly while delegated? (Override vs coexistence)
  • Is delegation proposal-specific or global?

Governance Attack Vectors

1. Flash Loan Governance Attack

An attacker borrows millions of tokens in a flash loan, votes, then returns tokens—all in one transaction.

The Attack:

// All in one transaction:
1. flashLoan(1_000_000 tokens)
2. delegate(attacker)
3. propose(malicious_proposal)  // or vote on existing
4. vote(FOR)
5. repay(flash_loan)

Defenses:

  • Snapshot voting power at proposal creation time
  • Require tokens to be held for N blocks before voting
  • Timelock between vote and execution

2. Vote Buying

An attacker pays token holders to vote a certain way off-chain.

Defense: “Dark DAOs” make votes secret until reveal, preventing verification of bought votes. Vote-escrowed tokens (veTokens) require time-locking, increasing the cost of vote buying.

3. Governance Extraction

Majority token holders drain the treasury to themselves through governance.

Defense:

  • Rage-quit: minority holders can exit with their share before extraction
  • Moloch-style: spending proposals require unanimous consent of affected parties
  • Time-delayed vesting: treasury unlocks slowly

4. Voter Apathy and Quorum Griefing

Low participation makes quorum hard to reach, blocking all governance.

Defense:

  • Decreasing quorum over time
  • Quadratic quorum (based on stake, not addresses)
  • Conviction voting (accumulates over time)

Historical Context: Compound Governor

The modern DAO governance standard was largely established by Compound’s Governor system:

Governor Alpha (2020):

  • Single proposal at a time per address
  • Fixed voting period (3 days)
  • No delegation

Governor Bravo (2021):

  • Multiple proposals per address
  • Adjustable voting periods
  • Vote delegation
  • Proposal cancellation

OpenZeppelin Governor (2021):

  • Modular architecture
  • Pluggable voting strategies
  • Extension system for timelock, quorum, etc.
  • Reference implementation widely adopted

Complete Project Specification

Functional Requirements

  1. Token Integration
    • Support ERC-20 governance tokens with vote snapshots
    • Implement ERC-20Votes extension (checkpointing)
    • Track voting power at any historical block
  2. Proposal System
    • Create proposals with multiple actions (targets, values, calldatas)
    • Proposal lifecycle: Pending → Active → Succeeded/Defeated → Queued → Executed
    • Proposal cancellation by proposer or guardian
    • Configurable proposal threshold
  3. Voting Mechanism
    • Token-weighted voting (1 token = 1 vote)
    • Vote options: For, Against, Abstain
    • Vote delegation with chain support
    • Optional: Quadratic voting module
  4. Timelock Controller
    • Queue approved proposals with delay
    • Execute after delay expires
    • Cancel queued proposals
    • Emergency action path (shorter delay)
  5. Quorum and Thresholds
    • Configurable quorum percentage
    • Configurable passing threshold
    • Dynamic quorum based on participation history
  6. Security Features
    • Snapshot voting power at proposal creation
    • Prevention of flash loan attacks
    • Guardian role for emergencies
    • Upgrade delay for contract changes

Non-Functional Requirements

  • Gas Efficiency: Voting should cost < 50,000 gas
  • Upgradeability: Use proxy pattern for future improvements
  • Security: Pass OpenZeppelin audit checklist
  • Compatibility: Match GovernorBravo interface for tooling
  • Documentation: NatSpec comments on all public functions

Smart Contract Interface

interface IGovernor {
    // Proposal lifecycle
    function propose(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        string memory description
    ) external returns (uint256 proposalId);

    function castVote(uint256 proposalId, uint8 support) external returns (uint256 balance);
    function castVoteWithReason(uint256 proposalId, uint8 support, string memory reason) external;
    function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external;

    function queue(uint256 proposalId) external;
    function execute(uint256 proposalId) external payable;
    function cancel(uint256 proposalId) external;

    // View functions
    function state(uint256 proposalId) external view returns (ProposalState);
    function proposalThreshold() external view returns (uint256);
    function quorum(uint256 blockNumber) external view returns (uint256);
    function getVotes(address account, uint256 blockNumber) external view returns (uint256);
    function hasVoted(uint256 proposalId, address account) external view returns (bool);
}

interface IVotes {
    function getVotes(address account) external view returns (uint256);
    function getPastVotes(address account, uint256 blockNumber) external view returns (uint256);
    function getPastTotalSupply(uint256 blockNumber) external view returns (uint256);
    function delegates(address account) external view returns (address);
    function delegate(address delegatee) external;
    function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external;
}

interface ITimelock {
    function schedule(bytes32 id, address target, uint256 value, bytes calldata data, bytes32 predecessor, uint256 delay) external;
    function execute(bytes32 id, address target, uint256 value, bytes calldata data, bytes32 predecessor) external payable;
    function cancel(bytes32 id) external;
    function getMinDelay() external view returns (uint256);
}

Solution Architecture

Contract Structure

contracts/
├── governance/
│   ├── Governor.sol              # Main governance contract
│   ├── GovernorSettings.sol      # Voting delay, period, threshold
│   ├── GovernorVotes.sol         # Vote counting and delegation
│   ├── GovernorVotesQuorum.sol   # Quorum tracking
│   ├── GovernorTimelockControl.sol # Timelock integration
│   └── extensions/
│       ├── GovernorCountingSimple.sol    # For/Against/Abstain
│       ├── GovernorCountingQuadratic.sol # Quadratic voting
│       └── GovernorProposalGuard.sol     # Flash loan protection
├── token/
│   ├── GovernanceToken.sol       # ERC20 with voting
│   └── ERC20Votes.sol            # Vote checkpointing
├── timelock/
│   ├── TimelockController.sol    # Execution delay
│   └── EmergencyTimelock.sol     # Fast-path for emergencies
└── test/
    ├── GovernorTest.sol
    └── mocks/
        └── MockToken.sol

Core Data Structures

struct ProposalCore {
    // Slot 1
    uint64 voteStart;      // Block number when voting starts
    uint64 voteEnd;        // Block number when voting ends
    bool executed;         // Whether proposal has been executed
    bool canceled;         // Whether proposal has been canceled
    // Slot 2
    address proposer;      // Address that created the proposal
}

struct ProposalVote {
    uint256 againstVotes;  // Total votes against
    uint256 forVotes;      // Total votes for
    uint256 abstainVotes;  // Total abstentions
    mapping(address => bool) hasVoted;  // Has address voted?
}

struct Receipt {
    bool hasVoted;         // Whether address has voted
    uint8 support;         // Vote direction (0=Against, 1=For, 2=Abstain)
    uint96 votes;          // Voting power used
}

// Vote checkpoint for historical lookups
struct Checkpoint {
    uint32 fromBlock;      // Block number of checkpoint
    uint224 votes;         // Voting power at that block
}

// Timelock operation
struct TimelockOperation {
    bytes32 id;            // Unique operation identifier
    address target;        // Contract to call
    uint256 value;         // ETH to send
    bytes data;            // Calldata
    uint256 readyTime;     // When operation can execute
    bool executed;         // Whether executed
}

Key Algorithms

Vote Power Checkpointing

function _writeCheckpoint(address account, uint256 newVotes):
    position = checkpointCount[account]

    if position > 0 AND checkpoints[account][position-1].fromBlock == block.number:
        // Update existing checkpoint in same block
        checkpoints[account][position-1].votes = newVotes
    else:
        // Create new checkpoint
        checkpoints[account][position] = Checkpoint(block.number, newVotes)
        checkpointCount[account] = position + 1

function getPastVotes(address account, uint256 blockNumber) → uint256:
    require(blockNumber < block.number, "Block not yet mined")

    // Binary search for checkpoint
    checkpoints = checkpoints[account]
    low = 0
    high = checkpointCount[account]

    while low < high:
        mid = (low + high + 1) / 2
        if checkpoints[mid].fromBlock <= blockNumber:
            low = mid
        else:
            high = mid - 1

    return low == 0 ? 0 : checkpoints[low - 1].votes

Delegation Chain Resolution

function _delegate(address delegator, address delegatee):
    currentDelegate = delegates[delegator]
    delegatorBalance = token.balanceOf(delegator)

    // Update delegation mapping
    delegates[delegator] = delegatee

    // Move voting power
    if currentDelegate != address(0):
        _moveVotingPower(currentDelegate, address(0), delegatorBalance)

    if delegatee != address(0):
        _moveVotingPower(address(0), delegatee, delegatorBalance)

function _moveVotingPower(address src, address dst, uint256 amount):
    if src != address(0):
        oldWeight = getVotes(src)
        newWeight = oldWeight - amount
        _writeCheckpoint(src, newWeight)

    if dst != address(0):
        oldWeight = getVotes(dst)
        newWeight = oldWeight + amount
        _writeCheckpoint(dst, newWeight)

Proposal State Machine

function state(uint256 proposalId) → ProposalState:
    proposal = proposals[proposalId]

    if proposal.canceled:
        return ProposalState.Canceled

    if proposal.executed:
        return ProposalState.Executed

    snapshot = proposalSnapshot(proposalId)

    if snapshot == 0:
        revert("Unknown proposal")

    if snapshot >= block.number:
        return ProposalState.Pending

    deadline = proposalDeadline(proposalId)

    if deadline >= block.number:
        return ProposalState.Active

    if _quorumReached(proposalId) AND _voteSucceeded(proposalId):
        if proposalEta(proposalId) == 0:
            return ProposalState.Succeeded
        else:
            if block.timestamp >= proposalEta(proposalId):
                return ProposalState.Expired  // Not executed in time
            else:
                return ProposalState.Queued

    return ProposalState.Defeated

Quadratic Vote Counting

function _countVotesQuadratic(uint256 proposalId, address account, uint8 support, uint256 weight):
    // Convert token weight to quadratic votes
    uint256 votes = sqrt(weight)

    if support == 0:
        proposalVotes[proposalId].againstVotes += votes
    else if support == 1:
        proposalVotes[proposalId].forVotes += votes
    else:
        proposalVotes[proposalId].abstainVotes += votes

function sqrt(uint256 x) → uint256:
    if x == 0:
        return 0

    // Newton's method
    z = (x + 1) / 2
    y = x

    while z < y:
        y = z
        z = (x / z + z) / 2

    return y

Phased Implementation Guide

Phase 1: Governance Token with Voting

Goal: Create an ERC-20 token with vote checkpointing and delegation.

Tasks:

  1. Implement standard ERC-20 functionality
  2. Add vote checkpointing on transfers
  3. Implement delegation with signature support
  4. Add historical vote lookup

Validation:

// Deploy token, transfer, check voting power
GovernanceToken token = new GovernanceToken("GOV", "GOV", 1_000_000e18);

// Self-delegate to activate voting power
token.delegate(alice);
assertEq(token.getVotes(alice), 1_000_000e18);

// Transfer and check snapshots
token.transfer(bob, 100_000e18);
assertEq(token.getVotes(alice), 900_000e18);
assertEq(token.getVotes(bob), 0);  // Bob hasn't delegated

bob.delegate(bob);
assertEq(token.getVotes(bob), 100_000e18);

Hints if stuck:

  • Use OpenZeppelin’s ERC20Votes as reference
  • Remember: transfers update checkpoints automatically
  • Delegation requires explicit action (self-delegation activates votes)

Phase 2: Basic Governor Contract

Goal: Implement proposal creation and simple voting.

Tasks:

  1. Define proposal struct and storage
  2. Implement propose() with threshold check
  3. Implement castVote() with vote recording
  4. Implement state() to track proposal lifecycle
  5. Add getVotes() using token snapshots

Validation:

Governor gov = new Governor(token, timelock);

// Create proposal
address[] memory targets = new address[](1);
targets[0] = address(treasury);
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = abi.encodeCall(treasury.withdraw, (100e18, alice));

uint256 proposalId = gov.propose(targets, new uint256[](1), calldatas, "Withdraw 100 ETH");

// Wait for voting to start
vm.roll(block.number + gov.votingDelay() + 1);

// Vote
gov.castVote(proposalId, 1);  // Vote FOR

assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Active));

Hints if stuck:

  • proposalId = keccak256(abi.encode(targets, values, calldatas, descriptionHash))
  • Snapshot voting power at proposal creation block
  • Voting delay is measured in blocks, not time

Phase 3: Vote Counting and Quorum

Goal: Implement full voting logic with quorum tracking.

Tasks:

  1. Implement For/Against/Abstain counting
  2. Add quorum calculation (percentage of total supply)
  3. Implement _quorumReached() and _voteSucceeded()
  4. Add vote-by-signature for gasless voting
  5. Track voting receipts per address

Validation:

// Vote on proposal
gov.castVote(proposalId, 1);  // FOR
gov.castVoteWithReason(proposalId, 0, "Too expensive");  // AGAINST with reason

// Check vote counts
(uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) = gov.proposalVotes(proposalId);

// Check quorum
uint256 quorum = gov.quorum(gov.proposalSnapshot(proposalId));
assertTrue(forVotes + againstVotes + abstainVotes >= quorum);

// Verify vote succeeded
assertTrue(gov.state(proposalId) == ProposalState.Succeeded);

Hints if stuck:

  • Quorum typically includes FOR + ABSTAIN, not AGAINST
  • Use EIP-712 for typed signature verification
  • Store votes in a Receipt struct to prevent double-voting

Phase 4: Vote Delegation

Goal: Implement full delegation with chain support.

Tasks:

  1. Implement delegate() for direct delegation
  2. Add delegateBySig() for gasless delegation
  3. Handle delegation chains (A → B → C)
  4. Implement delegation transfer on token transfer
  5. Add delegate() getter for UI integration

Validation:

// Alice delegates to Bob
alice.delegate(bob);
assertEq(token.delegates(alice), bob);
assertEq(token.getVotes(bob), aliceBalance + bobBalance);

// Bob votes with Alice's voting power
gov.castVote(proposalId, 1);

// Verify Alice's tokens counted
assertTrue(gov.hasVoted(proposalId, bob));

Hints if stuck:

  • Delegation doesn’t transfer tokens, only voting power
  • Update checkpoints when delegation changes
  • Consider max delegation chain depth for gas limits

Phase 5: Timelock Controller

Goal: Implement secure execution delay.

Tasks:

  1. Create timelock contract with delay configuration
  2. Implement queue() to schedule operations
  3. Implement execute() after delay
  4. Add cancel() for emergency stopping
  5. Integrate with Governor for proposal execution

Validation:

// Proposal succeeds, queue for execution
gov.queue(proposalId);
assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Queued));

// Cannot execute before delay
vm.expectRevert("TimelockController: operation not ready");
gov.execute(proposalId);

// Fast-forward past delay
vm.warp(block.timestamp + timelock.getMinDelay() + 1);

// Execute succeeds
gov.execute(proposalId);
assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Executed));

Hints if stuck:

  • Operation ID = keccak256(abi.encode(target, value, data, predecessor, salt))
  • Use predecessor for operation dependencies
  • Separate roles: PROPOSER, EXECUTOR, CANCELLER

Phase 6: Security Hardening

Goal: Protect against governance attacks.

Tasks:

  1. Prevent flash loan attacks with snapshot voting
  2. Add proposal threshold to prevent spam
  3. Implement guardian role for emergencies
  4. Add delay between vote end and queue
  5. Implement proposal cancellation conditions

Validation:

// Flash loan attack prevention
flashLoan.execute(1_000_000e18, abi.encodeCall(
    this.attackCallback,
    (proposalId)
));

function attackCallback(uint256 proposalId) external {
    // Voting power is snapshot at proposal creation
    // Attacker has 0 votes even with flash-loaned tokens
    gov.castVote(proposalId, 1);
    assertEq(token.getVotes(attacker), 0);  // No voting power!
}

Hints if stuck:

  • Use getPastVotes() with proposal snapshot block
  • Guardian can cancel but not execute
  • Add “grace period” after vote ends before queue

Phase 7: Quadratic Voting Extension

Goal: Implement optional quadratic voting module.

Tasks:

  1. Create modular vote counting interface
  2. Implement square root calculation
  3. Track tokens spent per proposal
  4. Handle vote changes with quadratic math
  5. Add Sybil resistance considerations

Validation:

GovernorQuadratic govQ = new GovernorQuadratic(token, timelock);

// User with 100 tokens
govQ.castVote(proposalId, 1);
(, uint256 forVotes,) = govQ.proposalVotes(proposalId);
assertEq(forVotes, 10);  // sqrt(100) = 10

// User with 10,000 tokens
whale.castVote(proposalId, 1);
(, forVotes,) = govQ.proposalVotes(proposalId);
assertEq(forVotes, 110);  // 10 + sqrt(10000) = 10 + 100

Hints if stuck:

  • Use integer square root (Newton’s method)
  • Consider vote credits vs token balance
  • Document Sybil attack risk in comments

Testing Strategy

Unit Tests

contract GovernorTokenTest is Test {
    GovernanceToken token;

    function setUp() public {
        token = new GovernanceToken("GOV", "GOV", 1_000_000e18);
    }

    function test_DelegateToSelf() public {
        token.delegate(address(this));
        assertEq(token.getVotes(address(this)), 1_000_000e18);
    }

    function test_DelegateToOther() public {
        address bob = makeAddr("bob");
        token.delegate(bob);
        assertEq(token.getVotes(bob), 1_000_000e18);
        assertEq(token.getVotes(address(this)), 0);
    }

    function test_CheckpointAfterTransfer() public {
        token.delegate(address(this));
        uint256 snapshot = block.number;

        vm.roll(snapshot + 1);
        token.transfer(makeAddr("bob"), 100_000e18);

        assertEq(token.getPastVotes(address(this), snapshot), 1_000_000e18);
        assertEq(token.getVotes(address(this)), 900_000e18);
    }
}

contract GovernorProposalTest is Test {
    Governor gov;
    GovernanceToken token;
    TimelockController timelock;

    function test_ProposeWithThreshold() public {
        // User without enough tokens
        vm.prank(smallHolder);
        vm.expectRevert("Governor: proposer votes below threshold");
        gov.propose(...);

        // User with enough tokens
        vm.prank(largeHolder);
        gov.propose(...);  // Succeeds
    }

    function test_VoteRecordedCorrectly() public {
        uint256 proposalId = createProposal();
        skipToActive(proposalId);

        gov.castVote(proposalId, 1);  // FOR

        (uint256 against, uint256 forVotes,) = gov.proposalVotes(proposalId);
        assertEq(forVotes, token.getVotes(address(this)));
        assertEq(against, 0);
    }
}

Integration Tests

contract GovernorIntegrationTest is Test {
    function test_FullProposalLifecycle() public {
        // 1. Create proposal
        uint256 proposalId = gov.propose(
            targets, values, calldatas, "Grant 1000 tokens"
        );
        assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Pending));

        // 2. Wait for voting delay
        vm.roll(block.number + gov.votingDelay() + 1);
        assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Active));

        // 3. Vote
        gov.castVote(proposalId, 1);

        // 4. Wait for voting period to end
        vm.roll(block.number + gov.votingPeriod() + 1);
        assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Succeeded));

        // 5. Queue in timelock
        gov.queue(proposalId);
        assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Queued));

        // 6. Wait for timelock delay
        vm.warp(block.timestamp + timelock.getMinDelay() + 1);

        // 7. Execute
        gov.execute(proposalId);
        assertEq(uint256(gov.state(proposalId)), uint256(ProposalState.Executed));

        // 8. Verify effect
        assertTrue(treasury.balance(recipient) == 1000e18);
    }
}

Security Tests

contract GovernorSecurityTest is Test {
    function test_FlashLoanAttackPrevention() public {
        uint256 proposalId = createProposal();
        uint256 snapshot = gov.proposalSnapshot(proposalId);

        skipToActive(proposalId);

        // Attacker gets tokens via flash loan
        vm.prank(attacker);
        flashLender.flashLoan(1_000_000e18);

        // Inside flash loan callback
        vm.prank(attacker);
        gov.castVote(proposalId, 1);

        // Check: attacker's vote weight is 0 (snapshot was before flash loan)
        assertEq(token.getPastVotes(attacker, snapshot), 0);
        (, uint256 forVotes,) = gov.proposalVotes(proposalId);
        assertEq(forVotes, 0);
    }

    function test_DoubleVotingPrevented() public {
        uint256 proposalId = createProposal();
        skipToActive(proposalId);

        gov.castVote(proposalId, 1);

        vm.expectRevert("Governor: vote already cast");
        gov.castVote(proposalId, 0);
    }

    function test_VotingAfterDeadlinePrevented() public {
        uint256 proposalId = createProposal();
        skipPastDeadline(proposalId);

        vm.expectRevert("Governor: vote not currently active");
        gov.castVote(proposalId, 1);
    }

    function test_ExecutionBeforeDelayPrevented() public {
        uint256 proposalId = createAndPassProposal();
        gov.queue(proposalId);

        vm.expectRevert("TimelockController: operation is not ready");
        gov.execute(proposalId);
    }
}

Invariant Tests

contract GovernorInvariantTest is Test {
    function invariant_TotalVotingPowerEqualsSupply() public {
        uint256 totalVotes = 0;
        for (uint i = 0; i < voters.length; i++) {
            totalVotes += token.getVotes(voters[i]);
        }
        assertEq(totalVotes, token.totalSupply());
    }

    function invariant_ProposalStateTransitions() public {
        // Proposals can only move forward in state
        // Pending → Active → Succeeded/Defeated → Queued → Executed
        // Or any state → Canceled
    }

    function invariant_VotesNeverExceedQuorum() public {
        for (uint i = 0; i < proposals.length; i++) {
            (uint256 against, uint256 forV, uint256 abstain) = gov.proposalVotes(proposals[i]);
            uint256 total = against + forV + abstain;
            assertLe(total, token.getPastTotalSupply(gov.proposalSnapshot(proposals[i])));
        }
    }
}

Common Pitfalls & Debugging

Pitfall 1: Forgotten Self-Delegation

Problem: Users have tokens but can’t vote because voting power requires delegation.

Symptom: castVote() succeeds but vote weight is 0.

Solution:

// In token contract, auto-delegate on first receive
function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
    super._afterTokenTransfer(from, to, amount);

    // If first time receiving and not delegated, self-delegate
    if (from == address(0) && delegates(to) == address(0)) {
        _delegate(to, to);
    }
}

Pitfall 2: Snapshot Block Confusion

Problem: Using current voting power instead of historical snapshot.

Symptom: Flash loan attacks work, or votes change after casting.

Solution:

// WRONG
uint256 weight = token.getVotes(voter);

// CORRECT
uint256 snapshot = proposalSnapshot(proposalId);
uint256 weight = token.getPastVotes(voter, snapshot);

Pitfall 3: Timelock Operation ID Collision

Problem: Same operation queued twice creates ID collision.

Symptom: “Operation already scheduled” error on valid operations.

Solution:

// Include unique salt in operation ID
bytes32 id = keccak256(abi.encode(
    target,
    value,
    data,
    predecessor,
    keccak256(description)  // Description makes each proposal unique
));

Pitfall 4: Quorum Race Condition

Problem: Quorum calculated at wrong time, allowing manipulation.

Symptom: Proposals pass/fail unexpectedly based on token supply changes.

Solution:

// Calculate quorum at snapshot, not current block
function _quorumReached(uint256 proposalId) internal view returns (bool) {
    uint256 snapshot = proposalSnapshot(proposalId);
    uint256 quorum = token.getPastTotalSupply(snapshot) * quorumNumerator / 100;
    return proposalVotes[proposalId].forVotes >= quorum;
}

Pitfall 5: Re-entrancy in Execute

Problem: Malicious proposal exploits re-entrancy during execution.

Symptom: Same proposal executed multiple times.

Solution:

function _execute(uint256 proposalId, ...) internal {
    // Mark executed BEFORE external calls (CEI pattern)
    _proposals[proposalId].executed = true;

    for (uint i = 0; i < targets.length; i++) {
        (bool success,) = targets[i].call{value: values[i]}(calldatas[i]);
        require(success, "Call failed");
    }
}

Pitfall 6: Delegation Chain Gas Bomb

Problem: Deep delegation chains consume excessive gas.

Symptom: Voting reverts with out-of-gas for certain users.

Solution:

// Limit delegation depth or flatten chains
uint256 constant MAX_DELEGATION_DEPTH = 10;

function getVotes(address account) public view returns (uint256) {
    address current = delegates(account);
    uint256 depth = 0;

    while (current != address(0) && current != account && depth < MAX_DELEGATION_DEPTH) {
        current = delegates(current);
        depth++;
    }

    require(depth < MAX_DELEGATION_DEPTH, "Delegation chain too deep");
    // ...
}

Extensions and Challenges

Challenge 1: Optimistic Governance

Implement a governance system where proposals automatically pass unless vetoed. This inverts the typical flow:

  • Proposals enter “pending execution” immediately
  • Token holders can veto during a challenge period
  • If veto threshold reached, proposal is cancelled

This is faster but riskier—suitable for trusted DAOs.

Challenge 2: Conviction Voting

Implement time-weighted voting where conviction grows over time:

conviction = sum(stake * time_since_stake)

Proposals pass when conviction exceeds a threshold. This prevents last-minute vote swings and rewards long-term commitment.

Challenge 3: Multi-Token Governance

Support multiple governance tokens with configurable weights:

  • Token A: 1 vote per token
  • Token B (NFT): 100 votes per NFT
  • Combine for total voting power

This models complex governance like Curve’s veToken system.

Challenge 4: Private Voting (Shutter Network)

Implement encrypted voting that prevents:

  • Front-running votes
  • Vote buying verification
  • Bandwagon effects

Use commit-reveal scheme: vote encrypted during voting period, revealed after.

Challenge 5: Cross-Chain Governance

Implement governance that works across multiple chains:

  • Voting on L2 for gas efficiency
  • Execution on L1 for security
  • Message passing between chains

This mirrors real multi-chain DAO challenges.


Real-World Connections

Compound Governor

Compound Finance pioneered the Governor pattern. Their governance controls:

  • Interest rate model parameters
  • Collateral factor adjustments
  • New market listings
  • Treasury spending

Understanding this project means understanding how billions of dollars in DeFi are governed.

MakerDAO

MakerDAO governs the DAI stablecoin. Governance decisions include:

  • Stability fee (interest rate) changes
  • Collateral type additions
  • Risk parameter adjustments
  • Emergency shutdown authority

MakerDAO demonstrates governance at scale with real economic consequences.

Uniswap Governance

Uniswap’s governance controls:

  • Protocol fee switches
  • Treasury grants
  • Ecosystem fund allocation
  • V4 development direction

The UNI airdrop created one of the largest governance token distributions in history.

ENS DAO

Ethereum Name Service transitioned to DAO governance:

  • Fee structure decisions
  • Protocol upgrades
  • Public goods funding
  • Working group budgets

ENS shows how infrastructure can be governed by its users.


Resources

Primary References

  1. OpenZeppelin Governor: Documentation - Reference implementation
  2. Compound Governor Bravo: GitHub - Original implementation
  3. “Mastering Ethereum” Chapter 14 - Decentralized applications and governance

Academic Papers

  1. “Blockchain Governance 101” by Vlad Zamfir - Theoretical foundations
  2. “Moving Beyond Coin Voting Governance” by Vitalik Buterin - Problems with token voting
  3. “Quadratic Voting” by Glen Weyl - Mathematical foundations

Code References

  1. Tally: tally.xyz - Governance aggregator (see real proposals)
  2. Snapshot: snapshot.org - Off-chain voting (compare approaches)
  3. Gnosis Zodiac: GitHub - Modular governance

Tools and Frameworks

  1. Foundry: Testing and deployment
  2. Hardhat: Alternative development environment
  3. OpenZeppelin Defender: Governance automation

Self-Assessment Checklist

Before moving to the next project, verify:

  • I can explain the full proposal lifecycle from creation to execution
  • I understand why snapshot voting prevents flash loan attacks
  • I can implement both token-weighted and quadratic voting
  • I understand the purpose of timelocks and appropriate delay lengths
  • I can explain vote delegation and its implications for voting power
  • I understand quorum requirements and why they exist
  • I can identify at least 3 governance attack vectors and their mitigations
  • My implementation passes all security tests
  • I understand the trade-offs between governance speed and security
  • I can explain how real DAOs (Compound, MakerDAO) implement governance

What’s Next?

With DAO governance mastered, you now understand how decentralized organizations make decisions. In Project 13: Layer 2 Optimistic Rollup, you’ll learn how blockchain systems scale—using fraud proofs to move computation off-chain while maintaining security. You’ll build simplified rollup components including deposit/withdrawal bridges and challenge systems.