parent
03cd1915c8
commit
a7d7736f34
|
@ -9,7 +9,6 @@ export type VotingRoom = {
|
|||
community: string
|
||||
totalVotesFor: BigNumber
|
||||
totalVotesAgainst: BigNumber
|
||||
voters: string[]
|
||||
roomNumber: number
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
])
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue