DEV Community

Nacho Colomina Torregrosa
Nacho Colomina Torregrosa

Posted on

Building a ballot contract using Soroban plataform and Rust SDK

In this post, I would like to share with you my first experience developing an smart contract using stellar soroban platform.

This smart contract allows users to participate in a voting process. It's been built using Soroban Rust SDK and works as described bellow:

  • Contract's creator can register parties.
  • Contract's creator can register voters.
  • Party can only be registered once.
  • Voter can only be registered once.
  • Registered voters are allowed to vote once.
  • Voters can delegate their vote.
  • Voters who have their vote delegated cannot vote.
  • Voters who have delegated votes cannot delegate their vote.

If you want to see the entire contract code, you can go to my github account

Let's start looking how the contract works

Contract data keys

const PARTIES: Symbol = symbol!("parties");
const VOTERS: Symbol = symbol!("voters");
const VOTES: Symbol = symbol!("votes");
Enter fullscreen mode Exit fullscreen mode

We start creating 3 keys:

  • PARTIES: To store parties we can vote for
  • VOTERS: To store allowed voters
  • VOTES: To store voters which has already voted

Functions to get elements from the storage

fn get_parties(env: &Env) -> Vec<Symbol> {
    let parties: Vec<Symbol>= env
        .storage()
        .get(&PARTIES)
        .unwrap_or(Ok(Vec::new(&env)))
        .unwrap()
    ;

    parties
}

fn get_voters(env: &Env) -> Vec<Address> {
    let voters: Vec<Address>= env
        .storage()
        .get(&VOTERS)
        .unwrap_or(Ok(Vec::new(&env)))
        .unwrap()
    ;

    voters
}

fn get_votes(env: &Env) -> Vec<Address> {
    let votes: Vec<Address> = env
        .storage()
        .get(&VOTES)
        .unwrap_or(Ok(Vec::new(&env)))
        .unwrap()
    ;

    votes
}

fn get_delegated_votes(env: &Env, addr: &Address) -> Vec<Address> {
    let votes: Vec<Address> = env
        .storage()
        .get(addr)
        .unwrap_or(Ok(Vec::new(&env)))
        .unwrap()
    ;

    votes
}
Enter fullscreen mode Exit fullscreen mode

Above functions allow us to get parties, voters, votes and address delegated votes from the storage. We use unwrap_or to init an empty vec if there is still no elements stored.

Function to check whether vote is delegated

fn is_vote_delegated(env: &Env, voter: &Address, voters: &Vec<Address>) -> bool {
    let mut already_delegated = false;
    let mut i = 0;
    while i < voters.len() && !already_delegated {

        let voter = voters.get(i).unwrap();
        match voter {
            Ok(vot) => {
                let d_votes = get_delegated_votes(&env, &vot);
                if d_votes.contains(voter) {
                    already_delegated = true;
                }
            },
            Err(_e) => ()
        }

        i += 1;
    }

    already_delegated
}
Enter fullscreen mode Exit fullscreen mode

Above function checks whether a voter has his/her vote delegated on another voter. To do it, it follows the following steps:

  • Loops all voters
  • For each voter gets its delegated votes
  • If v_to_delegate is contained in voter delegated votes then v_to_delegate has his/her vote delegated

Party counter

#[contracttype]
pub enum PartyCounter {
    Counter(Symbol),
}
Enter fullscreen mode Exit fullscreen mode

Thanks to PartyCounter enum we'll be able to create a counter per party so we can count votes for each party and store them easily.

The contract

The contract is defined as a rust struct and follows the next implementation:

add_party function

pub fn add_party(env: Env, admin: Address, name: Symbol) -> u32  {
        admin.require_auth();
        let mut parties: Vec<Symbol>= get_parties(&env);

        if ! parties.contains(&name) {
            parties.push_back(name);
            env.storage().set(&PARTIES, &parties);
        }

        parties.len() as u32
    }
Enter fullscreen mode Exit fullscreen mode

First contract function is add_party. It allows contract admins to add new parties which voters can vote to. Parties can only be added once and only admins can add new parties.

add_voter function

pub fn add_voter(env: Env, admin: Address, addr: Address) -> u32 {
        admin.require_auth();
        let mut voters: Vec<Address> = get_voters(&env);

        if ! voters.contains(&addr) {
            voters.push_back(addr);
            env.storage().set(&VOTERS, &voters);
        }

        voters.len() as u32
    }
Enter fullscreen mode Exit fullscreen mode

As it occurs with add_party function, add_voter follows the same logic. Only admins can register new voters and a voter can only be registerted once.

delegate function

 pub fn delegate(env: Env, v_to_delegate: Address, v_delegate: Address) -> Vec<Address> {
        let voters = get_voters(&env);
        if !voters.contains(&v_to_delegate) || !voters.contains(&v_delegate) {
            panic_with_error!(&env, Error::VoterNotRegistered); // Voter and voter to delegate to must be registered
        }

        let d_vts_to_delegate = get_delegated_votes(&env, &v_to_delegate);
        if d_vts_to_delegate.len() > 0 {
            panic_with_error!(&env, Error::VoterHasDelegatedVotes);  // A voter which has delegated votes cannot delegate his/her vote
        }

        let already_delegated = is_vote_delegated(&env, &v_to_delegate, &voters);
        let mut d_votes = get_delegated_votes(&env, &v_delegate);

        // If v_to_delegate has not delegated his/her voter so far, then delegate it.
        if !already_delegated {
            d_votes.push_back(v_to_delegate);
            env.storage().set(&v_delegate, &d_votes);
        }

        d_votes
    }
Enter fullscreen mode Exit fullscreen mode

delegate function takes a voter and a voter to delegate vote to and delegates it when:

  • Voter and voter to delegate to are both registered as voters
  • Voter which want to delegate his/her vote cannot have votes delegated on him/her
  • Voter cannot delegate his vote twice

vote function

pub fn vote(env: Env, voter: Address, party: Symbol) -> bool {

        let voters: Vec<Address>      = get_voters(&env);

        if is_vote_delegated(&env, &voter, &voters) {
            panic_with_error!(&env, Error::VoterDelegated); // If voter has his/her vote delegated, cannot vote
        }

        let mut count_sum = 1;

        let parties: Vec<Symbol>      = get_parties(&env);
        let mut votes: Vec<Address>   = get_votes(&env);

        if !parties.contains(&party) {
            panic_with_error!(&env, Error::PartyNotRegistered); // cannot vote for a non registered party
        }

        if !voters.contains(&voter) {
            panic_with_error!(&env, Error::VoterNotRegistered); // Voter which is not registered cannot vote
        }

        if votes.contains(&voter) {
            panic_with_error!(&env, Error::VoterAlreadyVoted); // Voter cannot vote twice
        }

        let party_counter_key = PartyCounter::Counter(party);
        let mut count: u32 = env.storage().get(&party_counter_key).unwrap_or(Ok(0)).unwrap(); 

        let v_delegated_votes = get_delegated_votes(&env, &voter);
        if v_delegated_votes.len() > 0 {
            count_sum = v_delegated_votes.len() + 1; // count voter vote and his/her delegated votes
        }

        count += count_sum;
        env.storage().set(&party_counter_key, &count);
        votes.push_back(voter);
        env.storage().set(&VOTES, &votes);

        true
    }
Enter fullscreen mode Exit fullscreen mode

Function vote allows voter to vote. In order to count vote some rules must be followed:

  • Voter who has his/her vote delegated cannot vote.
  • Non registered voter cannot vote.
  • Voter cannot vote for a non registered party.
  • Voter cannot vote twice.

After cheking rules match, function vote count vote. If voter has delegated votes then function sums one more for every delegated vote.

count function

pub fn count(env: Env) -> Map<Symbol, u32> {

        let parties = get_parties(&env);
        let mut count_map: Map<Symbol, u32>= Map::new(&env);
        for party in parties.iter() {
            match party {
                Ok(p) => {
                    let party_counter_key = PartyCounter::Counter(p);
                    let party_count: u32 = env.storage().get(&party_counter_key).unwrap_or(Ok(0)).unwrap(); 
                    count_map.set(p, party_count);
                },
                _ => ()
            }

        }

        count_map

    }
Enter fullscreen mode Exit fullscreen mode

count function counts parties votes. It loops registered parties and, for each party, gets is counter key value. As a result, it returns a key-value map on which keys are parties names and values are its total votes.

Testing

Testing is a really important part when building your smart contract since it helps you to ensure contract behaves as expected. In the following sections we will see use cases which are covered by contract test suite:

Test add party

#[test]
fn add_party_test() {
    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let admin_addr = Address::random(&env);

    // Testing 
    assert_eq!(client.add_party(&admin_addr, &symbol!("Laborist")), 1 );
    assert_eq!(client.add_party(&admin_addr, &symbol!("Conserv")), 2);
    assert_eq!(client.add_party(&admin_addr, &symbol!("Conserv")), 2);

}

Enter fullscreen mode Exit fullscreen mode

add_party_test function tests that:

  • Party is registered successfully
  • Party is not registered twice (When it tries to store "Conserv" again total registered parties is still 2)

Test add voter

#[test]
fn add_voter_test() {
    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let addr2 = Address::random(&env);
    let admin_addr = Address::random(&env);


    assert_eq!(client.add_voter(&admin_addr, &addr1), 1 );
    assert_eq!(client.add_voter(&admin_addr,&addr2), 2 );
    assert_eq!(client.add_voter(&admin_addr, &addr2), 2 );
}
Enter fullscreen mode Exit fullscreen mode

add_voter_test function tests that:

  • Voter is registered successfully
  • Voter is not registered twice (When it tries to store addr2 again total registered voters is still 2)

Test vote

#[test]
fn vote_test() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let addr2 = Address::random(&env);
    let addr3 = Address::random(&env);
    let addr4 = Address::random(&env);
    let addr5 = Address::random(&env);

    let admin_addr = Address::random(&env);

    client.add_party(&admin_addr, &symbol!("Laborist"));
    client.add_party(&admin_addr, &symbol!("Conserv"));

    client.add_voter(&admin_addr, &addr1);
    client.add_voter(&admin_addr, &addr2);
    client.add_voter(&admin_addr, &addr3);
    client.add_voter(&admin_addr, &addr4);
    client.add_voter(&admin_addr, &addr5);

    client.delegate(&addr5, &addr4);

    assert_eq!(client.vote(&addr1, &symbol!("Laborist")), true);
    assert_eq!(client.vote(&addr2, &symbol!("Laborist")), true);
    assert_eq!(client.vote(&addr3, &symbol!("Conserv")), true);
    assert_eq!(client.vote(&addr4, &symbol!("Conserv")), true);

    let result = client.count();

    assert_eq!(result.get(symbol!("Laborist")).unwrap().ok(), Some(2));
    assert_eq!(result.get(symbol!("Conserv")).unwrap().ok(), Some(3));

}
Enter fullscreen mode Exit fullscreen mode

vote_test function tests that vote counts are right. To do it, it follows the next steps:

  • Register 2 parties
  • Register 5 voters
  • Delegates addr5 vote on addr4
  • addr1 and addr2 vote for Laborist party
  • addr3 and addr4 vote for Conserv party

Results of count votes should be:

  • Laborist: 2 votes
  • Conserv: 3 votes (since addr5 delegated his/her vote on addr4, addr4 vote counts for 2)

Test vote from voter with delegated vote

#[test]
#[should_panic(expected = "Error(1)")]
fn vote_delegated() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let addr2 = Address::random(&env);
    let admin_addr = Address::random(&env);

    client.add_party(&admin_addr, &symbol!("Laborist"));
    client.add_party(&admin_addr, &symbol!("Conserv"));

    client.add_voter(&admin_addr, &addr1);
    client.add_voter(&admin_addr, &addr2);

    client.delegate(&addr1, &addr2);
    client.vote(&addr1, &symbol!("Laborist"));
}
Enter fullscreen mode Exit fullscreen mode

In this case, addr1 cannot vote since he/she delegated his/her vote on addr2 so test should panic. Error(1) represents VoteDelegated error.

Test vote to non registered party

#[test]
#[should_panic(expected = "Error(3)")]
fn vote_party_not_registered() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let admin_addr = Address::random(&env);

    client.add_party(&admin_addr, &symbol!("Laborist"));
    client.add_voter(&admin_addr, &addr1);

    client.vote(&addr1, &symbol!("Conserv"));
}
Enter fullscreen mode Exit fullscreen mode

This test should panic since Conserv party has not been registered and voters cannot vote for non registered parties. Error(3) represents PartyNotRegistered.

Test vote from voters who has already voted

#[test]
#[should_panic(expected = "Error(4)")]
fn vote_voter_already_voted() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let admin_addr = Address::random(&env);

    client.add_party(&admin_addr, &symbol!("Laborist"));
    client.add_voter(&admin_addr, &addr1);

    client.vote(&addr1, &symbol!("Laborist"));
    client.vote(&addr1, &symbol!("Laborist"));
}
Enter fullscreen mode Exit fullscreen mode

This test should panic since addr1 tries to vote twice and that situation is not allowed. Error(4) represents VoterAlreadyVoted

Test vote from non registered voter

#[test]
#[should_panic(expected = "Error(2)")]
fn vote_voter_not_registered() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let admin_addr = Address::random(&env);

    client.add_party(&admin_addr, &symbol!("Laborist"));
    client.vote(&addr1, &symbol!("Laborist"));
}
Enter fullscreen mode Exit fullscreen mode

This test should panic since addr1 is not registered. Error(2) represents VoterNotRegistered

Test delegate vote

#[test]
fn delegate_test() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let addr2 = Address::random(&env);
    let admin_addr = Address::random(&env);

    client.add_voter(&admin_addr, &addr1);
    client.add_voter(&admin_addr, &addr2);

    let d_votes = client.delegate(&addr1, &addr2);
    let d_votes_2 = client.delegate(&addr1, &addr2);

    assert_eq!(d_votes.len(), 1);
    assert_eq!(d_votes_2.len(), 1);

}
Enter fullscreen mode Exit fullscreen mode

This test tests voter can delegate his/her vote and also tests that this voter cannot delegate his/her vote twice. To do it, delegate funcion is invoked twice with the same parameters and delegated votes length is always one.

Test delegate vote from non registered voters

#[test]
#[should_panic(expected = "Error(2)")]
fn delegate_fail() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);

    let addr1 = Address::random(&env);
    let addr2 = Address::random(&env);

    client.delegate(&addr1, &addr2);
}

Enter fullscreen mode Exit fullscreen mode

This test should panic since addr1 and addr2 are not registered as a voter so they cannot delegate.

Test voter wants to delegate his/her vote and has delegated votes

#[test]
#[should_panic(expected = "Error(5)")]
fn delegate_fail_voter() {

    let env = Env::default();
    let contract_id = env.register_contract(None, BallotContract);
    let client = BallotContractClient::new(&env, &contract_id);   

    let addr1 = Address::random(&env);
    let addr2 = Address::random(&env);
    let addr3 = Address::random(&env);
    let admin_addr = Address::random(&env);

    client.add_voter(&admin_addr, &addr1);
    client.add_voter(&admin_addr, &addr2);
    client.add_voter(&admin_addr, &addr3);

    client.delegate(&addr1, &addr2);
    client.delegate(&addr2, &addr3);

}
Enter fullscreen mode Exit fullscreen mode

This test should panic since addr2 tries to delegate his/her vote but addr1 has previously delegated his/her vote on addr2

Executing tests

To execute contract tests, you have to install soroban first. You can check how to do it here.
After doing it, you only have to execute:

cargo build
cargo test
Enter fullscreen mode Exit fullscreen mode

You should see the an output like this:

Contract tests output

Top comments (0)