Voting Contract, Part 3 — Signed Votes

Eszymi
7 min readSep 21, 2023

--

In the previous part, I presented code that allowed us to vote during an election, even without internet access during the voting period. However, that solution had one small caveat; it required us to trust a third party to vote in line with our beliefs. I said it was a small caveat, but in reality, I lied. Trusting the honesty of another person can sometimes be stressful, and there’s still a chance that our tokens could be used in a way that contradicts our views. Therefore, finding another solution is necessary.

Idea

In this part, I would like to present a solution based on what we can find in ERC20Permit. Specifically, we will utilize the ability to create a transaction just as we would like it to be executed and then sign it with our private key.

Signed transaction by your private key

Then, we can securely pass this signed information to a third party for execution when voting is possible. I know what you might be thinking: wasn’t this solution supposed to avoid relying on a third party? Yes, you’re right; it was supposed to. And in reality, it does. In the solution using delegate, the delegated person had the freedom to decide whether to vote or not and how to vote with our tokens. In this solution, the delegated person can decide whether to use our signed information or not, but they certainly cannot use it to vote differently from what we intended.

It may seem like a partial solution, but the person to whom we entrust our signed transaction doesn’t have to be a human. There are many possibilities; we can write a program that, at a specified time and date, sends a transaction to a designated address. However, this solution has a significant advantage that becomes apparent when we find more people who would also like to vote in this way.

More people, more options

If multiple people sign their transactions and all send them at the same time from one address, for example, using a multicall, the costs that one person has to bear are lower than if they were to do it individually. Additionally, the costs decrease as more people deposit their signed actions in a single transaction.

You can compare this to sending documents by mail. If you send them individually, you have to pay for each envelope and postage stamp. However, if multiple people need to send documents to the exact same place, they can coordinate to send them all in one package. While they will need a larger envelope than for a single set of documents and pay more for postage, the costs will be significantly lower than if they all sent them individually.

Of course, you don’t have to trust just one person. Let’s say a national referendum is taking place. Since referendums must be free, the government will likely set up places where you can deposit your signed actions. However, there will certainly be individuals or organizations who don’t trust the government and will create their own such locations. This means that we have several potential places where we can send our encrypted message. What’s even more beautiful is that if we send it to every possible location, we won’t have to pay gas fees, and at the same time, there’s a high probability it will be executed. Moreover, such an action is secure for us. This is because the signed transaction is for one-time use and allows for the execution of the specific action we chose earlier.

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.

At the very beginning, we need to import three new contracts: ECDSA, EIP712, and Nonce.

contract VotingWithPermit is EIP712, Nonces {
using ECDSA for bytes32;
.
.
.
}

You can find all of them in the OpenZeppelin repository. The code from ECDSA allows us to verify whether a message has been signed by the owner of the private key associated with the address provided when using the encrypted message. EIP712 is a standard for hashing and signing typed structured data, and Nonce will be used to verify whether the encrypted message is not being used again to prevent a replay attack.

We also need to modify the code in the token we are using. So far, we have been using a standard ERC20 token, but at this stage, it is not sufficiently advanced for our needs. One might think that ERC20Permit would be a good solution. However, besides transferring tokens, we also want to include information about which vote we want to participate in and whether we agree or disagree. Therefore, we have more information that we want to encrypt. For this reason, I decided to expand our original token standard to make it fit the entire project as closely as possible.

contract PermitToken is ERC20, Ownable {
address delegater;

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

function setDelegater(address _delegater) external onlyOwner {
require(delegater == address(0), "Delegater is set");
delegater = _delegater;
}

function approveForDelegate(address from, uint256 amount) external {
require(msg.sender == delegater, "You're not delegater");
_approve(from, delegater, amount);
}
}

As you can see, my proposal for expansion involved adding a new variable containing the authorized address to use the function, which I also implemented. We will understand the significance of this function shortly. Now, let’s focus on the last change in the program.

function permitVote(
address _owner,
uint256 numberOfProposal,
uint256 votes,
bool choose,
uint256 deadline,
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) public nonReentrant {
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}
bytes32 digest;
{
uint256 _nonce = nonces(_owner) + 1;
digest = keccak256(abi.encode(PERMIT_TYPEHASH, _owner,
numberOfProposal, votes, choose, _nonce, deadline))
.toEthSignedMessageHash();
}

require(hash == digest, "Permit: wrong hash");

address signer = ECDSA.recover(hash, v, r, s);
if (signer != _owner) {
revert ERC2612InvalidSigner(signer, _owner);
}

_useNonce(_owner);

voteToken.approveForDelegate(_owner, votes);

_vote(_owner, numberOfProposal, votes, choose);

emit PermitVoted(numberOfProposal, _owner, votes, choose);
}

The permitVote function allows voting using a properly constructed and signed transaction. Initially, it verifies whether the data provided as input to the function was indeed used in the signed transaction. This verification involves calculating the hash of the signed message. If it matches the provided hash, it means that all the data was also included in the analyzed transaction. Next, it checks whether the address that signed the transaction is the same as the address provided as _owner . This is where the v, r, and s values come into play. If everything matches, we can proceed with the voting process.

However, as we know from my previous posts, to use the votefunction, you must first execute an approveon the contract’s address. Yet, if we also required prior use of the approvefunction for the permitVotemethod, we would lose the opportunity for free voting. Hence, there is a function that we saw earlier in the code of the used token. It allows the contract (which is set as the delegator) to grant itself the right to execute transactions with tokens from the person who signed the message. Then, we have a straightforward voting process.

Of course, we see one more new thing — PERMIT-TYPEHASH. This is a constant defined to enhance the security of this method.

bytes32 public constant PERMIT_TYPEHASH = keccak256(
"Permit(address owner, uint256 numberOfProposal,
uint256 votes, bool choose, uint256 nonce, uint256 deadline)"
);

We now know how a contract can read an encrypted message and then perform operations specified in that message. However, the question arises: how do we encrypt a message?

function calculateRSV(
address owner,
string memory name,
uint256 numberOfProposal,
uint256 votes,
bool choose,
uint256 deadline
) public view returns (uint8, bytes32, bytes32, bytes32) {
uint256 _nonce = voting.nonces(owner) + 1;
bytes32 digest = keccak256(
abi.encode(voting.PERMIT_TYPEHASH(), address(owner),
numberOfProposal, votes, choose, _nonce, deadline)
).toEthSignedMessageHash();
(uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256(keccak256(bytes(name))), digest);
return (v, r, s, digest);
}

The code below was used for testing by me. The output parameters are the hashand v, rand s, which should be entered into the delegateVotefunction. The voting refers to the core contract we create in this series. What should be changed is to remove

uint256(keccak256(bytes(name)))

I use Foundry and test addresses, where a private key is made by this method, where name could be any arbitrary string. In normal use, you will have to put in the place of this element your private key.

tl;dr

Using a private key, we can create an encrypted message that allows us to cast a vote. Additionally, this enables us to reduce the cost of voting, even down to zero. So it’s time to scratch off the next item from the list.

List of things to solve

It can’t get any better!

To be continued…

In the next part, I will explain how you can have your cake and eat it too and protection against bought votes. See you…

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!

--

--