BlockchainSolidity11 min readUpdated

Governance Tokens on ERC20: UNI, AAVE, Compound Patterns

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Governance Tokens on ERC20: UNI, AAVE, Compound Patterns

Section 01 · Introduction

Why governance tokens are the most demanding ERC20 use case

A stablecoin is an ERC20 token that needs to move predictably. A governance token is an ERC20 token that needs to move predictably and also count votes correctly across time.

Quick answer

What is a governance token? A governance token is an ERC20 token whose holders can vote on protocol decisions on chain. The token contract adds a snapshot system that records voting power at each block, so a holder cannot vote, transfer the tokens to a second wallet, and vote again. Production stacks pair the token with a Governor contract for the proposal lifecycle and a Timelock for delayed execution.

Compound's COMP launched in 2020 and gave every other DeFi protocol a template. Uniswap's UNI followed. AAVE migrated from LEND to a governance plus safety module model. Today every serious DeFi protocol that wants to decentralize control issues some variant of the same pattern.

The pattern looks simple on the surface. Mint a token. Let holders vote. Execute the result on chain. Each of those three steps hides a sharp engineering problem. This post walks through the actual contract surface — the ERC20Votes extension, the Governor lifecycle, and the staking modules that sit alongside.

If you have already read the ERC standards explainer, you know the ERC20 interface is the easy part. Everything in this post is what production teams add on top.

Snapshot voting solves the double vote problem

Without snapshots, a holder could vote on a proposal, transfer tokens to a fresh wallet, and vote again with the same balance. ERC20Votes records the balance at the proposal's start block. Vote weight is read from that historical checkpoint, not the current balance.

Section 02 · ERC20Votes

The snapshot machinery that powers on chain voting

The ERC20Votes extension turns a plain token into a vote weighted token. Two ideas do most of the work: checkpoints and delegation.

Every time a holder's balance changes, the contract writes a new checkpoint: a tuple of block number and balance. When a Governor needs to know how much vote weight an address had at a past block, it walks the checkpoint array and returns the right snapshot.

solidity
// OpenZeppelin ERC20Votes — checkpointed balances for snapshot voting
contract GovToken is ERC20, ERC20Permit, ERC20Votes {
    constructor()
        ERC20("Protocol Governance", "PGOV")
        ERC20Permit("Protocol Governance")
    {}

    // Snapshots are taken automatically on transfer, mint, burn
    function _afterTokenTransfer(address from, address to, uint256 amount)
        internal
        override(ERC20, ERC20Votes)
    {
        super._afterTokenTransfer(from, to, amount);
    }

    function _mint(address to, uint256 amount)
        internal
        override(ERC20, ERC20Votes)
    {
        super._mint(to, amount);
    }

    function _burn(address account, uint256 amount)
        internal
        override(ERC20, ERC20Votes)
    {
        super._burn(account, amount);
    }
}

That looks like a normal ERC20 contract with a few overrides. The interesting work happens inside the _afterTokenTransfer override, which schedules the checkpoint write. The cost is roughly an extra 15,000 gas per transfer compared to a plain ERC20. For a governance token, that is a fair price.

Most holders do not vote directly. The realistic governance participation rate across major DAOs is below 10 percent of supply. The delegation system is what makes the remaining tokens politically active without forcing every holder to follow every proposal.

solidity
// Delegation transfers voting power without moving tokens
function delegate(address delegatee) public virtual override {
    _delegate(_msgSender(), delegatee);
}

// Internal — moves vote weight from current delegate to the new one,
// writes a fresh checkpoint at the current block number
function _delegate(address delegator, address delegatee) internal virtual {
    address currentDelegate = delegates(delegator);
    uint256 delegatorBalance = balanceOf(delegator);
    _delegates[delegator] = delegatee;

    emit DelegateChanged(delegator, currentDelegate, delegatee);

    _moveVotingPower(currentDelegate, delegatee, delegatorBalance);
}

// Vote weight at a past block — Governor uses this at proposal snapshot
function getPastVotes(address account, uint256 blockNumber)
    public
    view
    virtual
    override
    returns (uint256)
{
    require(blockNumber < block.number, "ERC20Votes: block not yet mined");
    return _checkpointsLookup(_checkpoints[account], blockNumber);
}

A holder calls delegate to assign their voting power to another address. The tokens stay in the holder's wallet. The vote weight moves to the delegate's checkpoint. When a proposal opens, the Governor reads getPastVotes against the delegate, not the original holder.

Three column flow diagram showing token holders on the left, a delegate address in the middle receiving vote weight from multiple holders, and a checkpoint snapshot stored against the delegate at a specific block number. The tokens remain in the original holder wallets while the vote weight aggregates at the delegate.
How delegation routes voting power without moving the underlying ERC20 balance.

Self delegation is a separate step

A common surprise: a fresh holder has zero voting power until they call delegate, even on their own address. ERC20Votes does not auto delegate on mint. Most protocol UIs prompt the holder to self delegate after the first transfer.

Section 03 · Governor Lifecycle

From proposal to on chain execution

The Governor contract owns the proposal lifecycle. The token contract holds the weights. The Timelock owns the protocol. Three contracts, one workflow.

Anyone with at least the proposal threshold of voting power can submit a proposal. The proposal carries a target contract address, a function selector, the calldata, and a description string. The Governor hashes those into a proposal id.

solidity
// Governor lifecycle — propose, vote, queue, execute
function propose(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    string memory description
) public returns (uint256) {
    require(
        getVotes(msg.sender, block.number - 1) >= proposalThreshold(),
        "Governor: proposer votes below threshold"
    );

    uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description)));

    proposals[proposalId] = ProposalCore({
        voteStart: block.number + votingDelay(),
        voteEnd: block.number + votingDelay() + votingPeriod(),
        executed: false,
        canceled: false
    });

    return proposalId;
}

function castVote(uint256 proposalId, uint8 support) public returns (uint256) {
    uint256 weight = token.getPastVotes(msg.sender, proposals[proposalId].voteStart);
    return _countVote(proposalId, msg.sender, support, weight);
}

Voting opens after a delay, runs for the configured period, and closes. If the proposal hits quorum and a majority vote in favor, anyone can queue it into the Timelock. The Timelock holds the proposal for the configured delay — usually 48 hours — before anyone can execute it. The delay is the protocol's emergency window: if a malicious proposal sneaks through voting, the community has 48 hours to react.

Six step proposal lifecycle diagram: a holder with voting power submits a proposal, voting delay starts, voters cast for and against votes during the voting window, the proposal succeeds or fails at quorum check, on success the proposal queues into the Timelock for a 48 hour delay, then anyone executes the call against the target protocol contract.
The Compound GovernorBravo lifecycle. Every major DeFi protocol runs a variant of this six step flow.
Typical Governor parameter ranges across major DeFi protocols.
ParameterCompoundUniswapNotes
Proposal threshold25,000 COMP2.5M UNIFloor for who can submit proposals
Voting delay13,140 blocks (~2 days)13,140 blocks (~2 days)Read time before vote opens
Voting period19,710 blocks (~3 days)40,320 blocks (~7 days)Length of the voting window
Quorum400,000 COMP40M UNIMinimum yes votes required
Timelock delay2 days2 daysBuffer between queue and execute

These parameter ranges look conservative. They are. Most proposals are routine — fee tweaks, asset listings, treasury allocations. The conservative parameters are insurance against the rare proposal that should never pass.

Section 04 · Staking

Why governance tokens stake

Staking turns a passive governance token into an active capital primitive. The token contract stays the same; a second contract wraps the stake.

AAVE's safety module is the cleanest production example. A holder stakes AAVE into the module and receives stkAAVE — itself an ERC20Votes token. The stake earns protocol rewards and serves as a shortfall buffer: if the AAVE protocol takes a loss, the safety module can be slashed up to 30 percent to cover it.

solidity
// AAVE safety module — stake AAVE, earn protocol fees, absorb shortfall
function stake(address onBehalfOf, uint256 amount) external override {
    require(amount > 0, "INVALID_ZERO_AMOUNT");

    uint256 balanceOfUser = balanceOf(onBehalfOf);
    uint256 accruedRewards = _updateUserAssetInternal(
        onBehalfOf,
        address(this),
        balanceOfUser,
        totalSupply()
    );

    if (accruedRewards != 0) {
        stakerRewardsToClaim[onBehalfOf] = stakerRewardsToClaim[onBehalfOf] + accruedRewards;
    }

    stakersCooldowns[onBehalfOf] = getNextCooldownTimestamp(0, amount, onBehalfOf, balanceOfUser);

    _mint(onBehalfOf, amount);
    IERC20(STAKED_TOKEN).safeTransferFrom(msg.sender, address(this), amount);
}

The stake function does four things. It updates the reward accrual for the staker. It records a cooldown timestamp that defines when the staker can unstake. It mints stkAAVE one to one against the deposited AAVE. It pulls the underlying AAVE into the safety module contract.

Curve's vote escrowed CRV uses a different shape. A holder locks CRV for a fixed duration — up to four years — and receives veCRV in exchange. veCRV is non transferable and decays linearly toward zero as the lock approaches expiry. Longer locks earn more vote weight. The mechanism aligns voting power with time committed, not just capital committed.

Compound's COMP — pure governance, no staking

COMP is the simplest of the production governance tokens. No staking layer. No vote escrow. Vote weight is one COMP, one vote, snapshotted at proposal time. Easy to model, easy to integrate.

Uniswap's UNI — governance with a treasury hatch

UNI matches the COMP pattern with one twist: a fee switch controlled by governance. Activating the fee switch routes a slice of swap fees to UNI holders. The threat of activation is the protocol's long term value capture mechanism.

AAVE — governance plus shortfall coverage

AAVE bundles voting with an insurance role through the safety module. Stakers earn AAVE rewards but accept up to a 30 percent slashing risk. The result is aligned skin in the game for the holders who care most about protocol solvency.

Curve's veCRV — vote escrowed alignment

Curve locks tokens for time to multiply voting power. The longer the lock, the larger the vote weight and the larger the share of protocol fees. The mechanism has become a template — Convex, Frax, and Balancer all run variants.

Section 05 · Integration

How to read a governance token from a contract

If your protocol cares about a governance token — to gate access, to count votes, to detect a takeover — the integration surface is wider than plain ERC20.

Read getPastVotes, not balanceOf, when you need the vote weight at a specific point in time. balanceOf returns the current balance, which a holder can move between checkpoints. getPastVotes returns the snapshot, which is what the Governor itself uses.

Treat delegation as the actual voting unit. A wallet with 10 million UNI that has not delegated has zero voting power on a proposal. A wallet with zero balance that holds 50 million UNI of delegated weight is a power user. Reading raw balances will mislead you.

Track the Governor address separately. The token contract is rarely upgraded. The Governor is. Compound migrated from GovernorAlpha to GovernorBravo without changing COMP. Your integration needs to follow the current Governor through that migration or risk pointing at a dead contract.

The Timelock is the protocol's real owner

On every major DeFi protocol, the role with admin rights on the core contracts is the Timelock, not a multisig and not the deployer. If you are auditing a protocol's decentralization story, that is the single most important contract to inspect.

For a deeper read on how on chain ownership composes with other ERC20 use cases like stablecoin custody flows, the same contract surface lessons apply: the standard interface is rarely where the operational risk lives.

Section 06 · FAQ

Common questions about ERC20 governance tokens

The questions that come up most often when teams design or integrate a governance system.

What is a governance token?

A governance token is an ERC20 token whose holders can vote on protocol decisions on chain. The contract adds a snapshot system so vote weight is recorded at the block a proposal opens, preventing double voting. UNI, COMP, AAVE, and MKR are the most widely held examples.

How does ERC20Votes work?

ERC20Votes is an OpenZeppelin extension that records a balance checkpoint every time a transfer, mint, or burn happens. When a Governor needs the voting power of an address at a past block, it walks the checkpoint array and returns the historical balance. The extension adds roughly 15,000 gas per transfer in exchange for snapshot voting.

Why do governance tokens use delegation?

Most token holders do not follow protocol governance actively. Delegation lets a passive holder assign their vote weight to a delegate they trust without moving tokens. Production DAOs typically see well under 10 percent of supply voting directly; the rest is delegated. The delegate becomes the politically active unit.

What is the difference between UNI and AAVE staking?

UNI does not have a native staking module. AAVE does — the safety module accepts staked AAVE in exchange for stkAAVE, pays protocol rewards, and serves as a shortfall buffer that can be slashed up to 30 percent to cover protocol losses. Curve uses a third pattern: vote escrowed lock with time decayed voting power.

Can governance tokens be paused?

The token contracts themselves usually have no pause switch. The Governor and Timelock contracts have controlled freeze paths through governance itself — a proposal can vote to halt a specific action. Pausing every transfer of the underlying token is unusual because it would freeze every wallet across every DEX simultaneously.

Section 07 · Next Steps

Design the token for the governance you actually want

Most teams pick a governance stack by copying a contract address. The right starting point is the parameter set and the threat model.

We help protocols ship governance and tokenomics systems end to end — the ERC20Votes token, the Governor, the Timelock, the staking module if you need one, and the operational playbook for running proposals once the system is live. The technical surface is tractable. The political surface is the harder part.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View blockchain development serviceSee ChainTrust case study

Related service

Blockchain Development

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →