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:
- Understand decentralized governance mechanisms including proposal lifecycles, voting strategies, and execution patterns
- Master vote weighting algorithms from simple token-weighted voting to quadratic voting and conviction voting
- Implement vote delegation enabling liquid democracy where users can delegate their voting power to representatives
- Build timelock controllers that enforce mandatory delays between proposal approval and execution for security
- Design quorum and threshold systems that determine when proposals are valid and pass
- 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:
- Security window: Users who disagree can exit before changes take effect
- Bug discovery: Community can review the exact transactions
- Attack mitigation: Prevents flash loan governance attacks
- 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
- Token Integration
- Support ERC-20 governance tokens with vote snapshots
- Implement ERC-20Votes extension (checkpointing)
- Track voting power at any historical block
- 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
- Voting Mechanism
- Token-weighted voting (1 token = 1 vote)
- Vote options: For, Against, Abstain
- Vote delegation with chain support
- Optional: Quadratic voting module
- Timelock Controller
- Queue approved proposals with delay
- Execute after delay expires
- Cancel queued proposals
- Emergency action path (shorter delay)
- Quorum and Thresholds
- Configurable quorum percentage
- Configurable passing threshold
- Dynamic quorum based on participation history
- 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:
- Implement standard ERC-20 functionality
- Add vote checkpointing on transfers
- Implement delegation with signature support
- 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:
- Define proposal struct and storage
- Implement propose() with threshold check
- Implement castVote() with vote recording
- Implement state() to track proposal lifecycle
- 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:
- Implement For/Against/Abstain counting
- Add quorum calculation (percentage of total supply)
- Implement _quorumReached() and _voteSucceeded()
- Add vote-by-signature for gasless voting
- 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:
- Implement delegate() for direct delegation
- Add delegateBySig() for gasless delegation
- Handle delegation chains (A → B → C)
- Implement delegation transfer on token transfer
- 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:
- Create timelock contract with delay configuration
- Implement queue() to schedule operations
- Implement execute() after delay
- Add cancel() for emergency stopping
- 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:
- Prevent flash loan attacks with snapshot voting
- Add proposal threshold to prevent spam
- Implement guardian role for emergencies
- Add delay between vote end and queue
- 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:
- Create modular vote counting interface
- Implement square root calculation
- Track tokens spent per proposal
- Handle vote changes with quadratic math
- 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
- OpenZeppelin Governor: Documentation - Reference implementation
- Compound Governor Bravo: GitHub - Original implementation
- “Mastering Ethereum” Chapter 14 - Decentralized applications and governance
Academic Papers
- “Blockchain Governance 101” by Vlad Zamfir - Theoretical foundations
- “Moving Beyond Coin Voting Governance” by Vitalik Buterin - Problems with token voting
- “Quadratic Voting” by Glen Weyl - Mathematical foundations
Code References
- Tally: tally.xyz - Governance aggregator (see real proposals)
- Snapshot: snapshot.org - Off-chain voting (compare approaches)
- Gnosis Zodiac: GitHub - Modular governance
Tools and Frameworks
- Foundry: Testing and deployment
- Hardhat: Alternative development environment
- 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.