Fantom Archive — VM: Simple Voting Contract

Andre Cronje
Fantom Foundation
Published in
12 min readJun 7, 2019

by Andre Cronje

1. Requirements

A robust, KYC compliant, fraud proof voting solution

2. Scope

This document covers the following areas;

  • Campaign Setup
  • Voter Registration
  • Campaign Candidate Registration
  • KYC Document Uploading
  • KYC Document Downloading
  • KYC Document Validation
  • Voting
  • Result Tracking

3. Assumptions

  • An independent, accredited party will be doing the validation on KYC documents.
  • Tooling will be provided to interact with the Campaign Smart Contract. (Campaign Setup, Voter Registration, KYC Document Upload/Download, Voting, Result Tracking)
  • The Voters/Candidates/KYC Party will interact with the voting solution using a Fantom address.

4. Proposed Solution

4.1 Campaign Setup

This is the first step to initiating a new voting campaign. Tooling will be used to setup a new voting campaign. The campaign setup will require the following information:

  • Set Independent KYC party access (Approval Authorization, view/download KYC Documents via their Signing Key)
  • Define how the campaign will run.
  • The Campaign Owner defines a start time and an end time for the voting. Results are tallied once voting closes.
  • The Campaign Owner will trigger an event to open the voting and they will trigger an event to close the voting. Results are tallied once voting closes.
  • The Campaign owner defines a vote count. Once that vote count is reached by any member, voting will close and that person will be elected as winner.
  • Define the visibility of the campaign progress. This determines who can view the total number of votes vs registered voters, as well as the current placing of the candidates before the voting is closed.
  • Optional — upload a list of candidates with verification documents if the campaign setup party wants a pre-defined candidate list. The alternate is campaign candidates can register to be a candidate on their own accord.

4.2 Voter Registration

Voters will require some baseline information to be registered to vote. A personal identification number, documents validating the personal identification number, their signing key.

The Identification Documents are encrypted using ring signatures to allow multiple parties access. The Independent KYC Party as well as the Registering Voter’s signing keys will be used to encrypt the documents so that both parties have access to download/view the documents.

Once the documents are encrypted, they will be stored in a Document Store (IPFS/StorJ/Amazon S3). Once the documents have been uploaded, a registration event will occur on the Campaign Smart Contract, storing the Encrypted Data Location together with the personal data.

4.3 Campaign Candidate Registration

Campaign Candidates will require some baseline information to be registered to be a candidate. A brief description of themselves, their personal identification number, documents validating the personal identification number, their signing key.

The Identification Documents are encrypted using ring signatures to allow multiple parties access. The Independent KYC Party as well as the Registering Campaign Candidate’s signing keys will be used to encrypt the documents.

Once the documents are encrypted, they will be stored in a Document Store (IPFS/StorJ/Amazon S3). Once the documents have been uploaded, a registration event will occur on the Campaign Smart Contract, storing the Encrypted Data Location together with the personal data.

4.4 KYC Process

Validation based on the Campaign Smart Contract to be done by an Independent KYC Party. Independent KYC Party queries the Campaign Smart Contract to see what new, unvalidated registrations have occurred. Independent KYC Party will download the Encrypted Document Object’s Document Store location from the Campaign Smart Contract, as well as personal details to validate against.

Independent KYC Party downloads the Encrypted Document Object from the Document Store. The Independent KYC Party will decrypt the Documents (Decrypted Documents) using their signing key.

Validation occurs however the Independent KYC Party’s in-house standard structures currently occur on the Decrypted Documents. Inevitably, a couple of outcomes can occur from the validation process.

  1. A Decrypted Document is invalid and a feedback loop needs to occur between the Registering Voter and the Campaign Smart Contract, requiring a new document to be processed. This needs to feed back to the Campaign Smart Contract to indicate that registration is incomplete.
  2. A Decrypted Document is invalid and a feedback loop is not required. (Fraud, duplication, etc) This needs to feed back to the Campaign Smart Contract to indicate that registration was failed.
  3. All Decrypted Documents are valid. This needs to feed back to the Campaign Smart Contract to indicate that registration was successful.

In the case of A) Decrypted Document is invalid where re-upload occurs. An event will trigger to the Registering Voter to inform them of the invalid document. They will upload a new Encrypted Document Object to the Document Store which will update their Registration Event in the Campaign Smart Contract to Pending again.

4.5 Voting

Before voting can occur, the following needs to occur:

  • The open voting criteria needs to be met (Owner opens voting or Vote Start Time has been reached)
  • Voter needs to be registered to vote (Registered Voter)

The Registered Voter can pull the current candidate list via the provided tooling. The Registered Voter selects a candidate to vote for. They submit their vote to the Campaign Smart Contract. The Registered Voter is only allowed to vote once, all subsequent votes will be rejected.

4.6 Result Tracking

Tooling can be provided to view the current status of the Campaign. This covers the following areas:

  • The voting status (Not Started, Open, Closed)
  • How many Votes have been submitted
  • How many Approved, Registered Voters there are
  • The current position of Campaign members before voting has closed
  • The winning Campaign member

5. Campaign Smart Contract Overview

The goal is to provide a secure set of libraries which control the registration, KYC proof and voting portion of the solution. The goal is to allow for flexibility in terms of the type of voting campaign that is set up, what level of voting restrictions are required as well as what the voting period required is.

Campaign owner deploys a voting contract. The contract can be one of or a combination of the following campaigns:

  • BasicCampaign — Interface for the campaign logic.

See Appendix A

  • StandardCampaign — The owner triggers an event which starts the campaign which opens the voting functionality. The owner then triggers an event which stops the campaign, stopping the voting functionality. The votes are then tallied and the winners are populated.

See Appendix B

  • TimespanCampaign — The owner deploys the smart contract with a start timestamp and an end timestamp. Voting starts off as disabled. Once the start timestamp is hit, voting is opened. Once the end timestamp is hit again, the voting is closed. The owner then triggers the call to calculate the results and populate the winners.

See Appendix C

  • VoteCountCampaign — The owner deploys the smart contract with a vote goal. The owner triggers an event which starts the campaign which opens the voting functionality. Once one of the candidates achieve the vote goal the voting concludes. The results are then calculated and the winners are populated.

See Appendix D

  • RegisteredCandidates — The owner deploys a contract where users can register to be a candidate. Before voting is opened, users can register to be a candidate. Voters can view the list of candidates and submit their votes only for the registered candidates.

See Appendix E

  • ApprovedCandidates — The owner deploys a contract where the list of candidates have to be approved. Before the voting is opened, candidates can be added to the registered candidates list. These candidates are approved by a party configured on creation of the smart contract. Once the voting process has started, only the candidates on the approved candidates list may be voted for.

See Appendix F

  • RegisteredVoters — The owner deploys a contract where users can register to vote. Before voting is opened, users can register to vote. Once the voting process has started, only the voters on the registered voters list may vote.

See Appendix G

  • ApprovedVoters — The owner deploys a contract where the list of voters have to be approved. Before the voting is opened, voters can be added to the registered voters list. These voters are approved by a party configured on creation of the smart contract. Once the voting process has started, only the voters on the approved voters list may vote.

See Appendix H

6. Appendices

6.1 Appendix A

BasicCampaign.sol

/**

* @title Campaign

* @dev Simple version of Campaign interface

*/

contract BasicCampaign {

function startVote() public;

function vote(address _address) public view returns (uint256);

function endVote() public;

function getResults() public view returns (address[])

event VoteStarted();

event VoteEnded();

}

6.2 Appendix B

StandardCampaign.sol

/**

* @title StandardCampaign

* @dev StandardCampaign is a basic campaign

*/

contract StandardCampaign {

Event VoteStarted()

Event VoteEnded()

boolean owner

boolean voteStarted = false

boolean voteEnded = false

mapping(address, address) voters

mapping(address, uint256) votes

address[] results

/**

* @dev Initialises the contract

*

* Set owner to msg.sender

*/

constructor()

/**

* @dev Modifier that only allows an action to be executed when the vote is not open

*

* Require voteStarted = false

*/

modifier voteNotStarted()

/**

* @dev Modifier that only allows an action to be executed when the vote is open

*

* Require voteStarted = true && voteEnded = false

*/

modifier voteStarted()

/**

* @dev Modifier that only allows an action to be executed when the vote has concluded

*

* Require voteEnded = true

*/

modifier voteEnded()

/**

* @dev Modifier that only allows the owner to execute an action

*

* Require msg.sender == owner

*/

modifier onlyOwner()

/**

* @dev Open the voting process

*

* Call _preValidateStartVote()

* Call _startVote()

* Call _postValidateStartVote()

*/

startVote() public onlyOwner

/**

* @dev Voters get to vote

*

* Call _preValidateVote()

* Increment the votes mapping at _address by 1

* Set the voters mapping at msg.sender to _address

* Call _postValidateVote()

*/

vote(address _address) public

/**

* @dev Close the voting process

*

* Call _preValidateEndVote()

* Call _endVote()

* Call _postValidateEndVote()

*/

endVote() public onlyOwner

/**

* @dev Returns an ordered array of votes addresses indicating the results of the election

*

* return results

*/

getResults() public returns(address[])

/**

* @dev

* just to be overridden

*/

_preValidateStartVote() internal

/**

* @dev

* set voteStarted = true

* Emit VoteStarted

*/

_startVote() internal

/**

* @dev

* just to be overridden

*/

_postValidateStartVote() internal

/**

* @dev basic validation that the voter hasn’t voted yet and that voting is open

*

* Validate that voters mapping at msg.sender = 0x0 (hasn’t voted)

* Validate that voteStarted is true and voteEnded is false

*/

_preValidateVote() internal

/**

* @dev

* just to be overridden

*/

_postValidateVote() internal

/**

* @dev

* just to be overridden

*/

_preValidateEndVote() internal

/**

* @dev

* set voteEnded = false

* Emit VoteEnded

* Call _calculateResults()

*/

_endVote() internal

/**

* @dev

* just to be overridden

*/

_postValidateEndVote() internal

/**

* @dev Votes are tallied and populates results with the ordered votes

*

* Sort the votes mapping by its uint256 value in descending order.

* Populate the results array with the sorted addresses

*/

_calculateResults() internal

}

6.3 Appendix C

TimespanCampaign.sol

/**

* @title TimespanCampaign

* @dev TimespanCampaign is a campaign that has a pre-determined start and end point

*/

contract TimespanCampaign is StandardCampaign {

uint256 startTime

uint256 endTime

/**

* @dev initates the contract with start and end time

*

* Require _startTime > currentBlock.timestamp

* Require _endTime > _startTime

*

* Set startTime to _startTime

* Set endTime to _endTime

*/

constructor(uint256 _startTime, unint256 _endTime)

/**

* Require false — we want to disable startingVote early

*/

_preValidateStartVote()

/**

* if(block.timestamp >= endTime)

* Call _endVote()

* exit

* else

* Require startTime <= currentBlock.timestamp

* Require endTime >= currentBlock.timestamp

* if(voteStarted = false)

* Call _startVote() //otherwise _preValidateVote will reject on StandardCampaign

*

* Call super._preValidateVote()

*

*/

_preValidateVote()

/**

* @dev if the event is called before a vote is called after the end time, we call this.

* require block.timestamp >= endTime

*/

_preValidateEndVote()

/**

* @dev Public function to calculate results

*/

calculateResults() public onlyOwner voteEnded

}

6.4 Appendix D

VoteCountCampaign.sol

/**

* @title VoteCountCampaign

* @dev VoteCountCampaign is a campaign that completes once a specific number of votes are reached

*/

contract VoteCountCampaign is StandardCampaign {

uint256 voteLimit

uint256 totalVotes = 0

/**

* @dev initates the contract with start and end time

*

* Require _voteLimit > 0

*

* Set voteLimit to _voteLimit

*/

constructor(uint256 _voteLimit)

/**

* Require _voteLimit >= totalVotes

* Call super._preValidateVote()

*/

_preValidateVote()

/**

* Increment totalVotes by 1

*/

_postValidateVote()

/**

* If voteLimit <= totalVotes then call _endVote()

* Call super._postValidateVote()

*/

_postValidateVote()

/**

* Require false — we want to disable endingVote early

*/

_preValidateEndVote()

}

6.5 Appendix E

RegisteredCandidatesCampaign.sol

/**

* @title RegisteredCandidatesCampaign

* @dev RegisteredCandidatesCampaign is where a person needs to register to vote

*/

contract RegisteredCandidatesCampaign is StandardCampaign {

Event CandidateRegistered(address _voterAddress)

CandidateStruct (

address address,

bytes32 idNumber,

bytes32 name,

bytes32 description,

bytes32 encryptedDocuments,

uint256 votes

)

mapping(address, CandidateStruct) registeredCandidates

address[] registeredCandidatesList

uint256 registeredCandidatesCount = 0

address registeredCandidateValidator

/**

* @dev Modifier that only allows an action to be executed when the sender is the validator defined in the constructor

*

* Require msg.sender == registeredCandidateValidator

*/

modifier onlyRegisteredCandidateValidator()

/**

* @dev initiates the contract by setting the validator’s address. Only the validator can get candidate information

*

* set registeredCandidateValidator = _registeredCandidateValidator

*/

constructor(address _registeredCandidateValidator) public

/**

* @dev candidate registers their address

* Require _name != ‘’

*

* Create new CandidateStruct. Add the Struct to the registeredCandidates mapping

* Add msg.sender to registeredCandidatesList

* registeredCandidatesCount++

*/

registerCandidate(bytes32 _name, bytes32 _idNumber, bytes32 _description, bytes32 _encryptedDocuments) public voteNotStarted

/**

* @dev returns the candidate at the input address for validation

* Require _address != 0x0

*

* Create an in-memory CandidateStruct “candidate” from registeredCandidates at _address

* Return candidate.address, candidate.name

*/

getRegisteredCandidate(address _address) public onlyRegisteredCandidateValidator returns (address _address, bytes32 _name, bytes32 _description, bytes32 _idNumber, bytes32 _encryptedDocuments)

/**

* @dev returns all the registeredCandidates addresses

*

* Return registeredCandidatesList

*/

getAllRegisteredCandidates() public returns (address[] _candidates)

/**

* Require that the registeredCandidates[_address].address != 0x0

* Call super._preValidateVote()

*/

_preValidateVote()

/*

* set registeredCandidates[_address].votes ++

* Call super._postValidateVote()

*/

_postValidateVote()

}

6.6 Appendix F

ApprovedCandidatesCampaign.sol

/**

* @title ApprovedCandidatesCampaign

* @dev ApprovedCandidatesCampaign is a campaign where the candidates list is controlled by the owner

*/

contract ApprovedCandidatesCampaign is StandardCampaign {

Event CandidateApproved(address _candidateAddress)

Event CandidateDeclined(address _candidateAddress)

Event CandidateRejected(address _candidateAddress)

CandidateStruct (

address address,

bytes32 name,

uint256 votes

)

mapping(address, CandidateStruct) approvedCandidates

mapping(address, boolean) rejectedCandidates

address[] approvedCandidatesList

uint256 approvedCandidatesCount = 0

address approvedCandidateValidator

/**

* @dev Modifier that only allows an action to be executed when the sender is the validator defined in the constructor

*

* Require msg.sender == approvedCandidateValidator

*/

modifier onlyApprovedCandidateValidator()

/**

* @dev initiates the contract by setting the validator’s address. Only the validator can approve candidate information

*

* set approvedCandidateValidator = _approvedCandidateValidator

*/

constructor(address _approvedCandidateValidator) public

/**

* @dev approve a candidate

* Require _address != 0x0

* Require rejectedCandidates[_address] != true

*

* Set approvedCandidates[_address] = true

* approvedCandidatesCount++

* Trigger Event CandidateApproved(_address)

*/

approveCandidate(address _address) public onlyApprovedCandidateValidator voteNotStarted

/**

* @dev Indicate to the candidate that their documentation is invalid.

* Require _address != 0x0

* Require approvedCandidates[_address] != true

* Require rejectedCandidates[_address] != true

*

* Trigger Event CandidateDeclined(_address)

*/

declineCandidate(address _address) public onlyApprovedCandidateValidator voteNotStarted

/**

* @dev Reject a candidate from voting. Hard stop on being able to run for candidate.

* Require _address != 0x0

* Require approvedCacndidates[_address] != true

*

* Set rejectedCandidates[_address] = true

* Trigger Event CandidateRejected(_address)

*/

rejectCandidate(address _address) public onlyApprovedCandidateValidator voteNotStarted

/**

* @dev returns the candidate at the input address

* Require _address != 0x0

*

* Create an in-memory CandidateStruct “candidate” from approvedCandidates at _address

* Return candidate.address, candidate.name

*/

getApprovedCandidate(address _address) public returns (address _address, bytes32 _name)

/**

* @dev returns all the approvedCandidates addresses

*

* Return approvedCandidatesList

*/

getAllApprovedCandidates() public returns (address[] _candidates)

/**

* Require that the approvedCandidates[_address].address != 0x0

* Call super._preValidateVote()

*/

_preValidateVote()

/*

* set approvedCandidates[_address].votes ++

* Call super._postValidateVote()

*/

_postValidateVote()

}

6.7 Appendix G

RegisteredVoterCampaign.sol

/**

* @title RegisteredVoterCampaign

* @dev RegisteredVoterCampaign is a campaign where the voters need to register to vote

*/

contract RegisteredVoterCampaign is StandardCampaign {

Event VoterRegistered(address _voterAddress)

VoterRegistrationStruct(

address address,

bytes32 idNumber,

bytes32 name,

bytes32 encryptedDocuments

)

mapping(address, VoterRegistrationStruct) registeredVoters

uint256 registeredVotersCount = 0

address registeredVoterValidator

/**

* @dev Modifier that only allows an action to be executed when the sender is the validator defined in the constructor

*

* Require msg.sender == registeredVoterValidator

*/

modifier onlyRegisteredVoterValidator()

/**

* @dev initiates the contract by setting the validator’s address. Only the validator can get voter information

*

* set registeredVoterValidator = _registeredVoterValidator

*/

constructor(address _registeredVoterValidator) public

/**

* @dev A voter registers to vote

*

* Create a new VoterRegistrationStruct with the incoming parameters

* Set registeredVoters[msg.sender] = struct

* registeredVotersCount++

*/

registerVoter(bytes32 _idNumber, bytes32 _name, bytes32 _encryptedDocuments) public voteNotStarted

/**

* @dev Returns a registrVoter event

*

* return registeredVoters[_address] details

*/

getRegisteredVoter(address _address) public onlyRegisteredVoterValidator returns(bytes32 _idNumber, bytes32 _name, bytes32 _encryptedDocuments)

/**

* Require that the registeredVoters[_address].address != “0x0”

* Call super._preValidateVote()

*/

_preValidateVote()

/**

* Trigger VoterRegisered

* Call super._postValidateVote()

*/

_postValidateVote()

}

6.8 Appendix H

ApprovedVoterCampaign.sol

/**

* @title ApprovedVoterCampaign

* @dev ApprovedVoterCampaign is a campaign where the voting list is controlled by the validator

*/

contract ApprovedVoterCampaign is StandardCampaign {

Event VoterApproved(address _voterAddress)

Event VoterDeclined(address _voterAddress)

Event VoterRejected(address _voterAddress)

mapping(address, boolean) approvedVoters

mapping(address, boolean) rejectedVoters

uint256 approvedVotersCount = 0

address approvedVoterValidator

/**

* @dev Modifier that only allows an action to be executed when the sender is the validator defined in the constructor

*

* Require msg.sender == approvedVoterValidator

*/

modifier onlyApprovedVoterValidator()

/**

* @dev initiates the contract by setting the validator’s address. Only the validator can approve voter information

*

* set approvedVoterValidator = _approvedVoterValidator

*/

constructor(address _approvedVoterValidator) public

/**

* @dev approve a voter to vote

* Require _address != 0x0

* Require rejectedVoters[_address] != true

*

* Set approvedVoters[_address] = true

* approvedVotersCount++

* Trigger Event VoterApproved(_address)

*/

approveVoter(address _address) public onlyApprovedVoterValidator voteNotStarted

/**

* @dev Indicate to the voter that their documentation is invalid.

* Require _address != 0x0

* Require approvedVoters[_address] != true

* Require rejectedVoters[_address] != true

*

* Trigger Event VoterDeclined(_address)

*/

declineVoter(address _address) public onlyApprovedVoterValidator voteNotStarted

/**

* @dev Reject a voter from voting. Hard stop on being able to vote.

* Require _address != 0x0

* Require approvedVoters[_address] != true

*

* Set rejectedVoters[_address] = true

* Trigger Event VoterRejected(_address)

*/

rejectVoter(address _address) public onlyApprovedVoterValidator voteNotStarted

/**

* Require that the approvedVoters[_address] == true

* Call super._preValidateVote()

*/

_preValidateVote()

}

--

--