Voting Contract, Part 4— Snapshots votes

Eszymi
Coinmonks
6 min readNov 9, 2023

--

In the previous part, I presented and discussed the code that allows you to take part in the elections even without access to the Internet and trust in third parties. Additionally, this method allowed you to take part in the vote even without having to pay gas costs. Another important change that our project could be extended to include some type of security to try to avoid vote buying in voting.

Idea

Let’s imagine a project in which voting is done using tokens issued by this project. This project is used by people who are interested in it, and they own most of the tokens. However, if a new proposal is suddenly created regarding the future of the project, it may happen that people who do not use this project on a daily basis may notice a potential profit for themselves in this proposal. And in such a case, these people will try to obtain tokens and decide about the future of the project they have not used before. This way, people who actually use this product have less influence on its future.

The idea to reduce the scale of this event is to use the so-called snapshot. They consist in the fact that at certain, publicly unknown moments, records are made of which address has how many tokens. And this saved data can be used as a basis for later votes.

In our example, this entry may be made just before the new proposal is announced. And even if, after being informed about it, third parties buy most of the tokens from the market, they will not take part in the voting. This is due to the fact that when recording, these people did not have tokens.

Additionally, data on the number of tokens is saved and unchangeable. Thanks to this, when writing the code enabling voting, we do not have to worry about movements related to the transfer of tokens and their repeated use, i.e. the so-called double spend tokens. This means that, unlike my previous contracts, in this one we can relatively easily implement a mechanism that allows you to take part in several votes at the same time.

Fantastic!

Analyze the program

Since we already know what we would like our code to be able to do, let’s see how we can write it. You will find all the codes presented here, along with their tests, on my GitHub.

The basis of this code is the so-called ERC20Snapshot. This is the ERC20 standard extended with a snapshot mechanism, i.e. recording how many tokens each address has. In my project, I extended the standard ERC20Snapshot with several small functionalities.

pragma solidity 0.8.19;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SnapshotToken is ERC20Snapshot, Ownable {
address public snapshoter;

constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {
}

function mintPerUser(address[] calldata users, uint256[] calldata amounts) external onlyOwner {
for (uint256 i; i < users.length; ++i) {
_mint(users[i], amounts[i]);
}
}

function mint(address user, uint256 amount) external onlyOwner {
_mint(user, amount);
}

function setSnapshoter(address _snapshoter) external onlyOwner {
require(snapshoter == address(0), "snapshoter is set");
snapshoter = _snapshoter;
}

function snapshot() external {
require(msg.sender == snapshoter);
_snapshot();
}
}

mintPerUser this is functionality that I included in every previous token I used. It allows you to extract tokens for multiple addresses using one function.

setSnapshoter is a function that allows you to assign the address responsible for creating a new snapshot, and therefore using the function snapshot.

The main program is created based on the code from the previous section (VotinWithPermit.sol). Therefore, in this article, I will focus solely on the differences between these two programs and explain what they are.

The first difference is the creation of a variable snapshoter, which contains an address that has access to the snapshot function and creates a corresponding modifier and a function that allows you to change its content

address public proposer; // address which is able to create a new Proposal
address public snapshoter; // address which is able make a new snapshot
SnapshotToken public voteToken;

constructor(address voteTokenAddress, string memory name) EIP712(name, "1") {
voteToken = SnapshotToken(voteTokenAddress);
proposer = msg.sender;
snapshoter = msg.sender;
}

modifier Snapshoter() {
require(msg.sender == snapshoter, "Modifier: you're not snapshoter");
_;
}

function changeSnapshoter(address _newSnapshoter) external Snapshoter {
snapshoter = _newSnapshoter;
emit NewSnapshoter(msg.sender, _newSnapshoter);
}

function makeSnapshot() external Snapshoter {
voteToken.snapshot();
}

An additional, big difference is a slight change in the definition of the structure defining the proposal by adding a parameter snapshotID. This parameter determines the state from which snapshot will be taken into account when voting on a given proposal.

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

Thanks to this, the person creating a new proposal can easily decide which provisions he or she wants to use when voting.

Additionally, I created two new mappings

mapping(uint256 => mapping(address => bool)) public tookParticipate;
mapping(uint256 => mapping(address => mapping(bool => bool))) public useDelegateVote; //one persone can voty by delegate twice, one for each option

First of them, tookParticiate, determines whether a given address has already voted on a given proposal or not. Introducing this option allows you to force everyone to vote only once. This is not a necessary procedure, but I introduced it to increase the security of the entire program. To make sure that one address does not vote multiple times, I created a second one with mapping via delegateVote, useDelegateVote. However, here we must remember that it may happen that many people have authorized us to vote on their behalf, and these people may have different positions. For this reason, using this mapping, we will give you the opportunity to vote on a given proposal once for each possible answer.

The big changes are the introduction of changes to functions _vote, permitVote, delegateand delegateVote, in which I removed token sending from the voting address to the contract address. Instead, information about how many tokens were used for voting is placed in the appropriate mapping. This solution allows you to vote on many different proposals at once because the tokens are not blocked for their duration. This solution also allows us to get rid of functions withdraw.

tl;dr

The introduction of several changes allowed us to expand the functionality of the contract with some resistance to market movements as new proposals appear. Thanks to this, people who have been part of the system for longer have a greater influence on its future. Additionally, this change allows simultaneous voting on different proposals.

List of things to solve

To be continued…

In the next part, I will show how, using a dozen or so new lines, we can expand our program so that we can remove the next two items from the list above.

Hint — two new mappings that I introduced will prove to be crucial.

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!

--

--