Implementing a voting system

A beginner's friendly series of articles in which you'll learn to code a Clarity Smart Contract. The goal is to develop an on-chain voting system.

  1. Write your first Clarity Smart Contract
  2. Store data in maps
  3. Implement the basic voting mechanism
  4. Test our Smart Contract with Clarinet
  5. Get the elected color
  6. Update or cancel a vote

Store data in maps

Allow only one vote per address

To do so, our contract will have to remember whether someone voted or not. By "someone", I mean the STX address of the voter which can be accessed with tx-sender. The data structure needed here is a data map, a key-value store where the key will be the address and the value a boolean.
Add this line right after the declaration of nb-of-voters (line 2):

(define-map votes principal boolean)

It tells Clarity to store a new map called votes. the keys will be STX addresses (principal) and the values will be booleans.

πŸ’‘ principal is a native Clarity type, just a like uint or boolean. It represents a wallet address or a contract address.

As a JS developer, my mental model for the vote map looks like some JSON:

{
  "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG": true,
  "ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC": true,
  //...
}

The next few lines will be quite theoretical. I'll give you all the informations you need to implement the check by yourself.

The function map-insert allows to add key-value pair in the map. It takes three arguments, the map-name, the key and the value.

(map-insert votes tx-sender true)

We'll add it to our code later. Let's see how we can use it to check if the caller already voted. To retrieve a value in map we'll use map-get?. It takes two arguments: the map-name and the key.

πŸ’‘ Noticed the ? in map-get? It's a convention meaning that the function returns an optional type. Which can either be (some value) or none.

To know if the caller already voted, we'll check if map-get? returns some value or not. The function is-none takes an optional value and returns true or false. Knowing that, we could use an if condition to check but a cleaner solution is to use asserts!. Here is a comparison of both.

;; the two functions behave exactly the same way

(define-private (ok-or-err1 (check bool))
  (if check
    (begin
      ;; do stuff...
      (ok true)
    )
    (err u1)
  )
)

(define-private (ok-or-err2 (check bool))
  (begin
    (asserts! check (err u1))
    ;; do stuff...
    (ok true)
  )
)

asserts! is cleaner for two reasons. It saves a level of indentation which is really useful when you want to perform multiple checks, you don't want nested ifs. Secondly, it makes it extra-clear that you want to throw an error if the condition is false.

πŸ’‘ asserts! ends with an !. It's again a convention to signal that a function may throw an error.

Exercise time

Now you will put this knowledge into practice. Now that you know about maps, is-none and asserts!, you may be able to check if the caller already voted. So that, if someone call the vote function twice, we can throw an error. Don't forget to add the map-insert as well.

Give it a try and look at the clues if you are stuck.

Clue 1: map-insert and tx-sender

So we want to insert the address of the caller in the votes map. We know that the address is stored in tx-sender. For now the value will simply be true.

(map-insert votes tx-sender true)
Clue 2: is-none and map-get?

We've seen that we could retrieve a value in a map with map-get?. In order to allow someone to call the vote function, we can check that the value returned by map-get? is equal to none.

(is-none (map-get? votes tx-sender))

It will return true or false.

Clue 3: assert!

The function assert! take two arguments: a boolean and an error. It does nothing if the boolean if true, otherwise it throws the error. So we can pass our previous `(is-none ...)```

(asserts! (is-none (map-get? votes tx-sender)) (err u403))

The error is just an error code. I arbitrarily used u403 because in HTTP it means "Forbidden". We'll talk more about error later.

To test your code, call the vote function several time with the clarinet console.

(contract-call? .color-vote vote) ;; should return (ok true)
(contract-call? .color-vote vote) ;; should return (err 403)
Solution

Here is what you code should look like at the end of this article. If you didn't, you can now look at the clues to get more in depth explication.

(define-data-var nb-of-voters uint u0)
(define-map votes principal bool)

(define-public (vote)
  (begin
    (asserts! (is-none (map-get? votes tx-sender)) (err u403))

    (map-insert votes tx-sender true)
    (ok (var-set nb-of-voters (+ (var-get nb-of-voters) u1)))
  )
)

(define-read-only (get-nb-of-voters) (var-get nb-of-voters))

Conclusion

You just learned the basics of the map data structures. We've seen define-map, map-insert and map-get?, soon we will also use similar functions to update and delete data in maps.
We've also seen how validate input data asserts!, we'll dot that a lot in the future. Error handling will be very important.
In the next article we'll implement the actual vote mechanism with a new data structure (lists) and our first private function.

πŸ’» Read the code on GitHub. The code of this article is on this branch.
There is a PR associated with this article.