The Voting Smart Contract#

This is the first part of a tutorial on smart contract development. In this part you will focus on how to write a smart contract in the Rust programming language using the concordium-std library.

The voting smart contract allows for conducting an election with several voting options. When the election is initialized, a deadline is set, after which no more votes may be cast. Only accounts (not other smart contracts) are eligible to vote. Each account can change its selected voting option as often as it desires until the deadline is reached.

Warning

This contract is not meant for production. It is an example to illustrate how to use the standard library and the tooling Concordium provides. There is no claim that the logic of the contract is reasonable or safe. Do not use these contracts as-is for anything other than experimenting.

Preparation#

Before you start, make sure to have the necessary tooling to build Rust contracts. The guide Install tools for development shows you how to do this.

You also need to set up a new smart contract project. Follow the guide Set up a smart contract project and return to this point afterwards.

Basic setup#

The source code of your smart contract is going to be in the src directory, which already contains the file lib.rs, assuming you follow the above guide to set up your project.

The smart contract template also includes some examples tests under the tests directory, which you can delete for now. You will come back to tests later in this tutorial.

In the lib.rs file, start by bringing everything from the concordium-std library into scope by adding the line:

use concordium_std::*;

This library contains everything needed to write a smart contract, such as some parameters, functions, and tests. It provides convenient wrappers around some low-level operations making your code more readable, and although it is not strictly necessary to use this, it will save a lot of code for the vast majority of contract developers.

The voting contract should allow for the following operations:

  • initializing the election.

  • viewing general information about the election.

  • voting for one of the options.

  • tallying the votes for a requested voting option.

A few basic functions are necessary for these operations to work:

  • init

  • view

  • vote

  • get_votes

Now, let’s examine the code in the example voting smart contract here.

The ElectionConfig data structure defines the configuration of the smart contract and is an input to the init function. In the example code, it contains a description of the election, the voting options, and the deadline of the election. Voting options is provided as a list of strings, however, it is important to remember that there is a limit to the parameter size (65535 bytes), so the maximum size of the list of voting options is large, but limited. For more information, see Contract instance limits.

The view function also returns the ElectionConfig type, so that users can, for instance, see the available options to vote for.

In the vote function, the contract specifies who may vote and when (only accounts may vote and only before the deadline). If a contract tries to vote, an error occurs.

let acc = match ctx.sender() {
    Address::Account(acc) => acc,
    Address::Contract(_) => return Err(VotingError::ContractVoter),
};

And if the deadline has passed, an error occurs.

ensure!(
    ctx.metadata().slot_time() <= host.state().config.deadline,
    VotingError::VotingFinished
);

get_votes gets the number of votes for a specific voting option.

State contains the state of the contract which can be mutated when invoking the vote entrypoint.

Initializing#

The election is open from the point in time that this smart contract is initialized until the deadline.

Performance considerations#

An important aspect of the voting smart contract to highlight is the way that votes are tallied.

Note the data duplication in the State struct. Two HashMaps are used: one that maps accounts to the option they voted for, and one that maps voting options to the number of votes it has received. However, if you think about it, only the first HashMap, the ballots field is necessary. The tally can of course always be determined by examining the ballots field. This requires looping over all ballots, counting how many votes exist for a specific option. However, such a loop would be practically unbounded, as it is only limited by the number of votes. Such a loop could exhaust the energy budget of the smart contract functions, potentially making the smart contract unusable and vulnerable to a kind of DDoS attack.

Thus, we favor duplicating the data in another HashMap where the vote count can be retrieved and updated in constant time.

It’s important to keep these performance considerations in mind when writing smart contracts. In general, writing smart contracts requires a different methodology than most other usual software development. Be sure to think carefully and read up on the common issues and security problems that may occur when you develop smart contracts.

In the next part of the tutorial, we will set up a frontend to make it easier to interact with the smart contract.

Was this article helpful?
Legal information