Introduction

In the previous article we touched briefly on what Fi is and went through a very basic example of its usage. In this article we are going to apply some more advanced concepts and create a contract that actually may have some use in the Tezos Ecosystem. The goal is to create a smart contract that allows a delegation service to pass on its voting privileges in a more democratic way to its delegations.

Goals:

This contract will allow only delegations participating in a service to vote.

The contract will allow for the owner to add ballets.

The contract will limit voters to one vote per delegation.

The contract will only allow the owner to add who can vote.

The contract will only allow the owner to update the description.

Time To Develop

The key to every good contract is a well thought out design. Let’s get started by sectioning out our program into the following sections:

Making Storage For A Voter

Adding Authorized Voters

Defining A Proposal And Making A Ballot

Adding A Proposal

Making The Vote Functionality

Adding A Contract Description

Making Storage For A Voter

In effort to be simplistic, all we really need to know about a voter is if they have voted, not what proposal they voted for. This also helps keep the anonymity of the vote, which is important when it comes to public permission-less blockchains.

storage map[address=>bool] voter;

As you can see above, we did just that. We map a voter by their address to a bool value representing whether the voter voted or not.

Definitions:

address : Addresses are untyped native contract addresses stored on the Tezos blockchain.

: Addresses are untyped native contract addresses stored on the Tezos blockchain. bool : Possible values being either true or false.

Adding Authorized Voters

In this contract we only want a select amount of voters to be able to vote. Because we are only allowing a finite amount of participants in this contract we want to make sure only the owner of the contract can add delegations to the voting map. To do this, we need to make an entry point for the owner to invoke. See below.

entry addAuthorizedVoter(address addr){

if (SENDER == OWNER) {

storage.voter.push(input.addr, bool false);

} else {

throw(string "Not authorized to add a voter.");

}

}

The above entry point introduces some new concepts not yet seen in the previous tutorial. Note the syntax of the if/else conditional, hopefully a fairly familiar concept to most developers. In addAuthorizedVoter we first check if the SENDER is equal to the OWNER .

By doing this we can authenticate if the person invoking this entry point is authorized to add a voter. If the person is not authorized we throw a message back letting them know. If the person is authenticated (the owner), the entry point’s input address is added to the voter map, with value initialized to false to show that the voter has not yet voted.

Definitions

if/else : If/Else statements allow you to branch your logic in multiple ways based on the resolution of an expression. If the expression resolves as true, the code within the curly braces is executed.

: If/Else statements allow you to branch your logic in multiple ways based on the resolution of an expression. If the expression resolves as true, the code within the curly braces is executed. SENDER : Returns address , the address of the current transaction source.

: Returns , the address of the current transaction source. OWNER : Returns address , the address of the current contract.

: Returns , the address of the current contract. throw : This allows the developer to exit execution of the script — an optional value can be provided as an argument, which will be returned as an error.

Defining A Proposal And Making A Ballot

Now that we have storage for a voter, let’s create an object to represent what is being voted on. In this case we need to make a Proposal object. The Proposal object will say what the proposal is, and how many votes the proposal has.

struct Proposal (

string description,

int votes

);

Now that we have a Proposal object let’s construct storage for the Proposal(s) in a ballot.

storage map[int=>Proposal] ballot;

Note that we created a another map but this time we mapped an int to a Proposal, calling the map ballot. We will use the integer key in the map as identification for the proposal to be voted on.

Definitions

int : Possible values being any integer value (negative and positive)

Adding A Proposal

Now we need the ability to let the contract owner add a proposal to the ballot to be voted on.

entry addProposal(int id, Proposal proposal){

if (SENDER == OWNER) {

storage.ballot.push(input.id, input.proposal);

} else {

throw(string "Not authorized to add a ballet.");

}

}

This entry point is very similar to the addAuthorizedVoter entry. We again check if the sender is indeed the owner, and throw an unauthorized message back to the sender if not. If the sender is authenticated we push a new proposal to the map with the inputs, id and proposal.

Definitions

push : Pushes item into map at index key, will add the element.

Making The Vote Functionality

Now that we have proposals in a ballot, and voters, we need to add an entry point to allow the voters to vote for a proposal.

entry placeVote(int vote){ if (in(storage.voter, SENDER) == bool false) {

throw(string "You are not authorized to vote.");

}



if (in(storage.ballot, input.vote) == bool false) {

throw(string "No ballot exists for your vote.");

}





let bool myVote = storage.voter.get(SENDER);

let Proposal proposal = storage.ballot.get(input.vote);



if (myVote == bool false) {

myVote = bool true;



proposal.votes.add(int 1);

storage.ballot.push(input.vote, proposal);

storage.voter.push(SENDER, myVote);

} else {

throw(string "You have already voted. Voters may only vote once.");

}

}

In this entry point we take the input vote which will represent the int key in the ballot map. We then use use an if conditional to check whether the sender exists as a voter in the contract. If the sender doesn’t exist we throw and unauthorized message back to the sender. This is made possible by an existing function in Fi for maps called in .

Additionally after checking whether the sender is authorized to vote we check if the vote input from the sender matches with an existing proposal.

We then get the current state of the sender in the voting map and store it in a bool variable called myVote. This is done by using let . It’s worth noting that you cannot use let inside a conditional. We again create another variable, this time of type Proposal called proposal. We assign proposal to the associated proposal that’s being voted for.

Next we check whether the voter has already voted in an if conditional. If the voter has previously voted, we throw a message back telling them. If the voter hasn’t voted yet, we enter the conditional. Next we assign myVote to true, to later notify the voter map that the voter has voted.

We then take the proposal retrieved earlier and use Fi’s built in add function to add the vote to the vote count under that proposal. We then push proposal and myVote back into their maps, updating the storage.

Definitions

in : Returns bool if the key exists as a map key in the map

: Returns bool if the key exists as a map key in the map get : Returns item with the index key. Fails if key doesn’t exist (it is advised to use in first)

: Returns item with the index key. Fails if key doesn’t exist (it is advised to use first) let : Defining a new variable.

: Defining a new variable. add : Returns the sum of two or more provided arguments, starting from left to right. Add can be used as a variable modifier, like it’s use above.

Adding A Contract Description

Now that we have the basis of the contract finish, it might not be a bad idea to create a convenience storage variable describing the ballot.

storage string info;

This will allow the owner to communicate any special information about the ballot or the voting process to the voters. Let’s create an additional entry point to allow the owner to add this info.

entry addInfo(string desc){

if (SENDER == OWNER) {

storage.info = input.desc;

} else {

throw(string "Not authorized to set description.");

}

}

We again check if the owner is the sender, and if not we throw an unauthorized message back to the sender. If the owner is the sender, we add the input string desc as storage for info.

Putting It All Together

struct Proposal (

string description,

int votes

); storage map[address=>bool] voter;

storage map[int=>Proposal] ballot;

storage string info; entry addAuthorizedVoter(address addr){

if (SENDER == OWNER) {

storage.voter.push(input.addr, bool false);

} else {

throw(string "Not authorized to add a voter.");

}

} entry addProposal(int id, Proposal proposal){

if (SENDER == OWNER) {

storage.ballot.push(input.id, input.proposal);

} else {

throw(string "Not authorized to add a ballet.");

}

} entry addInfo(string desc){

if (SENDER == OWNER) {

storage.info = input.desc;

} else {

throw(string "Not authorized to set description.");

}

} entry placeVote(int vote){ if (in(storage.voter, SENDER) == bool false) {

throw(string "You are not authorized to vote.");

}



if (in(storage.ballot, input.vote) == bool false) {

throw(string "No proposal exists for your vote.");

}





let bool myVote = storage.voter.get(SENDER);

let Proposal proposal = storage.ballot.get(input.vote);



if (myVote == bool false) {

myVote = bool true;



proposal.votes.add(int 1);

storage.ballot.push(input.vote, proposal);

storage.voter.push(SENDER, myVote);

} else {

throw(string "You have already voted. Voters may only vote once.");

}

}

Conclusion

Congratulations you’ve successfully developed a smart contract in Fi that will allow delegations to vote on proposals initiated by their delegate. This is especially important for delegation services that wish to pass on their voting power to their clients.

In this tutorial you hopefully picked up a few new skills with Fi:

How to use the if/else conditional.

conditional. The importance of the SENDER , and OWNER key words.

, and key words. How to properly make use of the in , get , and push functions for maps.

, , and functions for maps. How to properly use Fi’s add function.

function. How to define a variable using let .

. Understanding the bool , int , and address types.

In the next tutorial we will learn how to take the above contract and use the combination of the NodeJs fi-compiler module, and the eztz JavaScript library to make managing the contract easier.

A special thank you to TezTech for sponsoring this content.

Resources