Building Real-World dApps Smart Contract on Solana: Voting System & ToDo Manager
Master practical blockchain applications with Anchor Framework
Building Real-World dApps Smart Contract on Solana: Voting System & ToDo Manager
Master practical blockchain applications with Anchor Framework

⚠️ Note for Readers
This article is part of my Solana learning series.
👉 Before continuing, I highly recommend checking the complete series here: **View Full Solana Series**
Following the series in order will give you a much better understanding.
However, if you are already familiar with the basics, feel free to continue with this article.
📝 The Complete Code
use anchor_lang::prelude::*;
declare_id!("6VeiThsnKi8JpGRm9ZwFSTFYPuPeCuDMEhXkQ1jjCF89");
#[program]
pub mod vote {
use super::*;
pub fn initialize_candidate(
ctx: Context<InitializeCandidate>,
name: String
) -> Result<()> {
require!(name.len() > 0, CandidateError::InvalidName);
let candidate = &mut ctx.accounts.candidate;
candidate.set_inner(Candidate {
c_name: name,
vote_count: 0,
candidate_id: ctx.accounts.owner.key()
});
msg!("Candidate registration successful");
Ok(())
}
pub fn initialize_voter(
ctx: Context<InitializeVoter>,
name: String
) -> Result<()> {
require!(name.len() > 0, VoterError::InvalidName);
let voter = &mut ctx.accounts.voter;
voter.set_inner(Voter {
v_name: name,
is_voted: false,
voter_id: ctx.accounts.owner.key()
});
msg!("Voter registration successful");
Ok(())
}
pub fn cast_vote(ctx: Context<CastVote>) -> Result<()> {
let voter = &mut ctx.accounts.voter;
let candidate = &mut ctx.accounts.candidate;
// Ensure the voter is signing the transaction
require!(
ctx.accounts.voter_signer.key() == voter.voter_id,
VoterError::UnauthorizedVoter
);
// Ensure the voter has not already voted
require!(!voter.is_voted, VoterError::AlreadyVoted);
// Ensure the candidate exists
require!(
candidate.vote_count >= 0,
CandidateError::CandidateNotFound
);
// Prevent self-voting
require!(
voter.voter_id != candidate.candidate_id,
VoterError::CannotVoteForSelf
);
// Increment vote count with overflow check
candidate.vote_count = candidate.vote_count
.checked_add(1)
.ok_or(VoterError::VoteOverflow)?;
// Mark voter as voted
voter.is_voted = true;
msg!(
"Voter {} has voted for candidate {}",
voter.voter_id,
candidate.candidate_id
);
Ok(())
}
}
#[account]
#[derive(InitSpace)]
pub struct Candidate {
#[max_len(20)]
c_name: String,
vote_count: u8,
candidate_id: Pubkey,
}
#[account]
#[derive(InitSpace)]
pub struct Voter {
#[max_len(20)]
v_name: String,
is_voted: bool,
voter_id: Pubkey,
}
#[derive(Accounts)]
#[instruction(c_name: String)]
pub struct InitializeCandidate<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(
init,
seeds = [c_name.as_bytes(), owner.key().as_ref()],
bump,
space = 8 + Candidate::INIT_SPACE,
payer = owner
)]
pub candidate: Account<'info, Candidate>,
pub system_program: Program<'info, System>
}
#[derive(Accounts)]
#[instruction(v_name: String)]
pub struct InitializeVoter<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(
init,
seeds = [v_name.as_bytes(), owner.key().as_ref()],
bump,
space = 8 + Voter::INIT_SPACE,
payer = owner
)]
pub voter: Account<'info, Voter>,
pub system_program: Program<'info, System>
}
#[derive(Accounts)]
pub struct CastVote<'info> {
#[account(mut)]
pub voter: Account<'info, Voter>,
#[account(mut)]
pub candidate: Account<'info, Candidate>,
#[account(mut)]
pub voter_signer: Signer<'info>
}
#[error_code]
pub enum CandidateError {
#[msg("Candidate does not exist.")]
CandidateNotFound,
#[msg("Invalid name provided.")]
InvalidName,
}
#[error_code]
pub enum VoterError {
#[msg("Voter has already cast a vote.")]
AlreadyVoted,
#[msg("Candidates cannot vote for themselves.")]
CannotVoteForSelf,
#[msg("Vote count overflow occurred.")]
VoteOverflow,
#[msg("Invalid name provided.")]
InvalidName,
#[msg("Voter is not authorized.")]
UnauthorizedVoter,
}
ELECTION SETUP
══════════════
Alice registers as candidate
↓
[Candidate Account Created]
- Name: "Alice"
- Vote Count: 0
- Candidate ID: Alice's pubkey
Bob registers as voter
↓
[Voter Account Created]
- Name: "Bob"
- Has Voted: false
- Voter ID: Bob's pubkey
VOTE CASTING
══════════════
Bob wants to vote for Alice
↓
SECURITY CHECKS:
├─ Is Bob signing? ✅
├─ Has Bob voted before? ✅ (No)
├─ Does Alice exist? ✅
└─ Is Bob voting for himself? ✅ (No)
↓
EXECUTE VOTE:
├─ Alice.vote_count: 0 → 1
└─ Bob.is_voted: false → true
↓
VOTE RECORDED ✅
ATTEMPT DOUBLE VOTE
══════════════
Bob tries to vote again
↓
SECURITY CHECKS:
├─ Is Bob signing? ✅
└─ Has Bob voted before? ❌ (YES!)
↓
REJECTED! ❌
Error: "Voter has already cast a vote."
✅ ToDo Manager
Master CRUD operations and sequential PDAs
📝 The Complete Code
use anchor_lang::prelude::*;
declare_id!("3FZXiZRkziBxQpS926XhKWv2k4zcgnFmznuDRKyBxmk5");
#[program]
pub mod todo_app {
use super::*;
/// Creates a new ToDo item with a unique task_id per user.
pub fn create_task(
ctx: Context<CreateTask>,
task_id: u64,
name: String,
completed: bool,
) -> Result<()> {
let task = &mut ctx.accounts.todo;
task.id = task_id;
task.name = name;
task.completed = completed;
Ok(())
}
/// Updates the name of an existing ToDo item.
pub fn update_task_name(
ctx: Context<UpdateTaskName>,
new_name: String
) -> Result<()> {
let task = &mut ctx.accounts.todo;
task.name = new_name;
Ok(())
}
/// Toggles or sets the completion status of a ToDo item.
pub fn update_task_status(
ctx: Context<UpdateTaskStatus>,
completed: bool
) -> Result<()> {
let task = &mut ctx.accounts.todo;
task.completed = completed;
Ok(())
}
}
#[account]
#[derive(InitSpace)]
pub struct ToDo {
pub id: u64,
#[max_len(50)]
pub name: String,
pub completed: bool,
}
#[derive(Accounts)]
#[instruction(task_id: u64)]
pub struct CreateTask<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = 8 + ToDo::INIT_SPACE,
seeds = [
b"todo",
authority.key().as_ref(),
&task_id.to_le_bytes()
],
bump
)]
pub todo: Account<'info, ToDo>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateTaskName<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
mut,
seeds = [
b"todo",
authority.key().as_ref(),
&todo.id.to_le_bytes()
],
bump,
)]
pub todo: Account<'info, ToDo>,
}
#[derive(Accounts)]
pub struct UpdateTaskStatus<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
mut,
seeds = [
b"todo",
authority.key().as_ref(),
&todo.id.to_le_bytes()
],
bump,
)]
pub todo: Account<'info, ToDo>,
}
🏗️ Architecture: Sequential PDAs
The Three-Seed Pattern
seeds = [
b"todo", // Seed 1: Type identifier
authority.key().as_ref(), // Seed 2: User ownership
&task_id.to_le_bytes() // Seed 3: Task number
]
Why three seeds?
Seed 1: b"todo" (Static)
- Identifies account type
- Namespace separator
- Same for all users and tasks
Seed 2: authority.key().as_ref() (User-specific)
- Owner’s public key
- Each user gets unique tasks
- Provides user isolation
Seed 3: &task_id.to_le_bytes() (Sequential)
- Task number (0, 1, 2, 3…)
- Stored as little-endian bytes
- Unlimited tasks per user
Result:
User A, Task 0: hash(["todo", userA, 0])
User A, Task 1: hash(["todo", userA, 1])
User B, Task 0: hash(["todo", userB, 0])
Each task = unique, predictable address!
메타데이터
- post_id
- a71ed6ff4821
- slug
- building-real-world-dapps-smart-contract-on-solana-voting-system-todo-manager-a71ed6ff4821
- url
- https://medium.com/@d7511162/building-real-world-dapps-smart-contract-on-solana-voting-system-todo-manager-a71ed6ff4821
- canonical_url
- https://medium.com/@d7511162/building-real-world-dapps-smart-contract-on-solana-voting-system-todo-manager-a71ed6ff4821
- author_url
- https://medium.com/@d7511162
- status
- ok
- fetched_at
- 2026-07-16 20:09:11