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;
bytes sntAmount = 3;
string sign = 4;
uint32 nonce = 5;
uint64 sessionID = 6;
uint32 timestamp = 5;
uint64 roomID = 6;
}
message WakuFeature {

View File

@ -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

View File

@ -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,
}
})

View File

@ -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 })

View File

@ -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
},

View File

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

View File

@ -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
})

View File

@ -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
}
}

View File

@ -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]);
}
}
}

View File

@ -4,87 +4,110 @@
// 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
// global scope, and execute the script.
const hre = require("hardhat");
const hre = require('hardhat')
import { ERC20Mock, MultiCall } from '../abi';
import { BigNumber } from 'ethers';
import { ERC20Mock, MultiCall } from '../abi'
import { BigNumber } from 'ethers'
async function deployVotingContract(tokenAddress: string, votingLength, timeBetweenVoting) {
const contractFactory = await hre.ethers.getContractFactory('VotingContract');
const contract = await contractFactory.deploy(tokenAddress, votingLength, timeBetweenVoting);
await contract.deployed();
async function deployVotingContract(
tokenAddress: string,
votingLength: number,
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) {
const contractFactory = await hre.ethers.getContractFactory('Directory');
const contract = await contractFactory.deploy(votingContractAddress);
await contract.deployed();
const contractFactory = await hre.ethers.getContractFactory('Directory')
const contract = await contractFactory.deploy(votingContractAddress)
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) {
const contractFactory = await hre.ethers.getContractFactory(ERC20Mock.abi, ERC20Mock.bytecode);
const contract = await contractFactory.deploy('MSNT', 'Mock SNT', deployerAddress, BigNumber.from('0x33B2E3C9FD0803CE8000000'));
await contract.deployed();
const contractFactory = await hre.ethers.getContractFactory(ERC20Mock.abi, ERC20Mock.bytecode)
const contract = await contractFactory.deploy(
'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() {
const contractFactory = await hre.ethers.getContractFactory(MultiCall.abi, MultiCall.bytecode);
const contract = await contractFactory.deploy();
await contract.deployed();
const contractFactory = await hre.ethers.getContractFactory(MultiCall.abi, MultiCall.bytecode)
const contract = await contractFactory.deploy()
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) {
return chainId == 1337 || chainId == 31337;
function isTestNetwork(chainId: number) {
return chainId == 1337 || chainId == 31337
}
async function obtainTokenAddress(deployer, chainId): Promise<string> {
let tokenAddress = process.env.TOKEN_CONTRACT;
async function obtainTokenAddress(deployer: any, chainId: number): Promise<string> {
let tokenAddress = process.env.TOKEN_CONTRACT
if (!tokenAddress && isTestNetwork(chainId)) {
const tokenContract = await deployERC20MockContract(deployer.address);
tokenAddress = tokenContract.address;
const tokenContract = await deployERC20MockContract(deployer.address)
tokenAddress = tokenContract.address
} 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() {
const [deployer] = await hre.ethers.getSigners();
const network = await hre.ethers.provider.getNetwork();
const votingLengthInSeconds = isTestNetwork(network.chainId) ? 2 * 60 : 30 * 24 * 3600 // 2 minutes or 30 days
const [deployer] = await hre.ethers.getSigners()
const network = await hre.ethers.provider.getNetwork()
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
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)) {
await deployMultiCallContract();
await deployMultiCallContract()
}
const tokenAddress = await obtainTokenAddress(deployer, network.chainId);
const votingContract = await deployVotingContract(tokenAddress, votingLengthInSeconds, timeBetweenVotingInSeconds);
const directoryContract = await deployDirectoryContract(votingContract.address);
const tokenAddress = await obtainTokenAddress(deployer, network.chainId)
const votingContract = await deployVotingContract(
tokenAddress,
votingLengthInSeconds,
votingVerificationLengthInSeconds,
timeBetweenVotingInSeconds
)
const directoryContract = await deployDirectoryContract(votingContract.address)
await votingContract.setDirectory(directoryContract.address)
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
console.error(error)
process.exitCode = 1
})

View File

@ -10,7 +10,7 @@
"lint": "yarn lint:prettier --check && yarn lint:solhint && yarn lint:eslint",
"lint:fix": "yarn lint:prettier --write && yarn lint:eslint --fix",
"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",
"flatten": "hardhat flatten",
"test": "hardhat test"

View File

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