DAO Governance

Build a complete DAO (Decentralized Autonomous Organization) with proposal creation, voting, and treasury management. This capstone project demonstrates advanced Solana development patterns.

Project Overview

Text
┌─────────────────────────────────────────────────────────────────┐
│                        DAO Governance                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │ [Proposals] [Treasury] [Members]       [Connect Wallet]   │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  PROPOSALS TAB:                                                 │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  Active Proposals                    [ New Proposal ]     │ │
│  │                                                           │ │
│  │  ┌─────────────────────────────────────────────────────┐ │ │
│  │  │ #12: Fund Marketing Initiative                      │ │ │
│  │  │ Request: 5,000 USDC for Q1 marketing campaign       │ │ │
│  │  │ Status: VotingEnds in 2 days                     │ │ │
│  │  │                                                     │ │ │
│  │  │ For: ████████████░░░░ 72%  (36,000 votes)          │ │ │
│  │  │ Against: ████░░░░░░░░ 28%  (14,000 votes)          │ │ │
│  │  │                                                     │ │ │
│  │  │ Quorum: 50,000/100,000[ Vote For ] [ Against ]│ │ │
│  │  └─────────────────────────────────────────────────────┘ │ │
│  │                                                           │ │
│  │  ┌─────────────────────────────────────────────────────┐ │ │
│  │  │ #11: Protocol Upgrade v2.0                          │ │ │
│  │  │ Status: PassedExecuted                           │ │ │
│  │  └─────────────────────────────────────────────────────┘ │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  TREASURY TAB:                                                  │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  Treasury Balance                                         │ │
│  │  ┌─────────────┬─────────────┬─────────────┐             │ │
│  │  │    SOLUSDCTOKEN    │             │ │
│  │  │   1,25050,0001,000,000  │             │ │
│  │  └─────────────┴─────────────┴─────────────┘             │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Architecture

Text
┌─────────────────────────────────────────────────────────────────┐
│                      DAO Architecture                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Governance Token                                               │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  • SPL Token for voting power                           │   │
│  │  • Staking for increased weight                         │   │
│  │  • Delegation support                                   │   │
│  └─────────────────────────────────────────────────────────┘   │
│                           │                                     │
│                           ▼                                     │
│  DAO Program                                                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  ConfigProposalVote Record     │   │
│  │  ─────────────    │  ─────────────   │  ────────────    │   │
│  │  • voting_period  │  • title         │  • voter         │   │
│  │  • quorum         │  • description   │  • proposal      │   │
│  │  • threshold      │  • instructions  │  • weight        │   │
│  │  • treasury       │  • votes_for     │  • vote_type     │   │
│  │                   │  • votes_against │                  │   │
│  └─────────────────────────────────────────────────────────┘   │
│                           │                                     │
│                           ▼                                     │
│  Treasury (PDA)                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  • Holds DAO funds                                      │   │
│  │  • Controlled by governance                             │   │
│  │  • Executes approved proposals                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Project Setup

Bash
anchor init dao-governance
cd dao-governance
anchor build
solana address -k target/deploy/dao_governance-keypair.json
# Update program ID in lib.rs
anchor build

Anchor Program

Rust
// programs/dao-governance/src/lib.rs
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};

declare_id!("YOUR_PROGRAM_ID");

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

    /// Initialize the DAO with configuration
    pub fn initialize_dao(
        ctx: Context<InitializeDao>,
        config: DaoConfig,
    ) -> Result<()> {
        let dao = &mut ctx.accounts.dao;

        require!(config.voting_period > 0, DaoError::InvalidVotingPeriod);
        require!(config.quorum_votes > 0, DaoError::InvalidQuorum);
        require!(
            config.approval_threshold_percentage > 0 &&
            config.approval_threshold_percentage <= 100,
            DaoError::InvalidThreshold
        );

        dao.authority = ctx.accounts.authority.key();
        dao.governance_token_mint = ctx.accounts.governance_token_mint.key();
        dao.treasury = ctx.accounts.treasury.key();
        dao.proposal_count = 0;
        dao.config = config;
        dao.bump = ctx.bumps.dao;

        msg!("DAO initialized with {} day voting period", config.voting_period / 86400);
        Ok(())
    }

    /// Create a new proposal
    pub fn create_proposal(
        ctx: Context<CreateProposal>,
        title: String,
        description: String,
        instructions: Vec<ProposalInstruction>,
    ) -> Result<()> {
        require!(title.len() <= 100, DaoError::TitleTooLong);
        require!(description.len() <= 1000, DaoError::DescriptionTooLong);
        require!(instructions.len() <= 10, DaoError::TooManyInstructions);

        // Check proposer has minimum tokens
        let proposer_tokens = ctx.accounts.proposer_token_account.amount;
        require!(
            proposer_tokens >= ctx.accounts.dao.config.min_tokens_to_propose,
            DaoError::InsufficientTokens
        );

        let dao = &mut ctx.accounts.dao;
        let proposal = &mut ctx.accounts.proposal;

        proposal.id = dao.proposal_count;
        proposal.dao = dao.key();
        proposal.proposer = ctx.accounts.proposer.key();
        proposal.title = title.clone();
        proposal.description = description;
        proposal.instructions = instructions;
        proposal.votes_for = 0;
        proposal.votes_against = 0;
        proposal.created_at = Clock::get()?.unix_timestamp;
        proposal.voting_ends_at = proposal.created_at + dao.config.voting_period as i64;
        proposal.status = ProposalStatus::Active;
        proposal.bump = ctx.bumps.proposal;

        dao.proposal_count = dao.proposal_count.checked_add(1)
            .ok_or(DaoError::Overflow)?;

        msg!("Proposal #{} created: {}", proposal.id, title);
        Ok(())
    }

    /// Cast a vote on a proposal
    pub fn cast_vote(
        ctx: Context<CastVote>,
        vote_type: VoteType,
    ) -> Result<()> {
        let proposal = &mut ctx.accounts.proposal;
        let vote_record = &mut ctx.accounts.vote_record;
        let voter_tokens = ctx.accounts.voter_token_account.amount;

        // Check voting is still active
        let current_time = Clock::get()?.unix_timestamp;
        require!(
            proposal.status == ProposalStatus::Active,
            DaoError::ProposalNotActive
        );
        require!(
            current_time < proposal.voting_ends_at,
            DaoError::VotingEnded
        );

        // Check voter has tokens
        require!(voter_tokens > 0, DaoError::NoVotingPower);

        // Record the vote
        vote_record.voter = ctx.accounts.voter.key();
        vote_record.proposal = proposal.key();
        vote_record.vote_type = vote_type;
        vote_record.weight = voter_tokens;
        vote_record.timestamp = current_time;
        vote_record.bump = ctx.bumps.vote_record;

        // Update proposal vote counts
        match vote_type {
            VoteType::For => {
                proposal.votes_for = proposal.votes_for
                    .checked_add(voter_tokens)
                    .ok_or(DaoError::Overflow)?;
            }
            VoteType::Against => {
                proposal.votes_against = proposal.votes_against
                    .checked_add(voter_tokens)
                    .ok_or(DaoError::Overflow)?;
            }
            VoteType::Abstain => {
                // Abstain votes count toward quorum but not for/against
            }
        }

        msg!(
            "Vote cast on proposal #{}: {:?} with {} tokens",
            proposal.id,
            vote_type,
            voter_tokens
        );
        Ok(())
    }

    /// Finalize a proposal after voting ends
    pub fn finalize_proposal(ctx: Context<FinalizeProposal>) -> Result<()> {
        let proposal = &mut ctx.accounts.proposal;
        let dao = &ctx.accounts.dao;

        // Check voting has ended
        let current_time = Clock::get()?.unix_timestamp;
        require!(
            current_time >= proposal.voting_ends_at,
            DaoError::VotingNotEnded
        );
        require!(
            proposal.status == ProposalStatus::Active,
            DaoError::ProposalNotActive
        );

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

        // Check quorum
        if total_votes < dao.config.quorum_votes {
            proposal.status = ProposalStatus::Defeated;
            msg!("Proposal #{} defeated: quorum not reached", proposal.id);
            return Ok(());
        }

        // Check approval threshold
        let approval_percentage = (proposal.votes_for as u128)
            .checked_mul(100)
            .ok_or(DaoError::Overflow)?
            .checked_div(total_votes as u128)
            .ok_or(DaoError::Overflow)? as u8;

        if approval_percentage >= dao.config.approval_threshold_percentage {
            proposal.status = ProposalStatus::Succeeded;
            msg!("Proposal #{} passed with {}% approval", proposal.id, approval_percentage);
        } else {
            proposal.status = ProposalStatus::Defeated;
            msg!("Proposal #{} defeated with {}% approval", proposal.id, approval_percentage);
        }

        Ok(())
    }

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

        require!(
            proposal.status == ProposalStatus::Succeeded,
            DaoError::ProposalNotSucceeded
        );

        // Execute treasury transfer if specified
        if let Some(transfer) = &proposal.instructions.first() {
            if transfer.program_id == token::ID {
                let seeds = &[
                    b"treasury",
                    ctx.accounts.dao.key().as_ref(),
                    &[ctx.accounts.dao.bump],
                ];
                let signer = &[&seeds[..]];

                token::transfer(
                    CpiContext::new_with_signer(
                        ctx.accounts.token_program.to_account_info(),
                        Transfer {
                            from: ctx.accounts.treasury_token_account.to_account_info(),
                            to: ctx.accounts.destination_token_account.to_account_info(),
                            authority: ctx.accounts.treasury.to_account_info(),
                        },
                        signer,
                    ),
                    transfer.data[0] as u64, // Amount in first byte (simplified)
                )?;
            }
        }

        proposal.status = ProposalStatus::Executed;
        proposal.executed_at = Some(Clock::get()?.unix_timestamp);

        msg!("Proposal #{} executed", proposal.id);
        Ok(())
    }

    /// Delegate voting power to another address
    pub fn delegate(ctx: Context<Delegate>, delegate_to: Pubkey) -> Result<()> {
        let delegation = &mut ctx.accounts.delegation;

        delegation.delegator = ctx.accounts.delegator.key();
        delegation.delegate = delegate_to;
        delegation.amount = ctx.accounts.delegator_token_account.amount;
        delegation.timestamp = Clock::get()?.unix_timestamp;
        delegation.bump = ctx.bumps.delegation;

        msg!(
            "Delegated {} tokens from {} to {}",
            delegation.amount,
            delegation.delegator,
            delegate_to
        );
        Ok(())
    }

    /// Cancel delegation
    pub fn undelegate(ctx: Context<Undelegate>) -> Result<()> {
        msg!("Delegation cancelled for {}", ctx.accounts.delegator.key());
        Ok(())
    }
}

// ========== Accounts ==========

#[derive(Accounts)]
pub struct InitializeDao<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        init,
        payer = authority,
        space = Dao::SIZE,
        seeds = [b"dao", authority.key().as_ref()],
        bump
    )]
    pub dao: Account<'info, Dao>,

    /// CHECK: Validated by constraint
    pub governance_token_mint: AccountInfo<'info>,

    #[account(
        seeds = [b"treasury", dao.key().as_ref()],
        bump
    )]
    /// CHECK: PDA for treasury
    pub treasury: AccountInfo<'info>,

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

#[derive(Accounts)]
#[instruction(title: String)]
pub struct CreateProposal<'info> {
    #[account(mut)]
    pub proposer: Signer<'info>,

    #[account(
        mut,
        seeds = [b"dao", dao.authority.as_ref()],
        bump = dao.bump
    )]
    pub dao: Account<'info, Dao>,

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

    #[account(
        constraint = proposer_token_account.mint == dao.governance_token_mint,
        constraint = proposer_token_account.owner == proposer.key()
    )]
    pub proposer_token_account: Account<'info, TokenAccount>,

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

#[derive(Accounts)]
pub struct CastVote<'info> {
    #[account(mut)]
    pub voter: Signer<'info>,

    pub dao: Account<'info, Dao>,

    #[account(
        mut,
        constraint = proposal.dao == dao.key(),
        seeds = [b"proposal", dao.key().as_ref(), &proposal.id.to_le_bytes()],
        bump = proposal.bump
    )]
    pub proposal: Account<'info, Proposal>,

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

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

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

#[derive(Accounts)]
pub struct FinalizeProposal<'info> {
    pub dao: Account<'info, Dao>,

    #[account(
        mut,
        constraint = proposal.dao == dao.key(),
        seeds = [b"proposal", dao.key().as_ref(), &proposal.id.to_le_bytes()],
        bump = proposal.bump
    )]
    pub proposal: Account<'info, Proposal>,
}

#[derive(Accounts)]
pub struct ExecuteProposal<'info> {
    #[account(mut)]
    pub executor: Signer<'info>,

    pub dao: Account<'info, Dao>,

    #[account(
        mut,
        constraint = proposal.dao == dao.key(),
        seeds = [b"proposal", dao.key().as_ref(), &proposal.id.to_le_bytes()],
        bump = proposal.bump
    )]
    pub proposal: Account<'info, Proposal>,

    /// CHECK: Treasury PDA
    #[account(
        seeds = [b"treasury", dao.key().as_ref()],
        bump
    )]
    pub treasury: AccountInfo<'info>,

    #[account(mut)]
    pub treasury_token_account: Account<'info, TokenAccount>,

    #[account(mut)]
    pub destination_token_account: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
}

#[derive(Accounts)]
pub struct Delegate<'info> {
    #[account(mut)]
    pub delegator: Signer<'info>,

    pub dao: Account<'info, Dao>,

    #[account(
        init,
        payer = delegator,
        space = Delegation::SIZE,
        seeds = [b"delegation", dao.key().as_ref(), delegator.key().as_ref()],
        bump
    )]
    pub delegation: Account<'info, Delegation>,

    #[account(
        constraint = delegator_token_account.mint == dao.governance_token_mint,
        constraint = delegator_token_account.owner == delegator.key()
    )]
    pub delegator_token_account: Account<'info, TokenAccount>,

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

#[derive(Accounts)]
pub struct Undelegate<'info> {
    #[account(mut)]
    pub delegator: Signer<'info>,

    pub dao: Account<'info, Dao>,

    #[account(
        mut,
        close = delegator,
        seeds = [b"delegation", dao.key().as_ref(), delegator.key().as_ref()],
        bump = delegation.bump,
        constraint = delegation.delegator == delegator.key()
    )]
    pub delegation: Account<'info, Delegation>,
}

// ========== State ==========

#[account]
pub struct Dao {
    pub authority: Pubkey,
    pub governance_token_mint: Pubkey,
    pub treasury: Pubkey,
    pub proposal_count: u64,
    pub config: DaoConfig,
    pub bump: u8,
}

impl Dao {
    pub const SIZE: usize = 8 + // discriminator
        32 * 3 +  // pubkeys
        8 +       // proposal_count
        DaoConfig::SIZE +
        1;        // bump
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct DaoConfig {
    pub voting_period: u64,                   // seconds
    pub quorum_votes: u64,                    // minimum total votes
    pub approval_threshold_percentage: u8,    // % needed to pass
    pub min_tokens_to_propose: u64,           // minimum tokens to create proposal
}

impl DaoConfig {
    pub const SIZE: usize = 8 + 8 + 1 + 8;
}

#[account]
pub struct Proposal {
    pub id: u64,
    pub dao: Pubkey,
    pub proposer: Pubkey,
    pub title: String,
    pub description: String,
    pub instructions: Vec<ProposalInstruction>,
    pub votes_for: u64,
    pub votes_against: u64,
    pub created_at: i64,
    pub voting_ends_at: i64,
    pub executed_at: Option<i64>,
    pub status: ProposalStatus,
    pub bump: u8,
}

impl Proposal {
    pub const SIZE: usize = 8 +   // discriminator
        8 +                       // id
        32 * 2 +                  // pubkeys
        4 + 100 +                 // title
        4 + 1000 +                // description
        4 + (10 * ProposalInstruction::SIZE) + // instructions
        8 * 2 +                   // votes
        8 * 2 +                   // timestamps
        1 + 8 +                   // executed_at option
        1 +                       // status
        1;                        // bump
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct ProposalInstruction {
    pub program_id: Pubkey,
    pub accounts: Vec<Pubkey>,
    pub data: Vec<u8>,
}

impl ProposalInstruction {
    pub const SIZE: usize = 32 + 4 + (10 * 32) + 4 + 256;
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum ProposalStatus {
    Active,
    Succeeded,
    Defeated,
    Executed,
    Cancelled,
}

#[account]
pub struct VoteRecord {
    pub voter: Pubkey,
    pub proposal: Pubkey,
    pub vote_type: VoteType,
    pub weight: u64,
    pub timestamp: i64,
    pub bump: u8,
}

impl VoteRecord {
    pub const SIZE: usize = 8 + 32 * 2 + 1 + 8 + 8 + 1;
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub enum VoteType {
    For,
    Against,
    Abstain,
}

#[account]
pub struct Delegation {
    pub delegator: Pubkey,
    pub delegate: Pubkey,
    pub amount: u64,
    pub timestamp: i64,
    pub bump: u8,
}

impl Delegation {
    pub const SIZE: usize = 8 + 32 * 2 + 8 + 8 + 1;
}

// ========== Errors ==========

#[error_code]
pub enum DaoError {
    #[msg("Invalid voting period")]
    InvalidVotingPeriod,
    #[msg("Invalid quorum")]
    InvalidQuorum,
    #[msg("Invalid approval threshold")]
    InvalidThreshold,
    #[msg("Title too long")]
    TitleTooLong,
    #[msg("Description too long")]
    DescriptionTooLong,
    #[msg("Too many instructions")]
    TooManyInstructions,
    #[msg("Insufficient tokens to propose")]
    InsufficientTokens,
    #[msg("Proposal not active")]
    ProposalNotActive,
    #[msg("Voting has ended")]
    VotingEnded,
    #[msg("Voting has not ended")]
    VotingNotEnded,
    #[msg("No voting power")]
    NoVotingPower,
    #[msg("Proposal not succeeded")]
    ProposalNotSucceeded,
    #[msg("Math overflow")]
    Overflow,
}

Frontend Hook

TypeScript
// hooks/useDao.ts
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { Program, AnchorProvider, BN, setProvider } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { useMemo } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import idl from "@/idl/dao_governance.json";

const PROGRAM_ID = new PublicKey("YOUR_PROGRAM_ID");

export interface Proposal {
  publicKey: PublicKey;
  id: number;
  title: string;
  description: string;
  votesFor: number;
  votesAgainst: number;
  createdAt: number;
  votingEndsAt: number;
  status: "active" | "succeeded" | "defeated" | "executed";
  proposer: string;
}

export function useDao(daoAddress: PublicKey | null) {
  const { connection } = useConnection();
  const wallet = useWallet();
  const queryClient = useQueryClient();

  const program = useMemo(() => {
    if (!wallet.publicKey) return null;
    const provider = new AnchorProvider(connection, wallet as any, {
      commitment: "confirmed",
    });
    setProvider(provider);
    return new Program(idl as any, PROGRAM_ID, provider);
  }, [connection, wallet]);

  // Fetch DAO data
  const { data: dao } = useQuery({
    queryKey: ["dao", daoAddress?.toBase58()],
    queryFn: async () => {
      if (!program || !daoAddress) return null;
      return program.account.dao.fetch(daoAddress);
    },
    enabled: !!program && !!daoAddress,
  });

  // Fetch all proposals
  const { data: proposals } = useQuery({
    queryKey: ["proposals", daoAddress?.toBase58()],
    queryFn: async (): Promise<Proposal[]> => {
      if (!program || !daoAddress) return [];

      const accounts = await program.account.proposal.all([
        { memcmp: { offset: 8 + 8, bytes: daoAddress.toBase58() } },
      ]);

      return accounts.map((acc) => ({
        publicKey: acc.publicKey,
        id: acc.account.id.toNumber(),
        title: acc.account.title,
        description: acc.account.description,
        votesFor: acc.account.votesFor.toNumber(),
        votesAgainst: acc.account.votesAgainst.toNumber(),
        createdAt: acc.account.createdAt.toNumber(),
        votingEndsAt: acc.account.votingEndsAt.toNumber(),
        status: getStatusString(acc.account.status),
        proposer: acc.account.proposer.toBase58(),
      }));
    },
    enabled: !!program && !!daoAddress,
    refetchInterval: 30000,
  });

  // Fetch user's vote records
  const { data: userVotes } = useQuery({
    queryKey: ["userVotes", wallet.publicKey?.toBase58()],
    queryFn: async () => {
      if (!program || !wallet.publicKey) return {};

      const accounts = await program.account.voteRecord.all([
        { memcmp: { offset: 8, bytes: wallet.publicKey.toBase58() } },
      ]);

      const voteMap: Record<string, string> = {};
      accounts.forEach((acc) => {
        voteMap[acc.account.proposal.toBase58()] = getVoteTypeString(
          acc.account.voteType,
        );
      });
      return voteMap;
    },
    enabled: !!program && !!wallet.publicKey,
  });

  // Create proposal mutation
  const createProposal = useMutation({
    mutationFn: async ({
      title,
      description,
      instructions,
    }: {
      title: string;
      description: string;
      instructions: any[];
    }) => {
      if (!program || !wallet.publicKey || !daoAddress || !dao) {
        throw new Error("Not connected");
      }

      const [proposalPda] = PublicKey.findProgramAddressSync(
        [
          Buffer.from("proposal"),
          daoAddress.toBuffer(),
          new BN(dao.proposalCount).toArrayLike(Buffer, "le", 8),
        ],
        PROGRAM_ID,
      );

      // Get user token account
      const tokenAccount = await getAssociatedTokenAddress(
        dao.governanceTokenMint,
        wallet.publicKey,
      );

      return program.methods
        .createProposal(title, description, instructions)
        .accounts({
          proposer: wallet.publicKey,
          dao: daoAddress,
          proposal: proposalPda,
          proposerTokenAccount: tokenAccount,
        })
        .rpc();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["proposals"] });
      queryClient.invalidateQueries({ queryKey: ["dao"] });
    },
  });

  // Cast vote mutation
  const castVote = useMutation({
    mutationFn: async ({
      proposal,
      voteType,
    }: {
      proposal: Proposal;
      voteType: "for" | "against" | "abstain";
    }) => {
      if (!program || !wallet.publicKey || !daoAddress || !dao) {
        throw new Error("Not connected");
      }

      const [voteRecordPda] = PublicKey.findProgramAddressSync(
        [
          Buffer.from("vote"),
          proposal.publicKey.toBuffer(),
          wallet.publicKey.toBuffer(),
        ],
        PROGRAM_ID,
      );

      const tokenAccount = await getAssociatedTokenAddress(
        dao.governanceTokenMint,
        wallet.publicKey,
      );

      const voteTypeEnum =
        voteType === "for"
          ? { for: {} }
          : voteType === "against"
            ? { against: {} }
            : { abstain: {} };

      return program.methods
        .castVote(voteTypeEnum)
        .accounts({
          voter: wallet.publicKey,
          dao: daoAddress,
          proposal: proposal.publicKey,
          voteRecord: voteRecordPda,
          voterTokenAccount: tokenAccount,
        })
        .rpc();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["proposals"] });
      queryClient.invalidateQueries({ queryKey: ["userVotes"] });
    },
  });

  // Finalize proposal mutation
  const finalizeProposal = useMutation({
    mutationFn: async (proposal: Proposal) => {
      if (!program || !daoAddress) throw new Error("Not connected");

      return program.methods
        .finalizeProposal()
        .accounts({
          dao: daoAddress,
          proposal: proposal.publicKey,
        })
        .rpc();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["proposals"] });
    },
  });

  return {
    dao,
    proposals: proposals || [],
    userVotes: userVotes || {},
    createProposal: createProposal.mutateAsync,
    castVote: castVote.mutateAsync,
    finalizeProposal: finalizeProposal.mutateAsync,
    isCreating: createProposal.isPending,
    isVoting: castVote.isPending,
  };
}

function getStatusString(status: any): Proposal["status"] {
  if (status.active) return "active";
  if (status.succeeded) return "succeeded";
  if (status.defeated) return "defeated";
  if (status.executed) return "executed";
  return "active";
}

function getVoteTypeString(voteType: any): string {
  if (voteType.for) return "for";
  if (voteType.against) return "against";
  return "abstain";
}

async function getAssociatedTokenAddress(
  mint: PublicKey,
  owner: PublicKey,
): Promise<PublicKey> {
  const [address] = await PublicKey.findProgramAddress(
    [owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
    ASSOCIATED_TOKEN_PROGRAM_ID,
  );
  return address;
}

const TOKEN_PROGRAM_ID = new PublicKey(
  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
);
const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey(
  "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);

Proposal Card Component

TypeScript
// components/ProposalCard.tsx
"use client";

import { Proposal } from "@/hooks/useDao";

interface ProposalCardProps {
  proposal: Proposal;
  userVote: string | null;
  onVote: (voteType: "for" | "against" | "abstain") => void;
  onFinalize: () => void;
  isVoting: boolean;
  quorum: number;
}

export function ProposalCard({
  proposal,
  userVote,
  onVote,
  onFinalize,
  isVoting,
  quorum,
}: ProposalCardProps) {
  const totalVotes = proposal.votesFor + proposal.votesAgainst;
  const forPercentage = totalVotes > 0 ? (proposal.votesFor / totalVotes) * 100 : 0;
  const againstPercentage = totalVotes > 0 ? (proposal.votesAgainst / totalVotes) * 100 : 0;
  const quorumReached = totalVotes >= quorum;

  const now = Date.now() / 1000;
  const isActive = proposal.status === "active";
  const votingEnded = now >= proposal.votingEndsAt;
  const canVote = isActive && !votingEnded && !userVote;
  const canFinalize = isActive && votingEnded;

  const timeRemaining = proposal.votingEndsAt - now;
  const daysRemaining = Math.floor(timeRemaining / 86400);
  const hoursRemaining = Math.floor((timeRemaining % 86400) / 3600);

  return (
    <div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-lg">
      {/* Header */}
      <div className="flex justify-between items-start mb-4">
        <div>
          <span className="text-sm text-gray-500">#{proposal.id}</span>
          <h3 className="text-lg font-bold">{proposal.title}</h3>
        </div>
        <span
          className={`px-3 py-1 rounded-full text-sm font-medium ${
            proposal.status === "active"
              ? "bg-blue-100 text-blue-700"
              : proposal.status === "succeeded"
              ? "bg-green-100 text-green-700"
              : proposal.status === "executed"
              ? "bg-purple-100 text-purple-700"
              : "bg-red-100 text-red-700"
          }`}
        >
          {proposal.status}
        </span>
      </div>

      {/* Description */}
      <p className="text-gray-600 dark:text-gray-400 mb-6 line-clamp-2">
        {proposal.description}
      </p>

      {/* Vote Progress */}
      <div className="mb-4">
        <div className="flex justify-between text-sm mb-2">
          <span className="text-green-600">
            For: {forPercentage.toFixed(1)}% ({proposal.votesFor.toLocaleString()})
          </span>
          <span className="text-red-600">
            Against: {againstPercentage.toFixed(1)}% ({proposal.votesAgainst.toLocaleString()})
          </span>
        </div>
        <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden flex">
          <div
            className="bg-green-500 h-full"
            style={{ width: `${forPercentage}%` }}
          />
          <div
            className="bg-red-500 h-full"
            style={{ width: `${againstPercentage}%` }}
          />
        </div>
      </div>

      {/* Quorum */}
      <div className="flex items-center gap-2 mb-4 text-sm">
        <span className={quorumReached ? "text-green-600" : "text-orange-600"}>
          {quorumReached ? "✓" : "○"} Quorum: {totalVotes.toLocaleString()} / {quorum.toLocaleString()}
        </span>
      </div>

      {/* Time Remaining */}
      {isActive && (
        <p className="text-sm text-gray-500 mb-4">
          {votingEnded
            ? "Voting ended"
            : `${daysRemaining}d ${hoursRemaining}h remaining`}
        </p>
      )}

      {/* Actions */}
      <div className="flex gap-2">
        {canVote && (
          <>
            <button
              onClick={() => onVote("for")}
              disabled={isVoting}
              className="flex-1 py-2 bg-green-600 text-white rounded-lg font-medium
                         hover:bg-green-700 disabled:bg-gray-400"
            >
              Vote For
            </button>
            <button
              onClick={() => onVote("against")}
              disabled={isVoting}
              className="flex-1 py-2 bg-red-600 text-white rounded-lg font-medium
                         hover:bg-red-700 disabled:bg-gray-400"
            >
              Vote Against
            </button>
          </>
        )}

        {userVote && (
          <div className="flex-1 py-2 text-center bg-gray-100 dark:bg-gray-800 rounded-lg">
            You voted: <span className="font-medium capitalize">{userVote}</span>
          </div>
        )}

        {canFinalize && (
          <button
            onClick={onFinalize}
            className="flex-1 py-2 bg-purple-600 text-white rounded-lg font-medium
                       hover:bg-purple-700"
          >
            Finalize Proposal
          </button>
        )}
      </div>
    </div>
  );
}

What You'll Learn

ConceptImplementation
DAO StructureConfig, proposals, voting records
Voting MechanicsToken-weighted voting, quorum
DelegationTransfer voting power
TreasuryPDA-controlled funds
Proposal LifecycleCreate → Vote → Finalize → Execute

Security Considerations

Text
┌─────────────────────────────────────────────────────────────────┐
│                   DAO Security Checklist                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ☐ Prevent double voting                                       │
│  ☐ Validate voting period                                      │
│  ☐ Check token balance at vote time                            │
│  ☐ Secure treasury withdrawals                                 │
│  ☐ Proposal spam prevention                                    │
│  ☐ Timelock for execution                                      │
│  ☐ Emergency pause mechanism                                   │
│  ☐ Upgrade authority management                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Congratulations! 🎉

You've completed the Solana Handbook capstone projects! You now have the skills to:

  • Build fullstack decentralized applications
  • Implement DeFi protocols
  • Create governance systems
  • Write secure Anchor programs
  • Build React frontends for Solana

Continue Learning: