Governance contracts on ink!

Tarun Kumar
Firefly
Published in
11 min readNov 26, 2020

Special credits to Yameen Malik for coming up with the tutorial and the underlying implementations for the article. You can find similar tutorials here.

Governance allows decentralized networks to adapt to changing conditions. Network safety parameters on Firefly such as the liquidation ratio or features such as supported collaterals can only be iterated as the protocol continues to be tested by the public. This tutorial is the first step to building a robust governance infrastructure on ink! and it lays down some fundamental ink! concepts:

  • Building and storing custom structs in vectors and hashmaps
  • Safely retrieving and updating the stored structs using collections
  • Using traits like Clone, Debug, PackedLayout, and SpreadLayout

In this tutorial, a chairperson is the creator of the ballot and verifies each voter by assigning them a vote. Anyone can submit a proposal on the ballot, and the proposal with the highest number of verified voters is the winning proposal.

Let’s start with making a new ink! project to build the Ballot contract.

In your working directory, run:

cargo contract new ballot && cd ballot

Cargo Template

As discussed above, a Ballot contract will have the chair_person (the owner of the Ballot) overseeing the Voter voting on the Proposal

Our contract’s storage has AccountId initialized with the contract caller's ID in the constructor. Likewise, to retrieve the Ballot chairperson, we create the function get_chairperson.

Here’s the template so far:

// lib.rs
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod ballot {
/// Defines the storage of your contract.
/// Add new fields to the below struct in order
/// to add new static storage fields to your contract.
#[ink(storage)]
pub struct Ballot {
chair_person: AccountId,
}
impl Ballot {
#[ink(constructor)]
pub fn new() -> Self {
let owner = Self::env().caller();
Self {
chair_person:owner,
}
}
#[ink(message)]
pub fn get_chairperson(&self) -> AccountId {
self.chair_person
}
}
}

struct keyword:

In Rust, struct is a keyword to define a custom structure of primitive data types. You may have come across struct in previous Edgeware tutorials, however, its usage was limited to contract storage.

We’ll be usingstruct to define custom types that'll provide abstract implementations of different entities in our contract.Proposal and Voter structs within the contract are defined as:

The template for a Proposal in a ballot contains:

  • name: A field to store the name of the proposal.
  • vote_count: A 32 bit unsigned integer for storing the number of votes the proposal has received.

The template for the Voter of a Proposal contains:

  • weight: An unsigned integer indicating the weightage of the voter. This can vary based on election/network parameters.
  • voted: A boolean variable which is initially false and is set to true once the vote is cast.
  • delegate: A voter can choose to delegate their vote to someone else. Since it's not necessary for voters to delegate, this field is an Option.
  • vote: Index of the proposal to which a user casts vote. This is created as an Option and is None by default.

These structs will not be public as users don't need to interact with them directly.

Unlike our contract struct Ballot, we don't use the ink(storage) macro for our custom struct, as there must be only one storage struct in a contract.

mod ballot {
...
// Structure to store the Proposal information
struct Proposal {
name: String,
vote_count: i32,
}
// Structure to store the Voter information
pub struct Voter {
weight: i32,
voted: bool,
delegate: Option<AccountId>,
vote: Option<i32>,
}
...
}

Tests, Compilations, and Warnings

Below we define a simple testing framework and a unit test to verify that our contract is constructing a Ballot correctly. This is done by ensuring AccountId is the same as a Ballot's chair_person (In Rust, the contract's default address will be 2^16, hence the assertion check with ::from([0x1; 32]) )

mod ballot{
//contract definition

/// Unit tests in Rust are normally defined within such a `#[cfg(test)]`
/// module and test functions are marked with a `#[test]` attribute.
/// The below code is technically just normal Rust code.
#[cfg(test)]
mod tests {
/// Imports all the definitions from the outer scope so we can use them here.
use super::*;
// Alias `ink_lang` so we can use `ink::test`.
use ink_lang as ink;
#[ink::test]
fn new_works() {
let ballot = Ballot::new();
assert_eq!(ballot.get_chairperson(),AccountId::from([0x1: 32]));
}
}

Now to build the contract, execute:

cargo +nightly build

And run tests using:

cargo +nightly test

The contract will successfully compile and pass all tests, but the Rust compiler will give you the following warnings:

warning: struct is never constructed: `Proposal`
--> lib.rs:10:12
|
10 | struct Proposal {
| ^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: struct is never constructed: `Voter`
--> lib.rs:16:16
|
16 | pub struct Voter {
| ^^^^^
warning: 2 warnings emitted

This is just because the contracts defined are not used yet. Let’s fix that!

Collections

For this contract, we are going to store our voters in a HashMap collection (which acts as a key-value store) with AccountId as a key and Voter instance as value. The reason behind using HashMap is to ensure that two voters with the same AccountId cannot exist (which is primitively not allowed in a key-value pair store).

The HashMap class can be imported from the ink_storage crate by:

use ink_storage::collections::HashMap;

The proposals will be stored in a Vec collection that can be imported from the ink_prelude crate, which we import similar to above:

use ink_prelude::vec::Vec;

ink_prelude is a collection of data structures that operate on contract memory during contract execution.

We’ll update the constructor of Ballot contract to accommodate these collections using functions.

Some assumptions that can be made are:

  • For both Vec of proposals and HashMap of voters, we'll need to have retrieval and storage functions.
  • chair_person is also a Voter in the Ballot
...
use ink_storage::collections:shMap;
use ink_prelude::vec::Vec;
...
pub struct Ballot {
chair_person: AccountId,
voters: HashMap<AccountId, Voter>,
proposals: Vec<Proposal>
}
impl Ballot {
#[ink(constructor)]
pub fn new() -> Self {
...
// create empty propsal and voters
let proposals: Vec<Proposal> = Vec::new();
let mut voters = HashMap::new();
// initialize chair person's vote
voters.insert(chair_person, Voter{
weight:1,
voted:false,
delegate: None,
vote: None,
});
Self {
chair_person,
voters,
proposals,
}
}
#[ink(message)]
pub fn get_chairperson(&self) -> AccountId {...}
pub fn get_voter(&self, voter_id: AccountId) -> Option<&Voter>{
self.voters.get(&voter_id)
}
pub fn get_voter_count(&self) -> usize{
self.voters.len() as usize
}
/// the function adds the provided Voter ID into possible
/// list of voters. By default the voter has no voting right,
/// the contract owner must approve the voter before he can cast a vote
#[ink(message)]
pub fn add_voter(&mut self, voter_id: AccountId) -> bool{
let voter_opt = self.voters.get(&voter_id);
// the voter does not exists
if voter_opt.is_some() {
return false
}
self.voters.insert(voter_id, Voter{
weight:0,
voted:false,
delegate: None,
vote: None,
});
return true
}
/// given an index returns the name of the proposal at that index
pub fn get_proposal_name_at_index(&self, index:usize) -> &String {
let proposal = self.proposals.get(index).unwrap();
return &proposal.name
}
/// returns the number of proposals in Ballot
pub fn get_proposal_count(&self) -> usize {
return self.proposals.len()
}
/// adds the given proposal name in ballet
/// to do: check uniqueness of proposal,
pub fn add_proposal(&mut self, proposal_name: String){
self.proposals.push(
Proposal{
name:String::from(proposal_name),
vote_count: 0,
});
}
}

And accordingly, we’ll need to update our tests:

  • On new proposal initiation, voter count and proposal count becomes 1.
#[ink::test]
fn new_works() {
let mut proposal_names: Vec<String> = Vec::new();
proposal_names.push(String::from("Proposal # 1"));
let ballot = Ballot::new();
assert_eq!(ballot.get_voter_count(),1);
}
#[ink::test]
fn adding_proposals_works() {
let mut ballot = Ballot::new();
ballot.add_proposal(String::from("Proposal #1"));
assert_eq!(ballot.get_proposal_count(),1);
}
  • The same voter cannot be registered twice in a Ballot (i.e. the purpose of using HashMap).
#[ink::test]
fn adding_voters_work() {
let mut ballot = Ballot::new();
let account_id = AccountId::from([0x0; 32]);
assert_eq!(ballot.add_voter(account_id),true);
assert_eq!(ballot.add_voter(account_id),false);
}

Traits

If you’re familiar with languages like C#, Java, or other OOP-first languages, you'll know about the concept of an interface which acts as a template for behaviors that a class may have. In Rust, we have a similar concept of traits which derives the shared behavior a custom struct may have. You can read more about them here.

ink! has some built-in traits that are required to create custom contracts.

  • Debug: Allows debugging formatting in format strings.
  • Clone : Allows you to create a deep copy of the object.
  • Copy : Allows you to copy the value of a field.
  • PackedLayout: Types that can be stored to and loaded from a single contract storage cell.
  • SpreadLayout: Types that can be stored to and loaded from the contract storage.

You can learn more about these traits over here and here. These traits are implemented using the derive attribute:

#[derive(Clone, Debug, scale::Encode, scale::Decode, SpreadLayout, PackedLayout,scale_info::TypeInfo)]
struct Proposal {...}
#[derive(Clone, Debug, scale::Encode, scale::Decode, SpreadLayout, PackedLayout,scale_info::TypeInfo)]
pub struct Voter {...}

Adding the functionality

Our Ballot contract is still somewhat empty, we need to add implementations so that:

  • People can vote on proposals.
  • The chairperson can assign voting rights.
  • People can delegate their votes to other voters.

We’ll first start with creating a (different) Ballot constructor which will be able to accept a list of proposal names to initialize the ballot with.

...
#[ink(constructor)]
pub fn new(proposal_names: Option<Vec<String>> ) -> Self {
...
// ACTION : Check if proposal names are provided.
// * If yes then create and push proposal objects to proposals vector
// if proposals are provided
if proposal_names.is_some() {
// store the provided proposal names
let names = proposal_names.unwrap();
for name in &names {
proposals.push(
Proposal{
name: String::from(name),
vote_count: 0,
});
}
}
...
}
...
/// default constructor
#[ink(constructor)]
pub fn default() -> Self {
Self::new(Default::default())
}
...

Adding voting functionality

Previously, we created a function that allowed users to add themselves as a Voter. We initialized their initial weight to 0 because by default when a voter is created they have no voting right. So, let's create a function that will only allow the chair_person to update the voting weight of any voter to 1 (i.e. let them participate in the ballot).

.../// Give `voter` the right to vote on this ballot.
/// May only be called by `chairperson`.
#[ink(message)]
pub fn give_voting_right(&mut self, voter_id: AccountId) {
let caller = self.env().caller();
let voter_opt = self.voters.get_mut(&voter_id);
// ACTION: check if the caller is the chair_person
// * check if the voter_id exists in ballot
// * check if voter has not already voted
// * if everything alright update voters weight to 1
// only chair person can give right to vote
assert_eq!(caller,self.chair_person, "only chair person can give right to vote");
// the voter does not exists
assert_eq!(voter_opt.is_some(),true, "provided voterId does not exist");
let voter = voter_opt.unwrap(); // the voter should not have already voted
assert_eq!(voter.voted,false, "the voter has already voted");
voter.weight = 1;
}
...

Now that the voter has the right to cast a vote, let’s create a voting function that will:

  • Take the proposal index as input,
  • If the caller is a valid voter and has not already cast their vote, update the proposal with the weight of the voter,
  • Set voted property of Voter to true.

Voting

.../// Give your vote (including votes delegated to you)
/// to proposal `proposals[proposal]`.
#[ink(message)]
pub fn vote(&mut self, proposal_index: i32) {
let sender_id = self.env().caller();
let sender_opt = self.voters.get_mut(&sender_id);
// ACTION: check if the person calling the function is a voter
// * check if the person has not already voted
// * check if the person has the right to vote
assert_eq!(sender_opt.is_some(),true, "Sender is not a voter!"); let sender = sender_opt.unwrap();
assert_eq!(sender.voted,false, "You have already voted");
assert_eq!(sender.weight,1, "You have no right to vote"); // get the proposal
let proposal_opt = self.proposals.get_mut(proposal_index as usize);
// ACTION: check if the proposal exists
// * update voters.voted to true
// * update voters.vote to index of proposal to which he voted
// * Add weight of the voter to proposals.vote_count
assert_eq!(proposal_opt.is_some(),true, "Proposal index out of bound"); let proposal = proposal_opt.unwrap();
sender.voted = true;
sender.vote = Some(proposal_index);
proposal.vote_count += sender.weight;
}...

Winning proposal

Now as elections go, we elect the Proposal with maximum votes as the winner. Let's implement a function to retrieve this proposal.

/// @dev Computes the winning proposal taking all
/// previous votes into account.
fn winning_proposal(&self) -> Option<usize> {
let mut winning_vote_count:u32 = 0;
let mut winning_index: Option<usize> = None;
let mut index: usize = 0;
for val in self.proposals.iter() {
if val.vote_count > winning_vote_count {
winning_vote_count = val.vote_count;
winning_index = Some(index);
}
index += 1
}
return winning_index
}
/// Calls winning_proposal() function to get the index
/// of the winner contained in the proposals array and then
/// returns the name of the winner
pub fn get_winning_proposal_name(&self) -> &String {
// ACTION: use winning_proposal to get the index of winning proposal
// * check if any proposal has won
// * return winnning proposal name if exists
let winner_index: Option<usize> = self.winning_proposal();
assert_eq!(winner_index.is_some(),true, "No Proposal!");
let index = winner_index.unwrap();
let proposal = self.proposals.get(index).unwrap();
return &proposal.name
}

And now let’s get to the final implementation of Voter being able to delegate their voting rights to other voters.

Delegation

Our voter struct already has a delegate option which may contain an AccountId.

...
pub struct Voter {
...
delegate: Option<AccountId>,
...
}
...

The delegate function will:

  • Take AccountId (other than caller itself) as input.
  • Make caller’s voted to true.
  • Make caller’s delegate to the other AccountId.
  • We do the above prior to checking if the other AccountId to delegate the vote has already voted, this ensures when the function panics in such condition, the changes made to caller's voted and delegate are rolled back.
...
/// Delegate your vote to the voter `to`.
/// If the `to` has already voted, you vote is casted to
/// the same candidate as `to`
#[ink(message)]
pub fn delegate(&mut self, to: AccountId) {
// account id of the person who invoked the function
let sender_id = self.env().caller();
let sender_weight;
// self delegation is not allowed
assert_ne!(to,sender_id, "Self-delegation is disallowed.");
{
let sender_opt = self.voters.get_mut(&sender_id);
// the voter invoking the function should exist in our ballot
assert_eq!(sender_opt.is_some(),true, "Caller is not a valid voter");
let sender = sender_opt.unwrap();
// the voter must not have already casted their vote
assert_eq!(sender.voted,false, "You have already voted");
sender.voted = true;
sender.delegate = Some(to);
sender_weight = sender.weight;
}
{
let delegate_opt = self.voters.get_mut(&to);
// the person to whom the vote is being delegated must be a valid voter
assert_eq!(delegate_opt.is_some(),true, "The delegated address is not valid");
let delegate = delegate_opt.unwrap(); // the voter should not have already voted
if delegate.voted {
// If the delegate already voted,
// directly add to the number of votes
let voted_to = delegate.vote.unwrap() as usize;
self.proposals[voted_to].vote_count += sender_weight;
} else {
// If the delegate did not vote yet,
// add to her weight.
delegate.weight += sender_weight;
}
}
}
...

and accordingly, the new unit tests:

...
#[ink::test]
fn give_voting_rights_work() {
let mut ballot = Ballot::default();
let account_id = AccountId::from([0x0; 32]);
ballot.add_voter(account_id);
ballot.give_voting_right(account_id);
let voter = ballot.get_voter(account_id).unwrap();
assert_eq!(voter.weight,1);
}
#[ink::test]
fn voting_works() {
let mut ballot = Ballot::default();
ballot.add_proposal(String::from("Proposal #1"));
ballot.vote(0);
let voter = ballot.get_voter(ballot.get_chairperson()).unwrap();
assert_eq!(voter.voted,true);
}
#[ink::test]
fn delegation_works() {
let mut ballot = Ballot::default();
let to_id = AccountId::from([0x0; 32]);
ballot.add_voter(to_id);
ballot.delegate(to_id);
let voter = ballot.get_voter(ballot.get_chairperson()).unwrap();
assert_eq!(voter.delegate.unwrap(),to_id);
}
#[ink::test]
fn get_winning_proposal_name_working() {
let mut ballot = Ballot::default();
ballot.add_proposal(String::from("Proposal #1"));
ballot.add_proposal(String::from("Proposal #2"));
ballot.vote(0);
let proposal_name = ballot.get_winning_proposal_name();
assert_eq!(proposal_name, "Proposal #1");
}
...

This wraps up our tutorial on creating a Ballot contract on Edgeware.

If you want to play with the completed implementation of the Ballot contract, here’s the Github link.

We plan to continue this tutorial as a series on blockchain governance, so follow the publication to stay updated.

Learn more

  • Reach out for access to our beta release — hi@firefly.exchange
  • Join the conversation on Discord
  • Follow us on Twitter

Join us!

Help us reimagine finance — view our open roles.

--

--