diff --git a/contracts/Token/CustomToken.sol b/contracts/Token/CustomToken.sol index ec0457e..cf6a0dd 100644 --- a/contracts/Token/CustomToken.sol +++ b/contracts/Token/CustomToken.sol @@ -2,7 +2,7 @@ pragma solidity 0.6.1; pragma experimental ABIEncoderV2; import "../lib/Ownable.sol"; -import "../ZeroOne/IZeroOne.sol"; +import "../ZeroOne/Ballots/IBallots.sol"; /** * @title CustomToken @@ -191,8 +191,9 @@ contract CustomToken is Ownable { { for (uint i = 1; i < projects.length - 1; i++) { if (isTokenLocked(projects[i], _user)) { - IZeroOne project = IZeroOne(projects[i]); - project.updateUserVote(address(this), _user, balanceOf(_user)); + IBallots project = IBallots(projects[i]); + uint256 newBalance = balanceOf(_user); + project.updateUserVote(address(this), _user, newBalance); } } } @@ -333,7 +334,7 @@ contract CustomToken is Ownable { "This operation is not allowed for this address" ); require(_sender != address(0), "Address must be non-empty"); - require(balanceOf(_sender) > 0, "Balance of sender must be greater, then zero"); + require(balanceOf(_sender) >= _count, "Balance of sender must be greater, then amount"); if (msg.sender == owner()) { transfer(_sender, _reciepient, _count); @@ -357,7 +358,7 @@ contract CustomToken is Ownable { returns(bool isUnlocked) { require(isProjectAddress(_project), "Address is not in project list"); - IZeroOne project = IZeroOne(_project); + IBallots project = IBallots(_project); require( project.didUserVote(address(this), msg.sender) == true, "User not voted, nothing to unlock" diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol new file mode 100644 index 0000000..6242675 --- /dev/null +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -0,0 +1,392 @@ +pragma solidity 0.6.1; +pragma experimental ABIEncoderV2; + +import "./lib/Ballot.sol"; +import "./lib/BallotList.sol"; +import "./IBallots.sol"; +import "../../__vendor__/IERC20.sol"; + + +/** + * @title Ballots + * @dev stores Ballots + */ +contract Ballots { + + using BallotList for BallotList.List; + using BallotType for BallotType.Ballot; + using ZeroOneVM for ZeroOneVM.Ballot; + + BallotList.List ballots; + + event VotingStarted(uint votingId, uint questionId); + + event VotingEnded(uint votingId, VM.Vote descision); + + event UserVote(address user, VM.Vote descision); + + event UpdatedUserVote(address group, address user, VM.Vote userVote); + + /** + * @notice reverts on non-existing ballot id + * @param _id ballot id + */ + modifier ballotExist( + uint _id + ) { + require( + ballots.checkId(_id), + "Provided index out of bounds" + ); + _; + } + + modifier noActiveVotings() { + uint length = ballots.list.length; + require(length > 0 + ? ballots.list[length - 1].status != BallotType.BallotStatus.ACTIVE + : true, + "You have active voting" + ); + _; + } + + /** + * @dev returns the confirmation that this is a project + */ + function isProject() public pure returns (bool) { return true; } + + /** + * @dev add ballot to list. Requires no active votings in list + * @param _votingPrimary primary info of voting + * @param formula formula + * @param owners address of owners + * @return id + */ + function addVoting( + BallotList.BallotSimple memory _votingPrimary, + bytes storage formula, + address owners + ) + internal + noActiveVotings + returns (uint id) + { + id = ballots.add(_votingPrimary); + ballots.descriptors[id].executeDescriptors(formula, owners); + emit VotingStarted(id, _votingPrimary.questionId); + } + + /** + * @dev closes the voting by executing descriptors for result calculating + * and setting BallotStatus.CLOSED + * @param votingId id of voting + * @param formula formula of voting + * @param owners address of owners + * @return result + */ + function closeVoting( + uint votingId, + bytes storage formula, + address owners + ) + internal + returns ( + VM.Vote result + ) + { + require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); + + ballots.descriptors[votingId].executeResult(formula, owners); + ballots.list[votingId].close(); + + emit VotingEnded(votingId, ballots.descriptors[votingId].result); + + return ( + ballots.descriptors[votingId].result + ); + } + + /** + * @dev getting the voting by id + * @param _id id of voting + * @return startTime + * @return endTime + * @return starterGroupId + * @return starterAddress + * @return questionId + * @return status + * @return votingData + */ + function getVoting( + uint _id + ) + public + view + ballotExist(_id) + returns ( + uint startTime, + uint endTime, + uint starterGroupId, + address starterAddress, + uint questionId, + BallotType.BallotStatus status, + bytes memory votingData + ) + { + return ballots.list[_id].getPrimaryInfo(); + } + + /** + * @dev return amount of votings + * @return amount + */ + function getVotingsAmount() + public + view + returns (uint amount) + { + return ballots.list.length; + } + + /** + * @dev sets positive, negative votes and totalSupply for group in descriptors + * @param groupAddress address of group (for totalSupply) + * @param votingId id of voting + * @param groupIndex index of group in ZeroOneVM.Ballot struct + * @return success + */ + function setGroupVotes( + address groupAddress, + uint votingId, + uint groupIndex + ) + internal + returns(bool success) + { + (uint positive, uint negative, uint totalSupply) = ballots.list[votingId].getGroupVotes(groupAddress); + ballots.descriptors[votingId].groups[groupIndex].positive = positive; + ballots.descriptors[votingId].groups[groupIndex].negative = negative; + ballots.descriptors[votingId].groups[groupIndex].totalSupply = totalSupply; + return true; + } + + /** + * @dev set {_descision} of {_user} from {_group} + * method fetching balance of {_user} in {_group} and writing vote in voting struct + * @param _descision descision of {_user} + * @return success + */ + function setVote( + VM.Vote _descision + ) + public + returns (bool success) + { + uint votingId = ballots.list.length - 1; + + for (uint i = 0; i < 16; i++) { + DescriptorVM.Group storage group = ballots.descriptors[votingId].groups[i]; + DescriptorVM.User storage user = ballots.descriptors[votingId].users[i]; + + if (group.groupAddress != address(0)) { + if (!isUserExcluded(group.exclude, msg.sender)) { + if (!didUserVote(group.groupAddress, msg.sender)) { + ballots.list[votingId].setVote(group.groupAddress, msg.sender, _descision); + setGroupVotes(group.groupAddress, votingId, i); + } + } + } + + if (user.groupAddress != address(0)) { + address userAddress; + address groupAddress = group.groupAddress; + + if((user.userAddress != address(0)) && (user.userAddress == msg.sender)) { + userAddress = msg.sender; + } else if (user.admin == true && IERC20(user.groupAddress).owner() == msg.sender) { + user.userAddress = msg.sender; + userAddress = msg.sender; + } + + if(userAddress != address(0)){ + if (!didUserVote(user.groupAddress, user.userAddress)) { + ballots.list[votingId].setVote(groupAddress, userAddress, _descision); + user.vote = _descision; + } else { + user.vote = getUserVote(votingId, groupAddress, userAddress); + } + } + } + } + + emit UserVote(msg.sender, _descision); + return true; + } + + /** + * @dev updates vote of {_user} from {_group} with {_newVoteWeight} + * @param _group address of group + * @param _user address of user + * @param _newVoteWeight new tokens amount of {_user} + */ + function updateUserVote( + address _group, + address _user, + uint256 _newVoteWeight + ) + public + returns (bool success) + { + uint votingId = ballots.list.length - 1; + + for (uint i = 0; i < 16; i++) { + DescriptorVM.Group storage group = ballots.descriptors[votingId].groups[i]; + DescriptorVM.User storage user = ballots.descriptors[votingId].users[i]; + if (group.groupAddress != address(0) && group.groupAddress == _group) { + if (!isUserExcluded(group.exclude, _user)) { + if (didUserVote(group.groupAddress, _user)) { + ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); + setGroupVotes(group.groupAddress, votingId, i); + } + } + } + + if ( user.groupAddress != address(0) && user.groupAddress == _group ) { + if ( user.admin == true && IERC20(user.groupAddress).owner() == _user ) { + user.userAddress = _user; + } + + if (user.userAddress != address(0)) { + if (didUserVote(user.groupAddress, _user)) { + ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); + if (_newVoteWeight == 0) { + user.vote = VM.Vote.UNDEFINED; + } + } + } + } + } + + VM.Vote userVote = getUserVote(votingId, _group, _user); + emit UpdatedUserVote(_group, _user, userVote); + return true; + } + + /** + * @dev gets votes from voting with {_votingId} for {_group} + * returns positive votes, negative votes, totalSupply of {_group} + * @param _votingId id of voting + * @param _group address of group + * @return positive + * @return negative + * @return totalSupply + */ + function getGroupVotes( + uint _votingId, + address _group + ) + public + view + returns ( + uint256 positive, + uint256 negative, + uint256 totalSupply + ) + { + (positive, negative, totalSupply) = ballots.list[_votingId].getGroupVotes(_group); + } + + /** + * @dev checks, if {_user} address contains in {_exclude} list + * @param _exclude list of users, which excluded from voting + * @param _user user, which votes + */ + function isUserExcluded( + address[] memory _exclude, + address _user + ) + internal + pure + returns (bool excluded) + { + for (uint i = 0; i < _exclude.length; i++) { + if (_exclude[i] == _user) { + excluded = true; + break; + } + } + } + + /** + * @dev returns descision of {_user} from {_group} in voting with {_votingId} + * @param _votingId id of voting + * @param _group address of group + * @param _user address of user + * @return descision + */ + function getUserVote( + uint _votingId, + address _group, + address _user + ) + public + view + ballotExist(_votingId) + returns (VM.Vote descision) + { + return ballots.list[_votingId].votes[_group][_user]; + } + + /** + * @dev return vote weight of {_user} from {_group} in voting with {_votingId} + * @param _votingId id of voting + * @param _group address of group + * @param _user address of user + * @return weight + */ + function getUserVoteWeight( + uint _votingId, + address _group, + address _user + ) + public + view + returns (uint256 weight) + { + return ballots.list[_votingId].votesWeight[_group][_user]; + } + + /** + * @dev returns confirming that this {_user} from {_group} is voted + * @param _group address of group + * @param _user address of user + * @return confirm + */ + function didUserVote ( + address _group, + address _user + ) + public + view + returns(bool confirm) + { + uint votingId = ballots.list.length - 1; + confirm = ballots.list[votingId].votes[_group][_user] != VM.Vote.UNDEFINED; + } + + /** + * @dev gets voting result by {_votingId} + * @param _votingId id of voting + * @return result + */ + function getVotingResult ( + uint _votingId + ) + public + view + returns (VM.Vote result) + { + result = ballots.descriptors[_votingId].result; + } +} diff --git a/contracts/ZeroOne/Ballots/IBallots.sol b/contracts/ZeroOne/Ballots/IBallots.sol new file mode 100644 index 0000000..46d9fba --- /dev/null +++ b/contracts/ZeroOne/Ballots/IBallots.sol @@ -0,0 +1,17 @@ +pragma solidity 0.6.1; + + +interface IBallots { + + function updateUserVote(address tokenAddress, address user, uint256 newVoteWeight) external returns(bool); + + function didUserVote(address project, address user) external returns(bool); + + function getUserVoteWeight(uint votingId, address tokenAddr, address user) external view returns(uint256); + + function getUserVote(uint votingId, address tokenAddr, address user) external view returns(uint); + + function submitVoting() external; + + function setGroupAdmin(address tokenAddr, address newOwner) external; +} \ No newline at end of file diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol new file mode 100644 index 0000000..51e4859 --- /dev/null +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -0,0 +1,176 @@ +pragma solidity 0.6.1; + +import "../../../__vendor__/IERC20.sol"; +import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; + +/** + @title BallotType + @dev Ballot data type implementation +*/ + +library BallotType { + using ZeroOneVM for ZeroOneVM.Ballot; + + enum BallotStatus { CLOSED, ACTIVE } + + struct Ballot { + uint startBlock; + uint startTime; + uint endTime; + uint starterGroupId; + address starterAddress; + uint questionId; + BallotStatus status; + bytes votingData; + mapping(address => mapping(address => VM.Vote)) votes; + mapping(address => mapping(address => uint256)) votesWeight; + mapping(address => mapping(uint => uint256)) descisionWeights; + } + + /** + * @dev getting primary info about voting + * @return startTime + * @return endTime + * @return starterGroupId + * @return starterAddress + * @return questionId + * @return status + * @return votingData + */ + function getPrimaryInfo( + Ballot storage _self + ) + internal + view + returns ( + uint startTime, + uint endTime, + uint starterGroupId, + address starterAddress, + uint questionId, + BallotStatus status, + bytes storage votingData + ) + { + return ( + _self.startTime, + _self.endTime, + _self.starterGroupId, + _self.starterAddress, + _self.questionId, + _self.status, + _self.votingData + ); + } + + /** + @dev set vote of {_user} from {_group} + @param _group address of group + @param _user address of user + @param _descision descision of user + */ + function setVote( + Ballot storage _self, + address _group, + address _user, + VM.Vote _descision + ) + internal + returns (bool status) + { + require(_self.endTime > block.timestamp, "Votes recieving are closed"); + require( + _self.status != BallotStatus.CLOSED, + "Voting is closed, you must start new voting before vote" + ); + + IERC20 group = IERC20(_group); + uint256 voteWeight = group.balanceOf(_user); + require(group.transferFrom(_user, address(this), voteWeight)); + + _self.votes[_group][_user] = _descision; + _self.votesWeight[_group][_user] = voteWeight; + _self.descisionWeights[_group][uint(_descision)] = _self.descisionWeights[_group][uint(_descision)] + voteWeight; + return true; + } + + function updateUserVote( + Ballot storage _self, + address _group, + address _user, + uint256 _newVoteWeight + ) + internal + returns (bool status) + { + uint256 oldVoteWeight = _self.votesWeight[_group][_user]; + uint index = uint(_self.votes[_group][_user]); + uint256 oldDescisionWeight = _self.descisionWeights[_group][index]; + + if (_self.status == BallotStatus.ACTIVE) { + _self.votesWeight[_group][_user] = _newVoteWeight; + _self.descisionWeights[_group][index] = oldDescisionWeight - oldVoteWeight + _newVoteWeight; + if (_newVoteWeight == 0) { + _self.votes[_group][_user] = VM.Vote.UNDEFINED; + } + } + return true; + } + + function getGroupVotes( + Ballot storage _self, + address _group + ) + internal + view + returns( + uint256 positive, + uint256 negative, + uint256 totalSupply + ) + { + positive = _self.descisionWeights[_group][uint(VM.Vote.ACCEPTED)]; + negative = _self.descisionWeights[_group][uint(VM.Vote.DECLINED)]; + totalSupply = IERC20(_group).totalSupply(); + } + + /** + * @dev get user vote in this voting + * @param _group address of group + * @param _user address of user + * @return userVote + */ + function getUserVote( + Ballot storage _self, + address _group, + address _user + ) + internal + view + returns (VM.Vote userVote) + { + userVote = _self.votes[_group][_user]; + } + + /** + * @dev close ballot by calculating result and setting status "CLOSED" + * @param _self ballot + */ + function close( + Ballot storage _self + ) + internal + { + _self.status = BallotStatus.CLOSED; + } + + function validate( + //Ballot memory _self + ) + internal + pure + returns (bool) + { + return true; + } +} diff --git a/contracts/ZeroOne/Ballots/lib/BallotList.sol b/contracts/ZeroOne/Ballots/lib/BallotList.sol new file mode 100644 index 0000000..aead0bc --- /dev/null +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -0,0 +1,70 @@ +pragma solidity 0.6.1; + +import "./Ballot.sol"; +import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; + + +/** + * @title BallotList + * @dev stores votings + */ +library BallotList { + using BallotType for BallotType.Ballot; + + struct List { + BallotType.Ballot[] list; + mapping(uint => ZeroOneVM.Ballot) descriptors; + } + + struct BallotSimple { + uint starterGroupId; + uint endTime; + address starterAddress; + uint questionId; + bytes data; + } + + /** + * @dev add voting to list + * @param _votingPrimary voting primary info + * @return id + */ + function add( + List storage _self, + BallotSimple memory _votingPrimary + ) + internal + returns (uint id) + { + BallotType.Ballot memory _voting = BallotType.Ballot( + block.number, + block.timestamp, + _votingPrimary.endTime, + _votingPrimary.starterGroupId, + _votingPrimary.starterAddress, + _votingPrimary.questionId, + BallotType.BallotStatus.ACTIVE, + _votingPrimary.data + ); + + _self.list.push(_voting); + id = _self.list.length - 1; + } + + + /** + * @dev checks id existance + * @param _id id + * @return valid + */ + function checkId( + List storage _self, + uint _id + ) + internal + view + returns (bool valid) + { + return _self.list.length > _id; + } +} diff --git a/contracts/ZeroOne/IZeroOne.sol b/contracts/ZeroOne/IZeroOne.sol index 7704b90..d194f79 100644 --- a/contracts/ZeroOne/IZeroOne.sol +++ b/contracts/ZeroOne/IZeroOne.sol @@ -1,6 +1,8 @@ pragma solidity 0.6.1; pragma experimental ABIEncoderV2; +import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; + /** * @title IZeroOne * @dev implements ZeroOne interface @@ -17,18 +19,6 @@ interface IZeroOne { uint questionId; uint startBlock; uint endBlock; - Result result; + VM.Vote result; } - - function updateUserVote(address project, address user, uint256 newVoteWeight) external returns(bool); - - function didUserVote(address project, address user) external returns(bool); - - function getUserVoteWeight(address tokenAddr, address user) external view returns(uint256); - - function getUserVote(address tokenAddr, address user) external view returns(uint); - - function submitVoting() external; - - function setGroupAdmin(address tokenAddr, address newOwner) external; } diff --git a/contracts/ZeroOne/Questions/Questions.sol b/contracts/ZeroOne/Questions/Questions.sol index 23b750f..11bd733 100644 --- a/contracts/ZeroOne/Questions/Questions.sol +++ b/contracts/ZeroOne/Questions/Questions.sol @@ -73,8 +73,8 @@ contract Questions { function addQuestion( QuestionType.Question memory _question ) - public virtual + public returns (uint id) { id = questions.add(_question); diff --git a/contracts/ZeroOne/Questions/QuestionsWithGroups.sol b/contracts/ZeroOne/Questions/QuestionsWithGroups.sol index d165eb9..a2f1343 100644 --- a/contracts/ZeroOne/Questions/QuestionsWithGroups.sol +++ b/contracts/ZeroOne/Questions/QuestionsWithGroups.sol @@ -91,6 +91,7 @@ contract QuestionsWithGroups is Questions { function addQuestionGroup( GroupType.Group memory _questionGroup ) + virtual public returns (uint id) { diff --git a/contracts/ZeroOne/Questions/lib/Question.sol b/contracts/ZeroOne/Questions/lib/Question.sol index 5606ee0..35cdede 100644 --- a/contracts/ZeroOne/Questions/lib/Question.sol +++ b/contracts/ZeroOne/Questions/lib/Question.sol @@ -28,6 +28,8 @@ library QuestionType { string[] paramTypes; address target; bytes4 methodSelector; + string rawFormula; + bytes formula; } /** diff --git a/contracts/ZeroOne/UserGroups/UserGroups.sol b/contracts/ZeroOne/UserGroups/UserGroups.sol index ddedf2e..7427c3c 100644 --- a/contracts/ZeroOne/UserGroups/UserGroups.sol +++ b/contracts/ZeroOne/UserGroups/UserGroups.sol @@ -19,7 +19,7 @@ contract UserGroups { string name, address groupAddress ); - + /** * @notice reverts on non-existing group id * @param _id group id @@ -33,9 +33,7 @@ contract UserGroups { ); _; } - - constructor() public {} - + /** * @notice get group data * @param _id group id diff --git a/contracts/ZeroOne/UserGroups/lib/UserGroup.sol b/contracts/ZeroOne/UserGroups/lib/UserGroup.sol index 3d36ed5..4231b8e 100644 --- a/contracts/ZeroOne/UserGroups/lib/UserGroup.sol +++ b/contracts/ZeroOne/UserGroups/lib/UserGroup.sol @@ -1,6 +1,6 @@ pragma solidity 0.6.1; -import "../IERC20.sol"; +import "../../../__vendor__/IERC20.sol"; /** * @title Group diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 1dc8bf2..5886ad6 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -2,51 +2,27 @@ pragma solidity 0.6.1; pragma experimental ABIEncoderV2; import "./IZeroOne.sol"; +import "./Questions/QuestionsWithGroups.sol"; +import "./UserGroups/UserGroups.sol"; +import "./Ballots/Ballots.sol"; import "./Notifier/Notifier.sol"; import "../lib/Meta.sol"; -import "./UserGroups/IERC20.sol"; +import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; /** * @title ZeroOne * @dev main ZeroOne contract */ -contract ZeroOne is Notifier, IZeroOne { +contract ZeroOne is Notifier, IZeroOne, Ballots, UserGroups, QuestionsWithGroups { using Meta for bytes; - enum BallotStatus { CLOSED, ACTIVE } - - enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } - - struct Ballot { - uint startBlock; - uint startTime; - uint endTime; - uint starterGroupId; - address starterAddress; - uint questionId; - BallotStatus status; - BallotResult result; - bytes votingData; - mapping(address => mapping(address => BallotResult)) votes; - mapping(address => mapping(address => uint256)) votesWeight; - mapping(address => mapping(uint => uint256)) descisionWeights; - } + event ZeroOneCall( + MetaData _meta + ); - Ballot ballot; - - constructor() public { - ballot = Ballot({ - startBlock: block.number, - startTime: block.timestamp, - endTime: block.timestamp + 36000, - starterGroupId: 0, - starterAddress: msg.sender, - questionId: 1, - status: BallotStatus.ACTIVE, - result: BallotResult.NOT_ACCEPTED, - votingData: "0x" - }); + constructor(UserGroup.Group memory _group) public { + addUserGroup(_group); } /** @@ -60,6 +36,81 @@ contract ZeroOne is Notifier, IZeroOne { _; } + modifier groupIsAllowed( + uint _questionId, + uint _groupId + ) { + QuestionType.Question memory question = getQuestion(_questionId); + require( + _groupId == question.groupId, + "This group have no permissions to start voting with this question" + ); + _; + } + + + /** + * @dev creates new Ballot in list, emits {VotingStarted} + * @param _votingPrimary primary info about voting + * @return id of new voting + */ + function startVoting( + BallotList.BallotSimple memory _votingPrimary + ) + public + noActiveVotings + questionExists(_votingPrimary.questionId) + groupIsAllowed( + _votingPrimary.questionId, + _votingPrimary.starterGroupId + ) + returns (uint id) + { + _votingPrimary.endTime = block.timestamp + questions.list[_votingPrimary.questionId].timeLimit; + + id = Ballots.addVoting( + _votingPrimary, + questions.list[_votingPrimary.questionId].formula, + groups.list[0].groupAddress + ); + } + + + /** + * @dev closes last voting in list + * @return success + */ + function submitVoting() + public + returns (bool) + { + uint votingId = ballots.list.length - 1; + uint questionId = ballots.list[votingId].questionId; + + bytes storage formula = questions.list[questionId].formula; + address owners = groups.list[0].groupAddress; + + VM.Vote result = Ballots.closeVoting(votingId, formula, owners); + QuestionType.Question memory question = Questions.getQuestion(questionId); + + MetaData memory meta = MetaData({ + ballotId: votingId, + questionId: questionId, + startBlock: ballots.list[votingId].startBlock, + endBlock: block.number, + result: result + }); + + makeCall( + question.target, + question.methodSelector, + ballots.list[votingId].votingData, + meta + ); + + return true; + } + /** * @notice makes call to contract external method * with modified data (meta added) @@ -86,74 +137,82 @@ contract ZeroOne is Notifier, IZeroOne { } - function setVote(address tokenAddr, address user, BallotResult descision) public returns (BallotResult) { - IERC20 token = IERC20(tokenAddr); - uint256 tokenBalance = token.balanceOf(user); - require(token.transferFrom(user, address(this), tokenBalance)); - ballot.votes[tokenAddr][user] = descision; - ballot.votesWeight[tokenAddr][user] = tokenBalance; - } - - function updateUserVote(address tokenAddr, address user, uint256 newVoteWeight) public override returns(bool){ - uint256 oldVoteWeight = ballot.votesWeight[tokenAddr][user]; - uint index = uint(ballot.votes[tokenAddr][user]); - uint256 oldDescisionWeight = ballot.descisionWeights[tokenAddr][index]; - - if (ballot.status == BallotStatus.ACTIVE) { - ballot.votesWeight[tokenAddr][user] = newVoteWeight; - ballot.descisionWeights[tokenAddr][index] = oldDescisionWeight - oldVoteWeight + newVoteWeight; - if (newVoteWeight == 0) { - ballot.votes[tokenAddr][user] = BallotResult.NOT_ACCEPTED; - } - } - } - - function getUserVote(address tokenAddr, address user) - public - view - override - returns(uint) - { - return uint(ballot.votes[tokenAddr][user]); - } - - function getUserVoteWeight(address tokenAddr, address user) - public - view - override - returns(uint256) + /** + * @dev wrapper for QuestionsWithGroups.addQuestionGroup method + * @param _metaData IZeroOne.MetaData + * @param _questionGroup QuestionGroup, which will be added + */ + function addQuestionGroup( + MetaData memory _metaData, + GroupType.Group memory _questionGroup + ) + public + onlySelf() + returns (uint ballotId) { - return ballot.votesWeight[tokenAddr][user]; + QuestionsWithGroups.addQuestionGroup(_questionGroup); + emit ZeroOneCall(_metaData); + return _metaData.ballotId; } - function didUserVote(address tokenAddr, address user) - public - override - returns(bool) + /** + * @dev wrapper for Questions.addQuestion method + * @param _metaData IZeroOne.MetaData + * @param _question Question, which will be added + */ + function addQuestion( + MetaData memory _metaData, + QuestionType.Question memory _question + ) + public + onlySelf() + returns (uint ballotId) { - return ballot.votes[tokenAddr][user] != BallotResult.NOT_ACCEPTED; + Questions.addQuestion(_question); + emit ZeroOneCall(_metaData); + return _metaData.ballotId; } - function submitVoting() + /** + * @dev wrapper for UserGroups.addUserGroup method + * @param _metaData IZeroOne.MetaData + * @param _group UserGroup, which will be added + */ + function addUserGroup( + MetaData memory _metaData, + UserGroup.Group memory _group + ) public - override + onlySelf() + returns (uint ballotId) { - require(block.timestamp > ballot.endTime, "Time is not over"); - ballot.status = BallotStatus.CLOSED; + UserGroups.addUserGroup(_group); + emit ZeroOneCall(_metaData); + return _metaData.ballotId; } + /** + * @dev wrapper for setAdmin method + * @param _metaData IZeroOne.MetaData + * @param _group address of group, which admin will be changed + * @param _user address of user, which will be new admin + */ function setGroupAdmin( - address tokenAddr, - address newOwner + MetaData memory _metaData, + address _group, + address _user ) - public - override + internal + onlySelf() + returns(uint ballotId) { - tokenAddr.call(abi.encodeWithSignature("transferOwnership(address)", newOwner)); + _group.call(abi.encodeWithSignature("transferOwnership(address)", _user)); + emit ZeroOneCall(_metaData); + return _metaData.ballotId; } - function disableUserGroup(address tokenAddr) public { + function disableUserGroup(address tokenAddr) internal { tokenAddr.call(abi.encodeWithSignature("removeFromProjects(address)", address(this))); } -} \ No newline at end of file +} diff --git a/contracts/__mocks__/BallotsMock.sol b/contracts/__mocks__/BallotsMock.sol new file mode 100644 index 0000000..6d8f8f2 --- /dev/null +++ b/contracts/__mocks__/BallotsMock.sol @@ -0,0 +1,39 @@ +pragma solidity 0.6.1; +pragma experimental ABIEncoderV2; + +import "../ZeroOne/Ballots/Ballots.sol"; + +/** + * @title BallotsMock + * @dev Mock for testing Ballots contract + */ +contract BallotsMock is Ballots { + bytes public formula = hex"00BEF946538A29B7c330F6a77f61c5C1c8735a8ace010006010032320704000904000000"; + address owners = 0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace; + + /** + * @dev method for testing "addVoting" method in Ballots + * @param _votingPrimary primary info about voting + */ + function testAddVoting( + BallotList.BallotSimple memory _votingPrimary + ) + public + { + _votingPrimary.endTime = block.timestamp + 36000; + addVoting( + _votingPrimary, + formula, + owners + ); + } + + /** + * @dev method for testing "closeVoting" method in Ballots + */ + function testCloseVoting() + public + { + closeVoting(0, formula, msg.sender); + } + } \ No newline at end of file diff --git a/contracts/__mocks__/ERC20Mock.sol b/contracts/__mocks__/ERC20Mock.sol index 6b2bee6..51e6cf4 100644 --- a/contracts/__mocks__/ERC20Mock.sol +++ b/contracts/__mocks__/ERC20Mock.sol @@ -4,7 +4,7 @@ pragma solidity 0.6.1; * @title ERC20 * @dev simplest mock for testing user group methods */ -contract ERC20 { +contract ERC20Mock { uint256 private _totalSupply; diff --git a/contracts/__mocks__/ZeroOneMock.sol b/contracts/__mocks__/ZeroOneMock.sol deleted file mode 100644 index 4ad14ea..0000000 --- a/contracts/__mocks__/ZeroOneMock.sol +++ /dev/null @@ -1,36 +0,0 @@ -pragma solidity 0.6.1; -pragma experimental ABIEncoderV2; - -import "../ZeroOne/ZeroOne.sol"; - - -/** - * @title ZeroOneMock - * @dev wrapper to test some ZeroOne methods - */ -contract ZeroOneMock is ZeroOne { - /** - * @notice wrapper for internal makeCall method - * @param _target contract address to make call to - * @param _method method selector - * @param _data data to provide with call - * @param _metaData meta to update data - * @return result - */ - function testMakeCall( - address _target, - bytes4 _method, - bytes memory _data, - MetaData memory _metaData - ) - public - returns (bool result) - { - return makeCall( - _target, - _method, - _data, - _metaData - ); - } -} diff --git a/contracts/__vendor__/Context.sol b/contracts/__vendor__/Context.sol new file mode 100644 index 0000000..eb0561f --- /dev/null +++ b/contracts/__vendor__/Context.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.6.0; + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +contract Context { + // Empty internal constructor, to prevent people from mistakenly deploying + // an instance of this contract, which should be used via inheritance. + constructor () internal { } + + function _msgSender() internal view virtual returns (address payable) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} \ No newline at end of file diff --git a/contracts/__vendor__/ERC20.sol b/contracts/__vendor__/ERC20.sol new file mode 100644 index 0000000..b567a2a --- /dev/null +++ b/contracts/__vendor__/ERC20.sol @@ -0,0 +1,281 @@ +pragma solidity ^0.6.0; + +import "./Context.sol"; +import "./IERC20.sol"; +import "./SafeMath.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20Mintable}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context { + using SafeMath for uint256; + + mapping (address => uint256) private _balances; + + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + + string private _symbol; + + constructor( + string memory name, + string memory symbol, + uint256 totalSupply + ) public { + _name = name; + _symbol = symbol; + _totalSupply = totalSupply; + _balances[msg.sender] = totalSupply; + } + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for `sender`'s tokens of at least + * `amount`. + */ + function transferFrom(address sender, address recipient, uint256 amount) public virtual returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * + * This is internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`.`amount` is then deducted + * from the caller's allowance. + * + * See {_burn} and {_approve}. + */ + function _burnFrom(address account, uint256 amount) internal virtual { + _burn(account, amount); + _approve(account, _msgSender(), _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance")); + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of `from`'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of `from`'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:using-hooks.adoc[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} \ No newline at end of file diff --git a/contracts/ZeroOne/UserGroups/IERC20.sol b/contracts/__vendor__/IERC20.sol similarity index 100% rename from contracts/ZeroOne/UserGroups/IERC20.sol rename to contracts/__vendor__/IERC20.sol diff --git a/contracts/__vendor__/SafeMath.sol b/contracts/__vendor__/SafeMath.sol new file mode 100644 index 0000000..a163604 --- /dev/null +++ b/contracts/__vendor__/SafeMath.sol @@ -0,0 +1,156 @@ +pragma solidity ^0.6.0; + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + * + * _Available since v2.4.0._ + */ + function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} \ No newline at end of file diff --git a/migrations/3_userGroups.js b/migrations/3_userGroups.js index 9bba7dd..176040d 100644 --- a/migrations/3_userGroups.js +++ b/migrations/3_userGroups.js @@ -1,14 +1,14 @@ -const UserGroups = artifacts.require('UserGroups.sol'); +const CustomToken = artifacts.require('CustomToken.sol'); module.exports = async function(deployer, network, accounts) { const config = { from: accounts[0], }; - return deployer.deploy(UserGroups, config) + return deployer.deploy(CustomToken, "test", "tst", 1000, config) .then(() => { let output = new String(); output += '---------------------------------------------------------------------------\n'; - output += `| UserGroups | ${UserGroups.address} |\n`; + output += `| CustomToken | ${CustomToken.address} |\n`; output += '---------------------------------------------------------------------------'; console.log(output); }) diff --git a/package-lock.json b/package-lock.json index 20bc734..f90f4bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2467,6 +2467,31 @@ "camelcase": "^3.0.0", "lodash.assign": "^4.0.6" } + }, + "zeroone-translator": { + "version": "github:Neos1/zeroone-translator#ddf70025bd83f05e5546240b027e583f7074f794", + "from": "github:Neos1/zeroone-translator#master" + }, + "zeroone-voting-vm": { + "version": "github:Neos1/zeroone-voting-vm#abaa67309763c0f1ce3921cd69998508ea7ce83e", + "from": "github:Neos1/zeroone-voting-vm#master", + "requires": { + "solc": "^0.6.1", + "solidity-bytes-utils": "0.0.6", + "truffle": "^5.1.14" + }, + "dependencies": { + "truffle": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.1.14.tgz", + "integrity": "sha512-6rIy335igwHOR0a8xEtPZdlCPBAvDcMIuVQVWAVtPqDy7xMTxGm4A0C4YRsEvZUc5V8GfCBfQb/GQ5AXlXI+6w==", + "requires": { + "app-module-path": "^2.2.0", + "mocha": "5.2.0", + "original-require": "1.0.1" + } + } + } } } } diff --git a/package.json b/package.json index ecf4d80..09b5582 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "dependencies": { "solc": "^0.6.1", "solidity-bytes-utils": "0.0.6", - "truffle": "^5.1.8" + "truffle": "^5.1.8", + "zeroone-translator": "github:Neos1/zeroone-translator#master", + "zeroone-voting-vm": "github:Neos1/zeroone-voting-vm#master" }, "devDependencies": { "@neos1/truffle-plugin-docs": "^1.2.4" diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js new file mode 100644 index 0000000..cf243e8 --- /dev/null +++ b/test/10_Ballot.spec.js @@ -0,0 +1,243 @@ +const Ballot = artifacts.require('BallotsMock.sol'); +const ZeroOneVM = artifacts.require('ZeroOneVM.sol'); +const CustomToken = artifacts.require('CustomToken.sol'); + +const { getErrorMessage, getShortErrorMessage } = require('./helpers/get-error-message'); +const increaseTime = require('./helpers/increase-time'); + +contract('Ballot', ([from, secondary]) => { + let ballot; + let token; + + const primaryInfo = { + starterGroupId: 0, + starterAddress: secondary, + questionId: 0, + data: '0x', + endTime: 0, + } + + beforeEach( async () => { + question = { + active: true, + name: 'question name', + description: 'description', + groupId: 0, + timeLimit: 10 * 60 * 60, + paramNames: ['param1'], + paramTypes: ['uint256'], + target: from, + methodSelector: '0x12121212' + }; + + group = { + name: 'group name' + }; + + zeroOneVM = await ZeroOneVM.new(); + await Ballot.link("ZeroOneVM", zeroOneVM.address); + ballot = await Ballot.new({ from }); + token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + // await ballot.addQuestion(question) + }); + + describe('constructor()', () => { + it('should be successfully created', async () => { + ballot = await Ballot.new({ from }); + const amount = await ballot.getVotingsAmount(); + assert.strictEqual(amount.toNumber(), 0); + }); + }); + + describe('addVoting()', () => { + it('should add voting', async () => { + const tx = await ballot.testAddVoting(primaryInfo); + const {args : {votingId, questionId}} = tx.logs.find(element => element.event.match('VotingStarted')); + + assert.strictEqual(votingId.toNumber(), 0); + assert.strictEqual(questionId.toNumber(), primaryInfo.questionId); + + const amount = await ballot.getVotingsAmount(); + assert.strictEqual(amount.toNumber(), 1); + }); + + it('should fail on adding voting, while has active voting', async () => { + let error = false; + + await ballot.testAddVoting(primaryInfo); + try { + await ballot.testAddVoting(primaryInfo); + } catch ({ message }) { + error = true; + assert.strictEqual(message, getErrorMessage('You have active voting')); + } + assert.strictEqual(error, true); + }); + }) + + describe('getVoting()', () => { + it('should return information about voting', async () => { + await ballot.testAddVoting(primaryInfo); + const { + endTime, + starterGroupId, + starterAddress, + questionId, + status + } = await ballot.getVoting(0); + + assert.strictEqual(starterGroupId.toNumber(), 0); + assert.strictEqual(starterAddress, secondary); + assert.strictEqual(questionId.toNumber(), 0); + assert.strictEqual(status.toNumber(), 1); + }); + + it('should fail on getting non-existing voting', async () => { + let error = false; + try { + await ballot.getVoting(0); + } catch ({message}) { + error = true; + assert.strictEqual(message, getShortErrorMessage('Provided index out of bounds')) + } + assert.strictEqual(error, true); + }); + }) + + describe('getVotingsAmount()', () => { + it('should successfully return amount of votings', async () => { + let amount = await ballot.getVotingsAmount(); + assert.strictEqual(amount.toNumber(), 0); + + await ballot.testAddVoting(primaryInfo); + + amount = await ballot.getVotingsAmount(); + assert.strictEqual(amount.toNumber(), 1); + }); + }) + + describe('setVote()', () => { + it('should successfully set Positive vote', async () => { + + token.addToProjects(ballot.address); + + await ballot.testAddVoting(primaryInfo); + + const tx = await ballot.setVote(1); + const {args : { + user, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, from); + assert.strictEqual(descision.toNumber(), 1) + }) + + it('should successfully set Negative vote', async () => { + token.addToProjects(ballot.address); + + await ballot.testAddVoting(primaryInfo); + + const tx = await ballot.setVote(2); + const {args : { + user, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, from); + assert.strictEqual(descision.toNumber(), 2) + }) + + it('should successfully update vote of user to UNDEFINED', async () => { + token.addToProjects(ballot.address); + + await ballot.testAddVoting(primaryInfo); + const tx = await ballot.setVote(2); + const {args : { + user:sender, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(sender, from); + assert.strictEqual(descision.toNumber(), 2); + + await token.transferFrom(from, secondary, 1000); + const userVote = await ballot.getUserVote(0, token.address, from); + const voteWeight = await ballot.getUserVoteWeight(0, token.address, from); + assert.strictEqual(userVote.toNumber(), 0); + assert.strictEqual(voteWeight.toNumber(), 0); + }) + + it('should successfully remove vote', async () => { + token.addToProjects(ballot.address); + + await ballot.testAddVoting(primaryInfo); + const tx = await ballot.setVote(1); + const {args : { + user, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, from); + assert.strictEqual(descision.toNumber(), 1) + + await token.revoke(ballot.address) + + const userVote = await ballot.getUserVote(0, token.address, from); + assert.strictEqual(userVote.toNumber(), 0); + }) + + it('should not set new vote of user, which already voted', async () => { + token.addToProjects(ballot.address); + await ballot.testAddVoting(primaryInfo); + await ballot.setVote(1); + await ballot.setVote(2); + const userVote = await ballot.getUserVote(0, token.address, from) + assert.strictEqual(userVote.toNumber(), 1); + }) + }) + + describe('closeVoting()', () => { + it('should successfully close voting', async () => { + await ballot.testAddVoting(primaryInfo); + increaseTime(web3, 300000); + const tx = await ballot.testCloseVoting(); + const {args : {votingId, descision}} = tx.logs.find(element => element.event.match('VotingEnded')); + assert.strictEqual(votingId.toNumber(), 0); + assert.strictEqual(descision.toNumber(), 1); + }) + + it('should fail on close voting, when time is not over', async () => { + let error = false; + await ballot.testAddVoting(primaryInfo); + try { + await ballot.testCloseVoting(); + } catch ({message}) { + error = true; + assert.strictEqual(message, getErrorMessage('Time is not over yet')); + } + assert.strictEqual(error, true) + }); + }) + + describe('events', () => { + it('should fire VotingStarted event', async () => { + const tx = await ballot.testAddVoting(primaryInfo); + const {args : {votingId, questionId}} = tx.logs.find(element => element.event.match('VotingStarted')); + + assert.strictEqual(votingId.toNumber(), 0); + assert.strictEqual(questionId.toNumber(), primaryInfo.questionId); + }); + + it('should fire UserVote event', async () => { + token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + token.addToProjects(ballot.address); + + await ballot.testAddVoting(primaryInfo); + + const tx = await ballot.setVote(1); + const {args : { + user, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, from); + assert.strictEqual(descision.toNumber(), 1) + }); + }) +}); \ No newline at end of file diff --git a/test/11_1_ZeroOne.spec.js b/test/11_1_ZeroOne.spec.js new file mode 100644 index 0000000..a10eeb6 --- /dev/null +++ b/test/11_1_ZeroOne.spec.js @@ -0,0 +1,160 @@ +const ZeroOne = artifacts.require('./ZeroOne.sol'); +const ZeroOneVM = artifacts.require('zeroone-voting-vm/contracts/ZeroOneVM.sol'); +const ERC20 = artifacts.require('./ERC20.sol'); +const CustomToken = artifacts.require('./CustomToken.sol'); + +const Controlled = artifacts.require('./ControlledMock.sol'); +const BallotType = artifacts.require('./BallotType.sol'); + +const increase = require('./helpers/increase-time'); +const { compile } = require('zeroone-translator'); +const { questions } = require('./helpers/questions'); + +contract('ZeroOne', ([from, secondary, third]) => { + let zeroOne; + let token; + + const results = ["UNDEFINED", "ACCEPTED", "DECLINED"] + + + + + + const formulas = [ + `erc20{%s}->conditions{quorum>30%, positive>50% of all} and (custom{%t}->conditions{quorum>30%, positive>50% of all} and custom{%t}->admin)`, + `erc20{%s}->conditions{quorum>50%, positive>90% of quorum}`, + `erc20{%s}->conditions{quorum>50%, positive=100% of quorum}`, + `erc20{%s}->conditions{quorum>0%, positive>50% of all}`, + `erc20{%s}->conditions{quorum>50%,positive>90% of quorum} or custom{%t}->admin`, + `erc20{%s}->conditions{quorum>50%,positive>90% of quorum} and custom{%t}->admin`, + `erc20{%s}->conditions{quorum>50%,positive>90% of quorum} and custom{%t}->admin`, + ]; + + beforeEach(async () => { + + }); + + describe('fullVotingProcess', async () => { + it("should finish voting with ACCEPTED result", async () => { + for (formula of formulas) { + token = await ERC20.new('test', 'tst', 1000); + customToken = await CustomToken.new('test', 'tst', 1000); + + const zeroOneVm = await ZeroOneVM.new(); + const ballotType = await BallotType.new(); + + await ZeroOne.link("ZeroOneVM", zeroOneVm.address); + await ZeroOne.link("BallotType", ballotType.address); + + const group = { + name: "Owners", + groupAddress: token.address, + groupType: 0 + } + + zeroOne = await ZeroOne.new(group, { from }); + controlled = await Controlled.new(zeroOne.address, { from }); + + customToken.addToProjects(zeroOne.address); + + for (question of questions) { + question.rawFormula = formula; + question.formula = compile( + question.rawFormula + .replace(/\%s/g, token.address) + .replace(/\%t/g, customToken.address) + .replace(/\%u/g, from) + ) + question.target = zeroOne.address; + await zeroOne.addQuestion(question); + } + + const adminBalance = await token.balanceOf(from); + const data = web3.eth.abi.encodeParameters(['tuple(uint256,uint256,uint256,uint256,uint256)', 'tuple(string)'],[[0, 0, 0, 0, 0], ["test"]]) + const votingData = { + questionId: 2, + starterAddress: from, + starterGroupId: 0, + endTime: 0, + data, + } + + await zeroOne.startVoting(votingData); + try { + await token.approve(zeroOne.address, adminBalance); + await zeroOne.setVote(1); + + increase(web3, 320000); + await zeroOne.submitVoting(); + const [event] = await zeroOne.getPastEvents('VotingEnded'); + const {args: {descision}} = event; + assert.strictEqual(descision.toNumber(), 1); + console.log(`voting with formula --- ${formula} --- ✔️`); + } catch ({message}) { + console.log(`test #${formulas.indexOf(formula)} --- ${message} --- 🚫`) + } + } + }); + + it('should finish voting with DECLINED result', async () => { + for (formula of formulas) { + token = await ERC20.new('test', 'tst', 1000); + customToken = await CustomToken.new('test', 'tst', 1000); + + const zeroOneVm = await ZeroOneVM.new(); + const ballotType = await BallotType.new(); + + await ZeroOne.link("ZeroOneVM", zeroOneVm.address); + await ZeroOne.link("BallotType", ballotType.address); + + const group = { + name: "Owners", + groupAddress: token.address, + groupType: 0 + } + + zeroOne = await ZeroOne.new(group, { from }); + controlled = await Controlled.new(zeroOne.address, { from }); + + customToken.addToProjects(zeroOne.address); + + for (question of questions) { + question.rawFormula = formula; + question.formula = compile( + question.rawFormula + .replace(/\%s/g, token.address) + .replace(/\%t/g, customToken.address) + .replace(/\%u/g, from) + ) + question.target = zeroOne.address; + await zeroOne.addQuestion(question); + } + + const adminBalance = await token.balanceOf(from); + const data = web3.eth.abi.encodeParameters(['tuple(uint256,uint256,uint256,uint256,uint256)', 'tuple(string)'],[[0, 0, 0, 0, 0], ["test"]]) + const votingData = { + questionId: 2, + starterAddress: from, + starterGroupId: 0, + endTime: 0, + data, + } + + await zeroOne.startVoting(votingData); + try { + await token.approve(zeroOne.address, adminBalance); + await zeroOne.setVote(2); + + increase(web3, 320000); + await zeroOne.submitVoting(); + const [event] = await zeroOne.getPastEvents('VotingEnded'); + const {args: {descision}} = event; + assert.strictEqual(descision.toNumber(), 2); + console.log(`voting with formula --- ${formula} --- ✔️`); + } catch ({message}) { + console.log(`test #${formulas.indexOf(formula)} --- ${message} --- 🚫`) + } + } + }); + }); +}); \ No newline at end of file diff --git a/test/11_ZeroOne.spec.js b/test/11_ZeroOne.spec.js new file mode 100644 index 0000000..85474ca --- /dev/null +++ b/test/11_ZeroOne.spec.js @@ -0,0 +1,93 @@ +const ZeroOne = artifacts.require('./ZeroOne.sol'); +const ZeroOneVM = artifacts.require('zeroone-voting-vm/contracts/ZeroOneVM.sol'); +const ERC20 = artifacts.require('./ERC20.sol'); +const CustomToken = artifacts.require('./CustomToken.sol'); + +const Controlled = artifacts.require('./ControlledMock.sol'); +const BallotType = artifacts.require('./BallotType.sol'); + +const increase = require('./helpers/increase-time'); +const { compile } = require('zeroone-translator'); +const { questions } = require('./helpers/questions'); + +contract('ZeroOne', ([from, secondary]) => { + let zeroOne; + let token; + + + const formulas = [ + `erc20{${address}}->conditions{quorum>50%, positive=100% of quorum}`, + `erc20{${address}}->conditions{quorum>50%, positive>90% of quorum}`, + `erc20{${addresses[1]}}->conditions{quorum>50%,positive>90% of quorum} or custom{${address}}->admin`, + `erc20{${addresses[1]}}->conditions{quorum>50%,positive>90% of quorum} and custom{${address}}->admin`, + `erc20{${address}}->conditions{quorum>30%, positive>50% of all} + or ( + custom{${addresses[0]}}->conditions{quorum>30%, positive>50% of all} + and custom{${addresses[0]}}->admin + )` + ]; + + beforeEach(async () => { + token = await ERC20.new('test', 'tst', 1000); + customToken = await CustomToken.new('test', 'tst', 1000) + + const group = { + name: "Owners", + groupAddress: token.address, + groupType: 0 + } + + const zeroOneVm = await ZeroOneVM.new(); + const ballotType = await BallotType.new(); + + await ZeroOne.link("ZeroOneVM", zeroOneVm.address); + await ZeroOne.link("BallotType", ballotType.address); + + zeroOne = await ZeroOne.new(group, { from }); + controlled = await Controlled.new(zeroOne.address, { from }); + }); + + describe('addQuestion()', () => { + it('should add system questions', async () => { + for (let i = 0; i < questions.length; i++) { + questions[i].formula = compile(questions[i].rawFormula.replace('%s', token.address)) + questions[i].target = zeroOne.address; + questions[i].active = true; + await zeroOne.addQuestion(questions[i]); + } + const amount = await zeroOne.getQuestionsAmount() + assert.strictEqual(amount.toNumber(), 4); + }); + }); + + describe('fullVotingProcess', async () => { + it('should add question group', async () => { + for (let i = 0; i < questions.length; i++) { + questions[i].formula = compile(questions[i].rawFormula.replace('%s', token.address)) + questions[i].target = zeroOne.address; + questions[i].active = true; + await zeroOne.addQuestion(questions[i]); + } + const data = web3.eth.abi.encodeParameters(['tuple(uint256,uint256,uint256,uint256,uint256)', 'tuple(string)'], [[0, 0, 0, 0, 0], ["test"]]) + const votingData = { + questionId: 2, + starterAddress: from, + starterGroupId: 0, + endTime: 0, + data, + } + await zeroOne.startVoting(votingData); + const userBalance = await token.balanceOf(from); + await token.approve(zeroOne.address, userBalance); + await zeroOne.setVote(1); + increase(web3, 320000); + await zeroOne.submitVoting(); + + const questionGroupsAmount = await zeroOne.getQuestionGroupsAmount(); + assert.strictEqual(questionGroupsAmount.toNumber(), 2); + + const questionGroup = await zeroOne.getQuestionGroup(1); + assert.strictEqual(questionGroup.name, 'test'); + }); + }); +}); \ No newline at end of file diff --git a/test/3_Questions.spec.js b/test/3_Questions.spec.js index 9be55a1..7ec6f37 100644 --- a/test/3_Questions.spec.js +++ b/test/3_Questions.spec.js @@ -1,6 +1,8 @@ const Questions = artifacts.require('./Questions.sol'); const { getErrorMessage } = require('./helpers/get-error-message'); +const { compile, compileDescriptors } = require('zeroone-translator'); +const {questions} = require('./helpers/questions'); contract('Questions', (accounts) => { let questions; diff --git a/test/5_ZeroOne.spec.js b/test/5_ZeroOne.spec.js index 6032af0..c083c3c 100644 --- a/test/5_ZeroOne.spec.js +++ b/test/5_ZeroOne.spec.js @@ -29,8 +29,9 @@ contract('ZeroOne', (accounts) => { return prev; }, {}); + beforeEach(async () => { - zeroOne = await ZeroOne.new({ from: deployFrom }); + zeroOne = await ZeroOne.new(token.address, { from: deployFrom }); controlled = await Controlled.new(zeroOne.address, { from: deployFrom }); }); diff --git a/test/8_CustomTokens.spec.js b/test/8_CustomTokens.spec.js index 89eeabf..5c72886 100644 --- a/test/8_CustomTokens.spec.js +++ b/test/8_CustomTokens.spec.js @@ -1,5 +1,5 @@ const CustomToken = artifacts.require('./CustomToken.sol'); -const ZeroOne = artifacts.require('./ZeroOneMock.sol'); +// const ZeroOne = artifacts.require('./ZeroOneMock.sol'); const { getErrorMessage } = require('./helpers/get-error-message'); const increase = require('./helpers/increase-time'); @@ -15,13 +15,14 @@ contract('CustomToken', (accounts) => { beforeEach( async () => { token = await CustomToken.new( ...params, { from }); - zeroOne = await ZeroOne.new({from}); + // zeroOne = await ZeroOne.new({from}); admin = await token.owner(); }); describe('constructor()', () => { it('should be successfully created', async () => { token = await CustomToken.new( ...params, { from }); + console.log(token.address) const [name, symbol, totalSupply] = params; const tokenName = await token.name(); diff --git a/test/9_UserGroup.spec.js b/test/9_UserGroup.spec.js index cc76eb1..840e7f5 100644 --- a/test/9_UserGroup.spec.js +++ b/test/9_UserGroup.spec.js @@ -1,6 +1,6 @@ const UserGroupMock = artifacts.require('./UserGroupMock.sol'); const TokenMock = artifacts.require('./TokenMock.sol'); -const ERC20Mock = artifacts.require('./ERC20.sol'); +const ERC20Mock = artifacts.require('./ERC20Mock.sol'); const { getShortErrorMessage } = require('./helpers/get-error-message'); diff --git a/test/helpers/questions.js b/test/helpers/questions.js new file mode 100644 index 0000000..ec75627 --- /dev/null +++ b/test/helpers/questions.js @@ -0,0 +1,78 @@ +module.exports = { + questions : [{ + groupId: 0, + name: "Добавить Вопрос", + description: "Добавление нового вопроса", + timeLimit: 10 * 3600, + methodSelector: "0x9c88d333", + paramNames: [ + "GroupId", + "Name", + "Caption", + "Time", + "MethodSelector", + "Formula", + "paramNames", + "paramTypes" + ], + paramTypes: [ + "uint", + "string", + "string", + "uint", + "bytes4", + "string", + "string[]", + "string[]" + ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" + }, + { + groupId: 0, + name: "Подключить группу пользователей", + description: "Подключить новую группу пользователей для участия в голосованиях", + timeLimit: 10 * 3600, + methodSelector: "0x70b0e2c8", + paramNames: [ + "Name", + "Address", + "Type", + ], + paramTypes: [ + "string", + "address", + "string" + ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" + }, + { + groupId: 0, + name: "Добавить группу вопросов", + description: "Добавить новую группу вопросов", + timeLimit: 10 * 3600, + methodSelector: "0xb9253b2b", + paramNames: [ + "Name" + ], + paramTypes: [ + "string" + ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" + }, + { + groupId: 0, + name: "Установить администратора группы", + description: "Установка администратора в группе кастомных токенов", + timeLimit: 10 * 3600, + methodSelector: "0x9c88d333", + paramNames: [ + "Group Address", + "New Admin Address" + ], + paramTypes: [ + "address", + "address" + ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" + }] +} \ No newline at end of file