NOTE: This post has been updated, including adding a full video walkthrough. Please view here:

https://blog.enigma.co/secret-voting-an-update-code-walkthrough-605e8635e725

Introduction:

Blockchain-based voting has the potential to disrupt current voting systems and lead to new innovations in decentralized governance. Additionally, voting is a necessary component in cryptoeconomic systems such as TCRs. However, due to the public nature of blockchains, current voting implementations expose user information and allow anyone to trace how users have voted. While designs that protect votes during the polling process exist such as partial-lock commit reveal (PLCR), they present limited privacy because votes are public after the vote is concluded. Furthermore, the two-step process of commit-reveal has drawbacks for user experience.

The Enigma Protocol proposes a general decentralized off-chain layer for private computation and allows for a broad set of use cases related to secrecy such as secret voting, secret auctions, and secret TCRs. In this article, we present a private voting scheme that encrypts underlying votes and protects user privacy.

Voting Workflow:

The following diagram describes how a voting system would work on Enigma.

Workflow Description:

1 & 2. Send Ether to receive voting tokens. I have used a “Token Factory” to produce voting tokens which are OpenZeppelin Mintable tokens. The current arbitrary exchange rate is 10 tokens for 1 Ether.

3. Stake voting tokens into the contract to receive voting credits. These voting credits can be used to weight votes across any active poll.

4. Create a poll with a specified quorum percentage, poll description, and poll length. Anyone can create a poll so there is no sybil protection in place.

5. Vote in a poll. A user can only vote once in a poll and will specify their vote choice (which is encrypted by the dApp) and their vote weight (which is dependent on their staked token amount).

6. Currently, a poll is ended by the poll creator. They will manually need to end the poll after the poll’s voting period has expired.

7. Create a task for the Enigma Network. The Enigma Contract will gather the encrypted votes/weights, the vote tallying function, and other parameters and package them into a bundle called an Enigma Task.

8. The task will be sent out to a random SGX node on the Enigma Network so that it can privately compute our vote-tallying function.

9 & 10. The result of the computation will be sent back to the Enigma Contract, where it will then update the contract state on Ethereum. In our case, the contract will call a function that updates the outcome of the poll.

11. A user can withdraw tokens that are not in use.

Delving into the Voting Contract:

In this post, we will examine this voting contract: https://github.com/enigmampc/secret-contracts/blob/master/contracts/Voting.sol. Note that the repository containing the contract is extremely rough and a work-in-progress.

Imports:

At the top we have imported three files, SafeMath.sol, VotingToken.sol, and Enigma.sol

SafeMath.sol is a library from OpenZeppelin that prevents unsafe arithmetic such as integer overflow.

VotingToken.sol is our ERC20 token that serves as a voting token. You can modify this to import any ERC20 token.

Enigma.sol is the Enigma contract that is responsible for acting as a bridge between Ethereum and the Enigma Network. This file can be found in the enigma-contract repository and is required for a modifier that we will encounter later on.

Events:

Here we have declared three events:

The event pollStatusUpdate broadcasts the outcome of a poll after the votes have been tallied(e.g. Passed or Rejected)

These events can be listened for from your dApp in order to confirm that the contract worked properly.

Structs/State Variables:

This enum characterizes the stages of a poll. A poll is in the TALLY stage when an Enigma node is computing votes.

A poll struct has the following properties:

Most of these fields are straightforward. quorumPercentage defines the minimum percentage of votes that the poll requires before it can end. Also, recall that expirationTime identifies the timestamp in which the voting period will expire.

A Voter, which is stored in the Poll struct, looks like this:

Recall that “weight” is the number of voting tokens a voter has dedicated to their yes or no vote in the poll.

For a given voter, the TokenManager is responsible for keeping track of the total number of staked voting tokens, the number of tokens committed across active polls, and a past history of participated polls.

Below, the contract keeps track of every poll created and the token balances of every user. Each poll has a “Poll ID” which is denoted by pollCount. This is a global counter that is incremented every time a poll is created.

The constructor simply initializes the voting token and the Enigma contract.

Token Operations:

The first step for a user is to purchase voting tokens. This is done outside of the voting contract in TokenFactory.sol (see the secret-contracts repository). Additionally, VotingToken.sol serves as our voting token but can be customized by developers to be any ERC20 token.

Then, a user would stake their tokens in the voting contract. In return, the user receives token “credits” that can be used to weight their vote. For example, if a user stakes 10 tokens, that user can vote with a maximum of weight 10 for any active poll.

When a user wants to withdraw tokens, they can only withdraw tokens that are not in use. For example, let’s say Alice has a 10 token stake in the contract and has voted in two polls with weight 10 and 2 respectively. Now let’s say that the first poll has ended (the one where she voted with 10) and she wants to withdraw tokens. The maximum number of tokens she can withdraw is 8, since there are 2 tokens still in use by the second poll. This mechanism is modeled after Consensys’ PLCR voting implementation and results in greater voting liquidity.

Poll/Voting Operations:

In this contract, any address can create a poll. In order to create a poll, three fields need to be specified: the quorum percentage, the description of the poll, and the length of the poll in seconds. The new poll is associated with a poll ID, which is an integer derived from pollCount.

While a poll is active, any address can vote in the poll and must specify their vote choice and weight. In the current system, 1 = Yes and 0 = No. However, the voting dApp should encrypt the vote on the client side using the enigma-js library before passing it into the contract. Note that encrypted values on Enigma have to be stored as bytes. The user also specifies their weight, which is dependent on the number of voting tokens staked. Additionally, an address can only vote in a poll once.

After the voting period for a poll has expired, the creator of the poll is the only address that can end that poll. After this step, a dApp can now create an Enigma task to count the votes. As stated earlier, the task includes the function which will be executed inside the trusted part of the Enigma network as well as the encrypted arguments, contract address, and other parameters.

This helper function gets the encrypted vote and weight for a given voter in a poll. This function is required by the dApp in order to send the encrypted votes and weights to the SGX node for computation.

Callable and Callback:

Secret contracts written in Solidity need to be modified to include a “callable function” and “callback function. This is the defining feature when writing contracts with Enigma.

Callable:

After a poll has ended, the dApp will delegate vote tallying functionality to a secure enclave on the Enigma Network. As a result, the node will require a vote tallying function to compute. This function is called the “callable” function.

In the example of private voting, countVotes is the callable function and calculates the number of “yes” and “no” votes casted. This function takes in as arguments a poll ID, encrypted votes, and weights. The SGX node will automatically detect that the votes are encrypted and decrypt the arguments inside of the function. In the current release, there is no way to share contract state with the Enigma nodes so encrypted data must be passed as an argument.

Callback:

The “callback function” is the function that is called by the Enigma contract to finalize the state of the computation. It is the function that is immediately called by the contract if the callable function succeeds.

Again, with the example of private voting, this function is necessary for updating the status of a poll. Note that you should add a modifier that checks if the caller is the Enigma contract. Otherwise, anyone can update the state of the contract. Additionally, the return value(s) of the callable function must be the arguments of the callback function.

OPTIONAL: Getters/Helpers/Modifiers:

Here are the helper functions involved in the contract:

Conclusion:

This post examined a sample contract that used the Enigma Protocol to implement secret voting. We hope that future developers on Enigma can leverage the utility of private computation to create even more innovative privacy-preserving smart contracts. We are extremely excited to see what people will build.

The full contract is here: https://github.com/enigmampc/secret-contracts/blob/master/contracts/Voting.sol