From a4164dbe11508ec04b5f14b91be2fe4462cb1b5c Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Wed, 22 Mar 2023 21:47:49 +0100 Subject: [PATCH] feat(VotingContract): add verification period closes: #5 --- packages/DApp/src/helpers/loadProtons.ts | 4 +- packages/DApp/src/helpers/wakuVote.ts | 27 +- packages/DApp/src/hooks/useCommunities.ts | 13 +- packages/DApp/src/hooks/useSendWakuVote.ts | 3 +- packages/DApp/src/hooks/useTypedVote.ts | 4 +- packages/DApp/src/models/waku.ts | 4 +- .../helpers/wakuMessage/wakuMessage.test.ts | 40 +-- packages/DApp/types/protons/index.d.ts | 8 +- .../contracts/contracts/VotingContract.sol | 100 ++++--- packages/contracts/deploy/deploy.ts | 109 ++++--- packages/contracts/package.json | 2 +- .../contracts/test/1.votingContract.test.ts | 265 ++++++++++-------- 12 files changed, 330 insertions(+), 249 deletions(-) diff --git a/packages/DApp/src/helpers/loadProtons.ts b/packages/DApp/src/helpers/loadProtons.ts index b3b7a5a..ae49c56 100644 --- a/packages/DApp/src/helpers/loadProtons.ts +++ b/packages/DApp/src/helpers/loadProtons.ts @@ -6,8 +6,8 @@ message WakuVote { string vote = 2; bytes sntAmount = 3; string sign = 4; - uint32 nonce = 5; - uint64 sessionID = 6; + uint32 timestamp = 5; + uint64 roomID = 6; } message WakuFeature { diff --git a/packages/DApp/src/helpers/wakuVote.ts b/packages/DApp/src/helpers/wakuVote.ts index 0723e5e..5e902fe 100644 --- a/packages/DApp/src/helpers/wakuVote.ts +++ b/packages/DApp/src/helpers/wakuVote.ts @@ -14,27 +14,29 @@ function getContractParameters( address: string, room: number, type: number, - sntAmount: number -): [string, BigNumber, BigNumber] { - return [address, BigNumber.from(room).mul(2).add(type), BigNumber.from(sntAmount)] + sntAmount: number, + timestamp: number +): [string, BigNumber, BigNumber, BigNumber] { + return [address, BigNumber.from(room).mul(2).add(type), BigNumber.from(sntAmount), BigNumber.from(timestamp)] } export function filterVerifiedVotes( messages: WakuVoteData[] | undefined, alreadyVoted: string[], - getTypedData: (data: [string, BigNumber, BigNumber]) => TypedVote + getTypedData: (data: [string, BigNumber, BigNumber, BigNumber]) => TypedVote ) { if (!messages) { return [] } - const verified: [string, BigNumber, BigNumber, string, string][] = [] + const verified: [string, BigNumber, BigNumber, BigNumber, string, string][] = [] messages.forEach((msg) => { const params = getContractParameters( msg.address, - msg.sessionID, + msg.roomID, msg.vote == 'yes' ? 1 : 0, - msg.sntAmount.toNumber() + msg.sntAmount.toNumber(), + msg.timestamp ) if (utils.getAddress(recoverAddress(getTypedData(params), msg.sign)) == msg.address) { @@ -55,7 +57,7 @@ function decodeWakuVote(msg: WakuMessage): WakuVoteData | undefined { return undefined } const data = proto.WakuVote.decode(msg.payload) - if (data && data.address && data.nonce && data.sessionID && data.sign && data.sntAmount && data.vote) { + if (data && data.address && data.timestamp && data.roomID && data.sign && data.sntAmount && data.vote) { return { ...data, sntAmount: BigNumber.from(data.sntAmount) } } else { return undefined @@ -86,7 +88,8 @@ export async function createWakuVote( room: number, voteAmount: number, type: number, - getTypedData: (data: [string, BigNumber, BigNumber]) => any, + timestamp: number, + getTypedData: (data: [string, BigNumber, BigNumber, BigNumber]) => any, sig?: string ) { if (!account || !signer) { @@ -99,7 +102,7 @@ export async function createWakuVote( return undefined } - const message = getContractParameters(account, room, type, voteAmount) + const message = getContractParameters(account, room, type, voteAmount, timestamp) const data = getTypedData(message) const signature = sig ? sig : await provider?.send('eth_signTypedData_v3', [account, JSON.stringify(data)]) @@ -109,8 +112,8 @@ export async function createWakuVote( vote: type == 1 ? 'yes' : 'no', sntAmount: utils.arrayify(BigNumber.from(voteAmount)), sign: signature, - nonce: 1, - sessionID: room, + timestamp: timestamp, + roomID: room, }) return payload diff --git a/packages/DApp/src/hooks/useCommunities.ts b/packages/DApp/src/hooks/useCommunities.ts index c3a6e2b..65868ec 100644 --- a/packages/DApp/src/hooks/useCommunities.ts +++ b/packages/DApp/src/hooks/useCommunities.ts @@ -34,11 +34,16 @@ export function useCommunities(publicKeys: string[]) { const communityHistory = communitiesHistories[idx]?.[0] if (communityHistory && communityHistory.length > 0) { const votingHistory = communityHistory.map((room: any) => { - const endAt = new Date(room[1].toNumber() * 1000) + const endAt = new Date(room.endAt.toNumber() * 1000) return { - ID: room[7].toNumber(), - type: room[2] === 1 ? 'Add' : 'Remove', - result: endAt > new Date() ? 'Ongoing' : room[5].gt(room[6]) ? 'Passed' : 'Failed', + ID: room.roomNumber.toNumber(), + type: room.voteType === 1 ? 'Add' : 'Remove', + result: + endAt > new Date() + ? 'Ongoing' + : room.totalVotesFor.gt(room.totalVotesAgainst) + ? 'Passed' + : 'Failed', date: endAt, } }) diff --git a/packages/DApp/src/hooks/useSendWakuVote.ts b/packages/DApp/src/hooks/useSendWakuVote.ts index ca9e5a5..66618f9 100644 --- a/packages/DApp/src/hooks/useSendWakuVote.ts +++ b/packages/DApp/src/hooks/useSendWakuVote.ts @@ -14,7 +14,8 @@ export function useSendWakuVote() { const sendWakuVote = useCallback( async (voteAmount: number, room: number, type: number) => { - const msg = await createWakuVote(account, library?.getSigner(), room, voteAmount, type, getTypedVote) + const timestamp = Math.floor(Date.now() / 1000) + const msg = await createWakuVote(account, library?.getSigner(), room, voteAmount, type, timestamp, getTypedVote) if (msg) { if (waku) { await waku.lightPush.push(new EncoderV0(config.wakuTopic + room.toString()), { payload: msg }) diff --git a/packages/DApp/src/hooks/useTypedVote.ts b/packages/DApp/src/hooks/useTypedVote.ts index f2e0306..3c749d6 100644 --- a/packages/DApp/src/hooks/useTypedVote.ts +++ b/packages/DApp/src/hooks/useTypedVote.ts @@ -9,7 +9,7 @@ export function useTypedVote() { const { votingContract } = useContracts() const getTypedVote = useCallback( - (data: [string, BigNumber, BigNumber]) => { + (data: [string, BigNumber, BigNumber, BigNumber]) => { return { types: { EIP712Domain: [ @@ -22,6 +22,7 @@ export function useTypedVote() { { name: 'roomIdAndType', type: 'uint256' }, { name: 'sntAmount', type: 'uint256' }, { name: 'voter', type: 'address' }, + { name: 'timestamp', type: 'uint256' }, ], }, primaryType: 'Vote', @@ -35,6 +36,7 @@ export function useTypedVote() { roomIdAndType: data[1].toHexString(), sntAmount: data[2].toHexString(), voter: data[0], + timestamp: data[3].toHexString(), }, } as TypedVote }, diff --git a/packages/DApp/src/models/waku.ts b/packages/DApp/src/models/waku.ts index 0ea2dc5..1e48148 100644 --- a/packages/DApp/src/models/waku.ts +++ b/packages/DApp/src/models/waku.ts @@ -5,8 +5,8 @@ export type WakuVoteData = { address: string vote: string sign: string - nonce: number - sessionID: number + timestamp: number + roomID: number } export type WakuFeatureData = { diff --git a/packages/DApp/test/helpers/wakuMessage/wakuMessage.test.ts b/packages/DApp/test/helpers/wakuMessage/wakuMessage.test.ts index fa3f192..8a4aa3c 100644 --- a/packages/DApp/test/helpers/wakuMessage/wakuMessage.test.ts +++ b/packages/DApp/test/helpers/wakuMessage/wakuMessage.test.ts @@ -23,8 +23,8 @@ describe('wakuMessage', () => { const payload = proto.WakuVote.encode({ address: '0x0', - nonce: 1, - sessionID: 2, + timestamp: 1, + roomID: 2, sign: '0x1234', sntAmount: utils.arrayify(BigNumber.from(123)), vote: 'For', @@ -33,8 +33,8 @@ describe('wakuMessage', () => { const payload2 = proto.WakuVote.encode({ address: '0x01', - nonce: 1, - sessionID: 2, + timestamp: 1, + roomID: 2, sign: '0xabc1234', sntAmount: utils.arrayify(BigNumber.from(321)), vote: 'Against', @@ -45,16 +45,16 @@ describe('wakuMessage', () => { expect(data).to.not.be.undefined expect(data?.address).to.eq('0x0') - expect(data?.nonce).to.eq(1) - expect(data?.sessionID).to.eq(2) + expect(data?.timestamp).to.eq(1) + expect(data?.roomID).to.eq(2) expect(data?.sign).to.eq('0x1234') expect(data?.sntAmount).to.deep.eq(BigNumber.from(123)) expect(data?.vote).to.eq('For') expect(data2).to.not.be.undefined expect(data2?.address).to.eq('0x01') - expect(data2?.nonce).to.eq(1) - expect(data2?.sessionID).to.eq(2) + expect(data2?.timestamp).to.eq(1) + expect(data2?.roomID).to.eq(2) expect(data2?.sign).to.eq('0xabc1234') expect(data2?.sntAmount).to.deep.eq(BigNumber.from(321)) expect(data2?.vote).to.eq('Against') @@ -75,8 +75,8 @@ describe('wakuMessage', () => { const payload2 = proto.WakuVote.encode({ address: '0x01', - nonce: 1, - sessionID: 2, + timestamp: 1, + roomID: 2, sign: '0xabc1234', sntAmount: utils.arrayify(BigNumber.from(321)), vote: 'Against', @@ -89,8 +89,8 @@ describe('wakuMessage', () => { const data = response[0] expect(data).to.not.be.undefined expect(data?.address).to.eq('0x01') - expect(data?.nonce).to.eq(1) - expect(data?.sessionID).to.eq(2) + expect(data?.timestamp).to.eq(1) + expect(data?.roomID).to.eq(2) expect(data?.sign).to.eq('0xabc1234') expect(data?.sntAmount).to.deep.eq(BigNumber.from(321)) expect(data?.vote).to.eq('Against') @@ -142,6 +142,7 @@ describe('wakuMessage', () => { 1, 100, 1, + 1, () => [], '0x01' ) @@ -153,8 +154,8 @@ describe('wakuMessage', () => { expect(obj.address).to.eq(alice.address) expect(obj.vote).to.eq('yes') expect(BigNumber.from(obj.sntAmount)._hex).to.eq('0x64') - expect(obj.nonce).to.eq(1) - expect(obj.sessionID).to.eq(1) + expect(obj.timestamp).to.eq(1) + expect(obj.roomID).to.eq(1) } }) @@ -166,6 +167,7 @@ describe('wakuMessage', () => { 2, 1000, 0, + 1, () => [], '0x01' ) @@ -177,28 +179,28 @@ describe('wakuMessage', () => { expect(obj.address).to.eq(alice.address) expect(obj.vote).to.eq('no') expect(BigNumber.from(obj.sntAmount)._hex).to.eq('0x03e8') - expect(obj.nonce).to.eq(1) - expect(obj.sessionID).to.eq(2) + expect(obj.timestamp).to.eq(1) + expect(obj.roomID).to.eq(2) } }) it('no address', async () => { const encoder = new EncoderV0('/test/') - const payload = await wakuMessage.create(undefined, alice as unknown as JsonRpcSigner, 1, 100, 1, () => []) + const payload = await wakuMessage.create(undefined, alice as unknown as JsonRpcSigner, 1, 100, 1, 1, () => []) const msg = await encoder.toProtoObj({ payload }) expect(msg?.payload).to.be.undefined }) it('no signer', async () => { const encoder = new EncoderV0('/test/') - const payload = await wakuMessage.create(alice.address, undefined, 1, 100, 1, () => []) + const payload = await wakuMessage.create(alice.address, undefined, 1, 100, 1, 1, () => []) const msg = await encoder.toProtoObj({ payload }) expect(msg?.payload).to.be.undefined }) it('different signer', async () => { const encoder = new EncoderV0('/test/') - const payload = await wakuMessage.create(alice.address, bob as unknown as JsonRpcSigner, 1, 100, 1, () => []) + const payload = await wakuMessage.create(alice.address, bob as unknown as JsonRpcSigner, 1, 100, 1, 1, () => []) const msg = await encoder.toProtoObj({ payload }) expect(msg?.payload).to.be.undefined }) diff --git a/packages/DApp/types/protons/index.d.ts b/packages/DApp/types/protons/index.d.ts index a30e30e..7ac6366 100644 --- a/packages/DApp/types/protons/index.d.ts +++ b/packages/DApp/types/protons/index.d.ts @@ -4,10 +4,10 @@ declare module 'protons' { vote: string sntAmount: Uint8Array sign: string - nonce: number - sessionID: number + timestamp: number + roomID: number } - + export type WakuFeature = { voter: string sntAmount: Uint8Array @@ -27,4 +27,4 @@ declare module 'protons' { } } export = protons -} \ No newline at end of file +} diff --git a/packages/contracts/contracts/VotingContract.sol b/packages/contracts/contracts/VotingContract.sol index 0128927..97bdac2 100644 --- a/packages/contracts/contracts/VotingContract.sol +++ b/packages/contracts/contracts/VotingContract.sol @@ -14,8 +14,8 @@ contract VotingContract { using SafeMath for uint256; enum VoteType { - REMOVE, - ADD + AGAINST, + FOR } struct Vote { @@ -26,6 +26,8 @@ contract VotingContract { struct VotingRoom { uint256 startBlock; + uint256 startAt; + uint256 verificationStartAt; uint256 endAt; VoteType voteType; bool finalized; @@ -39,22 +41,26 @@ contract VotingContract { address voter; uint256 roomIdAndType; uint256 sntAmount; + uint256 timestamp; bytes32 r; bytes32 vs; } event VotingRoomStarted(uint256 roomId, bytes publicKey); + event VotingRoomVerificationStarted(uint256 roomId, bytes publicKey); event VotingRoomFinalized(uint256 roomId, bytes publicKey, bool passed, VoteType voteType); event VoteCast(uint256 roomId, address voter); event NotEnoughToken(uint256 roomId, address voter); event AlreadyVoted(uint256 roomId, address voter); + event InvalidSignature(uint256 roomId, address voter); address public owner; Directory public directory; IERC20 public token; uint256 public votingLength; + uint256 public votingVerificationLength; uint256 public timeBetweenVoting; VotingRoom[] public votingRooms; @@ -89,21 +95,23 @@ contract VotingContract { ); } - bytes32 public constant VOTE_TYPEHASH = keccak256('Vote(uint256 roomIdAndType,uint256 sntAmount,address voter)'); + bytes32 public constant VOTE_TYPEHASH = + keccak256('Vote(uint256 roomIdAndType,uint256 sntAmount,address voter,uint256 timestamp)'); function hash(SignedVote calldata vote) internal pure returns (bytes32) { - return keccak256(abi.encode(VOTE_TYPEHASH, vote.roomIdAndType, vote.sntAmount, vote.voter)); + return keccak256(abi.encode(VOTE_TYPEHASH, vote.roomIdAndType, vote.sntAmount, vote.voter, vote.timestamp)); } - function verify(SignedVote calldata vote) internal view returns (bool) { + function verifySignature(SignedVote calldata vote) internal view returns (bool) { bytes32 digest = keccak256(abi.encodePacked('\x19\x01', DOMAIN_SEPARATOR, hash(vote))); return digest.recover(vote.r, vote.vs) == vote.voter; } - constructor(IERC20 _address, uint256 _votingLength, uint256 _timeBetweenVoting) { + constructor(IERC20 _address, uint256 _votingLength, uint256 _votingVerificationLength, uint256 _timeBetweenVoting) { owner = msg.sender; token = _address; votingLength = _votingLength; + votingVerificationLength = _votingVerificationLength; timeBetweenVoting = _timeBetweenVoting; DOMAIN_SEPARATOR = hash( EIP712Domain({ @@ -172,10 +180,10 @@ contract VotingContract { function initializeVotingRoom(VoteType voteType, bytes calldata publicKey, uint256 voteAmount) public { require(activeRoomIDByCommunityID[publicKey] == 0, 'vote already ongoing'); - if (voteType == VoteType.REMOVE) { + if (voteType == VoteType.AGAINST) { require(directory.isCommunityInDirectory(publicKey), 'Community not in directory'); } - if (voteType == VoteType.ADD) { + if (voteType == VoteType.FOR) { require(!directory.isCommunityInDirectory(publicKey), 'Community already in directory'); } uint256 historyLength = roomIDsByCommunityID[publicKey].length; @@ -192,13 +200,15 @@ contract VotingContract { activeRoomIDByCommunityID[publicKey] = votingRoomID; roomIDsByCommunityID[publicKey].push(votingRoomID); - votesByRoomID[votingRoomID].push(Vote({ voter: msg.sender, voteType: VoteType.ADD, sntAmount: voteAmount })); + votesByRoomID[votingRoomID].push(Vote({ voter: msg.sender, voteType: VoteType.FOR, sntAmount: voteAmount })); votedAddressesByRoomID[votingRoomID][msg.sender] = true; votingRooms.push( VotingRoom({ startBlock: block.number, - endAt: block.timestamp.add(votingLength), + startAt: block.timestamp, + verificationStartAt: block.timestamp.add(votingLength), + endAt: block.timestamp.add(votingLength + votingVerificationLength), voteType: voteType, finalized: false, community: publicKey, @@ -220,7 +230,7 @@ contract VotingContract { for (uint256 i = 0; i < votesByRoomID[votingRoom.roomNumber].length; i++) { Vote storage vote = votesByRoomID[votingRoom.roomNumber][i]; if (token.balanceOf(vote.voter) >= vote.sntAmount) { - if (vote.voteType == VoteType.ADD) { + if (vote.voteType == VoteType.FOR) { votingRoom.totalVotesFor = votingRoom.totalVotesFor.add(vote.sntAmount); } else { votingRoom.totalVotesAgainst = votingRoom.totalVotesAgainst.add(vote.sntAmount); @@ -233,7 +243,7 @@ contract VotingContract { } function _populateDirectory(VotingRoom storage votingRoom) private { - if (votingRoom.voteType == VoteType.ADD) { + if (votingRoom.voteType == VoteType.FOR) { directory.addCommunity(votingRoom.community); } else { directory.removeCommunity(votingRoom.community); @@ -258,36 +268,46 @@ contract VotingContract { emit VotingRoomFinalized(roomId, votingRoom.community, passed, votingRoom.voteType); } + function castVote(SignedVote calldata vote) private { + if (!verifySignature(vote)) { + emit InvalidSignature(vote.roomIdAndType >> 1, vote.voter); + return; + } + + uint256 roomId = vote.roomIdAndType >> 1; + VotingRoom storage room = _getVotingRoom(roomId); + + require(block.timestamp < room.endAt, 'vote closed'); + require(!room.finalized, 'room finalized'); + require(vote.timestamp < room.verificationStartAt, 'invalid vote timestamp'); + require(vote.timestamp >= room.startAt, 'invalid vote timestamp'); + + if (votedAddressesByRoomID[roomId][vote.voter]) { + emit AlreadyVoted(roomId, vote.voter); + return; + } + + if (token.balanceOf(vote.voter) < vote.sntAmount) { + emit NotEnoughToken(roomId, vote.voter); + return; + } + + votedAddressesByRoomID[roomId][vote.voter] = true; + votesByRoomID[roomId].push( + Vote({ + voter: vote.voter, + voteType: vote.roomIdAndType & 1 == 1 ? VoteType.FOR : VoteType.AGAINST, + sntAmount: vote.sntAmount + }) + ); + + _evaluateVotes(room); // TODO: optimise - aggregate votes by room id and only then evaluate + emit VoteCast(roomId, vote.voter); + } + function castVotes(SignedVote[] calldata votes) public { for (uint256 i = 0; i < votes.length; i++) { - SignedVote calldata signedVote = votes[i]; - - if (verify(signedVote)) { - uint256 roomId = signedVote.roomIdAndType >> 1; - VotingRoom storage room = _getVotingRoom(roomId); - - require(room.endAt > block.timestamp, 'vote closed'); - require(!room.finalized, 'room finalized'); - - if (votedAddressesByRoomID[roomId][signedVote.voter] == false) { - if (token.balanceOf(signedVote.voter) >= signedVote.sntAmount) { - votedAddressesByRoomID[roomId][signedVote.voter] = true; - votesByRoomID[roomId].push( - Vote({ - voter: signedVote.voter, - voteType: signedVote.roomIdAndType & 1 == 1 ? VoteType.ADD : VoteType.REMOVE, - sntAmount: signedVote.sntAmount - }) - ); - _evaluateVotes(room); // TODO: optimise - aggregate votes by room id and only then evaluate - emit VoteCast(roomId, signedVote.voter); - } else { - emit NotEnoughToken(roomId, signedVote.voter); - } - } else { - emit AlreadyVoted(roomId, signedVote.voter); - } - } + castVote(votes[i]); } } } diff --git a/packages/contracts/deploy/deploy.ts b/packages/contracts/deploy/deploy.ts index 0b8a11f..6fa716c 100644 --- a/packages/contracts/deploy/deploy.ts +++ b/packages/contracts/deploy/deploy.ts @@ -4,87 +4,110 @@ // You can also run a script with `npx hardhat run