ChainScore Labs
LABS
Guides

Token-Based Voting Models in DeFi DAOs

Chainscore © 2025
concepts

Core Concepts of Token-Based Governance

Foundational mechanisms that define how token holders participate in and influence decentralized autonomous organizations.

01

Voting Power

Voting power is the influence a token holder exerts, typically proportional to their token holdings. This can be implemented as one-token-one-vote or through mechanisms like vote-escrowed tokens (veTokens).

  • Direct correlation between stake size and governance weight.
  • Quadratic voting models to reduce whale dominance.
  • Critical for determining proposal outcomes and protocol direction.
02

Proposal Submission

Proposal submission is the formal process for suggesting changes, requiring a creator to stake a minimum number of tokens as a spam-prevention deposit.

  • Thresholds prevent governance spam and ensure serious intent.
  • Includes treasury spend, parameter adjustment, and upgrade proposals.
  • The deposit is often slashed if the proposal fails to reach a quorum.
03

Quorum & Voting Period

Quorum is the minimum participation threshold required for a vote to be valid, while the voting period is the fixed timeframe for casting votes.

  • A low quorum risks decisions by a small, unrepresentative group.
  • Typical voting periods range from 3 to 7 days for sufficient deliberation.
  • These parameters balance efficiency with broad stakeholder input.
04

Delegation

Delegation allows token holders to assign their voting power to a representative or expert without transferring asset custody.

  • Enables participation for less active members via trusted delegates.
  • Creates a political layer with delegate platforms and platforms.
  • Essential for scaling governance and improving voter apathy in large DAOs.
05

Execution & Timelocks

Execution is the automated enactment of a passed proposal's code, often guarded by a timelock—a mandatory delay between vote conclusion and action.

  • Timelocks provide a final review period to detect malicious code.
  • Execution is typically permissionless via smart contracts.
  • This security mechanism is fundamental for managing upgrade risks.
06

Forking as Exit

Forking is the ultimate governance mechanism, where a dissenting group can copy the protocol's code and state to create a new chain with different rules.

  • Serves as a credible threat against governance attacks or stagnation.
  • Requires significant coordination and liquidity migration.
  • Embodies the permissionless and composable nature of DeFi governance.

The Proposal and Voting Lifecycle

Process overview

1

Draft and Submit the Proposal

Create a formal governance proposal with executable code or signaling text.

Detailed Instructions

Proposals are initiated by a proposer who must meet a minimum token threshold. The proposal payload is constructed, typically containing a target contract address, function signature, and calldata for execution. For signaling proposals, the description outlines the intended action.

  • Sub-step 1: Draft the proposal text and, if applicable, the on-chain transaction calldata.
  • Sub-step 2: Verify the target contract's function selector and parameters using a block explorer or local fork.
  • Sub-step 3: Use the governance contract's propose function, passing the target addresses, values, and calldata arrays.
solidity
// Example: Submitting a proposal in a Compound-style governor function propose( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) public returns (uint256 proposalId);

Tip: Test the proposal's effects on a forked mainnet environment before submission to avoid unintended consequences.

2

Navigate the Voting Delay and Snapshot

Understand the waiting period and how voter eligibility is determined.

Detailed Instructions

After submission, a voting delay period begins, during which the proposal is inactive. This allows the community to review the proposal details. The critical snapshot of voting power is typically taken at the start of the voting period, based on a specific block number.

  • Sub-step 1: Confirm the governance contract's votingDelay() parameter, often expressed in blocks (e.g., 6,500 blocks ~1 day).
  • Sub-step 2: Identify the snapshot block, usually proposalSnapshot(proposalId), which determines token balances for voting power.
  • Sub-step 3: Delegate or consolidate tokens before the snapshot block if necessary, as undelegated tokens do not grant voting power.
solidity
// Querying snapshot and delay for a proposal uint256 snapshotBlock = governor.proposalSnapshot(proposalId); uint256 delay = governor.votingDelay();

Tip: Voters must hold or have delegated tokens prior to the snapshot block. Voting power is not dynamic during the voting period.

3

Cast Votes During the Active Period

Token holders vote for, against, or abstain on the proposal.

Detailed Instructions

The voting period is the active window for casting votes. Votes are weighted by the voter's token balance at the snapshot. Common vote types are For, Against, and Abstain. Some systems also support voting with vote delegation where a delegate votes on behalf of others.

  • Sub-step 1: Call the castVote(proposalId, support) function, where support is 1 (For), 0 (Against), or 2 (Abstain).
  • Sub-step 2: Verify the vote transaction succeeded and check the emitted VoteCast event.
  • Sub-step 3: Monitor the proposal's current tally via proposalVotes(proposalId) to see the aggregate For, Against, and Abstain counts.
solidity
// Casting a vote in an OpenZeppelin Governor contract function castVote(uint256 proposalId, uint8 support) public returns (uint256); // Returns a struct with forVotes, againstVotes, abstainVotes Governor.ProposalVote memory votes = governor.proposalVotes(proposalId);

Tip: Gasless voting via signature (castVoteBySig) is often available to improve voter participation.

4

Execute or Queue the Successful Proposal

Finalize a passed proposal by executing its on-chain actions.

Detailed Instructions

If the proposal meets the quorum and has more For than Against votes, it succeeds. Execution may be direct or go through a timelock queue. The timelock introduces a mandatory delay, allowing users to exit systems if they disagree with the pending change.

  • Sub-step 1: Check the proposal state via state(proposalId); it should return Succeeded.
  • Sub-step 2: If a timelock is used, call queue to schedule the transaction, which will be executable after the delay.
  • Sub-step 3: After any delay, call execute with the original proposal parameters to run the on-chain transactions.
solidity
// Example execution flow with a timelock if (governor.state(proposalId) == Governor.ProposalState.Succeeded) { governor.queue(targets, values, calldatas, descriptionHash); } // After timelock delay... governor.execute(targets, values, calldatas, descriptionHash);

Tip: The execute call can be made by any address, not just the proposer, often creating a public good incentive for execution.

5

Monitor Execution and Handle Failures

Ensure proposal actions are carried out and understand failure modes.

Detailed Instructions

Execution can fail due to expired proposals, insufficient gas, or reverting logic in the target contract. A grace period may follow the voting period, after which a successful proposal expires. Monitor transaction receipts and contract state changes.

  • Sub-step 1: Verify the proposal state transitions to Queued, then Executed. A state of Defeated or Expired indicates failure.
  • Sub-step 2: If execution reverts, diagnose the cause: check if the target contract state invalidated the calldata or if a timelock operation was bypassed.
  • Sub-step 3: For complex multi-step proposals, use a proposal simulator or Tenderly fork to replay the execution before the real queue.
solidity
// Common reasons for execution failure require(block.timestamp >= eta, "Timelock: transaction not ready"); // Too early require(state(proposalId) == ProposalState.Queued, "Proposal not queued"); // Wrong state // Target contract logic may revert internally

Tip: Keep a record of the descriptionHash used in queue/execute calls, as it must match the original proposal's hash.

Comparison of Voting Models

A technical comparison of prevalent token-based voting mechanisms used in DeFi governance.

Governance FeatureSimple Token VotingConviction VotingQuadratic Voting

Vote Weight Calculation

Linear (1 token = 1 vote)

Linear, scaled by time commitment

Square root of token amount

Typical Quorum Requirement

2-20% of circulating supply

Dynamic, based on proposal size & history

Often fixed, low percentage (e.g., 1-5%)

Vote Cost (Avg. Gas)

~$5-50 (single transaction)

~$2-20 (stake/unstake streams)

~$10-100 (complex signature verification)

Resistance to Whale Dominance

Low

Medium (via time-locking)

High (via quadratic cost)

Proposal Execution Delay

1-7 days after vote

Instant upon conviction threshold

1-3 days after vote

Capital Efficiency for Voters

High (tokens remain liquid)

Low (tokens are locked)

High (tokens remain liquid)

Common Use Cases

Parameter changes, treasury approvals

Continuous funding, ecosystem grants

Public goods funding, sentiment gauging

Implementation Complexity

Low (standard Governor contracts)

High (requires streaming logic)

Medium (requires sybil resistance checks)

Implementation Patterns and Smart Contracts

How Voting Logic is Built

At its core, a token-based voting smart contract is a set of rules on the blockchain that manages proposals, tracks votes, and executes decisions. The most common pattern is a simple quorum and majority system, where a proposal passes if a minimum number of tokens vote (quorum) and a majority of those votes are in favor.

Key Components

  • Voting Token: The ERC-20 token that grants voting power, often the same as the DAO's governance token like UNI or COMP.
  • Proposal Lifecycle: A proposal moves through states: Created, Active, Succeeded/Failed, Queued, and Executed.
  • Vote Delegation: Voters can delegate their voting power to other addresses, a feature used in systems like Compound's Governor Bravo.
  • Timelock: A security delay between a vote passing and execution, preventing rushed or malicious actions.

Example Process

When a Uniswap DAO member creates a proposal to adjust a fee parameter, UNI token holders have a set period to vote. If the proposal reaches quorum and a majority 'For' votes, it is queued in a Timelock contract. After the delay, any member can trigger the execution, which calls the function to update the protocol.

challenges

Challenges and Attack Vectors

Token-based voting introduces significant security and governance risks that DAOs must actively mitigate. This section details the primary vulnerabilities, from direct financial attacks to systemic manipulation, that can undermine decision-making integrity.

01

Vote Buying and Bribery

Vote buying occurs when a party offers direct compensation to token holders to influence their vote, bypassing genuine governance debate. This can be done through off-chain agreements or on-chain bribe markets.

  • Platforms like Hidden Hand create markets for directing votes in exchange for fees.
  • Attackers can bribe voters to pass proposals that extract value from the treasury.
  • This corrupts the voting process, making decisions a function of capital rather than community interest.
02

Whale Dominance and Plutocracy

Plutocracy describes governance controlled by a few large token holders (whales), whose votes can override the collective will of smaller participants.

  • A single entity with >30% of voting power can often pass or veto proposals unilaterally.
  • This centralizes control and discourages broad participation.
  • It creates risks of self-dealing proposals that benefit whales at the expense of the wider DAO.
03

Sybil Attacks and Airdrop Farming

A Sybil attack involves one entity creating many pseudonymous identities to gain disproportionate voting influence, often by farming token airdrops.

  • Attackers use bot networks to farm governance tokens from liquidity mining or airdrop programs.
  • These sybil wallets can then vote in concert to manipulate outcomes.
  • This attacks the fundamental "one-token-one-vote" assumption, requiring robust sybil resistance mechanisms.
04

Voter Apathy and Low Participation

Voter apathy is a systemic risk where a majority of tokens do not participate in governance, allowing a small, potentially malicious minority to decide outcomes.

  • Low turnout makes proposals easier and cheaper to manipulate.
  • It often stems from complex proposals, lack of incentives, or gas costs.
  • This can lead to the approval of harmful proposals that an engaged majority would reject.
05

Flash Loan Governance Attacks

A flash loan attack uses uncollateralized, instant loans to temporarily acquire voting power, propose and pass a malicious action, and repay the loan within a single transaction block.

  • An attacker borrows millions in governance tokens, creates a self-benefiting proposal, and votes it through.
  • The attack is executed before the proposal's voting period ends, exploiting the temporary capital.
  • This requires careful protocol design to mitigate, such as vote delay mechanisms.
06

Economic Abstraction and Collateralization

Economic abstraction in voting allows tokens locked as collateral in DeFi (e.g., in lending protocols) to still be used for governance, creating conflicting incentives and security risks.

  • A voter may support proposals that increase the value of their collateralized position, even if it harms the DAO.
  • Liquidatable positions could have voting power suddenly transferred during a liquidation event.
  • This decouples voting power from long-term alignment with the protocol's health.

Implementing Vote Delegation

Process overview for setting up a delegation system in a DAO governance contract.

1

Design the Delegation Data Structure

Define the smart contract storage to track delegation relationships and voting power.

Detailed Instructions

Define the core storage variables to manage delegation. A common approach is to use a mapping from a delegator's address to their chosen delegate's address. You must also track the delegated voting power to prevent double-voting and ensure accurate vote weight calculations.

  • Sub-step 1: Declare a mapping mapping(address => address) public delegates; to store the delegate for each user.
  • Sub-step 2: Implement a mapping mapping(address => uint256) public delegatedVotes; to track the total voting power delegated to an address.
  • Sub-step 3: Create a variable mapping(address => uint256) public checkpoints; or a more complex struct array to maintain a history of delegated balances for each block, enabling gas-efficient historical lookups.
solidity
// Example storage structure address public token; mapping(address => address) public delegates; mapping(address => uint256) public delegatedVotesBalance;

Tip: Consider using a checkpointing library like OpenZeppelin's Checkpoints.sol for efficient historical vote power tracking, which is essential for snapshot-based voting.

2

Implement the Core Delegate Function

Create the function that allows token holders to delegate their voting power to another address.

Detailed Instructions

The delegate function is the primary user-facing operation. It must update the delegation mappings and transfer the delegated vote weight from the previous delegate to the new one. Critical checks include ensuring users cannot delegate to the zero address and handling the state transition correctly to avoid vote weight miscalculations.

  • Sub-step 1: Validate the input: require toDelegate != address(0) and toDelegate != msg.sender.
  • Sub-step 2: Get the sender's current delegate and current token balance using IERC20(token).balanceOf(msg.sender).
  • Sub-step 3: Update the delegates mapping for the sender. Subtract the sender's balance from the delegatedVotesBalance of the old delegate and add it to the new delegate's balance.
  • Sub-step 4: Emit a DelegateChanged(msg.sender, currentDelegate, toDelegate) event for off-chain tracking.
solidity
function delegate(address toDelegate) external { require(toDelegate != address(0), "Cannot delegate to zero address"); address currentDelegate = delegates[msg.sender]; uint256 balance = IERC20(token).balanceOf(msg.sender); delegates[msg.sender] = toDelegate; _moveDelegates(currentDelegate, toDelegate, balance); emit DelegateChanged(msg.sender, currentDelegate, toDelegate); }

Tip: The internal _moveDelegates function should handle the checkpointing logic to record historical vote power changes at the current block number.

3

Handle Token Transfers and Vote Power Updates

Ensure delegated voting power automatically adjusts when tokens are transferred between wallets.

Detailed Instructions

Voting power must be fluid and reflect real-time token ownership. This requires hooking into token transfer functions. If using a standard ERC-20, you may need a token with snapshot capabilities or override its transfer functions in a separate governance contract. The key is to update the delegated vote balances of the involved parties' delegates upon any transfer.

  • Sub-step 1: Intercept transfers. If building a custom token, override _beforeTokenTransfer. For a separate governance contract, the token must call into it on transfer (e.g., via afterTokenTransfer).
  • Sub-step 2: On a transfer from from to to of amount, get the delegates for both addresses: delegateFrom and delegateTo.
  • Sub-step 3: If delegateFrom exists, subtract amount from its delegatedVotesBalance. If delegateTo exists, add amount to its balance.
  • Sub-step 4: Update the checkpoint history for both delegates to reflect the change at the current block.
solidity
// Example hook in a token contract function _afterTokenTransfer(address from, address to, uint256 amount) internal override { super._afterTokenTransfer(from, to, amount); IGovernance(governance).moveDelegatesOnTransfer(from, to, amount); }

Tip: This pattern requires careful integration between the token and governance contracts. Using a system like Compound's Governor Bravo with a COMP-like token is a proven reference.

4

Calculate Voting Power for Proposals

Create the view function that determines an account's usable voting power at a given block.

Detailed Instructions

Votes are cast based on the voting power a user had at a specific past block (the proposal snapshot block). The getVotes function must query the historical checkpoint data, not the current balance. This ensures voting is based on a immutable snapshot and prevents manipulation by transferring tokens after a proposal is created.

  • Sub-step 1: The function signature should be getVotes(address account, uint256 blockNumber) public view returns (uint256).
  • Sub-step 2: Determine the delegate for the account at that blockNumber. This may require a historical lookup of the delegates mapping if delegation changes are also checkpointed.
  • Sub-step 3: Query the checkpoint history for the delegate's address at the specified blockNumber to retrieve their delegated vote balance at that time.
  • Sub-step 4: Return this historical balance. If the account self-delegates, this is simply their token balance at the snapshot.
solidity
function getVotes(address account, uint256 blockNumber) public view returns (uint256) { // Get the delegate at the historical block (requires delegate checkpointing) address delegateAddr = delegatesAt(account, blockNumber); // Return the delegate's historical vote balance return checkpoints.getAt(delegateAddr, blockNumber); }

Tip: Use binary search in the checkpoint array for O(log n) lookups. Libraries like OpenZeppelin's Checkpoints handle this efficiently.

5

Integrate with Governance Proposal Contract

Connect the delegation logic to a standard governance framework for creating and voting on proposals.

Detailed Instructions

The final step is to make your token's voting power usable within a governance system like OpenZeppelin Governor or a custom proposal contract. The governance contract will call your getVotes function to determine voting rights. You must ensure the vote weight interface is compatible.

  • Sub-step 1: Ensure your token/delegation contract implements the IVotes interface (EIP-5805), which standardizes getVotes, delegate, and related functions.
  • Sub-step 2: Deploy your governance proposal contract (e.g., GovernorContract) and configure it with your token contract's address as the voting token.
  • Sub-step 3: When a user votes via castVote, the governance contract will internally call token.getVotes(voter, proposalSnapshotBlock) to determine their voting power.
  • Sub-step 4: Test the flow end-to-end: delegate tokens, create a proposal, and verify votes are weighted correctly according to the snapshot.
solidity
// Governor contract initialization constructor(IVotes _token) Governor("MyDAO") { token = _token; } function _getVotes(address account, uint256 blockNumber, bytes memory) internal view override returns (uint256) { return token.getVotes(account, blockNumber); }

Tip: Thoroughly test with forked mainnet simulations using tools like Tenderly or Foundry to ensure delegation and snapshot logic works under real network conditions.

Governance Implementation FAQ

The primary trade-off is between finality and cost. On-chain voting executes proposals automatically via smart contracts, providing immediate execution but incurring high gas fees for all voters. Off-chain voting (e.g., Snapshot) uses signed messages for cost-free signaling but requires a trusted multisig for execution, creating a delay and centralization risk. For example, a complex Uniswap proposal might cost $50,000 in gas if voted on-chain, whereas off-chain is free but requires a 7-day timelock before the core team can execute the passed proposal.