Skip to content

Module 8: DApp 4 - DAO Governance

Learning Objectives

By the end of this module, you will:

  • Understand DAO fundamentals and governance architecture
  • Implement proposal creation and lifecycle management
  • Build voting mechanisms with token-weighted voting
  • Design treasury management with multi-sig patterns
  • Apply delegation and quorum requirements

Prerequisites

Complete Module 7: DeFi AMM first.


8.1 DAO Fundamentals

What is a DAO?

A Decentralized Autonomous Organization (DAO) is a blockchain-based governance system where:

  1. Proposals: Members submit suggestions for changes
  2. Voting: Token holders vote on proposals
  3. Execution: Approved proposals are automatically executed
  4. Treasury: Collective funds are managed by the DAO

Traditional vs DAO Governance

Traditional Organization          DAO
┌─────────────────────┐         ┌─────────────────────┐
│      Board of       │         │   Token Holders     │
│      Directors      │         │   (Members)         │
├─────────────────────┤         ├─────────────────────┤
│  - Centralized      │         │  - Decentralized    │
│  - Closed meetings  │         │  - On-chain voting  │
│  - Trust required   │         │  - Trustless        │
│  - Manual execution │         │  - Auto-execution   │
└─────────────────────┘         └─────────────────────┘
Aspect Traditional DAO
Decision Making Board/Executives Token holders
Transparency Limited Full on-chain
Execution Manual Smart contract
Participation Shareholders only Anyone with tokens
Trust Model Trust leadership Trust code

Key Components

  1. Governance Token: Voting power representation
  2. Proposal System: Structured change requests
  3. Voting Mechanism: How votes are counted
  4. Timelock: Delay before execution
  5. Treasury: Collective asset management

8.2 Governance Architecture

System Overview

┌─────────────────────────────────────────────────────────────┐
│                    Governance System                         │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │  Governance  │    │   Proposal   │    │    Vote      │  │
│  │   Account    │───▶│   Account    │◀───│   Record     │  │
│  │              │    │              │    │              │  │
│  │ - token_mint │    │ - proposer   │    │ - voter      │  │
│  │ - quorum     │    │ - votes_for  │    │ - weight     │  │
│  │ - voting_pd  │    │ - votes_agst │    │ - support    │  │
│  └──────────────┘    │ - state      │    └──────────────┘  │
│         │            └──────────────┘                       │
│         │                    │                              │
│         ▼                    ▼                              │
│  ┌──────────────┐    ┌──────────────┐                      │
│  │   Treasury   │    │  Timelock    │                      │
│  │   (PDA)      │◀───│  Queue       │                      │
│  │              │    │              │                      │
│  │ Holds SOL +  │    │ Pending      │                      │
│  │ Tokens       │    │ executions   │                      │
│  └──────────────┘    └──────────────┘                      │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Governance Account

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct Governance {
    /// Authority who can update governance parameters
    pub authority: Pubkey,

    /// Governance token mint (voting power)
    pub governance_token_mint: Pubkey,

    /// Treasury account holding DAO funds
    pub treasury: Pubkey,

    /// Total proposals created
    pub proposal_count: u64,

    /// Voting period in seconds
    pub voting_period: i64,

    /// Minimum tokens required to create a proposal
    pub proposal_threshold: u64,

    /// Minimum votes needed for proposal to pass (quorum)
    pub quorum_votes: u64,

    /// Delay after voting before execution (seconds)
    pub execution_delay: i64,

    /// PDA bump
    pub bump: u8,

    /// Treasury bump
    pub treasury_bump: u8,
}

Proposal Account

#[account]
#[derive(InitSpace)]
pub struct Proposal {
    /// Reference to governance
    pub governance: Pubkey,

    /// Who created the proposal
    pub proposer: Pubkey,

    /// Unique proposal ID
    pub proposal_id: u64,

    /// Short title (max 64 chars)
    #[max_len(64)]
    pub title: String,

    /// Description URI (IPFS hash or URL)
    #[max_len(128)]
    pub description_uri: String,

    /// Total votes in favor
    pub votes_for: u64,

    /// Total votes against
    pub votes_against: u64,

    /// When voting starts (unix timestamp)
    pub voting_starts_at: i64,

    /// When voting ends (unix timestamp)
    pub voting_ends_at: i64,

    /// Timestamp when proposal can be executed
    pub execution_time: i64,

    /// Current proposal state
    pub state: ProposalState,

    /// PDA bump
    pub bump: u8,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)]
pub enum ProposalState {
    /// Proposal created, not yet active
    Draft,
    /// Voting is active
    Active,
    /// Voting ended, passed quorum
    Succeeded,
    /// Voting ended, did not pass
    Defeated,
    /// Queued for execution after timelock
    Queued,
    /// Successfully executed
    Executed,
    /// Cancelled by proposer
    Cancelled,
}

Vote Record

#[account]
#[derive(InitSpace)]
pub struct VoteRecord {
    /// Reference to proposal
    pub proposal: Pubkey,

    /// Voter's pubkey
    pub voter: Pubkey,

    /// Vote weight (token balance at snapshot)
    pub vote_weight: u64,

    /// true = for, false = against
    pub support: bool,

    /// When vote was cast
    pub voted_at: i64,

    /// PDA bump
    pub bump: u8,
}

8.3 Proposal Lifecycle

State Machine

┌───────────┐
│   Draft   │──────────────────────┐
└─────┬─────┘                      │
      │ activate()                 │ cancel()
      ▼                            │
┌───────────┐                      │
│  Active   │──────────────────────┤
└─────┬─────┘                      │
      │ voting period ends         │
      ▼                            │
┌─────────────────────┐            │
│ finalize_votes()    │            │
├─────────┬───────────┤            │
│         │           │            │
▼         ▼           │            │
┌─────┐ ┌────────┐    │            │
│Succ-│ │Defeated│    │            │
│eeded│ └────────┘    │            │
└──┬──┘               │            │
   │ queue()          │            │
   ▼                  │            ▼
┌──────┐              │      ┌──────────┐
│Queued│──────────────┴─────▶│Cancelled │
└──┬───┘   cancel()          └──────────┘
   │ execute() after delay
┌────────┐
│Executed│
└────────┘

Proposal Thresholds

Parameter Description Example
Proposal Threshold Min tokens to create proposal 100,000 tokens
Quorum Min votes for valid result 4% of supply
Voting Period How long voting is open 3 days
Execution Delay Time after success before execute 2 days

8.4 Voting Mechanisms

Token-Weighted Voting

The simplest voting mechanism where vote weight equals token balance:

\[\text{Vote Weight} = \text{Token Balance}\]
pub fn cast_vote(
    ctx: Context<CastVote>,
    support: bool,
) -> Result<()> {
    let proposal = &mut ctx.accounts.proposal;
    let voter_tokens = ctx.accounts.voter_token_account.amount;

    require!(voter_tokens > 0, GovernanceError::NoVotingPower);

    // Record vote weight from token balance
    let vote_record = &mut ctx.accounts.vote_record;
    vote_record.vote_weight = voter_tokens;
    vote_record.support = support;

    // Add to proposal totals
    if support {
        proposal.votes_for = proposal.votes_for
            .checked_add(voter_tokens)
            .ok_or(GovernanceError::Overflow)?;
    } else {
        proposal.votes_against = proposal.votes_against
            .checked_add(voter_tokens)
            .ok_or(GovernanceError::Overflow)?;
    }

    Ok(())
}

Quadratic Voting (Advanced)

Reduces whale dominance by taking square root of token balance:

\[\text{Vote Weight} = \sqrt{\text{Token Balance}}\]
pub fn calculate_quadratic_weight(token_balance: u64) -> u64 {
    // Integer square root
    let balance = token_balance as u128;
    let mut x = balance;
    let mut y = (x + 1) / 2;

    while y < x {
        x = y;
        y = (x + balance / x) / 2;
    }

    x as u64
}

Comparison:

Token Balance Linear Weight Quadratic Weight
100 100 10
10,000 10,000 100
1,000,000 1,000,000 1,000

Vote Delegation

Allow token holders to delegate voting power:

#[account]
#[derive(InitSpace)]
pub struct Delegation {
    /// Who is delegating
    pub delegator: Pubkey,

    /// Who receives the voting power
    pub delegate: Pubkey,

    /// Amount delegated
    pub amount: u64,

    /// When delegation was created
    pub created_at: i64,

    pub bump: u8,
}

pub fn delegate_votes(
    ctx: Context<DelegateVotes>,
    amount: u64,
) -> Result<()> {
    let delegation = &mut ctx.accounts.delegation;
    delegation.delegator = ctx.accounts.delegator.key();
    delegation.delegate = ctx.accounts.delegate.key();
    delegation.amount = amount;
    delegation.created_at = Clock::get()?.unix_timestamp;

    emit!(VotesDelegated {
        delegator: delegation.delegator,
        delegate: delegation.delegate,
        amount,
    });

    Ok(())
}

8.5 Treasury Management

Treasury PDA

The treasury is a Program Derived Address controlled by the governance:

#[derive(Accounts)]
pub struct InitializeGovernance<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + Governance::INIT_SPACE,
        seeds = [b"governance", authority.key().as_ref()],
        bump,
    )]
    pub governance: Account<'info, Governance>,

    /// Treasury PDA - holds SOL and can hold token accounts
    #[account(
        seeds = [b"treasury", governance.key().as_ref()],
        bump,
    )]
    pub treasury: SystemAccount<'info>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>,
}

Treasury Operations

Proposals can execute treasury transfers:

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub enum ProposalInstruction {
    /// Transfer SOL from treasury
    TransferSol {
        recipient: Pubkey,
        amount: u64,
    },
    /// Transfer tokens from treasury
    TransferToken {
        recipient: Pubkey,
        mint: Pubkey,
        amount: u64,
    },
    /// Update governance parameters
    UpdateGovernance {
        voting_period: Option<i64>,
        quorum_votes: Option<u64>,
        execution_delay: Option<i64>,
    },
}

Executing Treasury Transfer

pub fn execute_proposal(ctx: Context<ExecuteProposal>) -> Result<()> {
    let proposal = &mut ctx.accounts.proposal;
    let governance = &ctx.accounts.governance;

    // Verify proposal is queued and delay has passed
    require!(
        proposal.state == ProposalState::Queued,
        GovernanceError::ProposalNotQueued
    );

    let now = Clock::get()?.unix_timestamp;
    require!(
        now >= proposal.execution_time,
        GovernanceError::ExecutionDelayNotPassed
    );

    // Execute treasury transfer using governance PDA as signer
    let governance_key = governance.key();
    let seeds = &[
        b"treasury",
        governance_key.as_ref(),
        &[governance.treasury_bump],
    ];
    let signer_seeds = &[&seeds[..]];

    // Example: Transfer SOL
    anchor_lang::system_program::transfer(
        CpiContext::new_with_signer(
            ctx.accounts.system_program.to_account_info(),
            anchor_lang::system_program::Transfer {
                from: ctx.accounts.treasury.to_account_info(),
                to: ctx.accounts.recipient.to_account_info(),
            },
            signer_seeds,
        ),
        proposal.transfer_amount,
    )?;

    proposal.state = ProposalState::Executed;

    emit!(ProposalExecuted {
        proposal: proposal.key(),
        executor: ctx.accounts.executor.key(),
    });

    Ok(())
}

8.6 Complete Implementation

Initialize Governance

use anchor_lang::prelude::*;

#[derive(Accounts)]
pub struct InitializeGovernance<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + Governance::INIT_SPACE,
        seeds = [b"governance", authority.key().as_ref()],
        bump,
    )]
    pub governance: Account<'info, Governance>,

    #[account(
        seeds = [b"treasury", governance.key().as_ref()],
        bump,
    )]
    pub treasury: SystemAccount<'info>,

    /// Governance token mint
    pub governance_token_mint: Account<'info, Mint>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(
    ctx: Context<InitializeGovernance>,
    voting_period: i64,
    proposal_threshold: u64,
    quorum_votes: u64,
    execution_delay: i64,
) -> Result<()> {
    require!(voting_period > 0, GovernanceError::InvalidVotingPeriod);
    require!(execution_delay >= 0, GovernanceError::InvalidExecutionDelay);

    let governance = &mut ctx.accounts.governance;
    governance.authority = ctx.accounts.authority.key();
    governance.governance_token_mint = ctx.accounts.governance_token_mint.key();
    governance.treasury = ctx.accounts.treasury.key();
    governance.proposal_count = 0;
    governance.voting_period = voting_period;
    governance.proposal_threshold = proposal_threshold;
    governance.quorum_votes = quorum_votes;
    governance.execution_delay = execution_delay;
    governance.bump = ctx.bumps.governance;
    governance.treasury_bump = ctx.bumps.treasury;

    emit!(GovernanceInitialized {
        governance: governance.key(),
        authority: governance.authority,
        voting_period,
        quorum_votes,
    });

    Ok(())
}

Create Proposal

#[derive(Accounts)]
#[instruction(title: String, description_uri: String)]
pub struct CreateProposal<'info> {
    #[account(
        mut,
        seeds = [b"governance", governance.authority.as_ref()],
        bump = governance.bump,
    )]
    pub governance: Account<'info, Governance>,

    #[account(
        init,
        payer = proposer,
        space = 8 + Proposal::INIT_SPACE,
        seeds = [
            b"proposal",
            governance.key().as_ref(),
            &governance.proposal_count.to_le_bytes()
        ],
        bump,
    )]
    pub proposal: Account<'info, Proposal>,

    #[account(
        constraint = proposer_token_account.mint == governance.governance_token_mint,
        constraint = proposer_token_account.owner == proposer.key(),
        constraint = proposer_token_account.amount >= governance.proposal_threshold
            @ GovernanceError::InsufficientTokensForProposal,
    )]
    pub proposer_token_account: Account<'info, TokenAccount>,

    #[account(mut)]
    pub proposer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(
    ctx: Context<CreateProposal>,
    title: String,
    description_uri: String,
) -> Result<()> {
    let governance = &mut ctx.accounts.governance;
    let proposal = &mut ctx.accounts.proposal;
    let now = Clock::get()?.unix_timestamp;

    proposal.governance = governance.key();
    proposal.proposer = ctx.accounts.proposer.key();
    proposal.proposal_id = governance.proposal_count;
    proposal.title = title;
    proposal.description_uri = description_uri;
    proposal.votes_for = 0;
    proposal.votes_against = 0;
    proposal.voting_starts_at = now;
    proposal.voting_ends_at = now + governance.voting_period;
    proposal.execution_time = 0;
    proposal.state = ProposalState::Active;
    proposal.bump = ctx.bumps.proposal;

    governance.proposal_count = governance.proposal_count
        .checked_add(1)
        .ok_or(GovernanceError::Overflow)?;

    emit!(ProposalCreated {
        proposal: proposal.key(),
        governance: governance.key(),
        proposer: proposal.proposer,
        proposal_id: proposal.proposal_id,
        title: proposal.title.clone(),
    });

    Ok(())
}

Cast Vote

#[derive(Accounts)]
pub struct CastVote<'info> {
    #[account(
        seeds = [b"governance", governance.authority.as_ref()],
        bump = governance.bump,
    )]
    pub governance: Account<'info, Governance>,

    #[account(
        mut,
        seeds = [
            b"proposal",
            governance.key().as_ref(),
            &proposal.proposal_id.to_le_bytes()
        ],
        bump = proposal.bump,
        constraint = proposal.state == ProposalState::Active @ GovernanceError::ProposalNotActive,
    )]
    pub proposal: Account<'info, Proposal>,

    #[account(
        init,
        payer = voter,
        space = 8 + VoteRecord::INIT_SPACE,
        seeds = [b"vote", proposal.key().as_ref(), voter.key().as_ref()],
        bump,
    )]
    pub vote_record: Account<'info, VoteRecord>,

    #[account(
        constraint = voter_token_account.mint == governance.governance_token_mint,
        constraint = voter_token_account.owner == voter.key(),
    )]
    pub voter_token_account: Account<'info, TokenAccount>,

    #[account(mut)]
    pub voter: Signer<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(ctx: Context<CastVote>, support: bool) -> Result<()> {
    let proposal = &mut ctx.accounts.proposal;
    let vote_record = &mut ctx.accounts.vote_record;
    let voter_tokens = ctx.accounts.voter_token_account.amount;
    let now = Clock::get()?.unix_timestamp;

    // Check voting period
    require!(
        now >= proposal.voting_starts_at && now <= proposal.voting_ends_at,
        GovernanceError::VotingPeriodEnded
    );

    require!(voter_tokens > 0, GovernanceError::NoVotingPower);

    // Record vote
    vote_record.proposal = proposal.key();
    vote_record.voter = ctx.accounts.voter.key();
    vote_record.vote_weight = voter_tokens;
    vote_record.support = support;
    vote_record.voted_at = now;
    vote_record.bump = ctx.bumps.vote_record;

    // Update proposal totals
    if support {
        proposal.votes_for = proposal.votes_for
            .checked_add(voter_tokens)
            .ok_or(GovernanceError::Overflow)?;
    } else {
        proposal.votes_against = proposal.votes_against
            .checked_add(voter_tokens)
            .ok_or(GovernanceError::Overflow)?;
    }

    emit!(VoteCast {
        proposal: proposal.key(),
        voter: vote_record.voter,
        support,
        weight: voter_tokens,
    });

    Ok(())
}

Finalize Proposal

#[derive(Accounts)]
pub struct FinalizeProposal<'info> {
    #[account(
        seeds = [b"governance", governance.authority.as_ref()],
        bump = governance.bump,
    )]
    pub governance: Account<'info, Governance>,

    #[account(
        mut,
        seeds = [
            b"proposal",
            governance.key().as_ref(),
            &proposal.proposal_id.to_le_bytes()
        ],
        bump = proposal.bump,
        constraint = proposal.state == ProposalState::Active @ GovernanceError::ProposalNotActive,
    )]
    pub proposal: Account<'info, Proposal>,
}

pub fn handler(ctx: Context<FinalizeProposal>) -> Result<()> {
    let governance = &ctx.accounts.governance;
    let proposal = &mut ctx.accounts.proposal;
    let now = Clock::get()?.unix_timestamp;

    // Check voting period has ended
    require!(
        now > proposal.voting_ends_at,
        GovernanceError::VotingPeriodNotEnded
    );

    let total_votes = proposal.votes_for
        .checked_add(proposal.votes_against)
        .ok_or(GovernanceError::Overflow)?;

    // Check quorum
    if total_votes < governance.quorum_votes {
        proposal.state = ProposalState::Defeated;
        emit!(ProposalDefeated {
            proposal: proposal.key(),
            reason: "Quorum not reached".to_string(),
        });
        return Ok(());
    }

    // Check if passed (simple majority)
    if proposal.votes_for > proposal.votes_against {
        proposal.state = ProposalState::Succeeded;
        emit!(ProposalSucceeded {
            proposal: proposal.key(),
            votes_for: proposal.votes_for,
            votes_against: proposal.votes_against,
        });
    } else {
        proposal.state = ProposalState::Defeated;
        emit!(ProposalDefeated {
            proposal: proposal.key(),
            reason: "Did not reach majority".to_string(),
        });
    }

    Ok(())
}

Queue Proposal

#[derive(Accounts)]
pub struct QueueProposal<'info> {
    #[account(
        seeds = [b"governance", governance.authority.as_ref()],
        bump = governance.bump,
    )]
    pub governance: Account<'info, Governance>,

    #[account(
        mut,
        seeds = [
            b"proposal",
            governance.key().as_ref(),
            &proposal.proposal_id.to_le_bytes()
        ],
        bump = proposal.bump,
        constraint = proposal.state == ProposalState::Succeeded @ GovernanceError::ProposalNotSucceeded,
    )]
    pub proposal: Account<'info, Proposal>,
}

pub fn handler(ctx: Context<QueueProposal>) -> Result<()> {
    let governance = &ctx.accounts.governance;
    let proposal = &mut ctx.accounts.proposal;
    let now = Clock::get()?.unix_timestamp;

    proposal.execution_time = now + governance.execution_delay;
    proposal.state = ProposalState::Queued;

    emit!(ProposalQueued {
        proposal: proposal.key(),
        execution_time: proposal.execution_time,
    });

    Ok(())
}

Execute Proposal

#[derive(Accounts)]
pub struct ExecuteProposal<'info> {
    #[account(
        seeds = [b"governance", governance.authority.as_ref()],
        bump = governance.bump,
    )]
    pub governance: Account<'info, Governance>,

    #[account(
        mut,
        seeds = [b"treasury", governance.key().as_ref()],
        bump = governance.treasury_bump,
    )]
    pub treasury: SystemAccount<'info>,

    #[account(
        mut,
        seeds = [
            b"proposal",
            governance.key().as_ref(),
            &proposal.proposal_id.to_le_bytes()
        ],
        bump = proposal.bump,
        constraint = proposal.state == ProposalState::Queued @ GovernanceError::ProposalNotQueued,
    )]
    pub proposal: Account<'info, Proposal>,

    /// CHECK: Validated by proposal data
    #[account(mut)]
    pub recipient: UncheckedAccount<'info>,

    pub executor: Signer<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(ctx: Context<ExecuteProposal>, transfer_amount: u64) -> Result<()> {
    let governance = &ctx.accounts.governance;
    let proposal = &mut ctx.accounts.proposal;
    let now = Clock::get()?.unix_timestamp;

    // Check execution delay has passed
    require!(
        now >= proposal.execution_time,
        GovernanceError::ExecutionDelayNotPassed
    );

    // Execute transfer from treasury
    let governance_key = governance.key();
    let seeds = &[
        b"treasury",
        governance_key.as_ref(),
        &[governance.treasury_bump],
    ];
    let signer_seeds = &[&seeds[..]];

    anchor_lang::system_program::transfer(
        CpiContext::new_with_signer(
            ctx.accounts.system_program.to_account_info(),
            anchor_lang::system_program::Transfer {
                from: ctx.accounts.treasury.to_account_info(),
                to: ctx.accounts.recipient.to_account_info(),
            },
            signer_seeds,
        ),
        transfer_amount,
    )?;

    proposal.state = ProposalState::Executed;

    emit!(ProposalExecuted {
        proposal: proposal.key(),
        executor: ctx.accounts.executor.key(),
        transfer_amount,
    });

    Ok(())
}

8.7 Cancel Proposal

Proposers can cancel their own proposals:

#[derive(Accounts)]
pub struct CancelProposal<'info> {
    #[account(
        seeds = [b"governance", governance.authority.as_ref()],
        bump = governance.bump,
    )]
    pub governance: Account<'info, Governance>,

    #[account(
        mut,
        seeds = [
            b"proposal",
            governance.key().as_ref(),
            &proposal.proposal_id.to_le_bytes()
        ],
        bump = proposal.bump,
        constraint = proposal.proposer == proposer.key() @ GovernanceError::NotProposer,
        constraint = proposal.state != ProposalState::Executed @ GovernanceError::ProposalAlreadyExecuted,
        constraint = proposal.state != ProposalState::Cancelled @ GovernanceError::ProposalAlreadyCancelled,
    )]
    pub proposal: Account<'info, Proposal>,

    pub proposer: Signer<'info>,
}

pub fn handler(ctx: Context<CancelProposal>) -> Result<()> {
    let proposal = &mut ctx.accounts.proposal;
    proposal.state = ProposalState::Cancelled;

    emit!(ProposalCancelled {
        proposal: proposal.key(),
        proposer: ctx.accounts.proposer.key(),
    });

    Ok(())
}

8.8 Security Considerations

Common Governance Vulnerabilities

Vulnerability Description Mitigation
Flash loan attacks Borrow tokens, vote, repay Snapshot voting power
Vote buying Off-chain bribes Use quadratic voting
Proposal spam Flood with proposals Proposal threshold
Timelock bypass Execute without delay Strict state machine
Treasury drain Malicious proposals Multi-sig + limits

Best Practices

  1. Snapshot Voting Power: Record token balance at proposal creation
  2. Minimum Voting Period: At least 24-72 hours for participation
  3. Execution Delay: 2-7 days to allow for emergency response
  4. Quorum Requirements: 4-10% of circulating supply
  5. Proposal Threshold: Prevent spam while allowing participation
  6. Emergency Pause: Guardian can pause execution in emergencies

Snapshot Implementation

#[account]
pub struct VotingPowerSnapshot {
    pub voter: Pubkey,
    pub proposal: Pubkey,
    pub balance_at_snapshot: u64,
    pub snapshot_slot: u64,
}

// Take snapshot when creating proposal
pub fn create_proposal_with_snapshot(
    ctx: Context<CreateProposalWithSnapshot>,
    title: String,
    description_uri: String,
) -> Result<()> {
    let proposal = &mut ctx.accounts.proposal;
    proposal.snapshot_slot = Clock::get()?.slot;

    // Voters must prove their balance at this slot
    // ...
}

8.9 Program Structure

File Organization

programs/dao-governance/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── state.rs
│   ├── error.rs
│   ├── events.rs
│   └── instructions/
│       ├── mod.rs
│       ├── initialize_governance.rs
│       ├── create_proposal.rs
│       ├── cast_vote.rs
│       ├── finalize_proposal.rs
│       ├── queue_proposal.rs
│       ├── execute_proposal.rs
│       └── cancel_proposal.rs

Program Entry Point

// src/lib.rs
use anchor_lang::prelude::*;

pub mod error;
pub mod events;
pub mod instructions;
pub mod state;

use instructions::*;

declare_id!("Gov11111111111111111111111111111111111111111");

#[program]
pub mod dao_governance {
    use super::*;

    pub fn initialize_governance(
        ctx: Context<InitializeGovernance>,
        voting_period: i64,
        proposal_threshold: u64,
        quorum_votes: u64,
        execution_delay: i64,
    ) -> Result<()> {
        instructions::initialize_governance::handler(
            ctx,
            voting_period,
            proposal_threshold,
            quorum_votes,
            execution_delay,
        )
    }

    pub fn create_proposal(
        ctx: Context<CreateProposal>,
        title: String,
        description_uri: String,
    ) -> Result<()> {
        instructions::create_proposal::handler(ctx, title, description_uri)
    }

    pub fn cast_vote(ctx: Context<CastVote>, support: bool) -> Result<()> {
        instructions::cast_vote::handler(ctx, support)
    }

    pub fn finalize_proposal(ctx: Context<FinalizeProposal>) -> Result<()> {
        instructions::finalize_proposal::handler(ctx)
    }

    pub fn queue_proposal(ctx: Context<QueueProposal>) -> Result<()> {
        instructions::queue_proposal::handler(ctx)
    }

    pub fn execute_proposal(
        ctx: Context<ExecuteProposal>,
        transfer_amount: u64,
    ) -> Result<()> {
        instructions::execute_proposal::handler(ctx, transfer_amount)
    }

    pub fn cancel_proposal(ctx: Context<CancelProposal>) -> Result<()> {
        instructions::cancel_proposal::handler(ctx)
    }
}

8.10 Interactive Exercise

Try It: Build Your DAO

Open Solana Playground and implement:

  1. Initialize a governance with your parameters
  2. Create a proposal for treasury spending
  3. Cast votes for/against
  4. Finalize and execute the proposal

Test Scenarios

describe("dao-governance", () => {
  it("creates and passes a proposal", async () => {
    // Initialize governance
    await program.methods
      .initializeGovernance(
        new anchor.BN(60 * 60), // 1 hour voting
        new anchor.BN(100),     // 100 tokens to propose
        new anchor.BN(1000),    // 1000 token quorum
        new anchor.BN(60)       // 1 minute delay
      )
      .accounts({ /* ... */ })
      .rpc();

    // Create proposal
    await program.methods
      .createProposal("Fund Development", "ipfs://...")
      .accounts({ /* ... */ })
      .rpc();

    // Cast vote
    await program.methods
      .castVote(true) // Vote in favor
      .accounts({ /* ... */ })
      .rpc();

    // Fast forward time in tests...

    // Finalize
    await program.methods
      .finalizeProposal()
      .accounts({ /* ... */ })
      .rpc();

    // Queue
    await program.methods
      .queueProposal()
      .accounts({ /* ... */ })
      .rpc();

    // Execute
    await program.methods
      .executeProposal(new anchor.BN(LAMPORTS_PER_SOL))
      .accounts({ /* ... */ })
      .rpc();
  });
});

Summary

In this module, you learned:

  • DAO Fundamentals: Decentralized governance vs traditional organizations
  • Governance Architecture: Governance, Proposal, and VoteRecord accounts
  • Proposal Lifecycle: Draft -> Active -> Succeeded/Defeated -> Queued -> Executed
  • Voting Mechanisms: Token-weighted and quadratic voting
  • Treasury Management: PDA-controlled funds with proposal-gated access
  • Security: Snapshots, timelock, quorum requirements

Next Steps

Continue to Module 9: Frontend Patterns to learn:

  • React wallet adapter integration
  • Transaction building with @solana/web3.js
  • State management for DApps
  • NextJS deployment

Back to Course Home