Building a ballot contract using Soroban plataform and Rust SDK
Tags: sorobanathon, stellar, contract, soroban
Published: 2023-03-23 08:11:42

In this post, I would like to share with you my first experience developing an smart contract using [stellar](https://stellar.org/) [soroban platform](https://soroban.stellar.org/). 

This smart contract allows users to participate in a voting process. It's been built using [Soroban Rust SDK](https://docs.rs/soroban-sdk/latest/soroban_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](https://github.com/icolomina/ballot-contract)

Let's start looking how the contract works

### Contract data keys

```rust
const PARTIES: Symbol = symbol!("parties");
const VOTERS: Symbol = symbol!("voters");
const VOTES: Symbol = symbol!("votes");
```
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

```rust
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
}
```
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

```rust
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
}
```

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

```rust
#[contracttype]
pub enum PartyCounter {
    Counter(Symbol),
}
```

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
```rust
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
    }
```

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
```rust
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
    }
```

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
```rust
 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
    }
```
_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
```rust
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
    }
```

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
```rust 
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

    }
```

_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

```rust
#[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);

}

```
_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

```rust
#[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 );
}
```
_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

```rust
#[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));
    
}
```
_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

```rust
#[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"));
}
```
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

```rust
#[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"));
}
```

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

```rust
#[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"));
}
```
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

```rust
#[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"));
}
```

This test should panic since _addr1_ is not registered. _Error(2)_ represents _VoterNotRegistered_

#### Test delegate vote

```rust
#[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);
    
}
```

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

```rust
#[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);
}

```

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

```rust
#[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);

}
```

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](https://soroban.stellar.org/docs/getting-started/setup).
After doing it, you only have to execute:

```shell
cargo build
cargo test
```

You should see the an output like this:

![Contract tests output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ooogyosvx8gdlcbhp80y.png)