
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 dTrade 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 using struct
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 initiallyfalse
and is set totrue
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 anOption
. -
vote
Index of the proposal to which a user casts vote. This is created as anOption
and isNone
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 defaultwarning: 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 andHashMap
of voters, we'll need to have retrieval and storage functions. -
chair_person
is also aVoter
in theBallot
...
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 usingHashMap
).
#[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 ofVoter
totrue
.
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 thancaller
itself) as input. - Make caller’s
voted
totrue
. -
Make caller’s
delegate
to the otherAccountId
. -
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'svoted
anddelegate
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.
- Firefly
Website:
https://bluefin.io
Twitter:
https://twitter.com/bluefinapp
Telegram:
https://t.me/bluefinapp