DEV Community

Cover image for Building a Simple Decentralized Voting System on Stellar
cauhlins
cauhlins

Posted on

Building a Simple Decentralized Voting System on Stellar

This is a submission for the Build Better on Stellar: Smart Contract Challenge : Create a Tutorial

Your Tutorial

Stellar is a powerful blockchain platform that enables developers to create decentralized applications (dApps) with ease. In this tutorial, we'll walk through the process of building a basic decentralized voting system on Stellar. This project will help you understand key Stellar concepts and give you hands-on experience with smart contract development using Soroban, Stellar's smart contract platform.

Table of Contents

  1. Introduction to Stellar and Soroban
  2. Setting Up the Development Environment
  3. Creating the Voting Contract
  4. Deploying the Contract
  5. Building the Frontend
  6. Testing the dApp

Introduction to Stellar and Soroban

Stellar is an open-source, decentralized protocol for digital currency to fiat money transfers. Soroban is Stellar's smart contract platform, which allows developers to write, deploy, and execute smart contracts on the Stellar network.

Key concepts we'll be using in this tutorial:

  • Smart Contracts: Self-executing contracts with the terms directly written into code.
  • Accounts: Entities on the Stellar network that can hold balances and send transactions.
  • Transactions: Operations that modify the state of the ledger.

Setting Up the Development Environment

Before we start coding, let's set up our development environment. We'll need to install the Stellar CLI, Rust, and create a new Soroban project.

  1. Install the Stellar CLI:
curl -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
cargo install --locked --version 20.0.0-rc2 soroban-cli
Enter fullscreen mode Exit fullscreen mode
  1. Create a new Soroban project:
soroban contract init voting-system
cd voting-system
Enter fullscreen mode Exit fullscreen mode

Creating the Voting Contract

Now, let's create our voting contract. We'll define structures for candidates and voters, and implement functions for registering candidates, casting votes, and retrieving results.

Edit the src/lib.rs file:

#![no_std]
use soroban_sdk::{contractimpl, symbol_short, vec, Env, Symbol, Vec};

#[derive(Clone)]
pub struct Candidate {
    name: Symbol,
    votes: u32,
}

#[derive(Clone)]
pub struct Voter {
    address: Symbol,
    has_voted: bool,
}

pub struct VotingContract;

#[contractimpl]
impl VotingContract {
    pub fn init(env: Env) -> Vec<Candidate> {
        let candidates = vec![&env];
        env.storage().set(&symbol_short!("candidates"), &candidates);
        candidates
    }

    pub fn add_candidate(env: Env, name: Symbol) -> Vec<Candidate> {
        let mut candidates: Vec<Candidate> = env.storage().get(&symbol_short!("candidates")).unwrap();
        candidates.push_back(Candidate { name, votes: 0 });
        env.storage().set(&symbol_short!("candidates"), &candidates);
        candidates
    }

    pub fn vote(env: Env, voter: Symbol, candidate_index: u32) -> Vec<Candidate> {
        let mut candidates: Vec<Candidate> = env.storage().get(&symbol_short!("candidates")).unwrap();
        let mut voters: Vec<Voter> = env.storage().get(&symbol_short!("voters")).unwrap_or(vec![&env]);

        // Check if voter has already voted
        if let Some(voter_index) = voters.iter().position(|v| v.address == voter) {
            if voters.get(voter_index).unwrap().has_voted {
                panic!("Voter has already cast a vote");
            }
            voters.set(voter_index, &Voter { address: voter, has_voted: true });
        } else {
            voters.push_back(Voter { address: voter, has_voted: true });
        }

        // Increment vote count for the chosen candidate
        let mut candidate = candidates.get(candidate_index).unwrap();
        candidate.votes += 1;
        candidates.set(candidate_index, &candidate);

        env.storage().set(&symbol_short!("candidates"), &candidates);
        env.storage().set(&symbol_short!("voters"), &voters);

        candidates
    }

    pub fn get_results(env: Env) -> Vec<Candidate> {
        env.storage().get(&symbol_short!("candidates")).unwrap()
    }
}
Enter fullscreen mode Exit fullscreen mode

This contract defines four main functions:

  • init: Initializes the contract with an empty list of candidates.
  • add_candidate: Adds a new candidate to the election.
  • vote: Allows a voter to cast a vote for a candidate.
  • get_results: Retrieves the current voting results.

Deploying the Contract

Now that we have our contract, let's deploy it to the Stellar testnet.

  1. Build the contract:
soroban contract build
Enter fullscreen mode Exit fullscreen mode
  1. Create a Stellar account for the contract:
soroban config identity generate contract-admin
soroban config network add testnet --rpc-url https://soroban-testnet.stellar.org:443 --network-passphrase "Test SDF Network ; September 2015"
soroban config identity fund contract-admin --network testnet
Enter fullscreen mode Exit fullscreen mode
  1. Deploy the contract:
soroban contract deploy \
    --wasm target/wasm32-unknown-unknown/release/voting_system.wasm \
    --source contract-admin \
    --network testnet
Enter fullscreen mode Exit fullscreen mode

Make note of the contract ID returned by this command, as we'll need it for interacting with the contract.

Building the Frontend

For the frontend, we'll use React with the @stellar/freighter-api library to interact with the Stellar network. First, set up a new React project:

npx create-react-app voting-dapp
cd voting-dapp
npm install @stellar/freighter-api
Enter fullscreen mode Exit fullscreen mode

Now, let's create a simple frontend for our voting dApp. Replace the content of src/App.js:

import React, { useState, useEffect } from 'react';
import { isConnected, getPublicKey, signTransaction } from '@stellar/freighter-api';
import { SorobanRpc, Transaction, xdr } from 'stellar-sdk';

const server = new SorobanRpc.Server("https://soroban-testnet.stellar.org:443");
const contractId = 'YOUR_CONTRACT_ID'; // Replace with your deployed contract ID

function App() {
  const [candidates, setCandidates] = useState([]);
  const [newCandidate, setNewCandidate] = useState('');
  const [selectedCandidate, setSelectedCandidate] = useState('');
  const [walletConnected, setWalletConnected] = useState(false);

  useEffect(() => {
    checkWalletConnection();
    fetchCandidates();
  }, []);

  const checkWalletConnection = async () => {
    const connected = await isConnected();
    setWalletConnected(connected);
  };

  const fetchCandidates = async () => {
    try {
      const result = await server.invokeHostFunction({
        contractId: contractId,
        functionName: 'get_results',
        args: []
      });
      setCandidates(result);
    } catch (error) {
      console.error('Error fetching candidates:', error);
    }
  };

  const addCandidate = async () => {
    if (!newCandidate) return;
    try {
      await server.invokeHostFunction({
        contractId: contractId,
        functionName: 'add_candidate',
        args: [xdr.ScSymbol.fromString(newCandidate)]
      });
      setNewCandidate('');
      fetchCandidates();
    } catch (error) {
      console.error('Error adding candidate:', error);
    }
  };

  const vote = async () => {
    if (!selectedCandidate) return;
    try {
      const publicKey = await getPublicKey();
      const transaction = await server.invokeHostFunction({
        contractId: contractId,
        functionName: 'vote',
        args: [
          xdr.ScSymbol.fromString(publicKey),
          xdr.ScSymbol.fromString(selectedCandidate)
        ]
      });
      const signedTransaction = await signTransaction(transaction.toXDR());
      await server.sendTransaction(signedTransaction);
      fetchCandidates();
    } catch (error) {
      console.error('Error voting:', error);
    }
  };

  return (
    <div className="App">
      <h1>Stellar Voting dApp</h1>
      {walletConnected ? (
        <>
          <h2>Add Candidate</h2>
          <input
            type="text"
            value={newCandidate}
            onChange={(e) => setNewCandidate(e.target.value)}
          />
          <button onClick={addCandidate}>Add Candidate</button>

          <h2>Vote</h2>
          <select onChange={(e) => setSelectedCandidate(e.target.value)}>
            <option value="">Select a candidate</option>
            {candidates.map((candidate, index) => (
              <option key={index} value={candidate.name}>
                {candidate.name}
              </option>
            ))}
          </select>
          <button onClick={vote}>Vote</button>

          <h2>Results</h2>
          <ul>
            {candidates.map((candidate, index) => (
              <li key={index}>
                {candidate.name}: {candidate.votes} votes
              </li>
            ))}
          </ul>
        </>
      ) : (
        <p>Please connect your Stellar wallet to use this dApp.</p>
      )}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This React component provides a simple interface for adding candidates, voting, and viewing results. It uses the Freighter API to interact with the user's Stellar wallet and the stellar-sdk to communicate with the Soroban RPC server.

Testing the dApp

To test the dApp:

  1. Start the React development server:
npm start
Enter fullscreen mode Exit fullscreen mode
  1. Open your browser and navigate to http://localhost:3000.

  2. Connect your Stellar wallet (e.g., Freighter) to the testnet.

  3. Add candidates, cast votes, and view the results.

Some key takeaways:

  • Soroban provides a powerful platform for building smart contracts on Stellar.
  • The Stellar SDK and Freighter API make it easy to interact with Stellar contracts from web applications.
  • Decentralized voting systems can provide transparency and immutability to election processes.

As you continue to explore Stellar development, you can consider adding more features to this dApp, such as:

  • Implementing vote delegation
  • Adding time limits for voting periods
  • Creating different types of voting systems (e.g., ranked choice)

What I Created

This is a tutorial on building a decentralized voting system on the Stellar network. It:

  1. Introduces developers to Stellar's smart contract platform, Soroban
  2. Showcases practical use of key Stellar concepts like accounts and transactions

The tutorial serves as a starting point for developers to create more complex applications on the Stellar network, encouraging innovation and adoption of the platform.

Journey

Implementation and Smart Contract Design

For this project, I implemented a basic decentralized voting system on Stellar using Soroban, Stellar's smart contract platform. The smart contract was designed to securely record votes, ensuring that each vote is immutable and transparent. Key features include user registration, vote casting, and vote tallying. Each voter can cast a vote only once, and all votes are recorded on the Stellar blockchain, ensuring transparency and tamper-proof results.

Motivation

The motivation behind this project was to develop a solution that addresses the need for free and fair elections, particularly in my country of origin. By leveraging Stellar's blockchain technology, the aim was to create a voting system that enhances electoral integrity, promotes trust in the democratic process, increases accessibility, and addresses specific electoral challenges.

What I Learned

Through this project, I gained a deeper understanding of the simplicity and power of Soroban for smart contract development. I learned best practices for designing decentralized applications on Stellar and the importance of creating user-friendly interfaces for blockchain applications. Additionally, I identified potential challenges and considerations when implementing voting systems on blockchain, such as security, scalability, and user adoption.

Experience with the Ecosystem

My experience with the Stellar ecosystem was highly positive. The comprehensive documentation and supportive community made it easy to get started with Soroban and smart contract development. The platform's robust features and ease of use allowed me to focus on implementing the core functionalities of the voting system without getting bogged down by technical complexities.

Future Plans

Moving forward, I hope to enhance the voting system by incorporating more advanced features, such as real-time vote tracking and improved security measures. I also plan to explore other use cases for decentralized applications on Stellar, leveraging the platform's capabilities to address various real-world challenges. Additionally, I aim to continue contributing to the Stellar community by sharing my experiences and helping other developers get started with blockchain and smart contract development.

This project has been a rewarding journey, and I am excited about the potential of Stellar and Soroban to drive innovation and positive change in various sectors.

Top comments (0)