feat(VotingContract): add verification period

closes: #5
This commit is contained in:
Patryk Osmaczko 2023-03-22 21:47:49 +01:00 committed by osmaczko
parent 135084aaf4
commit a4164dbe11
12 changed files with 330 additions and 249 deletions

View File

@ -6,8 +6,8 @@ message WakuVote {
string vote = 2; string vote = 2;
bytes sntAmount = 3; bytes sntAmount = 3;
string sign = 4; string sign = 4;
uint32 nonce = 5; uint32 timestamp = 5;
uint64 sessionID = 6; uint64 roomID = 6;
} }
message WakuFeature { message WakuFeature {

View File

@ -14,27 +14,29 @@ function getContractParameters(
address: string, address: string,
room: number, room: number,
type: number, type: number,
sntAmount: number sntAmount: number,
): [string, BigNumber, BigNumber] { timestamp: number
return [address, BigNumber.from(room).mul(2).add(type), BigNumber.from(sntAmount)] ): [string, BigNumber, BigNumber, BigNumber] {
return [address, BigNumber.from(room).mul(2).add(type), BigNumber.from(sntAmount), BigNumber.from(timestamp)]
} }
export function filterVerifiedVotes( export function filterVerifiedVotes(
messages: WakuVoteData[] | undefined, messages: WakuVoteData[] | undefined,
alreadyVoted: string[], alreadyVoted: string[],
getTypedData: (data: [string, BigNumber, BigNumber]) => TypedVote getTypedData: (data: [string, BigNumber, BigNumber, BigNumber]) => TypedVote
) { ) {
if (!messages) { if (!messages) {
return [] return []
} }
const verified: [string, BigNumber, BigNumber, string, string][] = [] const verified: [string, BigNumber, BigNumber, BigNumber, string, string][] = []
messages.forEach((msg) => { messages.forEach((msg) => {
const params = getContractParameters( const params = getContractParameters(
msg.address, msg.address,
msg.sessionID, msg.roomID,
msg.vote == 'yes' ? 1 : 0, msg.vote == 'yes' ? 1 : 0,
msg.sntAmount.toNumber() msg.sntAmount.toNumber(),
msg.timestamp
) )
if (utils.getAddress(recoverAddress(getTypedData(params), msg.sign)) == msg.address) { if (utils.getAddress(recoverAddress(getTypedData(params), msg.sign)) == msg.address) {
@ -55,7 +57,7 @@ function decodeWakuVote(msg: WakuMessage): WakuVoteData | undefined {
return undefined return undefined
} }
const data = proto.WakuVote.decode(msg.payload) 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) } return { ...data, sntAmount: BigNumber.from(data.sntAmount) }
} else { } else {
return undefined return undefined
@ -86,7 +88,8 @@ export async function createWakuVote(
room: number, room: number,
voteAmount: number, voteAmount: number,
type: number, type: number,
getTypedData: (data: [string, BigNumber, BigNumber]) => any, timestamp: number,
getTypedData: (data: [string, BigNumber, BigNumber, BigNumber]) => any,
sig?: string sig?: string
) { ) {
if (!account || !signer) { if (!account || !signer) {
@ -99,7 +102,7 @@ export async function createWakuVote(
return undefined return undefined
} }
const message = getContractParameters(account, room, type, voteAmount) const message = getContractParameters(account, room, type, voteAmount, timestamp)
const data = getTypedData(message) const data = getTypedData(message)
const signature = sig ? sig : await provider?.send('eth_signTypedData_v3', [account, JSON.stringify(data)]) 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', vote: type == 1 ? 'yes' : 'no',
sntAmount: utils.arrayify(BigNumber.from(voteAmount)), sntAmount: utils.arrayify(BigNumber.from(voteAmount)),
sign: signature, sign: signature,
nonce: 1, timestamp: timestamp,
sessionID: room, roomID: room,
}) })
return payload return payload

View File

@ -34,11 +34,16 @@ export function useCommunities(publicKeys: string[]) {
const communityHistory = communitiesHistories[idx]?.[0] const communityHistory = communitiesHistories[idx]?.[0]
if (communityHistory && communityHistory.length > 0) { if (communityHistory && communityHistory.length > 0) {
const votingHistory = communityHistory.map((room: any) => { const votingHistory = communityHistory.map((room: any) => {
const endAt = new Date(room[1].toNumber() * 1000) const endAt = new Date(room.endAt.toNumber() * 1000)
return { return {
ID: room[7].toNumber(), ID: room.roomNumber.toNumber(),
type: room[2] === 1 ? 'Add' : 'Remove', type: room.voteType === 1 ? 'Add' : 'Remove',
result: endAt > new Date() ? 'Ongoing' : room[5].gt(room[6]) ? 'Passed' : 'Failed', result:
endAt > new Date()
? 'Ongoing'
: room.totalVotesFor.gt(room.totalVotesAgainst)
? 'Passed'
: 'Failed',
date: endAt, date: endAt,
} }
}) })

View File

@ -14,7 +14,8 @@ export function useSendWakuVote() {
const sendWakuVote = useCallback( const sendWakuVote = useCallback(
async (voteAmount: number, room: number, type: number) => { 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 (msg) {
if (waku) { if (waku) {
await waku.lightPush.push(new EncoderV0(config.wakuTopic + room.toString()), { payload: msg }) await waku.lightPush.push(new EncoderV0(config.wakuTopic + room.toString()), { payload: msg })

View File

@ -9,7 +9,7 @@ export function useTypedVote() {
const { votingContract } = useContracts() const { votingContract } = useContracts()
const getTypedVote = useCallback( const getTypedVote = useCallback(
(data: [string, BigNumber, BigNumber]) => { (data: [string, BigNumber, BigNumber, BigNumber]) => {
return { return {
types: { types: {
EIP712Domain: [ EIP712Domain: [
@ -22,6 +22,7 @@ export function useTypedVote() {
{ name: 'roomIdAndType', type: 'uint256' }, { name: 'roomIdAndType', type: 'uint256' },
{ name: 'sntAmount', type: 'uint256' }, { name: 'sntAmount', type: 'uint256' },
{ name: 'voter', type: 'address' }, { name: 'voter', type: 'address' },
{ name: 'timestamp', type: 'uint256' },
], ],
}, },
primaryType: 'Vote', primaryType: 'Vote',
@ -35,6 +36,7 @@ export function useTypedVote() {
roomIdAndType: data[1].toHexString(), roomIdAndType: data[1].toHexString(),
sntAmount: data[2].toHexString(), sntAmount: data[2].toHexString(),
voter: data[0], voter: data[0],
timestamp: data[3].toHexString(),
}, },
} as TypedVote } as TypedVote
}, },

View File

@ -5,8 +5,8 @@ export type WakuVoteData = {
address: string address: string
vote: string vote: string
sign: string sign: string
nonce: number timestamp: number
sessionID: number roomID: number
} }
export type WakuFeatureData = { export type WakuFeatureData = {

View File

@ -23,8 +23,8 @@ describe('wakuMessage', () => {
const payload = proto.WakuVote.encode({ const payload = proto.WakuVote.encode({
address: '0x0', address: '0x0',
nonce: 1, timestamp: 1,
sessionID: 2, roomID: 2,
sign: '0x1234', sign: '0x1234',
sntAmount: utils.arrayify(BigNumber.from(123)), sntAmount: utils.arrayify(BigNumber.from(123)),
vote: 'For', vote: 'For',
@ -33,8 +33,8 @@ describe('wakuMessage', () => {
const payload2 = proto.WakuVote.encode({ const payload2 = proto.WakuVote.encode({
address: '0x01', address: '0x01',
nonce: 1, timestamp: 1,
sessionID: 2, roomID: 2,
sign: '0xabc1234', sign: '0xabc1234',
sntAmount: utils.arrayify(BigNumber.from(321)), sntAmount: utils.arrayify(BigNumber.from(321)),
vote: 'Against', vote: 'Against',
@ -45,16 +45,16 @@ describe('wakuMessage', () => {
expect(data).to.not.be.undefined expect(data).to.not.be.undefined
expect(data?.address).to.eq('0x0') expect(data?.address).to.eq('0x0')
expect(data?.nonce).to.eq(1) expect(data?.timestamp).to.eq(1)
expect(data?.sessionID).to.eq(2) expect(data?.roomID).to.eq(2)
expect(data?.sign).to.eq('0x1234') expect(data?.sign).to.eq('0x1234')
expect(data?.sntAmount).to.deep.eq(BigNumber.from(123)) expect(data?.sntAmount).to.deep.eq(BigNumber.from(123))
expect(data?.vote).to.eq('For') expect(data?.vote).to.eq('For')
expect(data2).to.not.be.undefined expect(data2).to.not.be.undefined
expect(data2?.address).to.eq('0x01') expect(data2?.address).to.eq('0x01')
expect(data2?.nonce).to.eq(1) expect(data2?.timestamp).to.eq(1)
expect(data2?.sessionID).to.eq(2) expect(data2?.roomID).to.eq(2)
expect(data2?.sign).to.eq('0xabc1234') expect(data2?.sign).to.eq('0xabc1234')
expect(data2?.sntAmount).to.deep.eq(BigNumber.from(321)) expect(data2?.sntAmount).to.deep.eq(BigNumber.from(321))
expect(data2?.vote).to.eq('Against') expect(data2?.vote).to.eq('Against')
@ -75,8 +75,8 @@ describe('wakuMessage', () => {
const payload2 = proto.WakuVote.encode({ const payload2 = proto.WakuVote.encode({
address: '0x01', address: '0x01',
nonce: 1, timestamp: 1,
sessionID: 2, roomID: 2,
sign: '0xabc1234', sign: '0xabc1234',
sntAmount: utils.arrayify(BigNumber.from(321)), sntAmount: utils.arrayify(BigNumber.from(321)),
vote: 'Against', vote: 'Against',
@ -89,8 +89,8 @@ describe('wakuMessage', () => {
const data = response[0] const data = response[0]
expect(data).to.not.be.undefined expect(data).to.not.be.undefined
expect(data?.address).to.eq('0x01') expect(data?.address).to.eq('0x01')
expect(data?.nonce).to.eq(1) expect(data?.timestamp).to.eq(1)
expect(data?.sessionID).to.eq(2) expect(data?.roomID).to.eq(2)
expect(data?.sign).to.eq('0xabc1234') expect(data?.sign).to.eq('0xabc1234')
expect(data?.sntAmount).to.deep.eq(BigNumber.from(321)) expect(data?.sntAmount).to.deep.eq(BigNumber.from(321))
expect(data?.vote).to.eq('Against') expect(data?.vote).to.eq('Against')
@ -142,6 +142,7 @@ describe('wakuMessage', () => {
1, 1,
100, 100,
1, 1,
1,
() => [], () => [],
'0x01' '0x01'
) )
@ -153,8 +154,8 @@ describe('wakuMessage', () => {
expect(obj.address).to.eq(alice.address) expect(obj.address).to.eq(alice.address)
expect(obj.vote).to.eq('yes') expect(obj.vote).to.eq('yes')
expect(BigNumber.from(obj.sntAmount)._hex).to.eq('0x64') expect(BigNumber.from(obj.sntAmount)._hex).to.eq('0x64')
expect(obj.nonce).to.eq(1) expect(obj.timestamp).to.eq(1)
expect(obj.sessionID).to.eq(1) expect(obj.roomID).to.eq(1)
} }
}) })
@ -166,6 +167,7 @@ describe('wakuMessage', () => {
2, 2,
1000, 1000,
0, 0,
1,
() => [], () => [],
'0x01' '0x01'
) )
@ -177,28 +179,28 @@ describe('wakuMessage', () => {
expect(obj.address).to.eq(alice.address) expect(obj.address).to.eq(alice.address)
expect(obj.vote).to.eq('no') expect(obj.vote).to.eq('no')
expect(BigNumber.from(obj.sntAmount)._hex).to.eq('0x03e8') expect(BigNumber.from(obj.sntAmount)._hex).to.eq('0x03e8')
expect(obj.nonce).to.eq(1) expect(obj.timestamp).to.eq(1)
expect(obj.sessionID).to.eq(2) expect(obj.roomID).to.eq(2)
} }
}) })
it('no address', async () => { it('no address', async () => {
const encoder = new EncoderV0('/test/') 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 }) const msg = await encoder.toProtoObj({ payload })
expect(msg?.payload).to.be.undefined expect(msg?.payload).to.be.undefined
}) })
it('no signer', async () => { it('no signer', async () => {
const encoder = new EncoderV0('/test/') 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 }) const msg = await encoder.toProtoObj({ payload })
expect(msg?.payload).to.be.undefined expect(msg?.payload).to.be.undefined
}) })
it('different signer', async () => { it('different signer', async () => {
const encoder = new EncoderV0('/test/') 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 }) const msg = await encoder.toProtoObj({ payload })
expect(msg?.payload).to.be.undefined expect(msg?.payload).to.be.undefined
}) })

View File

@ -4,8 +4,8 @@ declare module 'protons' {
vote: string vote: string
sntAmount: Uint8Array sntAmount: Uint8Array
sign: string sign: string
nonce: number timestamp: number
sessionID: number roomID: number
} }
export type WakuFeature = { export type WakuFeature = {

View File

@ -14,8 +14,8 @@ contract VotingContract {
using SafeMath for uint256; using SafeMath for uint256;
enum VoteType { enum VoteType {
REMOVE, AGAINST,
ADD FOR
} }
struct Vote { struct Vote {
@ -26,6 +26,8 @@ contract VotingContract {
struct VotingRoom { struct VotingRoom {
uint256 startBlock; uint256 startBlock;
uint256 startAt;
uint256 verificationStartAt;
uint256 endAt; uint256 endAt;
VoteType voteType; VoteType voteType;
bool finalized; bool finalized;
@ -39,22 +41,26 @@ contract VotingContract {
address voter; address voter;
uint256 roomIdAndType; uint256 roomIdAndType;
uint256 sntAmount; uint256 sntAmount;
uint256 timestamp;
bytes32 r; bytes32 r;
bytes32 vs; bytes32 vs;
} }
event VotingRoomStarted(uint256 roomId, bytes publicKey); event VotingRoomStarted(uint256 roomId, bytes publicKey);
event VotingRoomVerificationStarted(uint256 roomId, bytes publicKey);
event VotingRoomFinalized(uint256 roomId, bytes publicKey, bool passed, VoteType voteType); event VotingRoomFinalized(uint256 roomId, bytes publicKey, bool passed, VoteType voteType);
event VoteCast(uint256 roomId, address voter); event VoteCast(uint256 roomId, address voter);
event NotEnoughToken(uint256 roomId, address voter); event NotEnoughToken(uint256 roomId, address voter);
event AlreadyVoted(uint256 roomId, address voter); event AlreadyVoted(uint256 roomId, address voter);
event InvalidSignature(uint256 roomId, address voter);
address public owner; address public owner;
Directory public directory; Directory public directory;
IERC20 public token; IERC20 public token;
uint256 public votingLength; uint256 public votingLength;
uint256 public votingVerificationLength;
uint256 public timeBetweenVoting; uint256 public timeBetweenVoting;
VotingRoom[] public votingRooms; 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) { 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))); bytes32 digest = keccak256(abi.encodePacked('\x19\x01', DOMAIN_SEPARATOR, hash(vote)));
return digest.recover(vote.r, vote.vs) == vote.voter; 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; owner = msg.sender;
token = _address; token = _address;
votingLength = _votingLength; votingLength = _votingLength;
votingVerificationLength = _votingVerificationLength;
timeBetweenVoting = _timeBetweenVoting; timeBetweenVoting = _timeBetweenVoting;
DOMAIN_SEPARATOR = hash( DOMAIN_SEPARATOR = hash(
EIP712Domain({ EIP712Domain({
@ -172,10 +180,10 @@ contract VotingContract {
function initializeVotingRoom(VoteType voteType, bytes calldata publicKey, uint256 voteAmount) public { function initializeVotingRoom(VoteType voteType, bytes calldata publicKey, uint256 voteAmount) public {
require(activeRoomIDByCommunityID[publicKey] == 0, 'vote already ongoing'); require(activeRoomIDByCommunityID[publicKey] == 0, 'vote already ongoing');
if (voteType == VoteType.REMOVE) { if (voteType == VoteType.AGAINST) {
require(directory.isCommunityInDirectory(publicKey), 'Community not in directory'); require(directory.isCommunityInDirectory(publicKey), 'Community not in directory');
} }
if (voteType == VoteType.ADD) { if (voteType == VoteType.FOR) {
require(!directory.isCommunityInDirectory(publicKey), 'Community already in directory'); require(!directory.isCommunityInDirectory(publicKey), 'Community already in directory');
} }
uint256 historyLength = roomIDsByCommunityID[publicKey].length; uint256 historyLength = roomIDsByCommunityID[publicKey].length;
@ -192,13 +200,15 @@ contract VotingContract {
activeRoomIDByCommunityID[publicKey] = votingRoomID; activeRoomIDByCommunityID[publicKey] = votingRoomID;
roomIDsByCommunityID[publicKey].push(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; votedAddressesByRoomID[votingRoomID][msg.sender] = true;
votingRooms.push( votingRooms.push(
VotingRoom({ VotingRoom({
startBlock: block.number, startBlock: block.number,
endAt: block.timestamp.add(votingLength), startAt: block.timestamp,
verificationStartAt: block.timestamp.add(votingLength),
endAt: block.timestamp.add(votingLength + votingVerificationLength),
voteType: voteType, voteType: voteType,
finalized: false, finalized: false,
community: publicKey, community: publicKey,
@ -220,7 +230,7 @@ contract VotingContract {
for (uint256 i = 0; i < votesByRoomID[votingRoom.roomNumber].length; i++) { for (uint256 i = 0; i < votesByRoomID[votingRoom.roomNumber].length; i++) {
Vote storage vote = votesByRoomID[votingRoom.roomNumber][i]; Vote storage vote = votesByRoomID[votingRoom.roomNumber][i];
if (token.balanceOf(vote.voter) >= vote.sntAmount) { if (token.balanceOf(vote.voter) >= vote.sntAmount) {
if (vote.voteType == VoteType.ADD) { if (vote.voteType == VoteType.FOR) {
votingRoom.totalVotesFor = votingRoom.totalVotesFor.add(vote.sntAmount); votingRoom.totalVotesFor = votingRoom.totalVotesFor.add(vote.sntAmount);
} else { } else {
votingRoom.totalVotesAgainst = votingRoom.totalVotesAgainst.add(vote.sntAmount); votingRoom.totalVotesAgainst = votingRoom.totalVotesAgainst.add(vote.sntAmount);
@ -233,7 +243,7 @@ contract VotingContract {
} }
function _populateDirectory(VotingRoom storage votingRoom) private { function _populateDirectory(VotingRoom storage votingRoom) private {
if (votingRoom.voteType == VoteType.ADD) { if (votingRoom.voteType == VoteType.FOR) {
directory.addCommunity(votingRoom.community); directory.addCommunity(votingRoom.community);
} else { } else {
directory.removeCommunity(votingRoom.community); directory.removeCommunity(votingRoom.community);
@ -258,36 +268,46 @@ contract VotingContract {
emit VotingRoomFinalized(roomId, votingRoom.community, passed, votingRoom.voteType); 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 { function castVotes(SignedVote[] calldata votes) public {
for (uint256 i = 0; i < votes.length; i++) { for (uint256 i = 0; i < votes.length; i++) {
SignedVote calldata signedVote = votes[i]; castVote(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);
}
}
} }
} }
} }

View File

@ -4,87 +4,110 @@
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat // You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the // will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script. // global scope, and execute the script.
const hre = require("hardhat"); const hre = require('hardhat')
import { ERC20Mock, MultiCall } from '../abi'; import { ERC20Mock, MultiCall } from '../abi'
import { BigNumber } from 'ethers'; import { BigNumber } from 'ethers'
async function deployVotingContract(tokenAddress: string, votingLength, timeBetweenVoting) { async function deployVotingContract(
const contractFactory = await hre.ethers.getContractFactory('VotingContract'); tokenAddress: string,
const contract = await contractFactory.deploy(tokenAddress, votingLength, timeBetweenVoting); votingLength: number,
await contract.deployed(); votingVerificationLength: number,
timeBetweenVoting: number
) {
const contractFactory = await hre.ethers.getContractFactory('VotingContract')
const contract = await contractFactory.deploy(tokenAddress, votingLength, votingVerificationLength, timeBetweenVoting)
await contract.deployed()
console.log(`Voting contract deployed with address: ${contract.address}`); console.log(`Voting contract deployed with address: ${contract.address}`)
return contract; return contract
} }
async function deployDirectoryContract(votingContractAddress: string) { async function deployDirectoryContract(votingContractAddress: string) {
const contractFactory = await hre.ethers.getContractFactory('Directory'); const contractFactory = await hre.ethers.getContractFactory('Directory')
const contract = await contractFactory.deploy(votingContractAddress); const contract = await contractFactory.deploy(votingContractAddress)
await contract.deployed(); await contract.deployed()
console.log(`Directory contract deployed with address: ${contract.address}`); console.log(`Directory contract deployed with address: ${contract.address}`)
return contract; return contract
} }
async function deployERC20MockContract(deployerAddress: string) { async function deployERC20MockContract(deployerAddress: string) {
const contractFactory = await hre.ethers.getContractFactory(ERC20Mock.abi, ERC20Mock.bytecode); const contractFactory = await hre.ethers.getContractFactory(ERC20Mock.abi, ERC20Mock.bytecode)
const contract = await contractFactory.deploy('MSNT', 'Mock SNT', deployerAddress, BigNumber.from('0x33B2E3C9FD0803CE8000000')); const contract = await contractFactory.deploy(
await contract.deployed(); 'MSNT',
'Mock SNT',
deployerAddress,
BigNumber.from('0x33B2E3C9FD0803CE8000000')
)
await contract.deployed()
console.log(`ERC20Mock contract deployed with address: ${contract.address}`); console.log(`ERC20Mock contract deployed with address: ${contract.address}`)
return contract; const [_, bob, alice] = await hre.ethers.getSigners()
await contract.increaseAllowance(deployerAddress, BigNumber.from('0x33B2E3C9FD0803CE8000000'))
await contract.transferFrom(deployerAddress, bob.address, BigNumber.from('0x52B7D2DCC80CD2E4000000'))
await contract.transferFrom(deployerAddress, alice.address, BigNumber.from('0x52B7D2DCC80CD2E4000000'))
return contract
} }
async function deployMultiCallContract() { async function deployMultiCallContract() {
const contractFactory = await hre.ethers.getContractFactory(MultiCall.abi, MultiCall.bytecode); const contractFactory = await hre.ethers.getContractFactory(MultiCall.abi, MultiCall.bytecode)
const contract = await contractFactory.deploy(); const contract = await contractFactory.deploy()
await contract.deployed(); await contract.deployed()
console.log(`MultiCall contract deployed with address: ${contract.address}`); console.log(`MultiCall contract deployed with address: ${contract.address}`)
return contract; return contract
} }
function isTestNetwork(chainId) { function isTestNetwork(chainId: number) {
return chainId == 1337 || chainId == 31337; return chainId == 1337 || chainId == 31337
} }
async function obtainTokenAddress(deployer, chainId): Promise<string> { async function obtainTokenAddress(deployer: any, chainId: number): Promise<string> {
let tokenAddress = process.env.TOKEN_CONTRACT; let tokenAddress = process.env.TOKEN_CONTRACT
if (!tokenAddress && isTestNetwork(chainId)) { if (!tokenAddress && isTestNetwork(chainId)) {
const tokenContract = await deployERC20MockContract(deployer.address); const tokenContract = await deployERC20MockContract(deployer.address)
tokenAddress = tokenContract.address; tokenAddress = tokenContract.address
} else { } else {
throw new Error('TOKEN_ADDRESS should be provided'); throw new Error('TOKEN_ADDRESS should be provided')
} }
return tokenAddress ? tokenAddress : ''; return tokenAddress ? tokenAddress : ''
} }
async function main() { async function main() {
const [deployer] = await hre.ethers.getSigners(); const [deployer] = await hre.ethers.getSigners()
const network = await hre.ethers.provider.getNetwork(); const network = await hre.ethers.provider.getNetwork()
const votingLengthInSeconds = isTestNetwork(network.chainId) ? 2 * 60 : 30 * 24 * 3600 // 2 minutes or 30 days const votingLengthInSeconds = isTestNetwork(network.chainId) ? 4 * 60 : 14 * 24 * 3600 // 4 minutes or 14 days
const votingVerificationLengthInSeconds = isTestNetwork(network.chainId) ? 2 * 60 : 7 * 24 * 3600 // 2 minutes or 7 days
const timeBetweenVotingInSeconds = isTestNetwork(network.chainId) ? 60 : 7 * 24 * 3600 // 1 minute or 7 days const timeBetweenVotingInSeconds = isTestNetwork(network.chainId) ? 60 : 7 * 24 * 3600 // 1 minute or 7 days
console.log(`Deploying contracts on the network: ${network.name}(${network.chainId}), with the account: ${deployer.address}`); console.log(
`Deploying contracts on the network: ${network.name}(${network.chainId}), with the account: ${deployer.address}`
)
if (isTestNetwork(network.chainId)) { if (isTestNetwork(network.chainId)) {
await deployMultiCallContract(); await deployMultiCallContract()
} }
const tokenAddress = await obtainTokenAddress(deployer, network.chainId); const tokenAddress = await obtainTokenAddress(deployer, network.chainId)
const votingContract = await deployVotingContract(tokenAddress, votingLengthInSeconds, timeBetweenVotingInSeconds); const votingContract = await deployVotingContract(
const directoryContract = await deployDirectoryContract(votingContract.address); tokenAddress,
votingLengthInSeconds,
votingVerificationLengthInSeconds,
timeBetweenVotingInSeconds
)
const directoryContract = await deployDirectoryContract(votingContract.address)
await votingContract.setDirectory(directoryContract.address) await votingContract.setDirectory(directoryContract.address)
} }
// We recommend this pattern to be able to use async/await everywhere // We recommend this pattern to be able to use async/await everywhere
// and properly handle errors. // and properly handle errors.
main().catch((error) => { main().catch((error) => {
console.error(error); console.error(error)
process.exitCode = 1; process.exitCode = 1
}); })

View File

@ -10,7 +10,7 @@
"lint": "yarn lint:prettier --check && yarn lint:solhint && yarn lint:eslint", "lint": "yarn lint:prettier --check && yarn lint:solhint && yarn lint:eslint",
"lint:fix": "yarn lint:prettier --write && yarn lint:eslint --fix", "lint:fix": "yarn lint:prettier --write && yarn lint:eslint --fix",
"lint:eslint": "eslint './test/**/*.ts'", "lint:eslint": "eslint './test/**/*.ts'",
"lint:prettier": "yarn prettier './{contracts,test}/**/*.{ts,sol}'", "lint:prettier": "yarn prettier './{contracts,test,deploy}/**/*.{ts,sol}'",
"lint:solhint": "yarn solhint -f table contracts/**/*.sol", "lint:solhint": "yarn solhint -f table contracts/**/*.sol",
"flatten": "hardhat flatten", "flatten": "hardhat flatten",
"test": "hardhat test" "test": "hardhat test"

View File

@ -1,12 +1,12 @@
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { Contract } from 'ethers'
import { ERC20Mock } from '../abi'
import { time, loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { time, loadFixture } from '@nomicfoundation/hardhat-network-helpers'
import { expect } from 'chai' import { expect } from 'chai'
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import { ERC20Mock } from '../abi'
import { VotingContract } from '../typechain-types'
const { utils } = ethers const { utils } = ethers
const publicKeys = [ const publicKeys = [
@ -14,12 +14,23 @@ const publicKeys = [
'0xe84e64498172551d998a220e1d8e5893c818ee9aa90bdb855aec0c9e65e89014b8', '0xe84e64498172551d998a220e1d8e5893c818ee9aa90bdb855aec0c9e65e89014b8',
] ]
const votingLength = 1000
const votingVerificationLength = 200
const timeBetweenVoting = 3600
const votingWithVerificationLength = votingLength + votingVerificationLength
enum VoteType {
AGAINST,
FOR,
}
const typedData = { const typedData = {
types: { types: {
Vote: [ Vote: [
{ name: 'roomIdAndType', type: 'uint256' }, { name: 'roomIdAndType', type: 'uint256' },
{ name: 'sntAmount', type: 'uint256' }, { name: 'sntAmount', type: 'uint256' },
{ name: 'voter', type: 'address' }, { name: 'voter', type: 'address' },
{ name: 'timestamp', type: 'uint256' },
], ],
}, },
domain: { domain: {
@ -30,62 +41,47 @@ const typedData = {
}, },
} }
const createSignedVote = async (
signer: SignerWithAddress,
room: number,
type: VoteType,
sntAmount: number,
timestamp = 0
): Promise<VotingContract.SignedVoteStruct> => {
const vote = {
voter: signer.address,
roomIdAndType: BigNumber.from(room).mul(2).add(type),
sntAmount: BigNumber.from(sntAmount),
timestamp: timestamp ? BigNumber.from(timestamp) : BigNumber.from(await time.latest()),
}
const message = {
roomIdAndType: vote.roomIdAndType.toHexString(),
sntAmount: vote.sntAmount.toHexString(),
voter: vote.voter,
timestamp: vote.timestamp.toHexString(),
}
const signature = await signer._signTypedData(typedData.domain, typedData.types, message)
const splitSignature = utils.splitSignature(signature)
return { ...vote, r: splitSignature.r, vs: splitSignature._vs }
}
const getSignedVotes = async ( const getSignedVotes = async (
firstSigner: SignerWithAddress, firstSigner: SignerWithAddress,
secondSigner: SignerWithAddress, secondSigner: SignerWithAddress,
thirdSigner: SignerWithAddress thirdSigner: SignerWithAddress
): Promise<{ voter: string; roomIdAndType: BigNumber; sntAmount: BigNumber; r: string; vs: string }[]> => { ): Promise<VotingContract.SignedVoteStruct[]> => {
const votes = [ return [
{ await createSignedVote(firstSigner, 1, VoteType.FOR, 100),
voter: firstSigner, await createSignedVote(secondSigner, 1, VoteType.AGAINST, 100),
vote: 1, await createSignedVote(thirdSigner, 1, VoteType.FOR, 100),
sntAmount: BigNumber.from(100),
sessionID: 1,
},
{
voter: secondSigner,
vote: 0,
sntAmount: BigNumber.from(100),
sessionID: 1,
},
{
voter: thirdSigner,
vote: 1,
sntAmount: BigNumber.from(100),
sessionID: 1,
},
] ]
const messages: [string, BigNumber, BigNumber][] = votes.map((vote) => [
vote.voter.address,
BigNumber.from(vote.sessionID).mul(2).add(vote.vote),
vote.sntAmount,
])
const signedMessages = await Promise.all(
messages.map(async (msg, idx) => {
const message = { roomIdAndType: msg[1].toHexString(), sntAmount: msg[2].toHexString(), voter: msg[0] }
const signature = await votes[idx].voter._signTypedData(typedData.domain, typedData.types, message)
const sig = utils.splitSignature(signature)
return { voter: msg[0], roomIdAndType: msg[1], sntAmount: msg[2], r: sig.r, vs: sig._vs }
})
)
return signedMessages
} }
const voteAndFinalize = async (room: number, type: number, signer: SignerWithAddress, contract: Contract) => { const voteAndFinalize = async (room: number, type: VoteType, signer: SignerWithAddress, contract: VotingContract) => {
const vote: [string, BigNumber, BigNumber] = [ await contract.castVotes([await createSignedVote(signer, room, type, 100)])
signer.address, await time.increase(votingWithVerificationLength + 1)
BigNumber.from(room).mul(2).add(type),
BigNumber.from(100),
]
const message = { roomIdAndType: vote[1].toHexString(), sntAmount: vote[2].toHexString(), voter: vote[0] }
const signature = await signer._signTypedData(typedData.domain, typedData.types, message)
const sig = utils.splitSignature(signature)
await contract.castVotes([{ voter: vote[0], roomIdAndType: vote[1], sntAmount: vote[2], r: sig.r, vs: sig._vs }])
await time.increase(10000)
await contract.finalizeVotingRoom(room) await contract.finalizeVotingRoom(room)
} }
@ -99,7 +95,12 @@ async function fixture() {
await erc20Contract.transfer(thirdSigner.address, 10000) await erc20Contract.transfer(thirdSigner.address, 10000)
const votingContractFactory = await ethers.getContractFactory('VotingContract') const votingContractFactory = await ethers.getContractFactory('VotingContract')
const votingContract = await votingContractFactory.deploy(erc20Contract.address, 1000, 3600) const votingContract = await votingContractFactory.deploy(
erc20Contract.address,
votingLength,
votingVerificationLength,
timeBetweenVoting
)
const directoryContractFactory = await ethers.getContractFactory('Directory') const directoryContractFactory = await ethers.getContractFactory('Directory')
const directoryContract = await directoryContractFactory.deploy(votingContract.address) const directoryContract = await directoryContractFactory.deploy(votingContract.address)
@ -110,7 +111,6 @@ async function fixture() {
} }
before(async function () { before(async function () {
// this.timeout(10000)
const { votingContract: voting } = await loadFixture(fixture) const { votingContract: voting } = await loadFixture(fixture)
typedData.domain.chainId = 31337 typedData.domain.chainId = 31337
typedData.domain.verifyingContract = voting.address typedData.domain.verifyingContract = voting.address
@ -132,41 +132,41 @@ describe('VotingContract', () => {
describe('initialization', () => { describe('initialization', () => {
it('initializes', async () => { it('initializes', async () => {
const { votingContract } = await loadFixture(fixture) const { votingContract } = await loadFixture(fixture)
await expect(await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))) await expect(await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100)))
.to.emit(votingContract, 'VotingRoomStarted') .to.emit(votingContract, 'VotingRoomStarted')
.withArgs(1, publicKeys[0]) .withArgs(1, publicKeys[0])
await expect(await votingContract.initializeVotingRoom(1, publicKeys[1], BigNumber.from(100))) await expect(await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[1], BigNumber.from(100)))
.to.emit(votingContract, 'VotingRoomStarted') .to.emit(votingContract, 'VotingRoomStarted')
.withArgs(2, publicKeys[1]) .withArgs(2, publicKeys[1])
await expect(votingContract.initializeVotingRoom(1, publicKeys[1], BigNumber.from(100))).to.be.revertedWith( await expect(
'vote already ongoing' votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[1], BigNumber.from(100))
) ).to.be.revertedWith('vote already ongoing')
}) })
it('not enough token', async () => { it('not enough token', async () => {
const { votingContract } = await loadFixture(fixture) const { votingContract } = await loadFixture(fixture)
await expect( await expect(
votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(10000000000000)) votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(10000000000000))
).to.be.revertedWith('not enough token') ).to.be.revertedWith('not enough token')
}) })
describe('directory interaction', () => { describe('directory interaction', () => {
it('remove missing', async () => { it('remove missing', async () => {
const { votingContract } = await loadFixture(fixture) const { votingContract } = await loadFixture(fixture)
await expect(votingContract.initializeVotingRoom(0, publicKeys[0], BigNumber.from(100))).to.be.revertedWith( await expect(
'Community not in directory' votingContract.initializeVotingRoom(VoteType.AGAINST, publicKeys[0], BigNumber.from(100))
) ).to.be.revertedWith('Community not in directory')
}) })
it('add already in', async () => { it('add already in', async () => {
const { votingContract, directoryContract, firstSigner } = await loadFixture(fixture) const { votingContract, directoryContract, firstSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(1, 1, firstSigner, votingContract) await voteAndFinalize(1, VoteType.FOR, firstSigner, votingContract)
expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]]) expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]])
await expect(votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))).to.be.revertedWith( await expect(
'Community already in directory' votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
) ).to.be.revertedWith('Community already in directory')
}) })
}) })
}) })
@ -174,7 +174,7 @@ describe('VotingContract', () => {
it('gets', async () => { it('gets', async () => {
const { votingContract } = await loadFixture(fixture) const { votingContract } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
let votingRoom1 = await votingContract.votingRooms(0) let votingRoom1 = await votingContract.votingRooms(0)
expect(votingRoom1.voteType).to.eq(1) expect(votingRoom1.voteType).to.eq(1)
expect(votingRoom1.finalized).to.eq(false) expect(votingRoom1.finalized).to.eq(false)
@ -188,7 +188,7 @@ describe('VotingContract', () => {
expect(history[0].voteType).to.eq(1) expect(history[0].voteType).to.eq(1)
expect(history[0].community).to.eq(publicKeys[0]) expect(history[0].community).to.eq(publicKeys[0])
await votingContract.initializeVotingRoom(1, publicKeys[1], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[1], BigNumber.from(100))
const votingRoom2 = await votingContract.votingRooms(1) const votingRoom2 = await votingContract.votingRooms(1)
expect(votingRoom2.voteType).to.eq(1) expect(votingRoom2.voteType).to.eq(1)
expect(votingRoom2.finalized).to.eq(false) expect(votingRoom2.finalized).to.eq(false)
@ -209,12 +209,12 @@ describe('VotingContract', () => {
describe('history', () => { describe('history', () => {
it('saves to history', async () => { it('saves to history', async () => {
const { votingContract, firstSigner } = await loadFixture(fixture) const { votingContract, firstSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(1, 1, firstSigner, votingContract) await voteAndFinalize(1, VoteType.FOR, firstSigner, votingContract)
expect((await votingContract.getVotingHistory(publicKeys[0])).length).to.eq(1) expect((await votingContract.getVotingHistory(publicKeys[0])).length).to.eq(1)
await time.increase(10000) await time.increase(timeBetweenVoting + 1)
await votingContract.initializeVotingRoom(0, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.AGAINST, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(2, 0, firstSigner, votingContract) await voteAndFinalize(2, VoteType.AGAINST, firstSigner, votingContract)
const history = await votingContract.getVotingHistory(publicKeys[0]) const history = await votingContract.getVotingHistory(publicKeys[0])
expect(history.length).to.eq(2) expect(history.length).to.eq(2)
expect(history[0].voteType).to.eq(1) expect(history[0].voteType).to.eq(1)
@ -234,18 +234,18 @@ describe('VotingContract', () => {
it("can't start vote to fast", async () => { it("can't start vote to fast", async () => {
const { votingContract, firstSigner } = await loadFixture(fixture) const { votingContract, firstSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(1, 1, firstSigner, votingContract) await voteAndFinalize(1, VoteType.FOR, firstSigner, votingContract)
await expect(votingContract.initializeVotingRoom(0, publicKeys[0], BigNumber.from(100))).to.be.revertedWith( await expect(
'Community was in a vote recently' votingContract.initializeVotingRoom(VoteType.AGAINST, publicKeys[0], BigNumber.from(100))
) ).to.be.revertedWith('Community was in a vote recently')
}) })
}) })
describe('finalization', () => { describe('finalization', () => {
it('finalizes', async () => { it('finalizes', async () => {
const { votingContract } = await loadFixture(fixture) const { votingContract } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
let votingRoom = await votingContract.votingRooms(0) let votingRoom = await votingContract.votingRooms(0)
expect(votingRoom.voteType).to.eq(1) expect(votingRoom.voteType).to.eq(1)
expect(votingRoom.finalized).to.eq(false) expect(votingRoom.finalized).to.eq(false)
@ -254,7 +254,7 @@ describe('VotingContract', () => {
expect(votingRoom.totalVotesAgainst).to.eq(0) expect(votingRoom.totalVotesAgainst).to.eq(0)
expect(votingRoom.roomNumber).to.eq(1) expect(votingRoom.roomNumber).to.eq(1)
await time.increase(2000) await time.increase(votingWithVerificationLength + 1)
await expect(await votingContract.finalizeVotingRoom(1)) await expect(await votingContract.finalizeVotingRoom(1))
.to.emit(votingContract, 'VotingRoomFinalized') .to.emit(votingContract, 'VotingRoomFinalized')
.withArgs(1, publicKeys[0], true, 1) .withArgs(1, publicKeys[0], true, 1)
@ -270,14 +270,14 @@ describe('VotingContract', () => {
it('verifies votes', async () => { it('verifies votes', async () => {
const { votingContract, erc20Contract, firstSigner } = await loadFixture(fixture) const { votingContract, erc20Contract, firstSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
// clear balance // clear balance
const firstSignerBalance = await erc20Contract.balanceOf(firstSigner.address) const firstSignerBalance = await erc20Contract.balanceOf(firstSigner.address)
await erc20Contract.increaseAllowance(firstSigner.address, firstSignerBalance) await erc20Contract.increaseAllowance(firstSigner.address, firstSignerBalance)
await erc20Contract.transferFrom(firstSigner.address, erc20Contract.address, firstSignerBalance) await erc20Contract.transferFrom(firstSigner.address, erc20Contract.address, firstSignerBalance)
await time.increase(2000) await time.increase(votingWithVerificationLength + 1)
await expect(await votingContract.finalizeVotingRoom(1)) await expect(await votingContract.finalizeVotingRoom(1))
.to.emit(votingContract, 'NotEnoughToken') .to.emit(votingContract, 'NotEnoughToken')
.withArgs(1, firstSigner.address) .withArgs(1, firstSigner.address)
@ -288,8 +288,8 @@ describe('VotingContract', () => {
describe('directory interaction', () => { describe('directory interaction', () => {
it('add community', async () => { it('add community', async () => {
const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture) const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(1, 1, secondSigner, votingContract) await voteAndFinalize(1, VoteType.FOR, secondSigner, votingContract)
const votingRoom = await votingContract.votingRooms(0) const votingRoom = await votingContract.votingRooms(0)
expect(votingRoom.voteType).to.eq(1) expect(votingRoom.voteType).to.eq(1)
@ -304,33 +304,33 @@ describe('VotingContract', () => {
it('remove community', async () => { it('remove community', async () => {
const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture) const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(1, 1, secondSigner, votingContract) await voteAndFinalize(1, VoteType.FOR, secondSigner, votingContract)
expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]]) expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]])
await time.increase(10000) await time.increase(timeBetweenVoting + 1)
await votingContract.initializeVotingRoom(0, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.AGAINST, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(2, 1, secondSigner, votingContract) await voteAndFinalize(2, VoteType.FOR, secondSigner, votingContract)
expect(await directoryContract.getCommunities()).to.deep.eq([]) expect(await directoryContract.getCommunities()).to.deep.eq([])
}) })
it('failed add vote', async () => { it('failed add vote', async () => {
const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture) const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(1, 0, secondSigner, votingContract) await voteAndFinalize(1, VoteType.AGAINST, secondSigner, votingContract)
expect(await directoryContract.getCommunities()).to.deep.eq([]) expect(await directoryContract.getCommunities()).to.deep.eq([])
}) })
it('failed remove vote', async () => { it('failed remove vote', async () => {
const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture) const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(1, 1, secondSigner, votingContract) await voteAndFinalize(1, VoteType.FOR, secondSigner, votingContract)
expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]]) expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]])
await time.increase(10000) await time.increase(timeBetweenVoting + 1)
await votingContract.initializeVotingRoom(0, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.AGAINST, publicKeys[0], BigNumber.from(100))
await voteAndFinalize(2, 0, secondSigner, votingContract) await voteAndFinalize(2, VoteType.AGAINST, secondSigner, votingContract)
expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]]) expect(await directoryContract.getCommunities()).to.deep.eq([publicKeys[0]])
}) })
}) })
@ -340,7 +340,7 @@ describe('VotingContract', () => {
describe('helpers', () => { describe('helpers', () => {
it('getActiveVotingRoom', async () => { it('getActiveVotingRoom', async () => {
const { votingContract } = await loadFixture(fixture) const { votingContract } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
const votingRoom1 = await votingContract.getActiveVotingRoom(publicKeys[0]) const votingRoom1 = await votingContract.getActiveVotingRoom(publicKeys[0])
expect(votingRoom1.voteType).to.eq(1) expect(votingRoom1.voteType).to.eq(1)
@ -350,8 +350,7 @@ describe('VotingContract', () => {
expect(votingRoom1.totalVotesAgainst).to.eq(0) expect(votingRoom1.totalVotesAgainst).to.eq(0)
expect(votingRoom1.roomNumber).to.eq(1) expect(votingRoom1.roomNumber).to.eq(1)
await time.increase(10000) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[1], BigNumber.from(100))
await votingContract.initializeVotingRoom(1, publicKeys[1], BigNumber.from(100))
const votingRoom2 = await votingContract.getActiveVotingRoom(publicKeys[1]) const votingRoom2 = await votingContract.getActiveVotingRoom(publicKeys[1])
expect(votingRoom2.voteType).to.eq(1) expect(votingRoom2.voteType).to.eq(1)
@ -364,14 +363,17 @@ describe('VotingContract', () => {
it('get active votes', async () => { it('get active votes', async () => {
const { votingContract } = await loadFixture(fixture) const { votingContract } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
expect(await votingContract.getActiveVotingRooms()).to.deep.eq([BigNumber.from(1)]) expect(await votingContract.getActiveVotingRooms()).to.deep.eq([BigNumber.from(1)])
await votingContract.initializeVotingRoom(1, publicKeys[1], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[1], BigNumber.from(100))
expect(await votingContract.getActiveVotingRooms()).to.deep.eq([BigNumber.from(1), BigNumber.from(2)]) expect(await votingContract.getActiveVotingRooms()).to.deep.eq([BigNumber.from(1), BigNumber.from(2)])
await time.increase(2000)
await time.increase(votingWithVerificationLength + 1)
await votingContract.finalizeVotingRoom(1) await votingContract.finalizeVotingRoom(1)
expect(await votingContract.getActiveVotingRooms()).to.deep.eq([BigNumber.from(2)]) expect(await votingContract.getActiveVotingRooms()).to.deep.eq([BigNumber.from(2)])
await votingContract.finalizeVotingRoom(2) await votingContract.finalizeVotingRoom(2)
expect(await votingContract.getActiveVotingRooms()).to.deep.eq([]) expect(await votingContract.getActiveVotingRooms()).to.deep.eq([])
}) })
@ -380,8 +382,8 @@ describe('VotingContract', () => {
describe('voting', () => { describe('voting', () => {
it('check voters', async () => { it('check voters', async () => {
const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture) const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
const votes = await getSignedVotes(firstSigner, secondSigner, thirdSigner) const votes = await getSignedVotes(firstSigner, secondSigner, thirdSigner)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))
expect(await votingContract.listRoomVoters(1)).to.deep.eq([firstSigner.address]) expect(await votingContract.listRoomVoters(1)).to.deep.eq([firstSigner.address])
await votingContract.castVotes(votes.slice(2)) await votingContract.castVotes(votes.slice(2))
@ -390,20 +392,26 @@ describe('VotingContract', () => {
it('not enough tokens', async () => { it('not enough tokens', async () => {
const { votingContract, secondSigner } = await loadFixture(fixture) const { votingContract, secondSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100)) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
const vote: [string, BigNumber, BigNumber] = [ const vote: [string, BigNumber, BigNumber, BigNumber] = [
secondSigner.address, secondSigner.address,
BigNumber.from(1).mul(2).add(1), BigNumber.from(1).mul(2).add(1),
BigNumber.from(100000000000), BigNumber.from(100000000000),
BigNumber.from(await time.latest()),
] ]
const message = { roomIdAndType: vote[1].toHexString(), sntAmount: vote[2].toHexString(), voter: vote[0] } const message = {
roomIdAndType: vote[1].toHexString(),
sntAmount: vote[2].toHexString(),
voter: vote[0],
timestamp: vote[3].toHexString(),
}
const signature = await secondSigner._signTypedData(typedData.domain, typedData.types, message) const signature = await secondSigner._signTypedData(typedData.domain, typedData.types, message)
const sig = utils.splitSignature(signature) const sig = utils.splitSignature(signature)
await expect( await expect(
await votingContract.castVotes([ await votingContract.castVotes([
{ voter: vote[0], roomIdAndType: vote[1], sntAmount: vote[2], r: sig.r, vs: sig._vs }, { voter: vote[0], roomIdAndType: vote[1], sntAmount: vote[2], timestamp: vote[3], r: sig.r, vs: sig._vs },
]) ])
) )
.to.emit(votingContract, 'NotEnoughToken') .to.emit(votingContract, 'NotEnoughToken')
@ -420,8 +428,8 @@ describe('VotingContract', () => {
it('success', async () => { it('success', async () => {
const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture) const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
const votes = await getSignedVotes(firstSigner, secondSigner, thirdSigner) const votes = await getSignedVotes(firstSigner, secondSigner, thirdSigner)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))
await votingContract.castVotes(votes) await votingContract.castVotes(votes)
const votingRoom = await votingContract.votingRooms(0) const votingRoom = await votingContract.votingRooms(0)
@ -435,8 +443,8 @@ describe('VotingContract', () => {
it('double vote', async () => { it('double vote', async () => {
const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture) const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
const votes = await getSignedVotes(firstSigner, secondSigner, thirdSigner) const votes = await getSignedVotes(firstSigner, secondSigner, thirdSigner)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))
await votingContract.castVotes(votes) await votingContract.castVotes(votes)
await votingContract.castVotes(votes) await votingContract.castVotes(votes)
@ -457,19 +465,22 @@ describe('VotingContract', () => {
it('old room', async () => { it('old room', async () => {
const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture) const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture)
const votes = await getSignedVotes(firstSigner, secondSigner, thirdSigner) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))
await time.increase(2000) await time.increase(votingWithVerificationLength + 1)
await expect(votingContract.castVotes(votes)).to.be.reverted
const signedVotes = await getSignedVotes(firstSigner, secondSigner, thirdSigner)
await expect(votingContract.castVotes(signedVotes)).to.be.reverted
}) })
it('wrong signature', async () => { it('wrong signature', async () => {
const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture) const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture)
const signedVotes = await getSignedVotes(firstSigner, secondSigner, thirdSigner) await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))
await time.increase(2000)
const wronglySignedMessages = signedVotes.map((msg) => { await time.increase(votingWithVerificationLength + 1)
const signedVotes = await getSignedVotes(firstSigner, secondSigner, thirdSigner)
const wronglySignedVotes = signedVotes.map((msg) => {
return { return {
...msg, ...msg,
r: '0x2d63286985277c440b9f01a987fbbc9bc9ca32cb4e9e55ee3ffcab4e67c211e6', r: '0x2d63286985277c440b9f01a987fbbc9bc9ca32cb4e9e55ee3ffcab4e67c211e6',
@ -477,8 +488,22 @@ describe('VotingContract', () => {
} }
}) })
await votingContract.castVotes(wronglySignedMessages) await votingContract.castVotes(wronglySignedVotes)
await expect(await votingContract.listRoomVoters(1)).to.deep.eq([firstSigner.address]) await expect(await votingContract.listRoomVoters(1)).to.deep.eq([firstSigner.address])
}) })
it('validates vote timestamp', async () => {
const { votingContract, secondSigner } = await loadFixture(fixture)
const voteBefore = await createSignedVote(secondSigner, 1, VoteType.FOR, 100)
await time.increase(1)
await votingContract.initializeVotingRoom(VoteType.FOR, publicKeys[0], BigNumber.from(100))
await expect(votingContract.castVotes([voteBefore])).to.be.revertedWith('invalid vote timestamp')
await time.increase(votingLength + 1)
const voteAfter = await createSignedVote(secondSigner, 1, VoteType.FOR, 100)
await expect(votingContract.castVotes([voteAfter])).to.be.revertedWith('invalid vote timestamp')
})
}) })
}) })