Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signature voting PR #58

Open
wants to merge 7 commits into
base: signature-voting
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions README.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure you pull the latest from main branch. It will make some changes to the readme.

Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Template For Challenge - ETH Tech Tree
*--Change the above "Template For Challenge" to the challenge name--*
*--Add a paragraph sized story that pulls in the challenger to their mission--*
# Signature Voting - ETH Tech Tree

You're colonizing Mars and you have the opportunity to create a new society. Since it's known that you have some Solidity skills, the rest of your cohort asked you to code a 'trustless' voting system to make decisions about how this new world will be designed and governed. You need exactly one vote per wallet address per proposal. Imagine that each colonizer is issued exactly one wallet address so that there is no duplicate voting.

*--End of story section--*
Alice was working on some code for this voting system but didn't finish and decided to stay on Earth. Finish Alice's signature voting code.

## Contents
- [Requirements](#requirements)
Expand All @@ -28,7 +27,7 @@ foundryup

## Challenge Description
*--Edit this section--*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this line

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And any other lines with *--blah blah--*

Write challenge description here...
Voters can sign messages off chain. The sender of a 'vote' transaction may not be the wallet address that signed the message. So, we need a way to get the signer of a signed message in order to record their vote for the correct proposal.

Here are some helpful references:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure you remove these OR even better, add some references for the user to read.

*Replace with real resource links if any*
Expand Down
7 changes: 0 additions & 7 deletions packages/foundry/contracts/Challenge.sol

This file was deleted.

94 changes: 94 additions & 0 deletions packages/foundry/contracts/SignatureVoting.sol
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contract structure should match this:

// Layout of Contract:
  // version
  // imports

// Inside the contract declaration: 
  // errors
  // interfaces, libraries, contracts
  // Type declarations
  // State variables
  // Events
  // Modifiers
  // Functions

// Layout of Functions:
  // constructor
  // receive function (if exists)
  // fallback function (if exists)
  // external
  // public
  // internal
  // private
  // internal & private view & pure functions
  // external & public view & pure functions

Let's also designate the different sections with large block comments like this:

    ///////////////////
    // Public Functions
    ///////////////////

Lastly, use custom errors.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
//import { console2 } from "forge-std/console2.sol";

contract SignatureVoting {

struct Proposal {
string name;
uint256 voteCount;
}

// Create a way to track if someone has voted on a proposal already
mapping(address => mapping(uint256 => bool)) internal voted;

// A storage array of Proposal structs
Proposal[] public proposals;

// Creates a proposal
function createProposal (string memory proposalName) external {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every function should have NatSpec comments with a requirements section that tells the user what functionality is expected from the function. See the other completed challenges for an example.

proposals.push(Proposal({
name: proposalName,
voteCount: 0
}));
}

// Create a function to recover the signer from a signed message
function recoverSigner(
bytes32 _ethSignedMessageHash,
bytes memory _signature
) public pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);

return ecrecover(_ethSignedMessageHash, v, r, s);
}

// Split the signature into "r", "s", and "v" parameters for 'recoverSigner'
function splitSignature(bytes memory sig)
public
pure
returns (bytes32 r, bytes32 s, uint8 v)
{
require(sig.length == 65, "invalid signature length");

assembly {
/*
First 32 bytes stores the length of the signature

add(sig, 32) = pointer of sig + 32
effectively, skips first 32 bytes of signature

mload(p) loads next 32 bytes starting at the memory address p into memory
*/

// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}

// implicitly return (r, s, v)
}

// Create a function to vote on a proposal
function vote (bytes32 signedMessage, bytes32 hashedMessage, uint256 proposalId) public {
// Get the address that signed the message
// Not using msg.sender because the signer may not have sent the transaction
address voter = recoverSigner(signedMessage, abi.encodePacked(hashedMessage));

// Prevent duplicate votes from voter
// require(voted[voter][proposalId] == false, "Voter already voted for this proposal!");

// Verify hashed message is same as message
//require(hashedMessage == keccak256(abi.encodePacked(proposalId)), "Vote: Messages don't match!");

// Increase by one vote for the proposal
proposals[proposalId].voteCount += 1;

// Record that voter has voted for proposal
voted[voter][proposalId] == true;
}

// Query if voter voted on a proposal
function queryVoted (address voter, uint256 proposalId) public view returns(bool) {
return voted[voter][proposalId];
}

// Create a function to get name of a proposal by proposalId
function getProposalName (uint256 _proposalId) public view returns(string memory) {
Proposal storage proposal = proposals[_proposalId];
return proposal.name;
}
}
4 changes: 2 additions & 2 deletions packages/foundry/script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "../contracts/Challenge.sol";
import "../contracts/SignatureVoting.sol";
import "./DeployHelpers.s.sol";

contract DeployScript is ScaffoldETHDeploy {
Expand All @@ -15,7 +15,7 @@ contract DeployScript is ScaffoldETHDeploy {
);
}
vm.startBroadcast(deployerPrivateKey);
Challenge challenge = new Challenge();
SignatureVoting challenge = new SignatureVoting();
console.logString(
string.concat(
"Challenge deployed at: ",
Expand Down
9 changes: 0 additions & 9 deletions packages/foundry/test/Challenge.t.sol

This file was deleted.

72 changes: 72 additions & 0 deletions packages/foundry/test/SignatureVotingTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../contracts/SignatureVoting.sol";

contract SignatureVotingTest is Test {
// Declare variables
SignatureVoting public signatureVoting;

// Create users' privateKeys
// Need privateKeys to create wallets and to 'sign' messages
uint256 onePk = 0x123;
uint256 twoPk = 0x456;
uint256 threePk = 0x789;

// Create users' wallets
address public userOne = vm.addr(onePk);
address public userTwo = vm.addr(twoPk);
address public userThree = vm.addr(threePk);

// Create proposals
string public proposalOne = "Everyone must wear red";
string public proposalTwo = "Falafel Friday";
string public proposalThree = "Water is a public right";

// Create messages
uint256 messageOne = 0;
uint256 messageTwo = 1;
uint256 messageThree = 2;

// ToDo: Create messages
bytes32 hashOne = keccak256(abi.encodePacked(messageOne));
bytes32 hashTwo = keccak256(abi.encodePacked(messageTwo));
bytes32 hashThree = keccak256(abi.encodePacked(messageThree));

// Deploy contract and create proposals
function setUp() public {
signatureVoting = new SignatureVoting();
signatureVoting.createProposal(proposalOne);
signatureVoting.createProposal(proposalTwo);
signatureVoting.createProposal(proposalThree);
}

// Test proposals were created
function test_ProposalsCreated() public {
assertEq(signatureVoting.getProposalName(0), proposalOne);
assertEq(signatureVoting.getProposalName(1), proposalTwo);
assertEq(signatureVoting.getProposalName(2), proposalThree);
}

// Voters sign message and call 'vote'
function voterVotes() public {
// Sign the message
(uint8 v, bytes32 r, bytes32 s) = vm.sign(onePk, hashOne);
// Pack the message
bytes32 signedMessage = keccak256(abi.encodePacked(r, s, v));
// Call smart contract
signatureVoting.vote(signedMessage, hashOne, messageOne);
}

// Test that voters voted
function test_voterVotedProposal0() public {
assertTrue(signatureVoting.queryVoted(userOne, 0), "Voter did not vote for this proposal");
}

// Test for duplicate votes for single proposal
function test_DuplicateVoting() public {
vm.expectRevert(bytes("Voter already voted for this proposal!"));
signatureVoting.queryVoted(userOne, messageOne);
}
}