Voting Contract, Part 2 — Delegate Somebody

Eszymi
Coinmonks
5 min readSep 18, 2023

--

In the previous section, I presented the simplest contract that could be used for conducting a vote. However, as demonstrated towards the end, this method has several drawbacks. One of them is the fact that in order to vote, we need to have internet access during the voting period. However, sometimes this is not possible. So, does that mean we can’t vote? Of course not!

Idea

In this part of the series, I would like to introduce the first stage of expanding our program. It’s called the delegate function, which allows us to authorize someone else to vote on our behalf. Thanks to this, it’s enough that only this person has internet access during the voting period, and they can vote on your behalf. Fantastic! Let’s see what changes we need to make to the previous project in order to implement the ability to delegate to a third party.

Analyze the program

The whole program with the tests you could find on my GitHub.

Firstly, we need to establish some assumptions. While working on this project, I assumed that we can delegate our tokens to any address, in the function known as to. However, during this process, we must specify in the voting which proposal they should be used for, declaring numberOfProposal. Of course, except that we have to inform how much of our tokens we would like to delegate to someone.

function delegate(address to, uint256 numberOfProposal, uint256 votes) public nonReentrant {
require(voteToken.balanceOf(msg.sender) >= votes, "Delegate: not enought tokens");
voteToken.transferFrom(msg.sender, address(this), votes);
lockedDelegatedTokens[numberOfProposal][msg.sender] += votes;
delegatedTokens[numberOfProposal][to] += votes;
emit Delegated(msg.sender, to, numberOfProposal, votes);
}

Before using this function, we have to approve contract’s address to use the transferFrom function. Without that, we will see a big, red error.

We can see that in the function, we are using two mappings that were not present in the previous project. These are: lockedDelegatedTokens, delegatedTokens. The first one is used to save information how many tokens msg.sender delegated to the proposal. This helps monitor balance in the situation when someone would like to delegate a few different people to the same proposal, because we have all information in one place. The second mapping delegatedTokens contains information how many votes has delegated address. This is helpful if a few different people would like to delegate exactly the same person.

However, as we can see, in the delegation process, we need to specify the voting number in which our tokens should be used. However, when we delegate a vote, the proposal has not yet been created (because if it already exists, what’s the point of using delegate?). Therefore, it is necessary to introduce a system that will make it much easier for us to predict the number. As a reminder, previously, to create a new proposal, you needed to have more tokens than a specific value. However, such a mechanism is not resistant to flash loans. Therefore, it’s very easy to imagine that someone could spam the creation of new proposals, making it very difficult to delegate to someone. For this reason, in this extension, it was decided to change the condition that allows the creation of a new proposal. I implemented this by introducing proposer, which is an address authorized to create new proposals.

address public proposer;

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

function createProposal(bytes32 _name, uint256 lastBlocks)
public
Proposer
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);
}

By making this function not publicly accessible, it increases the predictability of proposal numbering. This solution introduces some centralization into the project. However, it can be reduced in a controlled manner, for example, by using a multiSig wallet as this address. Another solution is to replace a single address with a list that contains the addresses of accounts authorized to create a new proposal, and then verifying whether the person wanting to create a proposal is on that list.

The described changes are not all that needs to be added to our project to make it fully functional. We also need to create a function that allows the use of delegated tokens for voting. This is precisely the purpose of the function delegateVote.

function delegateVote(uint256 numberOfProposal, uint256 votes, bool choose) public nonReentrant {
require(proposals[numberOfProposal].deadline > block.number, "DelegateVote: too late");

require(votes <= delegatedTokens[numberOfProposal][msg.sender], "DelegatedVote: too many votes");
delegatedTokens[numberOfProposal][msg.sender] -= votes;

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

As we can see, this function is simple, and we use the previously mentioned mapping delegatedTokens. However, a drawback of this solution is that the person we have authorized to vote on our behalf may not vote in line with our preferences. This is a significant disadvantage that we will try to address in the next part of this series. For the time being, we assume that we trust the person we delegated and have confidence that they will vote as we expect.

The last change occurred in the withdrawfunction. Delegating someone else to vote on our behalf does not mean that the tokens now belong to them. Therefore, after the voting period for the selected proposal ends, the delegating person can withdraw the tokens that were locked in the contract address during delegation.

function withdraw(uint256 numberOfProposal) public nonReentrant {
require(proposals[numberOfProposal].deadline < block.number, "Withdraw: too early");
uint256 lockedAmount = lockedTokens[numberOfProposal][msg.sender];
lockedTokens[numberOfProposal][msg.sender] = 0;

uint256 delegatedAmount = lockedDelegatedTokens[numberOfProposal][msg.sender];
lockedDelegatedTokens[numberOfProposal][msg.sender] = 0;

if (lockedAmount + delegatedAmount < voteToken.balanceOf(address(this))) {
voteToken.transfer(msg.sender, lockedAmount + delegatedAmount);
} else {
voteToken.transfer(msg.sender, voteToken.balanceOf(address(this)));
}
}

tl;dr

As we can see, by making small changes to the code from the previous part, we have created a new functionality that allows us to participate in voting despite a lack of internet access during the voting period. However, a necessary condition is that the person we delegate to is someone we trust. But, I think, we can update our table of problems to be solved a bit.

List of things to solve

To be continued…

In the next part, I will explain how we can overcome this small hurdle and vote even without access to the internet and without a trusted person. Additionally, this method will reduce the cost of participating in the vote. But for now, without further spoilers.

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!

--

--