Using tokenization to control a soroban voting smart-contract
Tags: tokenization, smartcontract, soroban, rust
Published: 2024-03-22 17:30:50

## Introduction
A few months ago, I pushed an smart-contract to my github account which allows users to participate on a ballot process. That contract have the following features (among others):

- Voter cannot vote twice.
- Voter can delegate its vote.
- Voter who has delegated its vote cannot vote.
- Voter who has delegated votes cannot delegate its vote.

You can check the contract code [here](https://github.com/icolomina/ballot-contract).
To control this rules, the contract saves the votes and the delegated votes on the storage to check them later.

## The new contract
Recently, I've pushed a new version of this contract but, in this case, the vote delegation control and the voting allowance is controlled using a token (a contract which acts as a token). Concretely, the voting contract use the token to control the following:

- Voting delegation.
- Voting allowance.

In the next sections we will see the token contract code and how the new voting contract differs from the old.

## The token
Through this section, we will delve into the contract code and will learn what's the goal of each function.

> This token does not completely implements the [soroban token interface](https://soroban.stellar.org/docs/tokens/token-interface) since we do not need all functions. 

Let's start by the beginning:

```rust
#![no_std]

use soroban_sdk::{contract, contracttype, contractimpl, contracterror, symbol_short, Address, Env, Symbol};
pub const TOKEN_ADMIN: Symbol = symbol_short!("t_admin");

pub const DAY_IN_LEDGERS: u32 = 17280;
pub const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS;
pub const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS;

pub const BALANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS;
pub const BALANCE_LIFETIME_THRESHOLD: u32 = BALANCE_BUMP_AMOUNT - DAY_IN_LEDGERS;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
    TokenAlreadyInitialized = 1,
    AddressAlreadyHoldsToken = 2,
    AddressDoesNotHoldToken = 3,
    AddressAlreadyHasAllowance = 4,
    ExpirationLedgerLessThanCurrentLedger = 5
}

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Balance(Address),
    Allowance(Address),
    Blocking(Address)
}

fn has_admin(e: &Env) -> bool {
    let has_admin = e.storage().instance().has(&TOKEN_ADMIN);
    has_admin
}

fn get_balance(e: &Env, addr: Address)-> u32 {
    let key = DataKey::Balance(addr);
    if let Some(b) = e.storage().persistent().get::<DataKey, u32>(&key) {
        e.storage()
            .persistent()
            .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT);

        return b;
    }

    0
}

fn get_allowance(e: &Env, addr: Address) -> bool {
    let allowance_key = DataKey::Allowance(addr);
    if let Some(_a) = e.storage().temporary().get::<_, Address>(&allowance_key) {
        return true;
    }

    false
}

fn get_blocking(e: &Env, addr: Address) -> bool {
    let blocking_key = DataKey::Blocking(addr);
    if let Some(_b) = e.storage().temporary().get::<_, Address>(&blocking_key) {
        return true;
    }

    false
}
```
- The *Error* enum contains all the possible errors that the token contract can return.
- The *Datakey* enum contains three Address variants:
   - **Balance**: For the balance keys
   - **Allowance**: For the allowance keys
   - **Blocking**: For the blocking keys
- The *has_admin* function checks if the contract has and admin address
- The *get_balance* function checks whether the address holds the token. If so, it extends the address balance key ttl and returns the balance. Otherwise returns 0.
- The *get_allowance* function checks whether the address has been allowed to spend other address balance.
- The *get_blocking* function checks whether the address has allowed to another address to spend its balance.

### The token functions
Let's analyze now the contract functions:

#### Initialize function

```rust
pub fn initialize(e: Env, admin: Address) -> Result<bool, Error> {

   if has_admin(&e) {
        return Err(Error::TokenAlreadyInitialized);
   }

   e.storage().instance().set(&TOKEN_ADMIN, &admin);
   Ok(true)
        
}
```
The initialize function checks whether the contract has an admin or not. If so, it returns a *TokenAlreadyInitialized* error. Otherwise it stores the address received as an admin and returns Ok.

#### Mint function

```rust
pub fn mint(e: Env, addr: Address) -> Result<u32, Error> {
        
   let admin: Address = e.storage().instance().get(&TOKEN_ADMIN).unwrap();
   admin.require_auth();

   if get_balance(&e, addr.clone()) > 0 {
        return Err(Error::AddressAlreadyHoldsToken);
   }

   e.storage()
       .instance()
       .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

   let key = DataKey::Balance(addr.clone());
   let amount: u32 = 1;
   e.storage().persistent().set(&key, &amount);
   Ok(amount)
}
```
The *mint* function mints the user with new tokens. Minting is only allowed for the contract admin. Each address can only have one token so, if the current address balance is greater than 0, it returns an *AddressAlreadyHoldsToken* error. 
If the address does not hold the token yet, the function creates the key for the address and sets the amount to 1 for the key and stores it. Then it returns the amount. 

#### Get balance function

```rust
pub fn balance(e: Env, addr: Address) -> u32 {
   e.storage()
       .instance()
       .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

   let b: u32 = get_balance(&e, addr);
   b
}
```
The *balance* function returns the current address balance.

#### Transfer function

```rust
pub fn transfer(e: Env, from: Address, to: Address) -> Result<bool, Error> {
    from.require_auth();

    if get_balance(&e, from.clone()) == 0 {
        return Err(Error::AddressDoesNotHoldToken);
    }

    if get_balance(&e, to.clone()) > 0 {
        return Err(Error::AddressAlreadyHoldsToken);
    }

    e.storage()
       .instance()
       .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

    let from_key = DataKey::Balance(from.clone());
    let to_key   = DataKey::Balance(to.clone());
    let amount: u32 = 1;

    e.storage().persistent().remove(&from_key);
    e.storage().persistent().set(&to_key, &amount);
    e.storage()
       .persistent()
       .extend_ttl(&to_key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT);

    Ok(true)
}
```
The *transfer* function transfers the token between two addresses. The function requires from address to authorize the transfer. The function also requires that "from" address holds a token and "to" address does not. 
> Remember that only one token per address is allowed.

If all requirements match, the function proceeds following the next steps:

- Creates the "from" and "to" balance keys.
- Removes the "from" key. 
- Sets the amount for "to" key to 1.
- Extends the "to" balance key ttl.

#### Approve function

```rust
pub fn approve(e: Env, from: Address, spender: Address, expiration: u32) -> Result<bool, Error> {
   from.require_auth();
   if expiration < e.ledger().sequence(){
       return Err(Error::ExpirationLedgerLessThanCurrentLedger);
   }

   if get_blocking(&e, from.clone()) {
       return Err(Error::AddressAlreadyHasAllowance);
   }

   if get_allowance(&e, spender.clone()) {
       return Err(Error::AddressAlreadyHasAllowance);
   }

   if get_balance(&e, from.clone()) < 1 {
       return Err(Error::AddressDoesNotHoldToken);
   }

   if get_balance(&e, spender.clone()) < 1 {
        return Err(Error::AddressDoesNotHoldToken);
   }

   let allowance_key = DataKey::Allowance(spender.clone());
   let blocking_key  = DataKey::Blocking(from.clone());
   e.storage().temporary().set(&allowance_key, &from);
   e.storage().temporary().set(&blocking_key, &spender);

   let live_for = expiration
       .checked_sub(e.ledger().sequence())
       .unwrap()
   ;

   e.storage().temporary().extend_ttl(&allowance_key, live_for, live_for);
   e.storage().temporary().extend_ttl(&blocking_key, live_for, live_for);

   Ok(true)
}
```
This function allows spender to use the token which belongs to from address. Before setting the allowance, the function checks the following rules:

- **from** must not have allowed to another spender.
- **spender** must no have been allowed by another address.
- Both from and spender must hold token balance.

If these rules match, the function creates the allowance key for the spender address and the blocking key for the from address. Then it saves the keys and extends ttl based on the expiration value.

> As we will see later, this function will be used by an address to delegate its vote to another one.

#### Allowance function
```rust
pub fn allowance(e: &Env, from: Address) -> bool {

    e.storage()
        .instance()
        .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

    let allowance = get_allowance(&e, addr);
    allowance
 }
```

The allowance function simply checks if the address has an allowance.

#### Blocking function
```rust
pub fn blocking(e: &Env, from: Address) -> bool {

    e.storage()
        .instance()
        .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

    let blocking = get_blocking(e, addr);
    blocking
}
```

The blocking function simply checks if the address has a blocking, that is, is has allowed to another address.

### Burn function
```rust
pub fn burn(e: Env, addr: Address) {
    let admin: Address = e.storage().instance().get(&TOKEN_ADMIN).unwrap();
    admin.require_auth();

    e.storage()
        .instance()
        .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

    let from_key = DataKey::Balance(addr);
    e.storage().persistent().remove(&from_key);
}
```

The burn function removes the address token by removing its balance key.

## Using the token to control the ballot

Let's see now the key differences between the old contract and new one where the token is used to control some voting features. 

> You can see the new contract complete code [here](https://github.com/icolomina/ballot-with-token). The old contract is code is located [here](https://github.com/icolomina/ballot-contract)

### Checking address can vote

In the first version of the contract there wasn't any check before checking dates and the admin had to authorize the vote:

```rust
admin.require_auth();
if !check_dates(&env) {
     return Err(Error::BallotOutOfDate);
}
```

Now, the voter address must authorize the vote and the voter must hold enough balance to vote.

```rust
voter.require_auth();
let token = storage::get_token(&env);
let tk = token::Client::new(&env, &token);
if tk.balance(&voter) < 1 {
    return Err(Error::VoterDoesNotHoldToken);
}
        
if !check_dates(&env) {
    return Err(Error::BallotOutOfDate);
}
```

### Checking address can delegate
Just like the case in the previous section, the old contract's delegate function did not check anything before checking dates and was the admin address who had to authorize the function. 

```rust
admin.require_auth();
if !check_dates(&env) {
    return Err(Error::BallotOutOfDate);
}
```

Now, The voter who wants to delegate its vote is who authorize the function and both addresses must hold the token.

```rust
o_voter.require_auth();
let token = storage::get_token(&env);
let tk = token::Client::new(&env, &token);

if tk.balance(&o_voter) < 1 {
     return Err(Error::VoterDoesNotHoldToken);
}

if tk.balance(&d_voter) < 1 {
     return Err(Error::VoterDoesNotHoldToken);
}

if !check_dates(&env) {
     return Err(Error::BallotOutOfDate);
}
```

### Delegate a vote
The old contract had to save a voter delegated votes on a Vector:

```rust
let mut d_vot_delegs: Vec<Symbol> = storage::get_voter_delegated_votes(&env, &d_voter);
d_vot_delegs.push_back(o_voter.clone());
storage::update_voter_delegated_votes(&env, d_voter, d_vot_delegs);
``` 
Now, the voter who wants to delegate its vote has to approve the another address to vote on its name.

```rust
let expiration_ledger = (((config.to - config.from) / 5) + 60) as u32; // 5 seconds for every ledger. Add 5 extra minutes
tk.approve(&o_voter, &d_voter, &expiration_ledger);
```

### The function which checks whether an address has its vote delegated

The old contract saved on the storage an address delegated votes and then it checked the storage to check it.

```rust
fn is_delegated(&self, env: &Env) -> bool {
    let dvts: Vec<Symbol> = storage::get_delegated_votes(env);
    dvts.contains(self.id)
}
```

Now, the contract checks if the address token is blocked (token blocking function) which means that this address has approved another to vote in its name.

```rust
fn is_delegated(&self, env: &Env) -> bool {
   let token = storage::get_token(&env);
   let tk = token::Client::new(&env, &token);

   if tk.blocking(&self.id) {
       return true
   }

   false
}
```

### The function which checks whether an address has delegated votes
Again, just like the case in the previous section, the old contract checked the storage to check if there were delegated votes for an address.

```rust
fn has_delegated_votes(&self, env: &Env) -> bool {
    let dvotes = storage::get_voter_delegated_votes(env, self.id);
    if dvotes.len() > 0 {
        return true;
    }
    
    false
}
```
The new contract checks if another address has allowed (using the token approve function) the current address to vote for it.

```rust
fn has_delegated_vote(&self, env: &Env) -> bool {
    let token = storage::get_token(&env);
    let tk = token::Client::new(&env, &token);

    if tk.allowance(&self.id) {
        return true
    }

    false
}
```

### The vote
In the old contract, the vote function gets the address delegated votes vector and saves its vote plus the total of its delegated votes.

```rust
let d_votes: Vec<Symbol> = storage::get_voter_delegated_votes(&env, v.id);
let count = 1 + d_votes.len() + storage::get_candidate_votes_count(&env, &candidate_key);
```

The new contract uses the *has_delegated_vote* function (which internally uses the token *allowance* function) to check whether the address has a vote delegated. If so, it adds an extra vote.

```rust
let mut d_votes = 0;
if v.has_delegated_vote(&env) {
     d_votes = 1;
}
```

> At this point, we can notice that we have lost a feature compared to the old contract. The old contract allows an address to have more than one vote delegated. The new one doesn't since the an address can only hold one token. 
The token could be modified to allow addresses to hold more tokens and improve the vote delegation system.

## Conclusion
In this post, we have learned that tokenization can be used in many contexts not only finance. There are standards both on soroban (token interface) and ethereum (ERC-20) but we can also create and build tokens to fit custom needs.
we have to take into account the supply process, that is, how many tokens we are going to put into circulation. In this case, it can exist one token for each community member.