How Does the Solidity Voting Smart Contract Work?

With this article, we are starting a journey of going through smart contract examples in Solidity.

  • We’ll first lay out the entire smart contract example without the comments for readability purposes.
  • Then we’ll dissect it part by part, analyze it and explain it.
  • Following this path, we’ll get a hands-on experience with smart contracts, as well as good practices in coding, understanding, and debugging smart contracts.

🌍 Source code: Please find the original code in the official docs.

Smart contract – Voting

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Ballot {
    struct Voter {
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    }

    struct Proposal {
        bytes32 name;
        uint voteCount;
    }

    address public chairperson;

    mapping(address => Voter) public voters;

    Proposal[] public proposals;

    constructor(bytes32[] memory proposalNames) {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        for (uint i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    function giveRightToVote(address voter) external {
        require(
            msg.sender == chairperson,
            "Only chairperson can give right to vote."
        );
        require(
            !voters[voter].voted,
            "The voter already voted."
        );
        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

    function delegate(address to) external {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "You have no right to vote");
        require(!sender.voted, "You already voted.");

        require(to != msg.sender, "Self-delegation is disallowed.");

        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

            require(to != msg.sender, "Found loop in delegation.");
        }

        Voter storage delegate_ = voters[to];

        require(delegate_.weight >= 1);

        sender.voted = true;
        sender.delegate = to;

        if (delegate_.voted) {
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            delegate_.weight += sender.weight;
        }
    }

    function vote(uint proposal) external {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Has no right to vote");
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        proposals[proposal].voteCount += sender.weight;
    }

    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

    function winnerName() external view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

Code breakdown and analysis

A smart contract begins by specifying the compatible compiler versions range.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

To facilitate automated documentation generation from the smart contract code, we’re using special annotation syntax like @title (the contract/interface title description) and @dev (extra details description for a developer) compliant with Ethereum Natural Specification Format (more).

/// @title Voting with delegation.
/// @dev Implements voting process along with vote delegation
contract Ballot {

A new complex type declaration is given by a keyword and name struct Voter.

It represents a single voter and describes it by the variables

  • weight (how many votes the voter carries; more than one in case of vote delegation),
  • voted (flag showing if the voter voted),
  • delegate (the voter assigned to vote in someone else’s name), and
  • vote (index of the proposal voted for).
    struct Voter {
        uint weight;        // Weight is accumulated by delegation.
        bool voted;         // If true, that person already voted.
        address delegate;   // Person who will vote instead of us.
        uint vote;          // Index of the voted proposal.
    }

A new complex type declaration that represents a single voting proposal.

    struct Proposal {
        bytes32 name;       // A short name of at most 32 bytes in length.
        uint voteCount;     // Number of accumulated votes.
    }

The chairperson’s address.

    address public chairperson;

Declaration of an object, or state variable voters (a map; stored in the storage); it ties a Voter object to its address and enables us to find out each voter’s address. The voters object visibility is public, meaning it’s available to this and other smart contracts.

    mapping(address => Voter) public voters;

ℹ️ Info: when a mapping is initialized, it is virtually initialized for all possible keys, i.e. voter addresses. Their corresponding values contain bytes set to zero, i.e. our Voter complex type.

proposals is a dynamically-sized array of objects of type Proposal. It is used for direct access to each proposal in a loop.

    Proposal[] public proposals;

The smart contract constructor takes a string array of proposalNames:

    constructor(bytes32[] memory proposalNames) {

The public variable chairperson is assigned with the address (value) held in msg.sender.

        chairperson = msg.sender;

The chairperson is also a voter and his weight is assigned 1.

        voters[chairperson].weight = 1;

Each proposal name is used to create a Proposal object, which is added (pushed) to the array proposals, and its vote count is set to 0. When the loop finishes, proposals will form a ballot with all candidates’ names and vote counts.

        for (uint i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

The giveRightToVote() function allows only the chairperson to give other voters the right to vote.

    function giveRightToVote(address voter) external {
        require(

If the condition evaluates to false, i.e. the sender isn’t the chairperson, changes to the state and Ether balances are reverted.

            msg.sender == chairperson,
            "Only chairperson can give the right to vote."
        );

If a voter already voted, the function will terminate and the voter won’t get the right to vote (again).

        require(
            !voters[voter].voted,
            "The voter already voted."
        );

If the voter’s vote weight isn’t 0, the function will return. Otherwise, the voter’s vote weight is assigned with 1.

        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

Function delegate() is only available from other contracts and accounts. It forwards (forfeits) a voter’s voting right to a delegated person (account).

    function delegate(address to) external {

Object sender stores a reference to a voters mapping element.

        Voter storage sender = voters[msg.sender];

Sender must have a right to vote, i.e., voters[msg.sender].weight > 0. sender.weight is initialized with 0, so it’s sensible to expect

        require(sender.weight != 0, "You have no right to vote");

The sender mustn’t have already voted.

        require(!sender.voted, "You already voted.");

A sender cannot delegate a vote to himself.

        require(to != msg.sender, "Self-delegation is disallowed.");

Iterates through the (potential) chain of voters with delegates, finding the voter without a delegate (with a .delegate initialized to address(0)).

        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

Ensures that a voter (sender) doesn’t indirectly become his delegate.

            require(to != msg.sender, "Found loop in delegation.");
        }

Stores a reference to an available delegate. There will be an available delegate because the function is executed past the delegation loop check.

        Voter storage delegate_ = voters[to];

The delegate must have a right to vote, otherwise, the function terminates.

        require(delegate_.weight >= 1);

The sender’s vote is marked (voted), and its delegate is set to the last delegate in a chain of delegates.

        sender.voted = true;
        sender.delegate = to;

If the delegate already voted, it will just increase the candidate’s number of votes, otherwise, the delegate’s weight is increased because it carries one more (voter’s) vote.

        if (delegate_.voted) {
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            delegate_.weight += sender.weight;
        }
    }

The vote() function is only available from other contracts and accounts. It implements an action of using a vote (sender’s vote and all delegated votes) by taking a proposal index

    function vote(uint proposal) external {

Stores a reference to the voter.

        Voter storage sender = voters[msg.sender];

The voter must have a right to vote, and he mustn’t have already voted, otherwise, the function terminates.

        require(sender.weight != 0, "Has no right to vote");
        require(!sender.voted, "Already voted.");

The sender’s vote is marked (voted) for the proposal index indicated by proposal.

        sender.voted = true;
        sender.vote = proposal;

The voting is executed by adding the sender’s votes to the overall number of votes.

        proposals[proposal].voteCount += sender.weight;
    }

The winningProposal() function is publicly available and returns the winning proposal’s index.

    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;

Loop scans and finds the proposal with the most votes. In case of a tie, the first candidate is marked as the winner.

        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

The winnerName() function is only available from other contracts and accounts; it cannot modify the contract state (read-only) and returns the winning proposal’s name.

    function winnerName() external view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

Our smart contract example is a simple one, enabling us to vote for any candidate from a list of proposal names given as an input argument to the smart contract.

When the contract instantiates via its constructor, it sets the contract creator as a chairperson and forms a list of proposals (a new data structure made of a proposal name and the vote count).

The contract recognizes two roles: a chairperson and voters.

A chairperson is a person, i.e. an account (address) that instantiated the smart contract. The voters are all the other accounts (addresses). Only a chairperson can give the voting right to any voter.

Once the voter has the right to vote (but not before), it can vote for a proposal or it can forward its vote to another voter, i.e. a delegate. In the case of a delegate who also decided to forward his voting right to another voter, the forwarded vote will be propagated along the delegation chain (a structure of multiple delegate-linked voters) until a voter without a delegate is found.

If that’s not the case, i.e. all voters are trying to delegate their vote to another voter, the delegation algorithm will inevitably make a full circle and try delegating the vote to the original voter and it will fail.

When voting succeeds, the proposal’s vote count increases by the voter’s weight. Every voter has a weight equal to 1, except for voters who are a without a voting right and have the weight 0, or delegated voters, whose weight is a multiple of 1.

Each time the winner is queried, current votes are counted and the proposal with the largest number of votes is proclaimed.

Appendix – The Contract Arguments

In this chapter we have the additional information for running the contract. We should expect that our accounts will change with each refresh of Remix, but bytes32-encoded name will stay unchanged.

ℹ️ Note: Each name is bytes32-encoded, meaning each character’s numerical value is encoded in hexadecimal form, appended to its predecessors, padded to 32-character length and prepended by 0x.

/*** The Address Book

The Chairperson:    0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

Proposal names:     Alice, Betty, Cecilia, Dana
Proposal names:     Alice, Betty, Cecilia, Dana
(name, bytes32-encoded name, account)

Alice:              0x416c696365000000000000000000000000000000000000000000000000000000  0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

Betty:              0x4265747479000000000000000000000000000000000000000000000000000000  0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db

Cecilia:            0x436563696c696100000000000000000000000000000000000000000000000000  0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB

Dana:               0x44616e6100000000000000000000000000000000000000000000000000000000  0x617F2E2fD72FD9D5503197092aC168c91465E7f2

Contract input argument:
["0x416c696365000000000000000000000000000000000000000000000000000000","0x4265747479000000000000000000000000000000000000000000000000000000","0x436563696c696100000000000000000000000000000000000000000000000000","0x44616e6100000000000000000000000000000000000000000000000000000000"]
***/

Conclusion

We started our smart contract example series with this article that implements a simple voting process.

First, we laid out clean source code (without any comments) for readability purposes. It’s much more convenient than wiping our glasses.

Second, we dissected the code, analyzed it, and explained each possibly non-trivial segment. As always with coding, the code is there to make the comments easier to read. Definitely not the other way around πŸ™‚