mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
4 years ago
12 changed files with 452 additions and 48 deletions
@ -1,3 +1,9 @@ |
|||||
{ |
{ |
||||
"extends": "solhint:default" |
"extends": "solhint:recommended", |
||||
|
"rules": { |
||||
|
"compiler-version": ["error","~0.8.0"], |
||||
|
"func-visibility": ["warn",{"ignoreConstructors" : true}], |
||||
|
"not-rely-on-time": "off", |
||||
|
"state-visibility": "off" |
||||
|
} |
||||
} |
} |
||||
|
@ -0,0 +1,109 @@ |
|||||
|
//SPDX-License-Identifier: MIT |
||||
|
pragma solidity 0.8.0; |
||||
|
|
||||
|
import "./Forum.sol"; |
||||
|
|
||||
|
contract PostVoting { |
||||
|
Forum public forum; |
||||
|
|
||||
|
constructor(Forum addr) { |
||||
|
forum = Forum(addr); |
||||
|
} |
||||
|
|
||||
|
enum Option { DEFAULT, UP, DOWN } // DEFAULT -> 0, UP -> 1, DOWN -> 2 |
||||
|
|
||||
|
struct PostBallot { |
||||
|
mapping(address => Option) votes; |
||||
|
mapping(Option => address[]) voters; |
||||
|
} |
||||
|
|
||||
|
mapping(uint => PostBallot) postBallots; |
||||
|
|
||||
|
event UserVotedPost(address userAddress, uint postID, Option option); |
||||
|
|
||||
|
function getVote(uint postID, address voter) public view returns (Option) { |
||||
|
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST()); |
||||
|
return postBallots[postID].votes[voter]; |
||||
|
} |
||||
|
|
||||
|
// Gets vote count for a specific option (Option.UP/ Option.DOWN only!) |
||||
|
function getVoteCount(uint postID, Option option) private view returns (uint) { |
||||
|
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST()); |
||||
|
return (postBallots[postID].voters[option].length); |
||||
|
} |
||||
|
|
||||
|
function getUpvoteCount(uint postID) public view returns (uint) { |
||||
|
return (getVoteCount(postID, Option.UP)); |
||||
|
} |
||||
|
|
||||
|
function getDownvoteCount(uint postID) public view returns (uint) { |
||||
|
return (getVoteCount(postID, Option.DOWN)); |
||||
|
} |
||||
|
|
||||
|
// Gets voters for a specific option (Option.UP/ Option.DOWN) |
||||
|
function getVoters(uint postID, Option option) private view returns (address[] memory) { |
||||
|
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST()); |
||||
|
return (postBallots[postID].voters[option]); |
||||
|
} |
||||
|
|
||||
|
function getUpvoters(uint postID) public view returns (address[] memory) { |
||||
|
return (getVoters(postID, Option.UP)); |
||||
|
} |
||||
|
|
||||
|
function getDownvoters(uint postID) public view returns (address[] memory) { |
||||
|
return (getVoters(postID, Option.DOWN)); |
||||
|
} |
||||
|
|
||||
|
function getVoterIndex(uint postID, address voter) private view returns (uint) { |
||||
|
require(forum.hasUserSignedUp(voter), forum.USER_HAS_NOT_SIGNED_UP()); |
||||
|
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST()); |
||||
|
|
||||
|
PostBallot storage postBallot = postBallots[postID]; |
||||
|
Option votedOption = getVote(postID, voter); |
||||
|
address[] storage optionVoters = postBallot.voters[votedOption]; |
||||
|
|
||||
|
for (uint voterIndex = 0; voterIndex < optionVoters.length; voterIndex++) |
||||
|
if (optionVoters[voterIndex] == voter) |
||||
|
return voterIndex; |
||||
|
|
||||
|
revert("Couldn't find voter's index!"); |
||||
|
} |
||||
|
|
||||
|
function vote(uint postID, Option option) private { |
||||
|
require(forum.hasUserSignedUp(msg.sender), forum.USER_HAS_NOT_SIGNED_UP()); |
||||
|
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST()); |
||||
|
|
||||
|
PostBallot storage postBallot = postBallots[postID]; |
||||
|
address voter = msg.sender; |
||||
|
Option prevOption = postBallot.votes[voter]; |
||||
|
|
||||
|
if (prevOption == option) |
||||
|
return; |
||||
|
|
||||
|
// Remove previous vote if exists |
||||
|
if (prevOption != Option.DEFAULT) { |
||||
|
uint voterIndex = getVoterIndex(postID, voter); |
||||
|
// Swap with last voter address and delete vote |
||||
|
postBallot.voters[prevOption][voterIndex] = postBallot.voters[prevOption][postBallot.voters[prevOption].length - 1]; |
||||
|
postBallot.voters[prevOption].pop(); |
||||
|
} |
||||
|
|
||||
|
// Add new vote |
||||
|
if (option != Option.DEFAULT) |
||||
|
postBallot.voters[option].push(voter); |
||||
|
postBallot.votes[voter] = option; |
||||
|
emit UserVotedPost(voter, postID, option); |
||||
|
} |
||||
|
|
||||
|
function upvote(uint postID) public { |
||||
|
vote(postID, Option.UP); |
||||
|
} |
||||
|
|
||||
|
function downvote(uint postID) public { |
||||
|
vote(postID, Option.DOWN); |
||||
|
} |
||||
|
|
||||
|
function unvote(uint postID) public { |
||||
|
vote(postID, Option.DEFAULT); |
||||
|
} |
||||
|
} |
@ -0,0 +1,155 @@ |
|||||
|
//SPDX-License-Identifier: MIT |
||||
|
pragma solidity 0.8.0; |
||||
|
|
||||
|
import "./Forum.sol"; |
||||
|
|
||||
|
contract Voting { |
||||
|
// Error messages for require() |
||||
|
string constant TOPIC_POLL_DIFFERENT_CREATOR = "Only topic's author can create a poll."; |
||||
|
string constant POLL_EXISTS = "Poll already exists."; |
||||
|
string constant POLL_DOES_NOT_EXIST = "Poll does not exist."; |
||||
|
string constant INVALID_OPTION = "Invalid option."; |
||||
|
string constant USER_HAS_NOT_VOTED = "User hasn't voted."; |
||||
|
|
||||
|
Forum public forum; |
||||
|
|
||||
|
constructor(Forum addr) { |
||||
|
forum = Forum(addr); |
||||
|
} |
||||
|
|
||||
|
struct Poll { |
||||
|
uint topicID; |
||||
|
uint numOptions; |
||||
|
string dataHash; |
||||
|
mapping(address => uint) votes; |
||||
|
mapping(uint => address[]) voters; |
||||
|
bool enableVoteChanges; |
||||
|
uint timestamp; |
||||
|
} |
||||
|
|
||||
|
mapping(uint => Poll) polls; |
||||
|
|
||||
|
event PollCreated(uint topicID); |
||||
|
event UserVotedPoll(address userAddress, uint topicID, uint vote); |
||||
|
|
||||
|
function pollExists(uint topicID) public view returns (bool) { |
||||
|
if (polls[topicID].timestamp != 0) |
||||
|
return true; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
function createPoll(uint topicID, uint numOptions, string memory dataHash, bool enableVoteChanges) public returns (uint) { |
||||
|
require(forum.hasUserSignedUp(msg.sender), forum.USER_HAS_NOT_SIGNED_UP()); |
||||
|
require(forum.topicExists(topicID), forum.TOPIC_DOES_NOT_EXIST()); |
||||
|
require(forum.getTopicAuthor(topicID) == msg.sender, TOPIC_POLL_DIFFERENT_CREATOR); |
||||
|
require(!pollExists(topicID), POLL_EXISTS); |
||||
|
|
||||
|
Poll storage poll = polls[topicID]; |
||||
|
poll.topicID = topicID; |
||||
|
poll.numOptions = numOptions; |
||||
|
poll.dataHash = dataHash; |
||||
|
poll.enableVoteChanges = enableVoteChanges; |
||||
|
poll.timestamp = block.timestamp; |
||||
|
|
||||
|
emit PollCreated(topicID); |
||||
|
return topicID; |
||||
|
} |
||||
|
|
||||
|
function getPollInfo(uint topicID) public view returns (uint, string memory, uint, uint) { |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
|
||||
|
uint totalVotes = getTotalVotes(topicID); |
||||
|
|
||||
|
return ( |
||||
|
polls[topicID].numOptions, |
||||
|
polls[topicID].dataHash, |
||||
|
polls[topicID].timestamp, |
||||
|
totalVotes |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function isOptionValid(uint topicID, uint option) public view returns (bool) { |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
if (option <= polls[topicID].numOptions) // Option 0 is valid as well (no option chosen) |
||||
|
return true; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
function hasVoted(uint topicID, address voter) public view returns (bool) { |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
if (polls[topicID].votes[voter] != 0) |
||||
|
return true; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
function getVote(uint topicID, address voter) public view returns (uint) { |
||||
|
require(hasVoted(topicID, voter), USER_HAS_NOT_VOTED); |
||||
|
return polls[topicID].votes[voter]; |
||||
|
} |
||||
|
|
||||
|
function getVoteCount(uint topicID, uint option) public view returns (uint) { |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
require(isOptionValid(topicID, option), INVALID_OPTION); |
||||
|
return (polls[topicID].voters[option].length); |
||||
|
} |
||||
|
|
||||
|
function getTotalVotes(uint topicID) public view returns (uint) { |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
|
||||
|
Poll storage poll = polls[topicID]; |
||||
|
uint totalVotes = 0; |
||||
|
|
||||
|
for (uint pollOption = 1; pollOption <= poll.numOptions; pollOption++) |
||||
|
totalVotes += poll.voters[pollOption].length; |
||||
|
|
||||
|
return totalVotes; |
||||
|
} |
||||
|
|
||||
|
// Gets voters for a specific option |
||||
|
function getVoters(uint topicID, uint option) public view returns (address[] memory) { |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
return (polls[topicID].voters[option]); |
||||
|
} |
||||
|
|
||||
|
function getVoterIndex(uint topicID, address voter) public view returns (uint) { |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
require(hasVoted(topicID, voter), USER_HAS_NOT_VOTED); |
||||
|
Poll storage poll = polls[topicID]; |
||||
|
uint votedOption = getVote(topicID, voter); |
||||
|
address[] storage optionVoters = poll.voters[votedOption]; |
||||
|
|
||||
|
for (uint voterIndex = 0; voterIndex < optionVoters.length; voterIndex++) |
||||
|
if (optionVoters[voterIndex] == voter) |
||||
|
return voterIndex; |
||||
|
|
||||
|
revert("Couldn't find voter's index!"); |
||||
|
} |
||||
|
|
||||
|
function vote(uint topicID, uint option) public { |
||||
|
require(forum.hasUserSignedUp(msg.sender), forum.USER_HAS_NOT_SIGNED_UP()); |
||||
|
require(pollExists(topicID), POLL_DOES_NOT_EXIST); |
||||
|
require(isOptionValid(topicID, option), INVALID_OPTION); |
||||
|
Poll storage poll = polls[topicID]; |
||||
|
address voter = msg.sender; |
||||
|
uint prevOption = poll.votes[voter]; |
||||
|
if (prevOption == option) |
||||
|
return; |
||||
|
|
||||
|
// Voter hasn't voted before |
||||
|
if (prevOption == 0) { |
||||
|
poll.voters[option].push(voter); |
||||
|
poll.votes[voter] = option; |
||||
|
emit UserVotedPoll(voter, topicID, option); |
||||
|
} |
||||
|
else if (poll.enableVoteChanges) { |
||||
|
uint voterIndex = getVoterIndex(topicID, voter); |
||||
|
// Swap with last voter address and delete vote |
||||
|
poll.voters[prevOption][voterIndex] = poll.voters[prevOption][poll.voters[prevOption].length - 1]; |
||||
|
poll.voters[prevOption].pop(); |
||||
|
if (option != 0) |
||||
|
poll.voters[option].push(voter); |
||||
|
poll.votes[voter] = option; |
||||
|
emit UserVotedPoll(voter, topicID, option); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,14 +1,16 @@ |
|||||
let Forum; |
let Forum, Voting, PostVoting; |
||||
|
|
||||
|
/* eslint-disable global-require */ |
||||
try { |
try { |
||||
// eslint-disable-next-line global-require
|
|
||||
Forum = require('./build/Forum.json'); |
Forum = require('./build/Forum.json'); |
||||
|
Voting = require('./build/Voting.json'); |
||||
|
PostVoting = require('./build/PostVoting.json'); |
||||
} catch (e) { |
} catch (e) { |
||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||
console.error("Could not require contract artifacts. Haven't you run compile yet?"); |
console.error("Could not require contract artifacts. Haven't you run compile yet?"); |
||||
} |
} |
||||
|
|
||||
module.exports = { |
module.exports = { |
||||
contracts: [Forum], |
contracts: [Forum, Voting, PostVoting], |
||||
forumContract: Forum, |
forumContract: Forum, |
||||
}; |
}; |
||||
|
@ -1,6 +1,12 @@ |
|||||
const Forum = artifacts.require('Forum'); |
const Forum = artifacts.require('Forum'); |
||||
|
const Voting = artifacts.require('Voting'); |
||||
|
const PostVoting = artifacts.require('PostVoting'); |
||||
|
|
||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||
module.exports = function (deployer) { |
module.exports = function (deployer) { |
||||
deployer.deploy(Forum); |
return deployer.deploy(Forum) |
||||
|
.then(async (forum) => Promise.all([ |
||||
|
deployer.deploy(Voting, forum.address), |
||||
|
deployer.deploy(PostVoting, forum.address), |
||||
|
])); |
||||
}; |
}; |
||||
|
@ -0,0 +1,112 @@ |
|||||
|
//SPDX-License-Identifier: MIT |
||||
|
pragma solidity 0.8.0; |
||||
|
|
||||
|
import "truffle/Assert.sol"; |
||||
|
import "truffle/DeployedAddresses.sol"; |
||||
|
import "../contracts/Forum.sol"; |
||||
|
import "../contracts/Voting.sol"; |
||||
|
|
||||
|
contract TestVoting { |
||||
|
Forum forum; |
||||
|
uint firstTopicId; |
||||
|
|
||||
|
function beforeAll() public { |
||||
|
forum = Forum(DeployedAddresses.Forum()); |
||||
|
|
||||
|
forum.signUp("testAccount"); |
||||
|
(firstTopicId,) = forum.createTopic(); |
||||
|
} |
||||
|
|
||||
|
function testIfPollExists() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
bool actual = voting.pollExists(firstTopicId); |
||||
|
|
||||
|
Assert.equal(actual, false, "Poll should not exist"); |
||||
|
} |
||||
|
|
||||
|
function testCreatePoll() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
uint actual = voting.createPoll(firstTopicId, 3, "asdf", false); |
||||
|
|
||||
|
Assert.equal(actual, firstTopicId, "Topic Id should be 1"); |
||||
|
} |
||||
|
|
||||
|
function testGetTotalVotes() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
uint actual = voting.getTotalVotes(firstTopicId); |
||||
|
|
||||
|
Assert.equal(actual, 0, "Topic Id should be 0"); |
||||
|
} |
||||
|
|
||||
|
function testGetPollInfo() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
(uint actualNumberOfOptions, string memory actualDataHash, , uint actualNumberOfVotes) = voting.getPollInfo(firstTopicId); |
||||
|
|
||||
|
Assert.equal(actualNumberOfOptions, 3, "Number of votes should be 0"); |
||||
|
Assert.equal(actualDataHash, "asdf", "Number of votes should be 0"); |
||||
|
Assert.equal(actualNumberOfVotes, 0, "Number of votes should be 0"); |
||||
|
} |
||||
|
|
||||
|
function testVote() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
voting.vote(firstTopicId, 1); |
||||
|
uint votesActual = voting.getTotalVotes(firstTopicId); |
||||
|
|
||||
|
Assert.equal(votesActual, 1, "Number of votes should be 1"); |
||||
|
} |
||||
|
|
||||
|
function testGetVoteCount() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
uint actualVotesOption0 = voting.getVoteCount(firstTopicId, 1); |
||||
|
uint actualVotesOption1 = voting.getVoteCount(firstTopicId, 2); |
||||
|
uint actualVotesOption2 = voting.getVoteCount(firstTopicId, 3); |
||||
|
|
||||
|
Assert.equal(actualVotesOption0, 1, "Vote count is not correct"); |
||||
|
Assert.equal(actualVotesOption1, 0, "Vote count is not correct"); |
||||
|
Assert.equal(actualVotesOption2, 0, "Vote count is not correct"); |
||||
|
} |
||||
|
|
||||
|
function testChangeVoteWhenDisabled() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
(uint topicId,) = forum.createTopic(); |
||||
|
voting.createPoll(topicId, 3, "asdf", false); |
||||
|
|
||||
|
voting.vote(topicId, 1); |
||||
|
uint actualVotesOption0 = voting.getVoteCount(topicId, 1); |
||||
|
uint actualVotesOption1 = voting.getVoteCount(topicId, 2); |
||||
|
voting.vote(topicId, 2); |
||||
|
uint actualVotesOption2 = voting.getVoteCount(topicId, 1); |
||||
|
uint actualVotesOption3 = voting.getVoteCount(topicId, 2); |
||||
|
|
||||
|
Assert.equal(actualVotesOption0, 1, "Number of votes should be 1"); |
||||
|
Assert.equal(actualVotesOption1, 0, "Number of votes should be 0"); |
||||
|
Assert.equal(actualVotesOption2, 1, "Number of votes should be 1"); |
||||
|
Assert.equal(actualVotesOption3, 0, "Number of votes should be 0"); |
||||
|
} |
||||
|
|
||||
|
function testChangeVoteWhenEnabled() public { |
||||
|
Voting voting = Voting(DeployedAddresses.Voting()); |
||||
|
|
||||
|
(uint topicId,) = forum.createTopic(); |
||||
|
voting.createPoll(topicId, 3, "asdf", true); |
||||
|
|
||||
|
voting.vote(topicId, 1); |
||||
|
uint actualVotesOption0 = voting.getVoteCount(topicId, 1); |
||||
|
uint actualVotesOption1 = voting.getVoteCount(topicId, 2); |
||||
|
voting.vote(topicId, 2); |
||||
|
uint actualVotesOption2 = voting.getVoteCount(topicId, 1); |
||||
|
uint actualVotesOption3 = voting.getVoteCount(topicId, 2); |
||||
|
|
||||
|
Assert.equal(actualVotesOption0, 1, "Number of votes should be 1"); |
||||
|
Assert.equal(actualVotesOption1, 0, "Number of votes should be 0"); |
||||
|
Assert.equal(actualVotesOption2, 0, "Number of votes should be 0"); |
||||
|
Assert.equal(actualVotesOption3, 1, "Number of votes should be 1"); |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue