Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Part 3 β€” Module 8: Governance & DAOs

Difficulty: Intermediate

Estimated reading time: ~25 minutes | Exercises: ~1.5 hours

πŸ“š Table of Contents

On-Chain Governance

OpenZeppelin Governor in Practice

ve-Tokenomics & the Curve Wars

Governance Security

Governance Minimization


πŸ’‘ On-Chain Governance

Every major DeFi protocol needs a mechanism for parameter updates, upgrades, and strategic decisions. Governance is how protocols evolve after deployment β€” and also one of the most exploited attack surfaces in DeFi.

Why this matters for you:

  • Every DeFi protocol you work on will have governance β€” understanding the lifecycle and security is essential
  • ve-tokenomics (Curve, Velodrome) is one of the most important DeFi innovations β€” it reshapes protocol economics
  • The Beanstalk attack ($182M) shows what happens when governance security is wrong
  • Governance design is a frequent interview topic β€” β€œhow would you design governance for X protocol?”
  • Connection to Module 7: cross-chain governance (vote on L1, execute on L2) is the multi-chain standard
  • Connection to Part 2 Module 9: your capstone’s immutable design was itself a governance choice

πŸ’‘ Concept: Why Governance Exists

Protocols need to change after deployment:

  • Risk parameters: LTV ratios, interest rate curves, collateral types (Part 2 Module 4)
  • Fee management: swap fees, protocol revenue distribution
  • Treasury: fund allocation, grants, strategic investments
  • Upgrades: proxy implementations, new features (Part 1 Module 5)
  • Emergency: pause, parameter adjustment, shutdown (Part 2 Module 6 ESM)

The fundamental tension: Decentralization vs operational agility. A multisig can act in minutes; full on-chain governance takes 1-2 weeks. The right answer depends on what’s being governed and the protocol’s maturity stage.

The Proposal Lifecycle

Every on-chain governance system follows the same flow:

 PROPOSE        DELAY          VOTE          QUEUE         EXECUTE
───────── β†’ ─────────── β†’ ─────────── β†’ ─────────── β†’ ───────────
 Proposer     Community      Token         Timelock       Anyone
 submits      reviews        holders       enforces       triggers
 on-chain     proposal       vote          delay          execution
             (1-2 days)    (3-7 days)    (24-48h)

Total: 5-14 days from proposal to execution

Why each step exists:

  • Delay β€” prevents surprise proposals; community can review before voting starts
  • Vote β€” democratic decision with quorum requirements
  • Queue/Timelock β€” critical safety net: users who disagree can exit before changes take effect. If governance passes a malicious proposal, the timelock gives users time to withdraw.

Voting Power Mechanisms

MechanismFormulaProsConsUsed By
Token-weighted1 token = 1 voteSimple, transparentPlutocraticUniswap, Aave, Compound
DelegationDelegates accumulate voting powerReduces voter apathyDelegation centralizationAll major governors
Vote-escrow (ve)Lock duration Γ— amountAligns long-term incentivesComplex, illiquidCurve, Velodrome
Quadratic√tokens = votesMore egalitarianSybil-vulnerableGitcoin (off-chain)

πŸ’‘ OpenZeppelin Governor in Practice

πŸ’‘ Concept: The Standard Governance Stack

OpenZeppelin Governor is the industry standard β€” used by most new DeFi protocols. Understanding its code is essential.

The three contracts:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ERC20Votes      β”‚     β”‚  Governor        β”‚     β”‚  Timelock        β”‚
β”‚                  β”‚     β”‚                  β”‚     β”‚  Controller      β”‚
β”‚  β€’ delegate()    │────→│  β€’ propose()     │────→│  β€’ schedule()    β”‚
β”‚  β€’ getVotes()    β”‚     β”‚  β€’ castVote()    β”‚     β”‚  β€’ execute()     β”‚
β”‚  β€’ getPastVotes()β”‚     β”‚  β€’ queue()       β”‚     β”‚  β€’ cancel()      β”‚
β”‚                  β”‚     β”‚  β€’ execute()     β”‚     β”‚                  β”‚
β”‚  Checkpointing:  β”‚     β”‚                  β”‚     β”‚  Roles:          β”‚
β”‚  records balance β”‚     β”‚  Checks quorum,  β”‚     β”‚  PROPOSER_ROLE   β”‚
β”‚  at each block   β”‚     β”‚  threshold,      β”‚     β”‚  EXECUTOR_ROLE   β”‚
β”‚                  β”‚     β”‚  voting period   β”‚     β”‚  CANCELLER_ROLE  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The ERC20Votes Token

// Key concept: delegation activates voting power
// Holding tokens alone does NOT give voting power β€” you must delegate

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { ERC20Votes } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract GovernanceToken is ERC20, ERC20Votes, ERC20Permit {
    constructor() ERC20("GovToken", "GOV") ERC20Permit("GovToken") {
        _mint(msg.sender, 1_000_000e18);
    }

    // Required overrides for ERC20Votes
    function _update(address from, address to, uint256 value)
        internal override(ERC20, ERC20Votes)
    {
        super._update(from, to, value);
    }

    function nonces(address owner)
        public view override(ERC20Permit, Nonces) returns (uint256)
    {
        return super.nonces(owner);
    }
}

Critical detail β€” checkpointing:

// ERC20Votes stores voting power snapshots at each block
// This is what prevents flash loan attacks

// When alice delegates to herself:
token.delegate(alice);  // at block 100

// Alice's voting power at block 100+: 1000 tokens
// If alice gets more tokens at block 200 (via flash loan):
// Her voting power at block 100 is still 1000 (historical snapshot)

// Governor uses getPastVotes(alice, proposalSnapshot):
uint256 votes = token.getPastVotes(alice, proposalSnapshot);
// proposalSnapshot = block when proposal was created
// Flash-borrowed tokens at block 200 don't count for a proposal created at block 100

Governor + Timelock Integration

import { Governor } from "@openzeppelin/contracts/governance/Governor.sol";
import { GovernorVotes } from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import { GovernorCountingSimple } from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import { GovernorTimelockControl } from "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";

contract MyGovernor is Governor, GovernorVotes, GovernorCountingSimple, GovernorTimelockControl {

    constructor(IVotes _token, TimelockController _timelock)
        Governor("MyGovernor")
        GovernorVotes(_token)
        GovernorTimelockControl(_timelock)
    {}

    // Governance parameters β€” these define the security model
    function votingDelay() public pure override returns (uint256) {
        return 7200;    // ~1 day (in blocks, 12s/block)
    }

    function votingPeriod() public pure override returns (uint256) {
        return 50400;   // ~1 week
    }

    function proposalThreshold() public pure override returns (uint256) {
        return 10_000e18;  // need 10k tokens to propose
    }

    function quorum(uint256) public pure override returns (uint256) {
        return 100_000e18;  // 100k tokens must participate (10% of 1M supply)
    }
}

The Full Lifecycle in Code

// 1. PROPOSE β€” submit targets + values + calldatas + description
address[] memory targets = new address[](1);
targets[0] = address(myProtocol);
uint256[] memory values = new uint256[](1);
values[0] = 0;
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = abi.encodeCall(MyProtocol.setFee, (500)); // set fee to 5%

uint256 proposalId = governor.propose(
    targets, values, calldatas,
    "Proposal #1: Increase fee to 5%"
);
// Snapshot taken at this block β€” only current token holders can vote

// 2. VOTE β€” after votingDelay() blocks
governor.castVote(proposalId, 1);  // 0 = against, 1 = for, 2 = abstain

// 3. QUEUE β€” after votingPeriod() ends + quorum met + majority for
governor.queue(targets, values, calldatas, descriptionHash);
// β†’ Queued in TimelockController with delay

// 4. EXECUTE β€” after timelock delay expires
governor.execute(targets, values, calldatas, descriptionHash);
// β†’ TimelockController calls myProtocol.setFee(500)

πŸ’» Quick Try:

In Foundry, you can test the full governance lifecycle:

// In your test file:
function test_GovernanceLifecycle() public {
    // Setup: give alice tokens and have her delegate to herself
    token.transfer(alice, 200_000e18);
    vm.prank(alice);
    token.delegate(alice);
    vm.roll(block.number + 1); // checkpoint needs 1 block

    // Propose
    uint256 proposalId = governor.propose(targets, values, calldatas, "Set fee");

    // Advance past voting delay
    vm.roll(block.number + governor.votingDelay() + 1);

    // Vote
    vm.prank(alice);
    governor.castVote(proposalId, 1); // vote FOR

    // Advance past voting period
    vm.roll(block.number + governor.votingPeriod() + 1);

    // Queue in timelock
    governor.queue(targets, values, calldatas, keccak256("Set fee"));

    // Advance past timelock delay
    vm.warp(block.timestamp + timelock.getMinDelay() + 1);

    // Execute
    governor.execute(targets, values, calldatas, keccak256("Set fee"));

    // Verify the parameter changed
    assertEq(myProtocol.fee(), 500);
}

This test pattern is exactly what your Exercise 1 will use.

πŸ“– How to Study: OpenZeppelin Governor

  1. Start with ERC20Votes.sol β€” understand delegation and _checkpoints mapping
  2. Read Governor.propose() β€” how a proposal is created and the snapshot is taken
  3. Trace castVote() β†’ _countVote() β€” how votes are recorded and counted
  4. Follow queue() β†’ TimelockController.schedule() β€” how execution is delayed
  5. Study execute() β†’ TimelockController.execute() β€” how the timelock calls the target
  6. Read the access control: who can propose (threshold), who can execute (anyone after timelock)

πŸ” Code: OpenZeppelin Governor

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œWalk through the lifecycle of an on-chain governance proposal.”

    Answer
    • Good answer: β€œSomeone proposes a change, token holders vote, if it passes it goes through a timelock, then anyone can execute it.”
    • Great answer: β€œFive phases: (1) Propose β€” proposer submits targets, values, calldatas on-chain; a snapshot of all voting power is recorded. (2) Voting delay β€” 1-2 days for community review. (3) Active voting β€” holders cast for/against/abstain using power at the snapshot block, not current balance, preventing flash loan attacks. (4) Queue β€” if quorum met and majority for, queued in TimelockController with mandatory delay (24-48h) β€” the safety net giving users time to exit. (5) Execute β€” after timelock expires, anyone triggers execution. Total: 5-14 days.”

Interview Red Flags:

  • 🚩 Not knowing about snapshot-based voting and why it prevents flash loan attacks (votes recorded at proposal creation block, not at voting time)
  • 🚩 Not understanding why timelocks exist β€” it’s about user exit rights, not just β€œadding delay”
  • 🚩 Confusing delegation with voting β€” holding tokens alone does NOT give voting power; you must delegate first

Pro tip: In an interview, walk through the lifecycle with specific numbers (voting delay = 7200 blocks ~1 day, voting period = 50400 blocks ~1 week, timelock = 24-48h). Concrete parameters show you’ve actually configured governance, not just read about it.


🎯 Build Exercise: Governor + Timelock System

Workspace: workspace/src/part3/module8/

Build a complete on-chain governance system using OpenZeppelin Governor with TimelockController, and demonstrate that snapshot-based voting defeats flash loan attacks.

What you’ll implement:

  • GovernanceToken β€” ERC20Votes with delegation and checkpointing
  • MyGovernor β€” Governor with configurable voting delay, period, quorum, and threshold
  • Full proposal lifecycle: propose β†’ vote β†’ queue β†’ execute
  • Flash loan defense: prove snapshot voting blocks tokens acquired after proposal creation

Concepts exercised:

  • OpenZeppelin Governor framework integration
  • ERC20Votes delegation and checkpointing
  • TimelockController role configuration
  • The full governance lifecycle in code
  • Flash loan attack vector and why snapshots prevent it

🎯 Goal: Build production-standard governance and prove it’s secure against flash loan manipulation.

Run: forge test --match-contract GovernorTest -vvv


πŸ“‹ Key Takeaways: On-Chain Governance

After this section, you should be able to:

  • Trace the full proposal lifecycle: propose β†’ voting delay β†’ active vote β†’ queue in timelock β†’ execute, and explain why each phase exists (snapshot, deliberation, exit rights)
  • Explain ERC20Votes: how delegation activates voting power, why checkpointing records per-block snapshots, and how this prevents flash loan governance attacks
  • Configure the Governor + TimelockController role system: proposer, executor, canceller roles, and why the timelock enforces an execution delay that gives users exit rights before changes apply
  • Design quorum and threshold parameters that balance spam prevention with governance accessibility
Check your understanding
  • Proposal lifecycle: Propose (creates proposal with calldata, snapshot of voting power taken at this block) -> voting delay (1-2 days, gives community time to review and deliberate before voting starts) -> active vote (3-7 days, token holders vote for/against/abstain using power at the snapshot block) -> queue in timelock (1-2 days, enforced delay giving users exit rights) -> execute (permissionless call). Each phase exists for a reason: the snapshot at propose-time is the primary defense against flash loan attacks (borrowed tokens after the snapshot block give zero voting power); voting delay provides community review time; timelock gives users time to exit if they disagree with a passed proposal.
  • ERC20Votes: Voting power requires explicit delegation (even self-delegation) to activate. Checkpointing records per-block balance snapshots using binary search, so a proposal’s snapshot block determines voting power from historical balances β€” not current ones. This prevents flash loan governance attacks: borrowing tokens after the snapshot block gives zero voting power.
  • Governor + TimelockController: The Governor contract manages proposals and voting. The TimelockController holds execution authority over the protocol and enforces a minimum delay. Roles: proposer (Governor contract), executor (can be open or restricted), canceller (guardian multisig). The timelock delay is the critical security parameter β€” it gives users who disagree with a proposal time to withdraw funds before the change takes effect.
  • Quorum and thresholds: Quorum (minimum participation, e.g., 4% of total supply) prevents small groups from passing proposals when most holders aren’t paying attention. Proposal threshold (minimum tokens to propose, e.g., 0.1-1% of supply) prevents spam. Too high and governance becomes inaccessible; too low and it’s noisy or attackable. These must be calibrated to the token distribution and community activity.

πŸ’‘ ve-Tokenomics & the Curve Wars

πŸ’‘ Concept: Vote-Escrow: Locking for Influence

The ve (vote-escrow) model is one of DeFi’s most influential innovations. It transforms a governance token from a speculative asset into an incentive-alignment tool.

The veCRV Model

Lock CRV tokens for 1-4 years β†’ receive veCRV (non-transferable)

  Lock duration    veCRV per CRV
  ────────────     ─────────────
  4 years          1.00 veCRV      (maximum)
  2 years          0.50 veCRV
  1 year           0.25 veCRV
  1 week           0.0048 veCRV    (minimum)

πŸ” Deep Dive: veCRV Decay Math

The core formula β€” voting power decays linearly toward zero as the lock approaches expiry:

votingPower = lockedAmount Γ— (lockEnd - now) / MAX_LOCK_TIME

Where:
  lockedAmount = CRV tokens locked
  lockEnd = timestamp when lock expires
  MAX_LOCK_TIME = 4 years (126,144,000 seconds)
  now = current timestamp

Worked example:

Alice locks 10,000 CRV for 4 years:
  t = 0:   votingPower = 10,000 Γ— (4y - 0) / 4y = 10,000 veCRV
  t = 1y:  votingPower = 10,000 Γ— (4y - 1y) / 4y = 7,500 veCRV
  t = 2y:  votingPower = 10,000 Γ— (4y - 2y) / 4y = 5,000 veCRV
  t = 3y:  votingPower = 10,000 Γ— (4y - 3y) / 4y = 2,500 veCRV
  t = 4y:  votingPower = 10,000 Γ— (4y - 4y) / 4y = 0 veCRV (expired)

         veCRV
10,000 β”‚ ●
       β”‚   ●
 7,500 β”‚     ●
       β”‚       ●
 5,000 β”‚         ●
       β”‚           ●
 2,500 β”‚             ●
       β”‚               ●
     0 │─────────────────●──── time
       0    1y   2y   3y   4y

Why linear decay matters: It forces continuous re-locking. To maintain maximum voting power, you must keep extending your lock. This ensures that voters have ongoing skin in the game β€” they can’t vote and then immediately unlock.

In Solidity (simplified from Curve’s VotingEscrow.vy):

contract SimpleVotingEscrow {
    struct LockedBalance {
        uint256 amount;
        uint256 end;      // lock expiry timestamp
    }

    uint256 public constant MAX_LOCK = 4 * 365 days;
    mapping(address => LockedBalance) public locked;

    function createLock(uint256 amount, uint256 duration) external {
        require(duration >= 1 weeks && duration <= MAX_LOCK, "Invalid duration");
        require(locked[msg.sender].amount == 0, "Already locked");

        locked[msg.sender] = LockedBalance({
            amount: amount,
            end: block.timestamp + duration
        });

        token.transferFrom(msg.sender, address(this), amount);
    }

    function votingPower(address user) public view returns (uint256) {
        LockedBalance memory lock = locked[user];
        if (block.timestamp >= lock.end) return 0;
        return lock.amount * (lock.end - block.timestamp) / MAX_LOCK;
    }

    function withdraw() external {
        require(block.timestamp >= locked[msg.sender].end, "Still locked");
        uint256 amount = locked[msg.sender].amount;
        delete locked[msg.sender];
        token.transfer(msg.sender, amount);
    }
}

Three Powers of veCRV

1. Gauge voting β€” directing emissions:

Each Curve pool has a "gauge" that receives CRV emissions.
veCRV holders vote weekly on how to distribute emissions:

  Pool A (3CRV):     40% of votes β†’ 40% of weekly CRV emissions
  Pool B (stETH/ETH): 35% of votes β†’ 35% of weekly CRV emissions
  Pool C (FRAX/USDC):  25% of votes β†’ 25% of weekly CRV emissions

More emissions β†’ more rewards for LPs β†’ more liquidity β†’ deeper pool

2. Boosted LP rewards β€” up to 2.5x:

Base LP yield: 5% APR
With max boost (sufficient veCRV): 12.5% APR (2.5x)

Boost formula (simplified):
  boost = min(2.5, (0.4 * userLiquidity + 0.6 * totalLiquidity * (userVeCRV / totalVeCRV)) / userLiquidity)

Translation: your boost depends on your share of veCRV relative to your share of the pool

3. Protocol fee sharing β€” 50% of trading fees:

veCRV holders receive 50% of all Curve trading fees. Historically distributed in 3CRV (the stablecoin LP token), though some pools have shifted to distributing fees in crvUSD as Curve’s stablecoin gains adoption.

The Curve Wars

The gauge voting power creates a competitive market:

Protocol wants deep liquidity for its token pair on Curve
  β†’ Needs CRV emissions directed to its pool's gauge
  β†’ Options:
    1. Buy CRV, lock as veCRV, vote for own pool (expensive)
    2. Bribe existing veCRV holders to vote for their pool (cheaper!)

Enter Convex Finance:
  β†’ Aggregates CRV from thousands of users
  β†’ Locks ALL of it as veCRV (permanently!)
  β†’ vlCVX holders vote on how Convex directs its massive veCRV position
  β†’ Meta-governance: controlling Convex = controlling Curve emissions

The bribery market:
  Protocol pays $1 in bribes to vlCVX holders
  β†’ Those voters direct $1.50+ of CRV emissions to the protocol's pool
  β†’ Protocol gets $1.50 of liquidity incentives for $1 spent
  β†’ Voters earn $1 for voting (which is free for them)
  β†’ Everyone wins β€” the "Curve Wars" flywheel

  Bribe platforms: Votium, Hidden Hand, Paladin

Why this matters: The Curve Wars demonstrate that governance is not just about β€œvoting on proposals” β€” it’s an economic game where voting power has direct monetary value. Understanding this is essential for designing tokenomics.

Velodrome/Aerodrome: ve(3,3)

The ve(3,3) model fixes Curve’s incentive misalignment:

Curve's problem:
  veCRV holders earn fees from ALL pools, regardless of which pools they vote for
  β†’ Misaligned: voters can vote for bribed pools even if those pools generate no fees

Velodrome's fix:
  veVELO holders earn fees ONLY from pools they vote for
  β†’ Direct alignment: vote for high-volume pools = earn more fees
  β†’ No need for bribes on high-volume pools β€” the fees ARE the incentive
  β†’ Bribes only needed for new/low-volume pools that need bootstrapping

Anti-dilution: Voters receive proportional new emissions as a rebase. Non-voters get diluted over time. This incentivizes continuous participation.

Result: Velodrome is the dominant DEX on Optimism; Aerodrome is the dominant DEX on Base. The ve(3,3) model works especially well on L2s (Module 7 connection) because the cheap gas makes weekly voting/claiming practical for all users, not just whales.

πŸ”— DeFi Pattern Connection

Governance tokenomics across the program:

PatternWhere It AppearsModule
Token-weighted votingUniswap, Aave, Compound governanceThis module
Vote-escrow (ve)Curve, Velodrome, AerodromeThis module
Gauge emissionsDirecting liquidity incentivesThis module, P2M2
Protocol-owned liquidityTreasury as strategic assetThis module
Immutable governanceLiquity zero-governanceThis module, P2M9 capstone
Cross-chain governanceVote on L1, execute on L2Modules 6, 7
Emergency shutdownMakerDAO ESMPart 2 Module 6

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow does vote-escrow prevent governance manipulation?”

    Answer
    • Good answer: β€œUsers lock tokens for a period, so they can’t flash-borrow or quickly acquire and dump voting power.”
    • Great answer: β€œThree layers of resistance: (1) Time commitment β€” tokens locked 1-4 years, making flash-borrow impossible. An attacker must buy AND lock tokens, creating massive economic exposure. (2) Linear decay β€” power decreases as the lock approaches expiry, forcing continuous re-locking. (3) Incentive alignment β€” voters earn protocol fees, so voting against the protocol’s interest reduces their own revenue. The formula amount * (lockEnd - now) / maxLock means 10,000 tokens locked 4 years = 40,000 tokens locked 1 year β€” the market prices this cost.”
  2. β€œExplain the Curve Wars β€” what are protocols competing for?”

    Answer
    • Good answer: β€œProtocols compete for veCRV votes that direct CRV emissions to their pools, attracting liquidity.”
    • Great answer: β€œCurve emits CRV to LPs, but allocation depends on weekly gauge votes by veCRV holders. More emissions = higher LP rewards = more liquidity. Convex aggregates CRV permanently as veCRV; vlCVX holders control Convex’s votes, creating meta-governance. On Votium, protocols pay $1 in bribes to vlCVX holders, directing ~$1.50 of CRV emissions β€” profitable for both sides. The key insight: governance voting power has quantifiable economic value, and a market naturally forms around it.”

Interview Red Flags:

  • 🚩 Thinking governance is β€œjust voting” β€” ve-tokenomics proves it’s a complex economic system where voting power has direct monetary value
  • 🚩 Not knowing the decay formula (amount * (lockEnd - now) / maxLock) β€” this is the core mechanic that forces ongoing commitment
  • 🚩 Being unable to explain why Velodrome’s ve(3,3) improves on Curve’s model (voters earn fees only from pools they vote for)

Pro tip: If asked about tokenomics design, explaining the Curve Wars flywheel (lock β†’ vote β†’ bribe β†’ emissions β†’ liquidity) shows you understand governance as economic infrastructure, not just a voting mechanism. This perspective is what separates senior from junior DeFi engineers.


🎯 Build Exercise: Vote-Escrow Token

Workspace: workspace/src/part3/module8/

Build a simplified ve-token with time-weighted voting power, linear decay, and gauge-style emission allocation.

What you’ll implement:

  • createLock() β€” lock tokens for a specified duration (1 week to 4 years)
  • votingPower() β€” calculate current voting power with linear decay
  • increaseAmount() β€” add more tokens to an existing lock
  • increaseUnlockTime() β€” extend lock duration
  • voteForGauge() β€” allocate voting power to a gauge (emission target)
  • withdraw() β€” reclaim tokens after lock expires

Concepts exercised:

  • Vote-escrow mechanics (lock β†’ power β†’ decay)
  • Linear decay formula: amount Γ— (lockEnd - now) / maxLock
  • Gauge voting and weight allocation
  • The incentive structure that makes ve-tokenomics work
  • Why time-locking prevents governance manipulation

🎯 Goal: Build the core of a Curve-style vote-escrow system and understand why lock duration creates genuine skin-in-the-game for governance participants.

Run: forge test --match-contract VoteEscrowTest -vvv


πŸ“‹ Key Takeaways: ve-Tokenomics

After this section, you should be able to:

  • Explain the vote-escrow model: lock tokens for 1-4 years β†’ non-transferable voting power with linear decay (amount Γ— (lockEnd - now) / maxLock), and why this forces continuous re-locking to maintain influence
  • Describe veCRV’s three powers: gauge voting (directing CRV emissions), boosted LP rewards (up to 2.5x), fee sharing β€” and how the Curve Wars emerged from Convex aggregating veCRV into vlCVX meta-governance with bribery markets (Votium, Hidden Hand)
  • Compare Curve’s ve model with Velodrome/Aerodrome ve(3,3): how β€œvoters earn fees only from pools they vote for” fixes Curve’s incentive misalignment between voting and liquidity provision
Check your understanding
  • Vote-escrow model: Lock CRV tokens for 1-4 years to receive non-transferable veCRV. Voting power equals amount * (lockEnd - now) / maxLock and decays linearly toward zero as the lock approaches expiry. This forces continuous re-locking to maintain influence and aligns long-term holders with protocol governance β€” short-term speculators get minimal voting power.
  • veCRV’s three powers and Curve Wars: veCRV grants gauge voting (directing CRV emissions to pools), boosted LP rewards (up to 2.5x for LPs who hold veCRV), and fee sharing (portion of protocol revenue). Convex aggregates veCRV from many users into vlCVX (vote-locked CVX), creating meta-governance. Bribery markets (Votium, Hidden Hand) emerged where protocols pay vlCVX holders to vote emissions toward their pools β€” the β€œCurve Wars” are protocols competing for CRV emissions via bribes.
  • ve(3,3) improvement: In Curve, voters earn fees from ALL pools regardless of where they vote β€” creating a misalignment where voters direct emissions to bribed pools but earn fees from unrelated high-volume pools. Velodrome/Aerodrome’s ve(3,3) fixes this: voters earn trading fees ONLY from pools they vote for. This aligns incentives β€” voters direct emissions to pools that generate real fees, not just bribe revenue.

πŸ’‘ Governance Security

πŸ’‘ Concept: When Governance Itself Is the Attack Surface

The Beanstalk Attack ($182M, April 2022)

The most expensive governance attack in DeFi history β€” and entirely preventable.

What happened:

Beanstalk's governance had NO voting delay and NO timelock.
A proposal could be created, voted on, and executed in ONE transaction.

Attack:
1. Attacker flash-borrowed massive governance tokens from Aave + SushiSwap
2. Created a malicious proposal: "Transfer entire treasury to attacker"
3. Voted FOR with flash-borrowed tokens (overwhelming majority)
4. Proposal passed immediately (no quorum issues β€” massive tokens)
5. Executed the proposal in the SAME transaction
6. Returned flash-borrowed tokens
7. Kept $182M of drained treasury

Total time: 1 Ethereum transaction (~13 seconds)

Why it worked: No voting delay meant tokens acquired in the same block could vote. No timelock meant the proposal executed immediately. Flash loans provided unlimited temporary capital.

What would have prevented it:

Defense 1: SNAPSHOT-BASED VOTING (OpenZeppelin default)
  β†’ Voting power recorded at proposal creation block
  β†’ Tokens acquired AFTER snapshot don't count
  β†’ Flash-borrowed tokens are acquired after the proposal exists
  β†’ Attack fails: flash tokens have zero voting power

Defense 2: VOTING DELAY (1+ blocks)
  β†’ Gap between proposal creation and voting start
  β†’ Flash loan must span multiple blocks (impossible β€” single-tx only)
  β†’ Attack fails: can't vote in the proposal creation transaction

Defense 3: TIMELOCK
  β†’ Even if proposal passes, execution delayed 24-48h
  β†’ Community can review and respond
  β†’ Users can exit before malicious changes take effect
  β†’ Attack fails: treasury drain is visible and can be countered

Production protocols use ALL THREE. Beanstalk had NONE.

πŸ’» Quick Try:

In Foundry, prove that snapshot voting defeats flash loans:

contract FlashLoanDefenseDemo {
    // ERC20Votes token uses checkpoints β€” votes are recorded per block

    function test_flashLoanCantVote() public {
        // Block 100: proposal created, snapshot = block 100
        uint256 proposalId = governor.propose(...);

        // Block 100 (same block): attacker flash-borrows tokens
        // attacker's balance at block 100 BEFORE the borrow = 0
        // getPastVotes(attacker, block 100) = 0  ← checkpoint was 0!

        // Advance to voting period
        vm.roll(block.number + governor.votingDelay() + 1);

        // Attacker tries to vote β€” but has 0 votes at snapshot
        vm.prank(attacker);
        // governor.castVote(proposalId, 1);  ← would have 0 weight

        // Defense works: tokens acquired after snapshot have no power
    }
}

Other Governance Attack Vectors

Delegation attacks:

  • Accumulate delegated voting power from many small holders through social engineering
  • Vote maliciously before delegators can react and re-delegate
  • Defense: delegation monitoring, delegation caps, delegation lockup periods

Low-quorum exploitation:

  • Wait for low participation period (holidays, market crisis)
  • Pass controversial proposal with minimal opposition
  • Defense: adequate quorum thresholds, emergency guardian pause

Governance extraction:

  • Whale accumulates enough voting power to pass self-serving proposals
  • Example: redirect treasury funds to themselves, change fee structure
  • Defense: timelock (users can exit), guardian multisig (can veto), vote-escrow (long-term alignment)

Emergency Mechanisms

Production protocols combine governance with fast-response capabilities:

/// @notice Emergency guardian β€” can pause but NOT upgrade
contract EmergencyGuardian {
    address public guardian;  // multisig (e.g., 3/5 team members)
    IProtocol public protocol;

    // Guardian can PAUSE β€” immediate response to exploits
    function pause() external onlyGuardian {
        protocol.pause();
    }

    // Guardian can UNPAUSE β€” resume normal operation
    function unpause() external onlyGuardian {
        protocol.unpause();
    }

    // Guardian CANNOT: upgrade contracts, change parameters, move funds
    // Those require full governance (Governor + Timelock)
}

The pattern used by major protocols:

Aave:
  Guardian multisig β†’ can pause markets (fast, centralized)
  Governor + Timelock β†’ parameter changes, upgrades (slow, decentralized)

MakerDAO:
  Emergency Shutdown Module (ESM) β†’ anyone can trigger with enough MKR
  Governance β†’ parameter changes, new collateral types
  (Requires depositing MKR into ESM β€” tokens are burned, so it's costly to trigger)

Compound:
  Pause Guardian β†’ can pause individual markets
  Governor Bravo β†’ all parameter and upgrade changes

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow did the Beanstalk governance attack work, and how would you prevent it?”

    Answer
    • Good answer: β€œThe attacker flash-borrowed tokens, voted on a malicious proposal, and executed it all in one transaction. Prevention: snapshot voting and timelocks.”
    • Great answer: β€œBeanstalk had three fatal flaws: no snapshot voting (tokens acquired in the same block could vote), no voting delay (voting started immediately), and no timelock (proposals executed instantly). The attacker flash-borrowed $1B of tokens, created a proposal to drain the treasury, voted FOR, and executed it β€” all in one transaction ($182M loss). Standard OpenZeppelin Governor defaults prevent this entirely: snapshot voting means tokens must be held BEFORE proposal creation; voting delay prevents same-block voting; timelock delays execution 24-48h. The lesson: governance security is as critical as smart contract security.”

Interview Red Flags:

  • 🚩 Not knowing the Beanstalk attack β€” it’s the most important governance case study in DeFi ($182M, entirely preventable)
  • 🚩 Not being able to name the three defenses (snapshot voting, voting delay, timelock) and why each is necessary
  • 🚩 Thinking emergency mechanisms (guardian pause) and governance (Governor + Timelock) are the same thing β€” the guardian can pause but CANNOT upgrade or move funds

Pro tip: In interviews, showing awareness that governance is both a feature AND an attack surface immediately sets you apart. Most candidates think about governance from the β€œhow do we vote” perspective. Senior candidates think from β€œhow can this be exploited, and how do we minimize the attack surface.”


πŸ’‘ Governance Minimization

πŸ’‘ Concept: Less Governance Can Be Better

Every governable parameter is an attack surface. The more things governance can change, the more ways the protocol can be exploited or manipulated.

The Spectrum

FULL GOVERNANCE ──────────────────────────────── IMMUTABLE
  β”‚                    β”‚                    β”‚            β”‚
  Multisig          Governor +           Minimal       Zero
  (most agile)      Timelock            governance    governance
  β”‚                    β”‚                    β”‚            β”‚
  Team controls     Token holders       Only critical  Nothing
  everything        decide              params         changeable

  Risk: rug pull    Risk: slow to       Risk: can't    Risk: can't
                    respond             adapt quickly  fix bugs

  Example:          Example:            Example:       Example:
  Early protocols   Aave, Compound      Uniswap V2     Liquity

Liquity: Zero Governance

Liquity's approach:
  βœ“ All parameters hardcoded at deployment
  βœ“ Contracts are immutable (no proxy, no admin key)
  βœ“ Minimum collateral ratio: always 110%
  βœ“ Borrowing fee: algorithmic (not governed)
  βœ“ No admin, no multisig, no governance token

  Advantage: maximum trustlessness β€” "code is law" fully realized
  Disadvantage: can't fix bugs, can't adapt to market changes

  When this works: simple protocols with well-tested parameters
  When this doesn't: complex protocols that need ongoing tuning

Connection to Part 2 Module 9: Your capstone stablecoin was designed as immutable β€” no admin keys, no governance. This was a deliberate design choice that eliminates governance attack surfaces at the cost of adaptability.

Progressive Decentralization

Most protocols follow a maturation path:

Phase 1: MULTISIG (launch)
  Team controls everything via 3/5 or 4/7 multisig
  Fast iteration, bug fixes, parameter tuning
  Users must trust the team

Phase 2: GOVERNOR + TIMELOCK (growth)
  Token holders vote on changes
  Timelock gives users exit rights
  Team retains emergency guardian role

Phase 3: MINIMIZE GOVERNANCE (maturity)
  Reduce governable parameters over time
  Hardcode well-tested values
  Remove upgrade capability where possible
  Eventually: only emergency pause + critical parameters remain

Compound's progression:
  Admin key β†’ Governor Alpha β†’ Governor Bravo β†’ community governance
  Each step reduced team control and increased decentralization

The right question isn’t β€œgovernance or not” β€” it’s β€œwhat SHOULD be governable?”

SHOULD be governable:
  βœ“ Risk parameters (LTV, liquidation thresholds) β€” markets change
  βœ“ Fee levels β€” competitive dynamics
  βœ“ New asset listings β€” protocol growth
  βœ“ Emergency pause β€” security response

SHOULD NOT be governable (hardcode):
  βœ— Core accounting math β€” getting this wrong breaks everything
  βœ— Access control invariants β€” "only the borrower can repay their loan"
  βœ— Token supply (usually) β€” governance shouldn't be able to inflate supply

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œWhat are the tradeoffs between governance and immutability?”

    Answer
    • Good answer: β€œGovernance allows protocols to adapt but introduces attack surfaces. Immutability is more trustless but can’t fix bugs.”
    • Great answer: β€œThe spectrum runs from full governance (multisig) through token-based governance (Governor + Timelock) to zero governance (Liquity β€” no admin keys, no upgradability). Full governance enables rapid response but every governable parameter is an attack surface. Zero governance eliminates these risks but can’t adapt. The optimal approach is progressive decentralization: start with multisig, transition to token governance as the protocol matures, then systematically reduce what’s governable. The key principle: only make governable what MUST change β€” core accounting math should be immutable; risk parameters should be governable.”

Interview Red Flags:

  • 🚩 Treating all governance as good or all governance as bad β€” it’s a spectrum with real tradeoffs at every point
  • 🚩 Not being able to distinguish what SHOULD be governable (risk parameters, fees) from what should NOT (core accounting math, access control invariants)
  • 🚩 Not knowing about progressive decentralization β€” the standard maturation path from multisig to Governor to minimized governance

Pro tip: When asked β€œhow would you design governance for X protocol?”, frame your answer around what should be governable vs immutable, then describe the maturation path. This shows you think about governance as a design discipline, not just β€œadd a Governor contract.”


πŸ“‹ Key Takeaways: Governance & DAOs

After this section, you should be able to:

  • Analyze the Beanstalk governance attack: how flash-loaned governance tokens bypassed snapshot voting (no voting delay), and list the specific defenses that would have prevented it
  • Design an emergency mechanism for a governed protocol: guardian multisig with pause-only power, automatic timelock bypass for critical actions, and explain the trust trade-off
  • Position a protocol on the governance minimization spectrum (multisig β†’ governed β†’ immutable) and explain progressive decentralization: start governed, harden parameters over time, make core logic immutable
Check your understanding
  • Beanstalk governance attack: The attacker flash-borrowed massive governance tokens, created a malicious proposal to drain the treasury, voted it through, and executed it β€” all in one transaction ($182M lost). This was possible because Beanstalk had no voting delay (so the snapshot was the current block, allowing flash-loaned tokens to vote) and no timelock (so execution was immediate). Defenses: voting delay (snapshot before vote starts defeats flash loans), timelock (execution delay gives users exit rights), and quorum requirements calibrated to circulating supply.
  • Emergency mechanisms: A guardian multisig with pause-only power can halt the protocol immediately without waiting for governance. The guardian CANNOT change parameters, move funds, or bypass the timelock for non-emergency actions. The trust trade-off: you’re trusting a small group to correctly identify emergencies, but limiting their power to only pausing β€” they can stop damage but can’t extract value. Unpausing should require full governance.
  • Governance minimization: The spectrum runs from full multisig control (fast, centralized) through token governance (slower, more decentralized) to fully immutable (no changes possible). Progressive decentralization means starting with more governance authority for rapid iteration, then hardening parameters one by one as the protocol matures. Core logic (swap math, liquidation formulas) should become immutable first; operational parameters (fee rates, caps) may remain governed longer.


πŸ“š Resources

Production Code:

Documentation:

Key Reading:


Navigation: ← Module 7: L2-Specific DeFi | Module 9: Capstone β†’