349 lines
14 KiB
TypeScript
349 lines
14 KiB
TypeScript
import { expect, use } from 'chai'
|
|
import { loadFixture, deployContract, MockProvider, solidity } from 'ethereum-waffle'
|
|
import { VotingContract, ERC20Mock } from '../abi'
|
|
import { utils, Wallet, Contract } from 'ethers'
|
|
import { signTypedMessage, TypedMessage } from 'eth-sig-util'
|
|
import { BigNumber } from '@ethersproject/bignumber'
|
|
import { JsonRpcProvider } from '@ethersproject/providers'
|
|
|
|
use(solidity)
|
|
|
|
interface MessageTypeProperty {
|
|
name: string
|
|
type: string
|
|
}
|
|
interface MessageTypes {
|
|
EIP712Domain: MessageTypeProperty[]
|
|
[additionalProperties: string]: MessageTypeProperty[]
|
|
}
|
|
|
|
const typedData = {
|
|
types: {
|
|
EIP712Domain: [
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'version', type: 'string' },
|
|
{ name: 'chainId', type: 'uint256' },
|
|
{ name: 'verifyingContract', type: 'address' },
|
|
],
|
|
Vote: [
|
|
{ name: 'roomIdAndType', type: 'uint256' },
|
|
{ name: 'tokenAmount', type: 'uint256' },
|
|
{ name: 'voter', type: 'address' },
|
|
],
|
|
},
|
|
primaryType: 'Vote',
|
|
domain: {
|
|
name: 'Voting Contract',
|
|
version: '1',
|
|
chainId: 0,
|
|
verifyingContract: '',
|
|
},
|
|
}
|
|
|
|
const getSignedMessages = async (
|
|
alice: Wallet,
|
|
firstAddress: Wallet,
|
|
secondAddress: Wallet
|
|
): Promise<{ messages: any[]; signedMessages: any[] }> => {
|
|
const votes = [
|
|
{
|
|
voter: firstAddress,
|
|
vote: 0,
|
|
tokenAmount: BigNumber.from(100),
|
|
sessionID: 0,
|
|
},
|
|
{
|
|
voter: secondAddress,
|
|
vote: 1,
|
|
tokenAmount: BigNumber.from(100),
|
|
sessionID: 0,
|
|
},
|
|
]
|
|
const messages = votes.map((vote) => {
|
|
return [vote.voter.address, BigNumber.from(vote.sessionID).mul(2).add(vote.vote), vote.tokenAmount] as [
|
|
string,
|
|
BigNumber,
|
|
BigNumber
|
|
]
|
|
})
|
|
const signedMessages = messages.map((msg, idx) => {
|
|
const t: TypedMessage<MessageTypes> = {
|
|
...typedData,
|
|
message: { roomIdAndType: msg[1].toHexString(), tokenAmount: msg[2].toHexString(), voter: msg[0] },
|
|
}
|
|
const sig = utils.splitSignature(
|
|
signTypedMessage(Buffer.from(utils.arrayify(votes[idx].voter.privateKey)), { data: t }, 'V3')
|
|
)
|
|
return [...msg, sig.r, sig._vs]
|
|
})
|
|
|
|
return { messages, signedMessages }
|
|
}
|
|
|
|
async function fixture([alice, firstAddress, secondAddress, noTokensAddress]: any[], provider: JsonRpcProvider) {
|
|
const erc20 = await deployContract(alice, ERC20Mock, ['MSNT', 'Mock SNT', alice.address, 100000])
|
|
await erc20.transfer(firstAddress.address, 10000)
|
|
await erc20.transfer(secondAddress.address, 10000)
|
|
const contract = await deployContract(alice, VotingContract, [erc20.address, 1000])
|
|
const secondContract = await deployContract(alice, VotingContract, [erc20.address, 1000])
|
|
return { contract, secondContract, alice, firstAddress, secondAddress, provider, noTokensAddress }
|
|
}
|
|
|
|
before(async function () {
|
|
this.timeout(20000)
|
|
const { contract } = await loadFixture(fixture)
|
|
typedData.domain.chainId = 1
|
|
typedData.domain.verifyingContract = contract.address
|
|
})
|
|
|
|
describe('Contract', () => {
|
|
describe('Voting Room', () => {
|
|
describe('initialization', () => {
|
|
it('initializes', async () => {
|
|
const { contract } = await loadFixture(fixture)
|
|
|
|
await expect(await contract.initializeVotingRoom('test', 'short desc', BigNumber.from(100)))
|
|
.to.emit(contract, 'VotingRoomStarted')
|
|
.withArgs(0, 'test')
|
|
await expect(await contract.initializeVotingRoom('test2', 'short desc', BigNumber.from(100)))
|
|
.to.emit(contract, 'VotingRoomStarted')
|
|
.withArgs(1, 'test2')
|
|
})
|
|
|
|
it('not enough token', async () => {
|
|
const { contract } = await loadFixture(fixture)
|
|
await expect(
|
|
contract.initializeVotingRoom('test', 'short desc', BigNumber.from(10000000000000))
|
|
).to.be.revertedWith('not enough token')
|
|
})
|
|
|
|
it('no tokens address', async () => {
|
|
const { contract, noTokensAddress } = await loadFixture(fixture)
|
|
const noTokensContract = contract.connect(noTokensAddress)
|
|
await expect(
|
|
noTokensContract.initializeVotingRoom('test', 'short desc', BigNumber.from(10))
|
|
).to.be.revertedWith('not enough token')
|
|
})
|
|
|
|
it("can't start voting with 0 tokens", async () => {
|
|
const { contract, noTokensAddress } = await loadFixture(fixture)
|
|
const noTokensContract = contract.connect(noTokensAddress)
|
|
await expect(noTokensContract.initializeVotingRoom('test', 'short desc', BigNumber.from(0))).to.be.revertedWith(
|
|
'token amount must not be 0'
|
|
)
|
|
})
|
|
})
|
|
|
|
it('gets', async () => {
|
|
const { contract } = await loadFixture(fixture)
|
|
await contract.initializeVotingRoom('T1', 'short desc', BigNumber.from(100))
|
|
|
|
expect((await contract.votingRooms(0))[2]).to.eq('T1')
|
|
expect((await contract.votingRooms(0))[3]).to.eq('short desc')
|
|
expect((await contract.votingRooms(0))[4]).to.deep.eq(BigNumber.from(100))
|
|
expect((await contract.votingRooms(0))[5]).to.deep.eq(BigNumber.from(0))
|
|
|
|
await contract.initializeVotingRoom('T2', 'long desc', BigNumber.from(200))
|
|
expect((await contract.votingRooms(1))[2]).to.eq('T2')
|
|
expect((await contract.votingRooms(1))[3]).to.eq('long desc')
|
|
expect((await contract.votingRooms(1))[4]).to.deep.eq(BigNumber.from(200))
|
|
expect((await contract.votingRooms(1))[5]).to.deep.eq(BigNumber.from(0))
|
|
})
|
|
|
|
it('reverts no room', async () => {
|
|
const { contract } = await loadFixture(fixture)
|
|
await expect(contract.votingRooms(1)).to.be.reverted
|
|
await expect(contract.votingRooms(0)).to.be.reverted
|
|
await contract.initializeVotingRoom('T2', '', BigNumber.from(200))
|
|
await expect(contract.votingRooms(1)).to.be.reverted
|
|
})
|
|
|
|
it('getOngoingVotingRooms', async () => {
|
|
const { contract, provider } = await loadFixture(fixture)
|
|
|
|
await expect((await contract.getOngoingVotingRooms()).length).to.eq(0)
|
|
await contract.initializeVotingRoom('test1', 'short desc', BigNumber.from(100))
|
|
|
|
let rooms
|
|
rooms = await contract.getOngoingVotingRooms()
|
|
await expect(rooms.length).to.eq(1)
|
|
await expect(rooms[0][2]).to.eq('test1')
|
|
await provider.send('evm_increaseTime', [500])
|
|
await provider.send('evm_mine', [])
|
|
|
|
await contract.initializeVotingRoom('test2', 'short desc', BigNumber.from(100))
|
|
rooms = await contract.getOngoingVotingRooms()
|
|
await expect(rooms.length).to.eq(2)
|
|
await expect(rooms[0][2]).to.eq('test2')
|
|
await expect(rooms[1][2]).to.eq('test1')
|
|
await provider.send('evm_increaseTime', [600])
|
|
await provider.send('evm_mine', [])
|
|
|
|
rooms = await contract.getOngoingVotingRooms()
|
|
|
|
await expect(rooms.length).to.eq(1)
|
|
await expect(rooms[0][2]).to.eq('test2')
|
|
await provider.send('evm_increaseTime', [600])
|
|
await provider.send('evm_mine', [])
|
|
rooms = await contract.getOngoingVotingRooms()
|
|
await expect(rooms.length).to.eq(0)
|
|
})
|
|
})
|
|
describe('helpers', () => {
|
|
it('get voting rooms', async () => {
|
|
const { contract } = await loadFixture(fixture)
|
|
await contract.initializeVotingRoom('T1', 't1', BigNumber.from(100))
|
|
|
|
await contract.initializeVotingRoom('T2', 't2', BigNumber.from(200))
|
|
const votingRooms = await contract.getVotingRooms()
|
|
|
|
expect(votingRooms.length).to.eq(2)
|
|
|
|
expect(votingRooms[0][2]).to.eq('T1')
|
|
expect(votingRooms[0][3]).to.eq('t1')
|
|
expect(votingRooms[0][4]).to.deep.eq(BigNumber.from(100))
|
|
expect(votingRooms[0][5]).to.deep.eq(BigNumber.from(0))
|
|
|
|
expect(votingRooms[1][2]).to.eq('T2')
|
|
expect(votingRooms[1][3]).to.eq('t2')
|
|
expect(votingRooms[1][4]).to.deep.eq(BigNumber.from(200))
|
|
expect(votingRooms[1][5]).to.deep.eq(BigNumber.from(0))
|
|
})
|
|
})
|
|
|
|
describe('voting', () => {
|
|
it('check voters', async () => {
|
|
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
|
|
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
|
|
await contract.initializeVotingRoom('0xabA1eF51ef4aE360a9e8C9aD2d787330B602eb24', '', BigNumber.from(100))
|
|
|
|
expect(await contract.listRoomVoters(0)).to.deep.eq([alice.address])
|
|
await contract.castVotes(signedMessages.slice(1))
|
|
|
|
expect(await contract.listRoomVoters(0)).to.deep.eq([alice.address, secondAddress.address])
|
|
})
|
|
|
|
it('not enough tokens', async () => {
|
|
const { contract, firstAddress } = await loadFixture(fixture)
|
|
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
|
|
|
|
const msg = [firstAddress.address, BigNumber.from(0).mul(2).add(1), BigNumber.from(100000000000)]
|
|
const t: TypedMessage<MessageTypes> = {
|
|
...typedData,
|
|
message: { roomIdAndType: msg[1].toHexString(), tokenAmount: msg[2].toHexString(), voter: msg[0] },
|
|
}
|
|
const sig = utils.splitSignature(
|
|
signTypedMessage(Buffer.from(utils.arrayify(firstAddress.privateKey)), { data: t }, 'V3')
|
|
)
|
|
const signedMessage = [...msg, sig.r, sig._vs]
|
|
|
|
await expect(contract.castVotes([signedMessage])).to.be.revertedWith('voter doesnt have enough tokens')
|
|
|
|
const votingRoom = await contract.votingRooms(0)
|
|
expect(votingRoom[2]).to.eq('test')
|
|
expect(votingRoom[3]).to.eq('')
|
|
expect(votingRoom[4]).to.deep.eq(BigNumber.from(100))
|
|
expect(votingRoom[5]).to.deep.eq(BigNumber.from(0))
|
|
})
|
|
|
|
it('success', async () => {
|
|
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
|
|
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
|
|
await contract.initializeVotingRoom('test', 'test desc', BigNumber.from(100))
|
|
await contract.castVotes(signedMessages)
|
|
|
|
const votingRoom = await contract.votingRooms(0)
|
|
expect(votingRoom[2]).to.eq('test')
|
|
expect(votingRoom[3]).to.eq('test desc')
|
|
expect(votingRoom[4]).to.deep.eq(BigNumber.from(200))
|
|
expect(votingRoom[5]).to.deep.eq(BigNumber.from(100))
|
|
})
|
|
|
|
it('double vote', async () => {
|
|
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
|
|
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
|
|
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
|
|
await contract.castVotes(signedMessages)
|
|
await expect(contract.castVotes(signedMessages)).to.be.revertedWith('voter already voted')
|
|
|
|
const votingRoom = await contract.votingRooms(0)
|
|
expect(votingRoom[2]).to.eq('test')
|
|
expect(votingRoom[3]).to.eq('')
|
|
expect(votingRoom[4]).to.deep.eq(BigNumber.from(200))
|
|
expect(votingRoom[5]).to.deep.eq(BigNumber.from(100))
|
|
})
|
|
|
|
it('random bytes', async () => {
|
|
const { contract } = await loadFixture(fixture)
|
|
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
|
|
await expect(contract.castVotes([new Uint8Array([12, 12, 12])])).to.be.reverted
|
|
})
|
|
|
|
it('none existent room', async () => {
|
|
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
|
|
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
|
|
await expect(contract.castVotes(signedMessages)).to.be.revertedWith('vote not found')
|
|
})
|
|
|
|
it('old room', async () => {
|
|
const { contract, alice, firstAddress, secondAddress, provider } = await loadFixture(fixture)
|
|
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
|
|
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
|
|
await provider.send('evm_increaseTime', [10000])
|
|
await expect(contract.castVotes(signedMessages)).to.be.revertedWith('vote closed')
|
|
})
|
|
|
|
it('wrong signature', async () => {
|
|
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
|
|
const { messages } = await getSignedMessages(alice, firstAddress, secondAddress)
|
|
|
|
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
|
|
|
|
const signedMessages = await Promise.all(
|
|
messages.map(async (msg) => {
|
|
const t: TypedMessage<MessageTypes> = {
|
|
...typedData,
|
|
message: { roomIdAndType: msg[1].toHexString(), tokenAmount: msg[2].toHexString(), voter: msg[0] },
|
|
}
|
|
const sig = utils.splitSignature(
|
|
signTypedMessage(Buffer.from(utils.arrayify(firstAddress.privateKey)), { data: t }, 'V3')
|
|
)
|
|
const signedMessage = [...msg, sig.r, sig._vs]
|
|
return signedMessage
|
|
})
|
|
)
|
|
|
|
await expect(contract.castVotes(signedMessages)).to.be.revertedWith('vote has wrong signature')
|
|
})
|
|
|
|
it("vote can't be reused", async () => {
|
|
const createMessage = (voter: any, domainSeparator: any) => {
|
|
const correctMessageParams = [voter.address, BigNumber.from(0).mul(2).add(0), BigNumber.from(10)]
|
|
const correctMessageData: TypedMessage<MessageTypes> = {
|
|
...domainSeparator,
|
|
message: {
|
|
roomIdAndType: correctMessageParams[1].toHexString(),
|
|
tokenAmount: correctMessageParams[2].toHexString(),
|
|
voter: correctMessageParams[0],
|
|
},
|
|
}
|
|
|
|
const correctSig = utils.splitSignature(
|
|
signTypedMessage(Buffer.from(utils.arrayify(voter.privateKey)), { data: correctMessageData }, 'V3')
|
|
)
|
|
|
|
return [...correctMessageParams, correctSig.r, correctSig._vs]
|
|
}
|
|
|
|
const { contract, secondContract, firstAddress } = await loadFixture(fixture)
|
|
|
|
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
|
|
await secondContract.initializeVotingRoom('test', '', BigNumber.from(100))
|
|
|
|
const correctAddress = createMessage(firstAddress, typedData)
|
|
await expect(contract.castVotes([correctAddress])).not.to.be.reverted
|
|
await expect(secondContract.castVotes([correctAddress])).to.be.revertedWith('vote has wrong signature')
|
|
})
|
|
})
|
|
})
|