Tezos smart contracts have matured a lot – and it’s time to use them in iOS apps, closer to the users’ hands. 😉 Throughout this tutorial we will write a smart contract using SmartPy.io and then move to Xcode and get a hands-on experience with interacting with our contract in Swift using generated code from tezosgen. Note that this tutorial will touch on the most important parts of RateMyTeam app and is not a complete step-by-step tutorial.

Overview of our Contract

Firstly, let me give you a brief of overview about what our contract should do and what’s the problem it solves. Every year, our company divides a set amount of money for Christmas bonuses. To make this process more transparent, we can leverage Tezos! In our contract we want to define three main groups: candidates, voters and a manager address. Candidates will be the different teams in our company – design team, iOS team, etc. Voters will have a limited amount of votes which they can then use to vote for the various teams. Manager account will then have an option to end the voting – at that point the current balance of the contract will be divided and sent to the account of the given teams depending on how many votes they have received during the voting process.

Let’s get started

For reference, you can find the smart contract here. Our first step will be initializing the contract with a given initial storage. There are multiple properties that we want to hold in our smart contract: 

  • `name`: Name of our smart contract, will be displayed in iOS app for better navigation
  • `manager`: Will have the ability to end the contract
  • `ballot`: This type will be a little bit more complicated. It will be a `sp.TMap` where the key will be the address of the candidate. As a value we will hold a `sp.TRecord` with `candidateName` and `numberOfVotes`. `candidateName` will be for differentiating between different teams, `numberOfVotes` will help us with keeping track of the current number of votes the given candidate has received.
  • `voters`: This will be `sp.TMap` where key will be `sp.address` and the value will be `sp.TInt` representing the amount of votes the voter has left
  • `totalNumberOfVotes`: A total number of casted votes – will help us with 

So, our final initialization looks like this:

import smartpy as sp

class Ballot(sp.Contract):
    def __init__(self, name, manager, candidates, voters, numberOfVotes):
        initialBallot = {}
        for candidate in candidates:
            # Initializing initialBallot where we save `candidateName` and set `numberOfVotes` to zero
            initialBallot[candidate[0]] = sp.record(candidateName = candidate[1], numberOfVotes = 0)
        initialVoters = { }
        for voter in voters:
            # For every voter set their remaining votes to maximum
            initialVoters[voter] = numberOfVotes
        self.init(name = name, manager = manager, ballot = initialBallot, voters = initialVoters, hasEnded = False, totalNumberOfVotes = 0, votesPerVoter = numberOfVotes)

Vote function

Let’s implement a vote function now. We will define a new `@sp.entry_point` `vote` that will accept a `map` where key will be a `sp.TAddress` of the candidate and value will be `sp.TInt` representing number of votes that the voter wants to cast for the candidate. For example `contract.vote(votes = {addr1: 2})` would mean casting to votes to `addr1`.

@sp.entry_point
def vote(self, params):
    #1
    sp.verify(self.data.hasEnded == False)
    #2
    sp.verify(self.data.voters.contains(sp.sender))
    #3
    sp.for vote in params.votes.items():
        sp.verify(self.data.voters[sp.sender] >= vote.value)
        self.data.ballot[vote.key].numberOfVotes += vote.value
        self.data.voters[sp.sender] -= vote.value
        self.data.totalNumberOfVotes += vote.value
)

During step #1 we verify that the voting has not ended (for obvious reasons).

During step #2 we want to make sure that the `sp.sender` is, indeed, a voter (thus eligible to vote).

Then we go through all the votes for different candidates and account them into our ballot. We check that the voter has a sufficient number of votes available, add the votes to the candidate and `totalNumberOfVotes` and subtract the votes from the total number of votes available to the voter.

End function

The second part of our contract will be concerned with ending the contract. Our contract can be ended only by a manager account. Once the voting has ended, all of the contract’s balance will be distributed to the candidates (divided by the votes they have received). This leads to the following code:

@sp.entry_point
def end(self, params):
    #1
    sp.verify(sp.sender == self.data.manager)
    sp.verify(self.data.hasEnded == False)
    sp.for candidate in self.data.ballot.keys():
    #2
    sp.if self.data.ballot[candidate].numberOfVotes != 0:
        sp.send(candidate, sp.split_tokens(sp.balance, sp.as_nat(self.data.ballot[candidate].numberOfVotes), sp.as_nat(self.data.totalNumberOfVotes)))
        self.data.hasEnded = True

Let’s go through this code again to see what it does. In #1 we verify that the account that is ending the contract is a manager and that the contract has not already ended (to ensure that the contract cannot be ended multiple times which does not make sense). Then we go through all the candidates (`self.data.ballot.keys()`).

For each candidate we make sure that it has more than one vote. If that’s true, we send the appropriate amount of tez to the candidate’s address. Unfortunately, division of tez is not a trivial operation in smartpy. To achieve our desired result, we need to use `sp.splitTokens`. In smartpy’s reference manual it is defined as follows: `sp.split_tokens(m, quantity, totalQuantity)`: Computes m * quantity / totalQuantity. After some simple conversion, our `sp.send(candidate, sp.split_tokens(sp.balance, sp.as_nat(self.data.ballot[candidate].numberOfVotes), sp.as_nat(self.data.totalNumberOfVotes)))`translates to sending `balance * numberOfVoters / totalNumberOfVotes`. After doing that for each candidate, our balance should be zero and we can end the contract.

Final Notes

Apart from that, you can see that our final code also includes tests – you should *always* test your contract (after all, you want to make sure it works well before using it in the real world). Explaining how exactly the tests work is out of scope for this tutorial. However, you can find a detailed tutorial on this topic at smartpy.io. In the next part of this tutorial, we will see how we can interact with our created contract in our SwiftUI app. See you there!

Leave a Reply

Your email address will not be published. Required fields are marked *