Voting Contracts, Part 1— Simple Contract

Eszymi
Coinmonks
6 min readSep 12, 2023

--

In the previous part of this series, I described the possibilities that using blockchain technology offers for conducting voting. In this part, I would like to present the simplest contract that could be used for this purpose. This contract will also serve as a starting point for introducing more enhancements and capabilities.

Let’s begin by introducing the ideas that guided me in creating this program, and then I will proceed to analyze the individual parts of the contract. The entire code I am presenting can be found on my GitHub.

Ideas

The presented program allows for the creation of proposals to which eligible individuals can respond with “yes” or “no.” The voting period is determined during the creation of the proposal. After this time, it is possible to verify the results of the vote. I have also assumed that for the voting to be considered resolved, a minimum of 50% of all possible votes must be cast during its duration. To participate in the voting, one must possess the appropriate ERC20 token, the distribution of which among people can occur in any way. The number of votes one address can cast is equal to the number of tokens they hold. The contract also contains an arbitrary value that determines the minimum number of tokens an address must possess to create a new proposal.

Analyze

The foundational element of the program is the struct Proposal, which contains information about the proposal being voted on.

 struct Proposal {
bytes32 name; // short name (up to 32 bytes)
uint256 deadline; // expiring of the proposal
uint256 yesCount; // number of possitive votes
uint256 noCount; // number of negative votes
}

To create such a proposal, you need to call the createProposal function. As its arguments, you should provide the name of the proposal you are creating and the number of blocks during which it will be possible to vote on it.

function createProposal(bytes32 _name, uint256 lastBlocks)
public
enoughtTokens
nonReentrant
returns (uint256 numberOfProposal)
{
proposals.push(Proposal({name: _name, deadline: block.number + lastBlocks, yesCount: 0, noCount: 0}));
numberOfProposal = proposals.length - 1;
emit NowProposal(numberOfProposal, lastBlocks);
}

The new proposal is added to the proposals array

Proposal[] public proposals;

which serves as a kind of archive of all proposals created during the program’s lifespan. As we can see, the createProposal function has two modifiers: enoughTokens and nonReentrant. The role of nonReentrant is to prevent potential reentrancy attack, and its code has a straightforward structure.

bool public locked = false;
modifier nonReentrant() {
require(!locked, "NonReentract: locked");
locked = true;
_;
locked = false;
}

The second modifier, enoughTokens, checks whether the calling address has a sufficient number of tokens to perform such an operation. This ensures that only users with a minimum number of tokens can create proposals.

uint256 constant MIN_TOKENS_TO_CREATE_PROPOSAL = 10e18;
modifier enoughtTokens() {
require(voteToken.balanceOf(msg.sender) > MIN_TOKENS_TO_CREATE_PROPOSAL, "Modifier: not enought tokens");
_;
}

Certainly, here’s the information without any added characters or formatting:

In addition to creating new proposals, the program allows for voting on already created proposals. This is done using the vote function. When calling this function, you need to specify which proposal you want to vote for by providing its position in the proposals array. Additionally, you must decide how many votes you want to cast and whether you agree with the proposal or not. Voting can only take place during the lifetime of the proposal, which seems like a sensible condition.

// You need approve tokens to this contract before you call vote
function vote(uint256 numberOfProposal, uint256 votes, bool yes) public nonReentrant {
require(proposals[numberOfProposal].deadline > block.number, "Vote: too early");

voteToken.transferFrom(msg.sender, address(this), votes);
lockedTokens[numberOfProposal][msg.sender] += votes;

if (yes) {
proposals[numberOfProposal].yesCount += votes;
} else {
proposals[numberOfProposal].noCount += votes;
}
emit Voted(numberOfProposal, msg.sender, votes, yes);
}

Voting takes place by sending the appropriate number of tokens from your account to the smart contract’s address. These tokens will be locked there until the end of the proposal’s lifetime, for which you voted. After that period, you can recover them by calling the withdraw function.

function withdraw(uint256 numberOfProposal) public nonReentrant {
require(proposals[numberOfProposal].deadline < block.number, "Withdraw: too early");
uint256 amount = lockedTokens[numberOfProposal][msg.sender];
lockedTokens[numberOfProposal][msg.sender] = 0;
if (amount < voteToken.balanceOf(address(this))) {
voteToken.transfer(msg.sender, amount);
} else {
voteToken.transfer(msg.sender, voteToken.balanceOf(address(this)));
}
}

Such a solution aims to ensure that the same tokens are not used multiple times to vote on the same proposal. Additionally, it helps prevent attacks using flash loans. Another security measure against flash loans is the use of the block.number function rather than block.timestamp.

The symbolic way how we could set the moment of taking and giving back the flash loan to avoid the block.timestamp. The big rectangular is a symbol of the transaction in one block.

As we can see in the image, if we were to use block.timestamp to determine the time until which voting is allowed, and after which the withdraw function can be used, it would be possible to create an operation where an attack using flash loans could be feasible. This is because all these events happen within one block. However, using block.number ensures that the tokens used for voting must remain in the smart contract’s address for at least the duration of one block. This adds a layer of security against certain types of attacks, including flash loan attacks.

The last function implemented in your contract is the result function. Its purpose is to emit an event with the voting result. As you mentioned at the beginning of this entry, for the voting result to be accepted, a minimum of 50% of the tokens must have been used. This threshold is determined by the variable MINIMUM. By changing its value, you can modify the threshold at which you accept the voting result.

uint256 constant MINIMUM = 2; 
function result(uint256 numberOfProposal) public nonReentrant {
require(proposals[numberOfProposal].deadline < block.number, "Result: too early");
uint256 yes = proposals[numberOfProposal].yesCount;
uint256 no = proposals[numberOfProposal].noCount;
if (yes + no <= voteToken.totalSupply() / MINIMUM) {
emit NoResult(numberOfProposal);
} else {
bool outcome = yes > no ? true : false;
emit Result(numberOfProposal, outcome);
}
}

I have presented all the components of the simplest smart contract that can be used for voting. You can also find tests I have written to test this program on my GitHub.

Of course, the code presented here is very simple and has not undergone any audit, so it should be used with caution. Additionally, due to its simplicity, its shortcomings can be easily identified, which include:

  • the inability to vote without internet access;
  • the need to pay gas fees during voting;
  • tokens can be transferred from account to account, which can lead to unpredictable movements during votes on sensitive issues (vote-buying);
  • one person can acquire the majority of tokens;
  • in most situations, we want each person to have only one vote;
  • if two proposals are alive at the same time, you cannot vote on two simultaneously (during the vote on the first one, tokens are frozen in the smart contract address and cannot be used to vote on another proposal);
  • knowing which address voted in what way (it is possible to link an account address to a private individual, which may make people afraid to vote)

In the subsequent parts of this series, I plan to expand the program presented above to eliminate the mentioned problems.

I hope you find this post useful. If you have any idea, how could I make my posts better, let my know. I am always ready to learn. You can connect with me on LinkedIn and Telegram.

If you would like to talk with me about this or any other topic I wrote, feel free. I’m open to conversation.

Happy learning!

--

--