← Back to list

Building Real-World dApps Smart Contract on Solana: Voting System & ToDo Manager

Master practical blockchain applications with Anchor Framework

Dhruv Patel · 2026-03-16 11:33 · 0 claps · 3.6 min read
#solana-blockchain #dapps #smart-contract-blockchain #solana-ecosystem #solana-network
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3 🏛️ · Politics

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