verify balances at finalization

closes: #6
This commit is contained in:
Patryk Osmaczko 2023-03-16 23:26:00 +01:00 committed by osmaczko
parent 03cd1915c8
commit a7d7736f34
4 changed files with 113 additions and 70 deletions

View File

@ -9,7 +9,6 @@ export type VotingRoom = {
community: string
totalVotesFor: BigNumber
totalVotesAgainst: BigNumber
voters: string[]
roomNumber: number
}

View File

@ -14,7 +14,6 @@ describe('voting', () => {
community: '0x000',
totalVotesFor: BigNumber.from(100),
totalVotesAgainst: BigNumber.from(100),
voters: ['0x01', '0x02'],
roomNumber: 1,
}
const room = voting.fromRoom(votingRoom)
@ -32,7 +31,6 @@ describe('voting', () => {
community: '0x000',
totalVotesFor: BigNumber.from(1000),
totalVotesAgainst: BigNumber.from(100),
voters: ['0x01', '0x02'],
roomNumber: 1,
}
const room = voting.fromRoom(votingRoom)

View File

@ -15,11 +15,18 @@ contract VotingContract {
uint256 private constant VOTING_LENGTH = 1000;
uint256 private constant TIME_BETWEEN_VOTING = 3600;
enum VoteType {
REMOVE,
ADD
}
struct Vote {
address voter;
VoteType voteType;
uint256 sntAmount;
}
struct VotingRoom {
uint256 startBlock;
uint256 endAt;
@ -29,12 +36,23 @@ contract VotingContract {
uint256 totalVotesFor;
uint256 totalVotesAgainst;
uint256 roomNumber;
address[] voters;
}
struct SignedVote {
address voter;
uint256 roomIdAndType;
uint256 sntAmount;
bytes32 r;
bytes32 vs;
}
event VotingRoomStarted(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);
address public owner;
Directory public directory;
IERC20 public token;
@ -42,7 +60,9 @@ contract VotingContract {
VotingRoom[] public votingRooms;
mapping(bytes => uint256) public activeRoomIDByCommunityID;
mapping(bytes => uint256[]) private roomIDsByCommunityID;
mapping(uint256 => mapping(address => bool)) private voted;
mapping(uint256 => Vote[]) private votesByRoomID;
mapping(uint256 => mapping(address => bool)) private votedAddressesByRoomID;
bytes32 private constant EIP712DOMAIN_TYPEHASH =
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)');
@ -71,13 +91,13 @@ contract VotingContract {
bytes32 public constant VOTE_TYPEHASH = keccak256('Vote(uint256 roomIdAndType,uint256 sntAmount,address voter)');
function hash(Vote 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));
}
function verify(Vote calldata vote, bytes32 r, bytes32 vs) internal view returns (bool) {
function verify(SignedVote calldata vote) internal view returns (bool) {
bytes32 digest = keccak256(abi.encodePacked('\x19\x01', DOMAIN_SEPARATOR, hash(vote)));
return digest.recover(r, vs) == vote.voter;
return digest.recover(vote.r, vote.vs) == vote.voter;
}
constructor(IERC20 _address) {
@ -134,8 +154,11 @@ contract VotingContract {
return returnVotingRooms;
}
function listRoomVoters(uint256 roomId) public view returns (address[] memory) {
return _getVotingRoom(roomId).voters;
function listRoomVoters(uint256 roomID) public view returns (address[] memory roomVoters) {
roomVoters = new address[](votesByRoomID[roomID].length);
for (uint i = 0; i < votesByRoomID[roomID].length; i++) {
roomVoters[i] = votesByRoomID[roomID][i].voter;
}
}
function getVotingHistory(bytes calldata publicKey) public view returns (VotingRoom[] memory returnVotingRooms) {
@ -167,22 +190,54 @@ contract VotingContract {
activeRoomIDByCommunityID[publicKey] = votingRoomID;
roomIDsByCommunityID[publicKey].push(votingRoomID);
votesByRoomID[votingRoomID].push(Vote({ voter: msg.sender, voteType: VoteType.ADD, sntAmount: voteAmount }));
votedAddressesByRoomID[votingRoomID][msg.sender] = true;
VotingRoom memory newVotingRoom;
newVotingRoom.startBlock = block.number;
newVotingRoom.endAt = block.timestamp.add(VOTING_LENGTH);
newVotingRoom.voteType = voteType;
newVotingRoom.community = publicKey;
newVotingRoom.roomNumber = votingRoomID;
newVotingRoom.totalVotesFor = voteAmount;
voted[votingRoomID][msg.sender] = true;
votingRooms.push(
VotingRoom({
startBlock: block.number,
endAt: block.timestamp.add(VOTING_LENGTH),
voteType: voteType,
finalized: false,
community: publicKey,
totalVotesFor: 0,
totalVotesAgainst: 0,
roomNumber: votingRoomID
})
);
votingRooms.push(newVotingRoom);
_getVotingRoom(votingRoomID).voters.push(msg.sender);
_evaluateVotes(_getVotingRoom(votingRoomID));
emit VotingRoomStarted(votingRoomID, publicKey);
}
function _evaluateVotes(VotingRoom storage votingRoom) private returns (bool) {
votingRoom.totalVotesFor = 0;
votingRoom.totalVotesAgainst = 0;
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) {
votingRoom.totalVotesFor = votingRoom.totalVotesFor.add(vote.sntAmount);
} else {
votingRoom.totalVotesAgainst = votingRoom.totalVotesAgainst.add(vote.sntAmount);
}
} else {
emit NotEnoughToken(votingRoom.roomNumber, vote.voter);
}
}
return votingRoom.totalVotesFor > votingRoom.totalVotesAgainst;
}
function _populateDirectory(VotingRoom storage votingRoom) private {
if (votingRoom.voteType == VoteType.ADD) {
directory.addCommunity(votingRoom.community);
} else {
directory.removeCommunity(votingRoom.community);
}
}
function finalizeVotingRoom(uint256 roomId) public {
VotingRoom storage votingRoom = _getVotingRoom(roomId);
@ -193,53 +248,42 @@ contract VotingContract {
votingRoom.endAt = block.timestamp;
activeRoomIDByCommunityID[votingRoom.community] = 0;
bool passed = votingRoom.totalVotesFor > votingRoom.totalVotesAgainst;
bool passed = _evaluateVotes(votingRoom);
if (passed) {
if (votingRoom.voteType == VoteType.ADD) {
directory.addCommunity(votingRoom.community);
}
if (votingRoom.voteType == VoteType.REMOVE) {
directory.removeCommunity(votingRoom.community);
}
_populateDirectory(votingRoom);
}
emit VotingRoomFinalized(roomId, votingRoom.community, passed, votingRoom.voteType);
}
event VoteCast(uint256 roomId, address voter);
event NotEnoughToken(uint256 roomId, address voter);
struct Vote {
address voter;
uint256 roomIdAndType;
uint256 sntAmount;
bytes32 r;
bytes32 vs;
}
function castVotes(Vote[] calldata votes) public {
function castVotes(SignedVote[] calldata votes) public {
for (uint256 i = 0; i < votes.length; i++) {
Vote calldata vote = votes[i];
SignedVote calldata signedVote = votes[i];
if (verify(vote, vote.r, vote.vs)) {
uint256 roomId = vote.roomIdAndType >> 1;
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 (voted[roomId][vote.voter] == false) {
if (token.balanceOf(vote.voter) >= vote.sntAmount) {
if (vote.roomIdAndType & 1 == 1) {
room.totalVotesFor = room.totalVotesFor.add(vote.sntAmount);
} else {
room.totalVotesAgainst = room.totalVotesAgainst.add(vote.sntAmount);
}
room.voters.push(vote.voter);
voted[roomId][vote.voter] = true;
emit VoteCast(roomId, vote.voter);
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, vote.voter);
emit NotEnoughToken(roomId, signedVote.voter);
}
} else {
emit AlreadyVoted(roomId, signedVote.voter);
}
}
}

View File

@ -106,7 +106,7 @@ async function fixture() {
await votingContract.setDirectory(directoryContract.address)
return { votingContract, directoryContract, firstSigner, secondSigner, thirdSigner }
return { votingContract, directoryContract, erc20Contract, firstSigner, secondSigner, thirdSigner }
}
before(async function () {
@ -116,19 +116,6 @@ before(async function () {
typedData.domain.verifyingContract = voting.address
})
describe('voting', () => {
it('check voters', async () => {
const { votingContract, firstSigner, secondSigner, thirdSigner } = await loadFixture(fixture)
const messages = 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(messages.slice(2))
expect(await votingContract.listRoomVoters(1)).to.deep.eq([firstSigner.address, thirdSigner.address])
})
})
describe('VotingContract', () => {
it('deploys properly', async () => {
const { votingContract, directoryContract } = await loadFixture(fixture)
@ -282,6 +269,23 @@ describe('VotingContract', () => {
])
})
it('verifies votes', async () => {
const { votingContract, erc20Contract, firstSigner } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, 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 expect(await votingContract.finalizeVotingRoom(1))
.to.emit(votingContract, 'NotEnoughToken')
.withArgs(1, firstSigner.address)
.to.emit(votingContract, 'VotingRoomFinalized')
.withArgs(1, publicKeys[0], false, 1)
})
describe('directory interaction', () => {
it('add community', async () => {
const { votingContract, directoryContract, secondSigner } = await loadFixture(fixture)
@ -335,7 +339,7 @@ describe('VotingContract', () => {
describe('helpers', () => {
it('getActiveVotingRoom', async () => {
const { votingContract, firstSigner } = await loadFixture(fixture)
const { votingContract } = await loadFixture(fixture)
await votingContract.initializeVotingRoom(1, publicKeys[0], BigNumber.from(100))
expect((await votingContract.getActiveVotingRoom(publicKeys[0])).slice(2)).to.deep.eq([
1,
@ -344,7 +348,6 @@ describe('VotingContract', () => {
BigNumber.from(100),
BigNumber.from(0),
BigNumber.from(1),
[firstSigner.address],
])
await time.increase(10000)
@ -356,7 +359,6 @@ describe('VotingContract', () => {
BigNumber.from(100),
BigNumber.from(0),
BigNumber.from(2),
[firstSigner.address],
])
})