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: Voting • Ends 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: Passed • Executed │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ TREASURY TAB: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Treasury Balance │ │
│ │ ┌─────────────┬─────────────┬─────────────┐ │ │
│ │ │ SOL │ USDC │ TOKEN │ │ │
│ │ │ 1,250 │ 50,000 │ 1,000,000 │ │ │
│ │ └─────────────┴─────────────┴─────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Architecture
Text
┌─────────────────────────────────────────────────────────────────┐
│ DAO Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Governance Token │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • SPL Token for voting power │ │
│ │ • Staking for increased weight │ │
│ │ • Delegation support │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ DAO Program │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Config │ Proposal │ Vote 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
| Concept | Implementation |
|---|---|
| DAO Structure | Config, proposals, voting records |
| Voting Mechanics | Token-weighted voting, quorum |
| Delegation | Transfer voting power |
| Treasury | PDA-controlled funds |
| Proposal Lifecycle | Create → 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: