DEV Community

Cover image for How To Implement A Voting Smart Contract
Niharika Singh ⛓
Niharika Singh ⛓

Posted on • Edited on

9 4

How To Implement A Voting Smart Contract

This article is a part of a series called Corporate Governance on Blockchain.

Part 1 of the series outlines the potential applications of blockchain in corporate governance. It also provides an overview of the technical architecture of the shareholder voting solution to be implemented in this series and walks through how to set up your project to follow this series. Read part 1 here.

In this article, we will write a shareholder voting smart contract using Solidity.

Here is the overview of the idea we want to capture in the smart contract. We will write a function to:

  • Add candidates
  • Get candidates
  • Cast a vote

We will assume that each 'Annual General Meeting (AGM)', the annual meeting where shareholders vote on agendas to make key decisions for the company, will be different, so a different smart contract would be developed for each one.

Assuming that you've set up your project. You can find Dapp.sol

packages
  - dapplib
    - contracts
      -> Dapp.sol

Let's start by defining what our candidate will look like. For sake of simplicity, let's assume it will have an id, name and, voteCount.

struct Candidate {
uint id;
string name;
uint voteCount;
}
view raw Dapp.sol hosted with ❤ by GitHub

Now, let's define the addCandidate function. This function's job is to create a candidate listing on the blockchain. Instead of a candidate, you can also vote on agendas. The choice is yours.

The initial voteCount for each candidate will be zero.

mapping(uint => Candidate) public candidateLookup; // Each candidate will be identified with a unique ID
uint public candidateCount; // Keeps a track of the number of candidates and also fulfills the function to generate a unique ID
function addCandidate(string memory name) public {
candidateLookup[candidateCount] = Candidate(candidateCount, name, 0);
candidateCount++;
}
view raw dapp.sol hosted with ❤ by GitHub

We want to invoke the addCandidate function as soon as the smart contract is deployed, so we will call it from the Constructor() function.

constructor() public {
addCandidate("Kitty"); // First candidate ID: 0
addCandidate("Doggo"); // Second candidate ID: 1
}
view raw Dapp.sol hosted with ❤ by GitHub

The next step would be to fetch all candidates. There are two approaches you could choose from:

  1. Fetch candidates by ID: This means that if there are 50 candidates, each user will have to query the smart contracts 50 times. Let's say there are 20,000 voters, this means that your smart contract will be invoked 20,000 * 50 times (1,000,000 times). I don't think this is wise, you'd end up wasting a lot of bandwidth. Nonetheless, I'll still show you what this code might look like.
function getCandidate(uint id) external view returns (string memory name, uint voteCount) {
name = candidateLookup[id].name;
voteCount = candidateLookup[id].voteCount;
}
view raw Dapp.sol hosted with ❤ by GitHub
  1. Fetch all candidates at once. You will return ALL candidates at once. This means your smart contract will be invoked 20,000 times (# of users) instead of a million times. This is the approach we will go with.
function getCandidates() external view returns (string[] memory, uint[] memory) {
string[] memory names = new string[](candidateCount);
uint[] memory voteCounts = new uint[](candidateCount);
for (uint i = 0; i < candidateCount; i++) {
names[i] = candidateLookup[i].name;
voteCounts[i] = candidateLookup[i].voteCount;
}
return (names, voteCounts);
}
view raw Dapp.sol hosted with ❤ by GitHub

Here we are returning an array of names and an array of voteCounts. We can manipulate these arrays later using JavaScript to fit our needs.

Note 1: Since getCandidates() is NOT called from within the smart contract but will be called from the outside, we will use external keyword here.

Note 2: getCandidates() is a read only function. Reading from the blockchain is free. So we use the keyword view here.

Finally we need to add voting logic in our smart contract. Let's call this function vote. Since this function will be called from outside of the smart contract, we will use external keyword here. We will NOT use view keyword because this function writes your vote to the blockchain and writing to the blockchain is not free.

mapping(address => bool) public voterLookup;
function vote(uint id) external {
require (!voterLookup[msg.sender]);
require (id >= 0 && id <= candidateCount-1);
candidateLookup[id].voteCount++;
emit votedEvent(id);
}
event votedEvent(uint indexed id);
view raw Dapp.sol hosted with ❤ by GitHub

At the end, your smart contract should look like this-

pragma solidity >=0.5.0;
pragma experimental ABIEncoderV2;
import "./interfaces/IDappState.sol";
import "./DappLib.sol";
/*
VERY IMPORTANT SECURITY NOTE:
You will want to restrict some of your state contract functions so only authorized
contracts can call them. This can be achieved in four steps:
1) Include the "Access Control: Contract Access" feature block when creating your project.
This adds all the functionality to manage white-listing of external contracts in your
state contract.
2) Add the "requireContractAuthorized" function modifiers to those state contract functions
that should be restricted.
3) Deploy the contract that will be calling into the state contract (like this one, for example).
4) Call the "authorizeContract()" function in the state contract with the deployed address of the
calling contract. This adds the calling contract to a white-list. Thereafter, any calls to any
function in the state contract that use the "requireContractAuthorized" function modifier will
succeed only if the calling contract (or any caller for that matter) is white-listed.
*/
contract Dapp {
// Allow DappLib(SafeMath) functions to be called for all uint256 types
// (similar to "prototype" in Javascript)
using DappLib for uint256;
IDappState state;
// During deployment, the address of the contract that contains data (or "state")
// is provided as a constructor argument. The "state" variable can then call any
// function in the state contract that it is aware of (by way of IDappState).
constructor
(
address dappStateContract
)
public
{
state = IDappState(dappStateContract);
addCandidate("Kitty");
addCandidate("Doggo");
}
/**
* @dev Example function to demonstrate cross-contract READ call
*
*/
function getStateContractOwner()
external
view
returns(address)
{
return state.getContractOwner();
}
/**
* @dev Example function to demonstrate cross-contract WRITE call
*
*/
function incrementStateCounter
(
uint256 increment
)
external
{
return state.incrementCounter(increment);
}
/**
* @dev Another example function to demonstrate cross-contract WRITE call
*
*/
function getStateCounter()
external
view
returns(uint256)
{
return state.getCounter();
}
struct Candidate {
uint id;
string name;
uint voteCount;
}
uint public candidateCount;
mapping(address => bool) public voterLookup;
mapping(uint => Candidate) public candidateLookup;
function addCandidate(string memory name) public {
candidateLookup[candidateCount] = Candidate(candidateCount, name, 0);
candidateCount++;
}
// function getCandidate(uint id) external view returns (string memory name, uint voteCount) {
// name = candidateLookup[id].name;
// voteCount = candidateLookup[id].voteCount;
// }
function getCandidates() external view returns (string[] memory, uint[] memory) {
string[] memory names = new string[](candidateCount);
uint[] memory voteCounts = new uint[](candidateCount);
for (uint i = 0; i < candidateCount; i++) {
names[i] = candidateLookup[i].name;
voteCounts[i] = candidateLookup[i].voteCount;
}
return (names, voteCounts);
}
function vote(uint id) external {
require (!voterLookup[msg.sender]);
require (id >= 0 && id <= candidateCount-1);
candidateLookup[id].voteCount++;
emit votedEvent(id);
}
event votedEvent(uint indexed id);
}
view raw Dapp.sol hosted with ❤ by GitHub

In this article, we explored the various approaches to writing a resource-efficient smart contract. We also covered the important keywords (external, view) to understand and use properly when developing a smart contract. In part 3, we’ll outline how to connect your smart contract to a UI to make it simple for shareholders to interact with the smart contract functions we’ve written here.

Start building with DappStarter.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs