From 8ca5bc1eed9c83afc66b90797c27cb46942bd607 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Fri, 7 Feb 2020 17:57:04 +0700 Subject: [PATCH 01/24] Ballots started --- contracts/ZeroOne/Ballots/Ballots.sol | 0 contracts/ZeroOne/Ballots/lib/Ballot.sol | 41 +++++++++++++++++ contracts/ZeroOne/Ballots/lib/BallotList.sol | 48 ++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 contracts/ZeroOne/Ballots/Ballots.sol create mode 100644 contracts/ZeroOne/Ballots/lib/Ballot.sol create mode 100644 contracts/ZeroOne/Ballots/lib/BallotList.sol diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol new file mode 100644 index 0000000..412acfe --- /dev/null +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -0,0 +1,41 @@ +pragma solidity 0.6.1; + +/** + @title BallotType + @dev Ballot data type implementation +*/ + +library BallotType { + enum BallotStatus { ACTIVE, CLOSED } + + enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } + + struct Ballot { + uint startTime; + uint starterAddress; + uint questionId; + BallotStatus status; + BallotResult result; + bytes votingData; + mapping(address => mapping(address => BallotResult)) votes; + } + + /** + @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, + BallotResult _descision + ) + internal + returns (bool status) + { + _self.votes[_group][_user] = _descision; + return true; + } +} \ No newline at end of file diff --git a/contracts/ZeroOne/Ballots/lib/BallotList.sol b/contracts/ZeroOne/Ballots/lib/BallotList.sol new file mode 100644 index 0000000..f04d884 --- /dev/null +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -0,0 +1,48 @@ +pragma solidity 0.6.1; + +import "./Ballot.sol"; + +/** + @title BallotList + @dev stores votings + */ +library BallotList { + using BallotType for BallotType.Ballot; + + struct List { + BallotType.Ballot[] list; + } + + /** + @dev add voting to list + @param _voting + @return id + */ + function addVoting( + List storage _self, + BallotType.Ballot memory _voting + ) + internal + returns (uint id) + { + _self.list.push(_voting); + return _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; + } + +} From 1b12a00a66524445f62d2373ccda6656fd834e8c Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Mon, 10 Feb 2020 18:02:30 +0700 Subject: [PATCH 02/24] first implementation of votings --- contracts/ZeroOne/Ballots/Ballots.sol | 73 +++++++++++ contracts/ZeroOne/Ballots/lib/Ballot.sol | 124 ++++++++++++++----- contracts/ZeroOne/Ballots/lib/BallotList.sol | 73 ++++++----- 3 files changed, 200 insertions(+), 70 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index e69de29..3ad7561 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -0,0 +1,73 @@ +pragma solidity 0.6.1; +pragma experimental ABIEncoderV2; + +import "./lib/Ballot.sol"; +import "./lib/BallotList.sol"; + +/** + * @title Ballots + * @dev stores Ballots + */ +contract Ballots { + using BallotList for BallotList.List; + using BallotType for BallotType.Ballot; + + BallotList.List ballots; + + event VotingStarted(uint votingId, uint questionId); + + event VotingEnded(uint votingId, BallotType.BallotResult descision); + + event UserVote(address group, address user, BallotType.BallotResult descision); + + /** + * @notice reverts on non-existing ballot id + * @param _id ballot id + */ + modifier ballotExist( + uint _id + ) { + require( + ballots.checkId(_id), + "Provided index out of bounds" + ); + _; + } + + constructor() public {} + + /** + * @dev getting the voting by id + * @return ballot + */ + function getVoting( + uint _id + ) + public + view + ballotExist(_id) + returns (BallotType.Ballot memory ballot) + { + // TODO: Return primary voting info (mapping error); + return ballots.list[_id]; + } + + /** + * @dev return amount of votings + * @return amount + */ + function getVotingsAmount() + public + view + returns (uint amount) + { + return ballots.list.length; + } + + function closeVoting() + public + { + uint votingId = ballots.list.length - 1; + ballots.list[votingId].closeVoting(); + } +} diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index 412acfe..8747562 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -6,36 +6,94 @@ pragma solidity 0.6.1; */ library BallotType { - enum BallotStatus { ACTIVE, CLOSED } - - enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } - - struct Ballot { - uint startTime; - uint starterAddress; - uint questionId; - BallotStatus status; - BallotResult result; - bytes votingData; - mapping(address => mapping(address => BallotResult)) votes; - } - - /** - @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, - BallotResult _descision - ) - internal - returns (bool status) - { - _self.votes[_group][_user] = _descision; - return true; - } -} \ No newline at end of file + enum BallotStatus { ACTIVE, CLOSED } + + enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } + + struct Ballot { + uint startTime; + uint starterGroupId; + uint starterAddress; + uint questionId; + BallotStatus status; + BallotResult result; + bytes votingData; + mapping(address => mapping(address => BallotResult)) votes; + mapping(address => mapping(address => uint256)) votesWeight; + } + + /** + @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, + BallotResult _descision + ) + internal + returns (bool status) + { + _self.votes[_group][_user] = _descision; + return true; + } + + /** + * @dev set status in voting + * @param _status new Voting status + * @return success + */ + function setStatus( + Ballot storage _self, + BallotStatus _status + ) + internal + returns (bool success) + { + _self.status = _status; + return _self.status == _status; + } + + /** + * @dev set result and status "Closed" to voting + * @param _result calculated result of voting + * @return success + */ + function setResult( + Ballot storage _self, + BallotResult _result + ) + internal + returns (bool success) + { + require(setStatus(_self, BallotStatus.CLOSED), "Problem with setting status"); + _self.result = _result; + return _self.result == _result; + } + + /** + * @dev calculates result of voting + */ + // TODO Implement this after ready formula parser + function calculateResult() + internal + pure + returns (BallotResult) + { + return BallotResult.NOT_ACCEPTED; + } + + function closeVoting( + Ballot storage _self + ) + internal + returns (BallotResult result) + { + BallotResult _result = calculateResult(); + setResult(_self, _result); + return _result; + } +} diff --git a/contracts/ZeroOne/Ballots/lib/BallotList.sol b/contracts/ZeroOne/Ballots/lib/BallotList.sol index f04d884..135ac50 100644 --- a/contracts/ZeroOne/Ballots/lib/BallotList.sol +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -3,46 +3,45 @@ pragma solidity 0.6.1; import "./Ballot.sol"; /** - @title BallotList - @dev stores votings + * @title BallotList + * @dev stores votings */ library BallotList { - using BallotType for BallotType.Ballot; + using BallotType for BallotType.Ballot; - struct List { - BallotType.Ballot[] list; - } + struct List { + BallotType.Ballot[] list; + } - /** - @dev add voting to list - @param _voting - @return id - */ - function addVoting( - List storage _self, - BallotType.Ballot memory _voting - ) - internal - returns (uint id) - { - _self.list.push(_voting); - return _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; - } + /** + * @dev add voting to list + * @param _voting voting + * @return id + */ + function addVoting( + List storage _self, + BallotType.Ballot memory _voting + ) + internal + returns (uint id) + { + _self.list.push(_voting); + return _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; + } } From ba42cf511a2ab882cf7b53523adc7dbd178ea3a4 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 11 Feb 2020 16:59:25 +0700 Subject: [PATCH 03/24] added some new methods for ballots --- contracts/ZeroOne/Ballots/Ballots.sol | 43 ++++++++++-- contracts/ZeroOne/Ballots/lib/Ballot.sol | 73 +++++++++++++++++++- contracts/ZeroOne/Ballots/lib/BallotList.sol | 3 +- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 3ad7561..86046f4 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -14,6 +14,7 @@ contract Ballots { BallotList.List ballots; + event VotingStarted(uint votingId, uint questionId); event VotingEnded(uint votingId, BallotType.BallotResult descision); @@ -38,7 +39,6 @@ contract Ballots { /** * @dev getting the voting by id - * @return ballot */ function getVoting( uint _id @@ -46,10 +46,17 @@ contract Ballots { public view ballotExist(_id) - returns (BallotType.Ballot memory ballot) + returns ( + uint startTime, + uint starterGroupId, + uint starterAddress, + uint questionId, + BallotType.BallotStatus status, + BallotType.BallotResult result, + bytes memory votingData + ) { - // TODO: Return primary voting info (mapping error); - return ballots.list[_id]; + return ballots.list[_id].getPrimaryInfo(); } /** @@ -64,10 +71,36 @@ contract Ballots { return ballots.list.length; } + function setVote( + address _group, + address _user, + BallotType.BallotResult _descision, + uint256 _voteWeight + ) + public + returns (bool success) + { + uint votingId = ballots.list.length - 1; + + require( + ballots.list[votingId].status != BallotType.BallotStatus.CLOSED, + "Voting is closed, you must start new voting before vote" + ); + ballots.list[votingId].setVote(_group, _user, _descision, _voteWeight); + return true; + } + + /** + * @dev closes last voting in list + * @return result + */ function closeVoting() public + returns ( + BallotType.BallotResult result + ) { uint votingId = ballots.list.length - 1; - ballots.list[votingId].closeVoting(); + return ballots.list[votingId].closeVoting(); } } diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index 8747562..49a9332 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -22,6 +22,42 @@ library BallotType { mapping(address => mapping(address => uint256)) votesWeight; } + /** + * @dev getting primary info about voting + * @return startTime + * @return starterGroupId + * @return starterAddress + * @return questionId + * @return status + * @return result + * @return votingData + */ + function getPrimaryInfo( + Ballot storage _self + ) + internal + view + returns ( + uint startTime, + uint starterGroupId, + uint starterAddress, + uint questionId, + BallotStatus status, + BallotResult result, + bytes storage votingData + ) + { + return ( + _self.startTime, + _self.starterGroupId, + _self.starterAddress, + _self.questionId, + _self.status, + _self.result, + _self.votingData + ); + } + /** @dev set vote of {_user} from {_group} @param _group address of group @@ -32,15 +68,35 @@ library BallotType { Ballot storage _self, address _group, address _user, - BallotResult _descision + BallotResult _descision, + uint256 _voteWeight ) internal returns (bool status) { _self.votes[_group][_user] = _descision; + _self.votesWeight[_group][_user] = _voteWeight; return true; } + /** + * @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 (BallotResult userVote) + { + userVote = _self.votes[_group][_user]; + } + /** * @dev set status in voting * @param _status new Voting status @@ -86,6 +142,10 @@ library BallotType { return BallotResult.NOT_ACCEPTED; } + /** + * @dev close ballot by calculating result and setting status "CLOSED" + * @param _self ballot + */ function closeVoting( Ballot storage _self ) @@ -96,4 +156,15 @@ library BallotType { setResult(_self, _result); return _result; } + + + 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 index 135ac50..47230ed 100644 --- a/contracts/ZeroOne/Ballots/lib/BallotList.sol +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -18,7 +18,7 @@ library BallotList { * @param _voting voting * @return id */ - function addVoting( + function add( List storage _self, BallotType.Ballot memory _voting ) @@ -29,6 +29,7 @@ library BallotList { return _self.list.length - 1; } + /** * @dev checks id existance * @param _id id From c07e298339595d2cfc272c1460e27d76ce0bb33b Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 11 Feb 2020 18:12:36 +0700 Subject: [PATCH 04/24] Create voting function --- contracts/ZeroOne/Ballots/Ballots.sol | 13 ++++++++++++ contracts/ZeroOne/Ballots/lib/Ballot.sol | 4 ++-- contracts/ZeroOne/Ballots/lib/BallotList.sol | 21 ++++++++++++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 86046f4..058e92f 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -37,6 +37,19 @@ contract Ballots { constructor() public {} + /** + * @dev creates new Ballot in list + */ + function startVoting( + BallotList.BallotSimple memory _votingPrimary + ) + public + returns (bool) + { + ballots.add(_votingPrimary); + return true; + } + /** * @dev getting the voting by id */ diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index 49a9332..b72b57c 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -13,7 +13,7 @@ library BallotType { struct Ballot { uint startTime; uint starterGroupId; - uint starterAddress; + address starterAddress; uint questionId; BallotStatus status; BallotResult result; @@ -40,7 +40,7 @@ library BallotType { returns ( uint startTime, uint starterGroupId, - uint starterAddress, + address starterAddress, uint questionId, BallotStatus status, BallotResult result, diff --git a/contracts/ZeroOne/Ballots/lib/BallotList.sol b/contracts/ZeroOne/Ballots/lib/BallotList.sol index 47230ed..b518f46 100644 --- a/contracts/ZeroOne/Ballots/lib/BallotList.sol +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -13,18 +13,35 @@ library BallotList { BallotType.Ballot[] list; } + struct BallotSimple { + uint starterGroupId; + address starterAddress; + uint questionId; + bytes data; + } + /** * @dev add voting to list - * @param _voting voting + * @param _votingPrimary voting primary info * @return id */ function add( List storage _self, - BallotType.Ballot memory _voting + BallotSimple memory _votingPrimary ) internal returns (uint id) { + BallotType.Ballot memory _voting = BallotType.Ballot({ + startTime: block.timestamp, + starterGroupId: _votingPrimary.starterGroupId, + starterAddress: _votingPrimary.starterAddress, + questionId: _votingPrimary.questionId, + status: BallotType.BallotStatus.ACTIVE, + result: BallotType.BallotResult.NOT_ACCEPTED, + votingData: _votingPrimary.data + }); + _self.list.push(_voting); return _self.list.length - 1; } From 5e9d2a8ee706256519608d09ce4d01ed53b8fe1e Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 11 Feb 2020 18:16:02 +0700 Subject: [PATCH 05/24] small fix --- contracts/ZeroOne/Ballots/Ballots.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 058e92f..4e0e8f3 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -62,7 +62,7 @@ contract Ballots { returns ( uint startTime, uint starterGroupId, - uint starterAddress, + address starterAddress, uint questionId, BallotType.BallotStatus status, BallotType.BallotResult result, From 4e2eaae02475678267ba78a39ad0894e48c12421 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Wed, 12 Feb 2020 18:00:18 +0700 Subject: [PATCH 06/24] added modifier for active voting --- contracts/ZeroOne/Ballots/Ballots.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 4e0e8f3..a4828e3 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -35,6 +35,15 @@ contract Ballots { _; } + modifier noActiveVotings() { + uint length = ballots.list.length - 1; + require( + ballots.list[length].status != BallotType.BallotStatus.ACTIVE, + "You have already active voting" + ); + _; + } + constructor() public {} /** @@ -44,6 +53,7 @@ contract Ballots { BallotList.BallotSimple memory _votingPrimary ) public + noActiveVotings() returns (bool) { ballots.add(_votingPrimary); From ef4ca3459c8e77b837e0ac30f305e06d2e69997b Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Thu, 13 Feb 2020 18:03:09 +0700 Subject: [PATCH 07/24] some fixes and some tests --- contracts/ZeroOne/Ballots/Ballots.sol | 17 +-- contracts/ZeroOne/Ballots/lib/Ballot.sol | 2 +- test/10_Ballot.spec.js | 153 +++++++++++++++++++++++ 3 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 test/10_Ballot.spec.js diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index a4828e3..0a8999b 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -34,12 +34,12 @@ contract Ballots { ); _; } - modifier noActiveVotings() { - uint length = ballots.list.length - 1; - require( - ballots.list[length].status != BallotType.BallotStatus.ACTIVE, - "You have already active voting" + uint length = ballots.list.length; + require(length > 0 + ? ballots.list[length - 1].status != BallotType.BallotStatus.ACTIVE + : true, + "You have active voting" ); _; } @@ -54,10 +54,10 @@ contract Ballots { ) public noActiveVotings() - returns (bool) + returns (uint id) { - ballots.add(_votingPrimary); - return true; + id = ballots.add(_votingPrimary); + emit VotingStarted(id, _votingPrimary.questionId); } /** @@ -110,6 +110,7 @@ contract Ballots { "Voting is closed, you must start new voting before vote" ); ballots.list[votingId].setVote(_group, _user, _descision, _voteWeight); + emit UserVote(_group, _user, _descision); return true; } diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index b72b57c..5a2781c 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -6,7 +6,7 @@ pragma solidity 0.6.1; */ library BallotType { - enum BallotStatus { ACTIVE, CLOSED } + enum BallotStatus { CLOSED, ACTIVE } enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js new file mode 100644 index 0000000..3b98fe6 --- /dev/null +++ b/test/10_Ballot.spec.js @@ -0,0 +1,153 @@ +const Ballot = artifacts.require('Ballots.sol'); +const Questions = artifacts.require('QuestionsWithGroups.sol'); +const UserGroups = artifacts.require('UserGroups.sol'); +const CustomToken = artifacts.require('CustomToken.sol'); + +const { getErrorMessage, getShortErrorMessage } = require('./helpers/get-error-message'); + +contract('Ballot', ([from, secondary]) => { + let ballot; + let questions; + let usergroups; + let customToken; + + const primaryInfo = { + starterGroupId: 1, + starterAddress: secondary, + questionId: 0, + data: '0x' + } + + beforeEach( async () => { + ballot = await Ballot.new({ from }); + questions = await Questions.new({ from }); + usergroups = await UserGroups.new({ from }); + customToken = await CustomToken.new('test', 'tst', 1000, { from }); + }); + + describe('constructor()', () => { + it('should be successfully created', async () => { + ballot = await Ballot.new({ from }); + const amount = await ballot.getVotingsAmount(); + assert.strictEqual(amount.toNumber(), 0) + }); + }); + + describe('startVoting()', () => { + it('should start voting', async () => { + + const tx = await ballot.startVoting(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 start voting, while has active voting', async () => { + let error = false; + + await ballot.startVoting(primaryInfo); + try { + await ballot.startVoting(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.startVoting(primaryInfo); + const { + startTime, + starterGroupId, + starterAddress, + questionId, + status, + result + } = await ballot.getVoting(0); + + assert.strictEqual(starterGroupId.toNumber(), 1); + assert.strictEqual(starterAddress, secondary); + assert.strictEqual(questionId.toNumber(), 0); + assert.strictEqual(status.toNumber(), 1); + assert.strictEqual(result.toNumber(), 0); + }); + + 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.startVoting(primaryInfo); + + amount = await ballot.getVotingsAmount(); + assert.strictEqual(amount.toNumber(), 1); + }); + }) + + describe('setVote()', () => { + it('should successfully set Positive vote', async () => { + await ballot.startVoting(primaryInfo); + + const tx = await ballot.setVote(from, secondary, 1, 200) + const {args : { + user, group, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, secondary); + assert.strictEqual(group, from) + assert.strictEqual(descision.toNumber(), 1) + }) + + it('should successfully set Negative vote', async () => { + await ballot.startVoting(primaryInfo); + + const tx = await ballot.setVote(from, secondary, 0, 200) + const {args : { + user, group, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, secondary); + assert.strictEqual(group, from) + assert.strictEqual(descision.toNumber(), 0) + }) + + it('should successfully remove vote', async () => { + + }) + + it('should fail on remove vote of user, which not vote', async () => { + + }) + }) + + describe('closeVoting()', () => { + it('should successfully close voting', async () => { + + }) + + it('should fail on close voting, when time is not over', async () => { + + }); + }) + + describe('events', () => {}) +}); \ No newline at end of file From 851d0cd063676e8cdc10e5b122622f41280c580e Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Fri, 14 Feb 2020 17:47:21 +0700 Subject: [PATCH 08/24] Added: - endTime for voting - new modifiers - included questions and usergroups - tests for creating, voting, closing voting and events --- contracts/ZeroOne/Ballots/Ballots.sol | 50 +++++++- contracts/ZeroOne/Ballots/lib/Ballot.sol | 4 + contracts/ZeroOne/Ballots/lib/BallotList.sol | 2 + contracts/ZeroOne/ZeroOne.sol | 4 +- test/10_Ballot.spec.js | 118 +++++++++++++++---- 5 files changed, 150 insertions(+), 28 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 0a8999b..2272dd2 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -3,12 +3,14 @@ pragma experimental ABIEncoderV2; import "./lib/Ballot.sol"; import "./lib/BallotList.sol"; +import "../Questions/QuestionsWithGroups.sol"; +import "../UserGroups/UserGroups.sol"; /** * @title Ballots * @dev stores Ballots */ -contract Ballots { +contract Ballots is QuestionsWithGroups, UserGroups { using BallotList for BallotList.List; using BallotType for BallotType.Ballot; @@ -34,6 +36,7 @@ contract Ballots { ); _; } + modifier noActiveVotings() { uint length = ballots.list.length; require(length > 0 @@ -44,6 +47,30 @@ contract Ballots { _; } + modifier userNotVoted( + address _group, + address _user + ) { + uint _id = ballots.list.length - 1; + + require( + ballots.list[_id].votes[_group][_user] == BallotType.BallotResult.NOT_ACCEPTED, + "User already vote" + ); + _; + } + + 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" + ); + _; + } constructor() public {} /** @@ -54,8 +81,14 @@ contract Ballots { ) public noActiveVotings() + questionExists(_votingPrimary.questionId) + groupIsAllowed( + _votingPrimary.questionId, + _votingPrimary.starterGroupId + ) returns (uint id) { + _votingPrimary.endTime = block.timestamp + questions.list[_votingPrimary.questionId].timeLimit; id = ballots.add(_votingPrimary); emit VotingStarted(id, _votingPrimary.questionId); } @@ -71,6 +104,7 @@ contract Ballots { ballotExist(_id) returns ( uint startTime, + uint endTime, uint starterGroupId, address starterAddress, uint questionId, @@ -101,10 +135,14 @@ contract Ballots { uint256 _voteWeight ) public + userNotVoted( + _group, + _user + ) returns (bool success) { uint votingId = ballots.list.length - 1; - + require(ballots.list[votingId].endTime > block.timestamp, "Votes recieving are closed"); require( ballots.list[votingId].status != BallotType.BallotStatus.CLOSED, "Voting is closed, you must start new voting before vote" @@ -116,15 +154,17 @@ contract Ballots { /** * @dev closes last voting in list - * @return result + * @return descision */ function closeVoting() public returns ( - BallotType.BallotResult result + BallotType.BallotResult descision ) { uint votingId = ballots.list.length - 1; - return ballots.list[votingId].closeVoting(); + require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); + descision = ballots.list[votingId].closeVoting(); + emit VotingEnded(votingId, descision); } } diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index 5a2781c..6ad76d9 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -12,6 +12,7 @@ library BallotType { struct Ballot { uint startTime; + uint endTime; uint starterGroupId; address starterAddress; uint questionId; @@ -25,6 +26,7 @@ library BallotType { /** * @dev getting primary info about voting * @return startTime + * @return endTime * @return starterGroupId * @return starterAddress * @return questionId @@ -39,6 +41,7 @@ library BallotType { view returns ( uint startTime, + uint endTime, uint starterGroupId, address starterAddress, uint questionId, @@ -49,6 +52,7 @@ library BallotType { { return ( _self.startTime, + _self.endTime, _self.starterGroupId, _self.starterAddress, _self.questionId, diff --git a/contracts/ZeroOne/Ballots/lib/BallotList.sol b/contracts/ZeroOne/Ballots/lib/BallotList.sol index b518f46..544df93 100644 --- a/contracts/ZeroOne/Ballots/lib/BallotList.sol +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -15,6 +15,7 @@ library BallotList { struct BallotSimple { uint starterGroupId; + uint endTime; address starterAddress; uint questionId; bytes data; @@ -34,6 +35,7 @@ library BallotList { { BallotType.Ballot memory _voting = BallotType.Ballot({ startTime: block.timestamp, + endTime: _votingPrimary.endTime, starterGroupId: _votingPrimary.starterGroupId, starterAddress: _votingPrimary.starterAddress, questionId: _votingPrimary.questionId, diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index c26193e..61766ae 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -4,13 +4,15 @@ pragma experimental ABIEncoderV2; import "./IZeroOne.sol"; import "./Notifier/Notifier.sol"; import "../lib/Meta.sol"; +import "./Ballots/Ballots.sol"; + /** * @title ZeroOne * @dev main ZeroOne contract */ -contract ZeroOne is Notifier, IZeroOne { +contract ZeroOne is Notifier, IZeroOne, Ballots { using Meta for bytes; /** diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index 3b98fe6..3d55eea 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -4,32 +4,45 @@ const UserGroups = artifacts.require('UserGroups.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 questions; - let usergroups; - let customToken; const primaryInfo = { - starterGroupId: 1, + starterGroupId: 0, starterAddress: secondary, questionId: 0, - data: '0x' + 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' + }; ballot = await Ballot.new({ from }); - questions = await Questions.new({ from }); - usergroups = await UserGroups.new({ from }); customToken = await CustomToken.new('test', 'tst', 1000, { from }); + + 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) + assert.strictEqual(amount.toNumber(), 0); }); }); @@ -63,8 +76,8 @@ contract('Ballot', ([from, secondary]) => { describe('getVoting()', () => { it('should return information about voting', async () => { await ballot.startVoting(primaryInfo); - const { - startTime, + const { + endTime, starterGroupId, starterAddress, questionId, @@ -72,7 +85,7 @@ contract('Ballot', ([from, secondary]) => { result } = await ballot.getVoting(0); - assert.strictEqual(starterGroupId.toNumber(), 1); + assert.strictEqual(starterGroupId.toNumber(), 0); assert.strictEqual(starterAddress, secondary); assert.strictEqual(questionId.toNumber(), 0); assert.strictEqual(status.toNumber(), 1); @@ -107,47 +120,108 @@ contract('Ballot', ([from, secondary]) => { it('should successfully set Positive vote', async () => { await ballot.startVoting(primaryInfo); - const tx = await ballot.setVote(from, secondary, 1, 200) + const tx = await ballot.setVote(from, secondary, 1, 200); const {args : { user, group, descision }} = tx.logs.find(element => element.event.match('UserVote')); assert.strictEqual(user, secondary); - assert.strictEqual(group, from) + assert.strictEqual(group, from); assert.strictEqual(descision.toNumber(), 1) }) it('should successfully set Negative vote', async () => { await ballot.startVoting(primaryInfo); - const tx = await ballot.setVote(from, secondary, 0, 200) + const tx = await ballot.setVote(from, secondary, 2, 200); const {args : { user, group, descision }} = tx.logs.find(element => element.event.match('UserVote')); assert.strictEqual(user, secondary); - assert.strictEqual(group, from) - assert.strictEqual(descision.toNumber(), 0) + assert.strictEqual(group, from); + assert.strictEqual(descision.toNumber(), 2) }) it('should successfully remove vote', async () => { - + await ballot.startVoting(primaryInfo); + + const tx = await ballot.setVote(from, secondary, 0, 0); + const {args : { + user, group, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, secondary); + assert.strictEqual(group, from); + assert.strictEqual(descision.toNumber(), 0) }) - it('should fail on remove vote of user, which not vote', async () => { - + it('should fail on set new vote of user, which already voted', async () => { + let error = false; + await ballot.startVoting(primaryInfo); + await ballot.setVote(from, secondary, 1, 1000); + try { + error = true; + await ballot.setVote(from, secondary, 2, 0); + } catch ({message}) { + assert.strictEqual(message, getErrorMessage('User already vote')); + } + assert.strictEqual(error, true) }) }) describe('closeVoting()', () => { it('should successfully close voting', async () => { - + await ballot.startVoting(primaryInfo); + increaseTime(web3, 300000); + const tx = await ballot.closeVoting(); + const {args : {votingId, descision}} = tx.logs.find(element => element.event.match('VotingEnded')); + assert.strictEqual(votingId.toNumber(), 0); + assert.strictEqual(descision.toNumber(), 0); }) it('should fail on close voting, when time is not over', async () => { - + let error = false; + await ballot.startVoting(primaryInfo); + try { + await ballot.closeVoting(); + } catch ({message}) { + error = true; + assert.strictEqual(message, getErrorMessage('Time is not over yet')); + } + assert.strictEqual(error, true) }); }) - describe('events', () => {}) + describe('events', () => { + it('should fire VotingStarted event', async () => { + const tx = await ballot.startVoting(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 VotingEnded event', async () => { + await ballot.startVoting(primaryInfo); + increaseTime(web3, 300000); + const tx = await ballot.closeVoting(); + const {args : {votingId, descision}} = tx.logs.find(element => element.event.match('VotingEnded')); + assert.strictEqual(votingId.toNumber(), 0); + assert.strictEqual(descision.toNumber(), 0); + }); + + it('should fire UserVote event', async () => { + await ballot.startVoting(primaryInfo); + + const tx = await ballot.setVote(from, secondary, 1, 200); + const {args : { + user, group, descision + }} = tx.logs.find(element => element.event.match('UserVote')); + + assert.strictEqual(user, secondary); + assert.strictEqual(group, from); + assert.strictEqual(descision.toNumber(), 1) + }); + }) }); \ No newline at end of file From 4310b4b473a2bd589cd0bc4ba373d1f252a931fb Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Mon, 17 Feb 2020 18:42:47 +0700 Subject: [PATCH 09/24] Some fixes and additions --- contracts/Token/CustomToken.sol | 2 - contracts/ZeroOne/Ballots/Ballots.sol | 21 ++ contracts/ZeroOne/UserGroups/UserGroups.sol | 4 +- .../ZeroOne/UserGroups/lib/UserGroup.sol | 2 +- contracts/__mocks__/ERC20Mock.sol | 2 +- contracts/__mocks__/ZeroOneMock.sol | 9 + contracts/__vendor__/Context.sol | 26 ++ contracts/__vendor__/ERC20.sol | 281 ++++++++++++++++++ .../UserGroups => __vendor__}/IERC20.sol | 0 contracts/__vendor__/SafeMath.sol | 156 ++++++++++ test/5_ZeroOne.spec.js | 40 ++- test/9_UserGroup.spec.js | 2 +- test/helpers/questions.js | 74 +++++ 13 files changed, 610 insertions(+), 9 deletions(-) create mode 100644 contracts/__vendor__/Context.sol create mode 100644 contracts/__vendor__/ERC20.sol rename contracts/{ZeroOne/UserGroups => __vendor__}/IERC20.sol (100%) create mode 100644 contracts/__vendor__/SafeMath.sol create mode 100644 test/helpers/questions.js diff --git a/contracts/Token/CustomToken.sol b/contracts/Token/CustomToken.sol index f8954e5..baf73d8 100644 --- a/contracts/Token/CustomToken.sol +++ b/contracts/Token/CustomToken.sol @@ -266,8 +266,6 @@ contract CustomToken is Ownable { } // TODO:implements this after making Ballot - // add check for address (address is project ?) - // Make transfer to voting after Ballot will be maked // Make "returning" tokens from project } diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 2272dd2..61d8a3b 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -73,6 +73,11 @@ contract Ballots is QuestionsWithGroups, UserGroups { } constructor() public {} + /** + * @dev returns the confirmation that this is a project + */ + function isProject() public pure returns (bool) { return true; } + /** * @dev creates new Ballot in list */ @@ -152,6 +157,22 @@ contract Ballots is QuestionsWithGroups, UserGroups { return true; } + /** + * @dev returns confirming that this user is voted + * @return confirm + */ + function isUserVoted ( + address _group, + address _user + ) + public + view + returns(bool confirm) + { + uint votingId = ballots.list.length - 1; + confirm = ballots.list[votingId].votes[_group][_user] != BallotType.BallotResult.NOT_ACCEPTED; + } + /** * @dev closes last voting in list * @return descision diff --git a/contracts/ZeroOne/UserGroups/UserGroups.sol b/contracts/ZeroOne/UserGroups/UserGroups.sol index ddedf2e..511b8d9 100644 --- a/contracts/ZeroOne/UserGroups/UserGroups.sol +++ b/contracts/ZeroOne/UserGroups/UserGroups.sol @@ -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/__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 index 4ad14ea..bf022d5 100644 --- a/contracts/__mocks__/ZeroOneMock.sol +++ b/contracts/__mocks__/ZeroOneMock.sol @@ -9,6 +9,15 @@ import "../ZeroOne/ZeroOne.sol"; * @dev wrapper to test some ZeroOne methods */ contract ZeroOneMock is ZeroOne { + + constructor(address _owners) public { + UserGroup.Group memory _group = UserGroup.Group({ + name: "Owners", + groupAddress: _owners, + groupType: UserGroup.Type.ERC20 + }); + addUserGroup(_group); + } /** * @notice wrapper for internal makeCall method * @param _target contract address to make call to 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/test/5_ZeroOne.spec.js b/test/5_ZeroOne.spec.js index 6032af0..b32410f 100644 --- a/test/5_ZeroOne.spec.js +++ b/test/5_ZeroOne.spec.js @@ -1,9 +1,13 @@ const ZeroOne = artifacts.require('./ZeroOneMock.sol'); const Controlled = artifacts.require('./ControlledMock.sol'); +const ERC20 = artifacts.require('./ERC20.sol'); + +const {questions} = require('./helpers/questions'); contract('ZeroOne', (accounts) => { let zeroOne; let controlled; + let token; const deployFrom = accounts[0]; const param1 = 1; const param2 = 'test'; @@ -30,7 +34,8 @@ contract('ZeroOne', (accounts) => { }, {}); beforeEach(async () => { - zeroOne = await ZeroOne.new({ from: deployFrom }); + token = await ERC20.new('test', 'tst', 1000); + zeroOne = await ZeroOne.new(token.address, { from: deployFrom }); controlled = await Controlled.new(zeroOne.address, { from: deployFrom }); }); @@ -62,6 +67,39 @@ contract('ZeroOne', (accounts) => { }); }); + describe('addQuestion()', () => { + it('should add system questions', async () => { + for (let i = 0; i < questions.length; i++) { + 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 make all voting process', async () => { + for (let i = 0; i < questions.length; i++) { + questions[i].target = zeroOne.address; + questions[i].active = true; + await zeroOne.addQuestion(questions[i]); + } + const data = web3.eth.abi.encodeParameters(['string'], ["test"]) + const votingData = { + questionId: 2, + starterAddress: deployFrom, + starterGroupId: 0, + endTime: 0, + data, + } + await zeroOne.startVoting(votingData); + const voting = await zeroOne.getVoting(0); + + }); + }); + describe('events', () => { it('should fire Call event after successful call', async () => { const { selector } = controlledMethods.testSuccess; diff --git a/test/9_UserGroup.spec.js b/test/9_UserGroup.spec.js index c9e7b0d..92cb8c1 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..b228c4d --- /dev/null +++ b/test/helpers/questions.js @@ -0,0 +1,74 @@ +module.exports = { + questions : [{ + groupId: 0, + name: "Добавить Вопрос", + description: "Добавление нового вопроса", + timeLimit: 10 * 3600, + methodSelector: "0x686c52c4", + paramNames: [ + "GroupId", + "Name", + "Caption", + "Time", + "MethodSelector", + "Formula", + "paramNames", + "paramTypes" + ], + paramTypes: [ + "uint", + "string", + "string", + "uint", + "bytes4", + "string", + "string[]", + "string[]" + ], + }, + { + groupId: 0, + name: "Подключить группу пользователей", + description: "Подключить новую группу пользователей для участия в голосованиях", + timeLimit: 10 * 3600, + methodSelector: "0x952b627c", + paramNames: [ + "Name", + "Address", + "Type", + ], + paramTypes: [ + "string", + "address", + "string" + ], + }, + { + groupId: 0, + name: "Добавить группу вопросов", + description: "Добавить новую группу вопросов", + timeLimit: 10 * 3600, + methodSelector: "0x9e26e4a0", + paramNames: [ + "Name" + ], + paramTypes: [ + "string" + ], + }, + { + groupId: 0, + name: "Установить администратора группы", + description: "Установка администратора в группе кастомных токенов", + timeLimit: 10 * 3600, + methodSelector: "0x3cc0a953", + paramNames: [ + "Group Address", + "New Admin Address" + ], + paramTypes: [ + "address", + "address" + ], + }] +} \ No newline at end of file From 4535a06088f767e8ffa03e059cf883429a1c2a65 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 18 Feb 2020 18:19:00 +0700 Subject: [PATCH 10/24] some fixes and tests (need to fix) --- contracts/ZeroOne/Ballots/Ballots.sol | 61 +++++++++---- contracts/ZeroOne/Ballots/lib/Ballot.sol | 3 +- contracts/ZeroOne/Ballots/lib/BallotList.sol | 1 + contracts/ZeroOne/ZeroOne.sol | 32 +++++++ test/11_ZeroOne.spec.js | 96 ++++++++++++++++++++ test/5_ZeroOne.spec.js | 39 +------- test/helpers/questions.js | 6 +- 7 files changed, 177 insertions(+), 61 deletions(-) create mode 100644 test/11_ZeroOne.spec.js diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 61d8a3b..4f791cb 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -5,6 +5,7 @@ import "./lib/Ballot.sol"; import "./lib/BallotList.sol"; import "../Questions/QuestionsWithGroups.sol"; import "../UserGroups/UserGroups.sol"; +import "../../__vendor__/IERC20.sol"; /** * @title Ballots @@ -133,11 +134,13 @@ contract Ballots is QuestionsWithGroups, UserGroups { return ballots.list.length; } + /** + * @dev set {_descision} of {_user} from {_group} with {_voteWeight} + */ function setVote( address _group, address _user, - BallotType.BallotResult _descision, - uint256 _voteWeight + BallotType.BallotResult _descision ) public userNotVoted( @@ -152,11 +155,47 @@ contract Ballots is QuestionsWithGroups, UserGroups { ballots.list[votingId].status != BallotType.BallotStatus.CLOSED, "Voting is closed, you must start new voting before vote" ); - ballots.list[votingId].setVote(_group, _user, _descision, _voteWeight); + IERC20 group = IERC20(_group); + uint256 voteWeight = group.balanceOf(_user); + ballots.list[votingId].setVote(_group, _user, _descision, voteWeight); emit UserVote(_group, _user, _descision); return true; } + + /** + * @dev get userVote + */ + function getUserVote( + uint votingId, + address _group, + address _user + ) + public + view + returns (BallotType.BallotResult descision) + { + + return ballots.list[votingId].votes[_group][_user]; + } + + /** + * @dev get user vote 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 is voted * @return confirm @@ -172,20 +211,4 @@ contract Ballots is QuestionsWithGroups, UserGroups { uint votingId = ballots.list.length - 1; confirm = ballots.list[votingId].votes[_group][_user] != BallotType.BallotResult.NOT_ACCEPTED; } - - /** - * @dev closes last voting in list - * @return descision - */ - function closeVoting() - public - returns ( - BallotType.BallotResult descision - ) - { - uint votingId = ballots.list.length - 1; - require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); - descision = ballots.list[votingId].closeVoting(); - emit VotingEnded(votingId, descision); - } } diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index 6ad76d9..6f608e9 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -11,6 +11,7 @@ library BallotType { enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } struct Ballot { + uint startBlock; uint startTime; uint endTime; uint starterGroupId; @@ -143,7 +144,7 @@ library BallotType { pure returns (BallotResult) { - return BallotResult.NOT_ACCEPTED; + return BallotResult.POSITIVE; } /** diff --git a/contracts/ZeroOne/Ballots/lib/BallotList.sol b/contracts/ZeroOne/Ballots/lib/BallotList.sol index 544df93..4cfce82 100644 --- a/contracts/ZeroOne/Ballots/lib/BallotList.sol +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -34,6 +34,7 @@ library BallotList { returns (uint id) { BallotType.Ballot memory _voting = BallotType.Ballot({ + startBlock: block.number, startTime: block.timestamp, endTime: _votingPrimary.endTime, starterGroupId: _votingPrimary.starterGroupId, diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 61766ae..99fbd09 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -26,6 +26,38 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { _; } + /** + * @dev closes last voting in list + * @return descision + */ + function closeVoting() + public + returns ( + BallotType.BallotResult descision + ) + { + uint votingId = ballots.list.length - 1; + require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); + descision = ballots.list[votingId].closeVoting(); + uint questionId = ballots.list[votingId].questionId; + + MetaData memory meta = MetaData({ + ballotId: votingId, + questionId: questionId, + startBlock: ballots.list[votingId].startBlock, + endBlock: block.number, + result: Result.ACCEPTED + }); + + makeCall( + questions.list[questionId].target, + questions.list[questionId].methodSelector, + ballots.list[votingId].votingData, + meta + ); + emit VotingEnded(votingId, descision); + } + /** * @notice makes call to contract external method * with modified data (meta added) diff --git a/test/11_ZeroOne.spec.js b/test/11_ZeroOne.spec.js new file mode 100644 index 0000000..7da7510 --- /dev/null +++ b/test/11_ZeroOne.spec.js @@ -0,0 +1,96 @@ +const ZeroOne = artifacts.require('./ZeroOne.sol'); +const Controlled = artifacts.require('./ControlledMock.sol'); +const ERC20 = artifacts.require('./ERC20.sol'); + +const {questions} = require('./helpers/questions'); +const increase = require('./helpers/increase-time'); + +contract('ZeroOne', ([from, secondary]) => { + let zeroOne; + let token; + + beforeEach(async () => { + token = await ERC20.new('test', 'tst', 1000); + zeroOne = await ZeroOne.new(token.address, { 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].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].target = zeroOne.address; + questions[i].active = true; + await zeroOne.addQuestion(questions[i]); + } + const data = web3.eth.abi.encodeParameters(['tuple(uint256,uint256,uint256,uint256,uint256)', '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(token.address, from, 1); + increase(web3, 320000); + const tx = await zeroOne.closeVoting(); + const log = tx.logs.find(element => element.event.match('Call')); + const group = await zeroOne.getQuestionGroup(1); + // console.log(group); + // assert.strictEqual(group.name, 'test'); + + }); + it('should add question', async () => { + for (let i = 0; i < questions.length; i++) { + 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(bool, string, string, uint256, uint256, string[], string[], address, bytes4)'], + [ + [0, 0, 0, 0, 0], + [true, "test question", "some text", 0, 360000, ['test'], ['string'], secondary, "0x00000000"] + ] + ) + const votingData = { + questionId: 0, + starterAddress: from, + starterGroupId: 0, + endTime: 0, + data, + } + let amount = await zeroOne.getQuestionsAmount(); + try { + console.log(`amount = ${amount.toNumber()}`) + await zeroOne.startVoting(votingData); + const userBalance = await token.balanceOf(from); + await token.approve(zeroOne.address, userBalance); + await zeroOne.setVote(token.address, from, 1); + increase(web3, 320000); + const tx = await zeroOne.closeVoting(); + const {args:{data}} = tx.logs.find(element => element.event.match('Call')); + console.log(web3.eth.abi.decodeParameters(['tuple(bool, string, string, uint256, uint256, string[], string[], address, bytes4)'], data)); + amount = await zeroOne.getQuestionsAmount(); + //assert.strictEqual(amount.toNumber(), 5); + } catch({message}) { + console.log(message); + } + }); + }); +}); \ No newline at end of file diff --git a/test/5_ZeroOne.spec.js b/test/5_ZeroOne.spec.js index b32410f..c083c3c 100644 --- a/test/5_ZeroOne.spec.js +++ b/test/5_ZeroOne.spec.js @@ -1,13 +1,9 @@ const ZeroOne = artifacts.require('./ZeroOneMock.sol'); const Controlled = artifacts.require('./ControlledMock.sol'); -const ERC20 = artifacts.require('./ERC20.sol'); - -const {questions} = require('./helpers/questions'); contract('ZeroOne', (accounts) => { let zeroOne; let controlled; - let token; const deployFrom = accounts[0]; const param1 = 1; const param2 = 'test'; @@ -33,8 +29,8 @@ contract('ZeroOne', (accounts) => { return prev; }, {}); + beforeEach(async () => { - token = await ERC20.new('test', 'tst', 1000); zeroOne = await ZeroOne.new(token.address, { from: deployFrom }); controlled = await Controlled.new(zeroOne.address, { from: deployFrom }); }); @@ -67,39 +63,6 @@ contract('ZeroOne', (accounts) => { }); }); - describe('addQuestion()', () => { - it('should add system questions', async () => { - for (let i = 0; i < questions.length; i++) { - 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 make all voting process', async () => { - for (let i = 0; i < questions.length; i++) { - questions[i].target = zeroOne.address; - questions[i].active = true; - await zeroOne.addQuestion(questions[i]); - } - const data = web3.eth.abi.encodeParameters(['string'], ["test"]) - const votingData = { - questionId: 2, - starterAddress: deployFrom, - starterGroupId: 0, - endTime: 0, - data, - } - await zeroOne.startVoting(votingData); - const voting = await zeroOne.getVoting(0); - - }); - }); - describe('events', () => { it('should fire Call event after successful call', async () => { const { selector } = controlledMethods.testSuccess; diff --git a/test/helpers/questions.js b/test/helpers/questions.js index b228c4d..257bee8 100644 --- a/test/helpers/questions.js +++ b/test/helpers/questions.js @@ -4,7 +4,7 @@ module.exports = { name: "Добавить Вопрос", description: "Добавление нового вопроса", timeLimit: 10 * 3600, - methodSelector: "0x686c52c4", + methodSelector: "0xea5d11ff", paramNames: [ "GroupId", "Name", @@ -31,7 +31,7 @@ module.exports = { name: "Подключить группу пользователей", description: "Подключить новую группу пользователей для участия в голосованиях", timeLimit: 10 * 3600, - methodSelector: "0x952b627c", + methodSelector: "0x70b0e2c8", paramNames: [ "Name", "Address", @@ -61,7 +61,7 @@ module.exports = { name: "Установить администратора группы", description: "Установка администратора в группе кастомных токенов", timeLimit: 10 * 3600, - methodSelector: "0x3cc0a953", + methodSelector: "0x704b6c02", paramNames: [ "Group Address", "New Admin Address" From 1344f40b01e2d45130823501866722e05439b1e9 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Wed, 19 Feb 2020 17:43:17 +0700 Subject: [PATCH 11/24] some fixes and improvements --- contracts/Token/CustomToken.sol | 109 ++++++++++-------- contracts/ZeroOne/Ballots/Ballots.sol | 4 +- contracts/ZeroOne/Questions/Questions.sol | 2 +- .../ZeroOne/Questions/QuestionsWithGroups.sol | 1 + contracts/ZeroOne/ZeroOne.sol | 79 +++++++++++++ contracts/lib/Ownable.sol | 20 ---- test/11_ZeroOne.spec.js | 84 ++++++++++---- test/helpers/questions.js | 6 +- 8 files changed, 210 insertions(+), 95 deletions(-) diff --git a/contracts/Token/CustomToken.sol b/contracts/Token/CustomToken.sol index baf73d8..b1be485 100644 --- a/contracts/Token/CustomToken.sol +++ b/contracts/Token/CustomToken.sol @@ -1,16 +1,20 @@ pragma solidity 0.6.1; +pragma experimental ABIEncoderV2; import "../lib/Ownable.sol"; +import "../ZeroOne/IZeroOne.sol"; /** @title CustomToken @dev Contract implements custom tokens for ZeroOne */ -contract CustomToken is Ownable { +contract CustomToken is IZeroOne, Ownable { mapping (address => uint256) private _balances; mapping (address => mapping (address => bool)) private _tokenLocks; + mapping (address => bool) private _isProjects; + uint256 private _totalSupply; string private _name; @@ -24,13 +28,15 @@ contract CustomToken is Ownable { event Transfer(address from, address to, uint256 count); - event TokenLocked (address project, address user); + event TokenLocked(address project, address user); + + event ZeroOneCall(MetaData meta); /** - @dev Contrsuctor of tokens - @param name name of token - @param symbol short name of token - @param totalSupply count of tokens + * @dev Contrsuctor of tokens + * @param name name of token + * @param symbol short name of token + * @param totalSupply count of tokens */ constructor( string memory name, @@ -44,22 +50,27 @@ contract CustomToken is Ownable { _holders.push(msg.sender); } + modifier onlyZeroOne(address _caller) { + require(_isProjects[_caller] = true, "Address not contains in projects"); + _; + } + /** - @dev returns count of tokens - @return totalSupply + * @dev returns count of tokens + * @return totalSupply */ function totalSupply() public view returns (uint256) { return _totalSupply; } /** - @dev returns count of tokens - @return name of token + * @dev returns count of tokens + * @return name of token */ function name() public view returns(string memory) { return _name; } /** - @dev returns count of tokens - @return symbol of token + * @dev returns count of tokens + * @return symbol of token */ function symbol() public view returns(string memory) { return _symbol; } @@ -75,25 +86,8 @@ contract CustomToken is Ownable { } /** - @dev Sets new administrator in Ownable - @param _newAdmin address of new administrator - @return is admin successfully changed - */ - function setAdmin( - address _newAdmin - ) - public - returns (bool) - { - require(_newAdmin != address(0), "Address must be non-empty"); - require(!isProjectAddress(_newAdmin), "Address used as project"); - transferOwnership(_newAdmin); - return owner() == _newAdmin; - } - - /** - @dev add ballot project to list - @param _project address of ballot project + * @dev add ballot project to list + * @param _project address of ballot project */ function addToProjects( address _project @@ -104,11 +98,12 @@ contract CustomToken is Ownable { require(_project != address(0), "Address must be non-empty"); require(!isProjectAddress(_project), "Address already in list"); _projects.push(_project); + _isProjects[_project] = true; return true; } /** - @dev Transfers tokens from _sender to _recipient + * @dev Transfers tokens from _sender to _recipient */ function _transfer( address _sender, @@ -129,8 +124,8 @@ contract CustomToken is Ownable { } /** - @dev Set lock status of user tokens in project - @return status + * @dev Set lock status of user tokens in project + * @return status */ function _lockTokens( address _project, @@ -146,8 +141,8 @@ contract CustomToken is Ownable { } /** - @dev Set unlock status of user tokens in project - @return status + * @dev Set unlock status of user tokens in project + * @return status */ function _unlockTokens( address _project, @@ -161,8 +156,8 @@ contract CustomToken is Ownable { } /** - @dev getter for status of user tokens lock in project - @return isLocked + * @dev getter for status of user tokens lock in project + * @return isLocked */ function isTokenLocked( address _project, @@ -177,9 +172,12 @@ contract CustomToken is Ownable { } /** - @dev Transfer tokens from User to admin; + * @dev Transfer tokens from {_from} to {_to}; + * @param _from adress of user, which tokens will be sended + * @param _to address of user, which will be token recipient + * @param _count count of {_to} user tokens */ - function transferFrom( + function transferBeetweenUsers( address _from, address _to, uint256 _count @@ -197,8 +195,8 @@ contract CustomToken is Ownable { } /** - @dev getting projects list - @return project list + * @dev getting projects list + * @return project list */ function getProjects() public @@ -209,8 +207,8 @@ contract CustomToken is Ownable { } /** - @dev check if address using as project - @return isProject + * @dev check if address using as project + * @return isProject */ function isProjectAddress( address _address @@ -238,21 +236,22 @@ contract CustomToken is Ownable { @dev lock tokens of msg.sender and sending vote to ballot @param _project address of ballot project */ - function sendVote( + function transferFrom( + address _sender, address _project ) public + onlyZeroOne(msg.sender) { require(_project != address(0), "Address must be non-empty"); require(isProjectAddress(_project), "Address is not in project list"); - _lockTokens(_project, msg.sender); - // TODO: implement sending descision in project + _lockTokens(_project, _sender); } /** - @dev unlocks the tokens of msg.sender - @param _project address of project - @return isLocked + * @dev unlocks the tokens of msg.sender + * @param _project address of project + * @return isLocked */ function returnFromVoting( address _project @@ -264,6 +263,16 @@ contract CustomToken is Ownable { _unlockTokens(_project, msg.sender); return !isTokenLocked(_project, msg.sender); } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(MetaData memory _meta, address newOwner) public onlyZeroOne(msg.sender) { + _transferOwnership(newOwner); + emit ZeroOneCall(_meta); + } + // TODO:implements this after making Ballot // add check for address (address is project ?) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 4f791cb..891b3bf 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -154,9 +154,11 @@ contract Ballots is QuestionsWithGroups, UserGroups { require( ballots.list[votingId].status != BallotType.BallotStatus.CLOSED, "Voting is closed, you must start new voting before vote" - ); + ); + IERC20 group = IERC20(_group); uint256 voteWeight = group.balanceOf(_user); + group.transferFrom(_user, address(this), voteWeight); ballots.list[votingId].setVote(_group, _user, _descision, voteWeight); emit UserVote(_group, _user, _descision); return true; 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/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 99fbd09..0173371 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -15,6 +15,10 @@ import "./Ballots/Ballots.sol"; contract ZeroOne is Notifier, IZeroOne, Ballots { using Meta for bytes; + event ZeroOneCall( + MetaData _meta + ); + /** * @notice for modified functions allows only self external call */ @@ -82,4 +86,79 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { _data.addMetaData(abi.encode(_metaData)) ); } + + + /** + * @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) + { + QuestionsWithGroups.addQuestionGroup(_questionGroup); + emit ZeroOneCall(_metaData); + return _metaData.ballotId; + } + + /** + * @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) + { + Questions.addQuestion(_question); + emit ZeroOneCall(_metaData); + return _metaData.ballotId; + } + + /** + * @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 + onlySelf() + returns (uint ballotId) + { + 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( + MetaData memory _metaData, + address _group, + address _user + ) + public + onlySelf() + returns(uint ballotId) + { + _group.call(abi.encodeWithSignature("setAdmin(address)", _user)); + emit ZeroOneCall(_metaData); + return _metaData.ballotId; + } } diff --git a/contracts/lib/Ownable.sol b/contracts/lib/Ownable.sol index 604c809..d7bce8d 100644 --- a/contracts/lib/Ownable.sol +++ b/contracts/lib/Ownable.sol @@ -46,26 +46,6 @@ contract Ownable is Context { return _msgSender() == _owner; } - /** - * @dev Leaves the contract without owner. It will not be possible to call - * `onlyOwner` functions anymore. Can only be called by the current owner. - * - * NOTE: Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. - */ - function renounceOwnership() public onlyOwner { - emit OwnershipTransferred(_owner, address(0)); - _owner = address(0); - } - - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) public onlyOwner { - _transferOwnership(newOwner); - } - /** * @dev Transfers ownership of the contract to a new account (`newOwner`). */ diff --git a/test/11_ZeroOne.spec.js b/test/11_ZeroOne.spec.js index 7da7510..a1ec06d 100644 --- a/test/11_ZeroOne.spec.js +++ b/test/11_ZeroOne.spec.js @@ -9,10 +9,57 @@ contract('ZeroOne', ([from, secondary]) => { let zeroOne; let token; + let func = { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum IZeroOne.Result", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_meta", + "type": "tuple" + }, + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }; + beforeEach(async () => { token = await ERC20.new('test', 'tst', 1000); zeroOne = await ZeroOne.new(token.address, { from }); controlled = await Controlled.new(zeroOne.address, { from }); + console.log(web3.eth.abi.encodeFunctionSignature(func)); }); describe('addQuestion()', () => { @@ -34,7 +81,7 @@ contract('ZeroOne', ([from, secondary]) => { questions[i].active = true; await zeroOne.addQuestion(questions[i]); } - const data = web3.eth.abi.encodeParameters(['tuple(uint256,uint256,uint256,uint256,uint256)', 'string'], [[0, 0, 0, 0, 0], "test"]) + 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, @@ -47,11 +94,9 @@ contract('ZeroOne', ([from, secondary]) => { await token.approve(zeroOne.address, userBalance); await zeroOne.setVote(token.address, from, 1); increase(web3, 320000); - const tx = await zeroOne.closeVoting(); - const log = tx.logs.find(element => element.event.match('Call')); + await zeroOne.closeVoting(); const group = await zeroOne.getQuestionGroup(1); - // console.log(group); - // assert.strictEqual(group.name, 'test'); + assert.strictEqual(group.name, 'test'); }); it('should add question', async () => { @@ -76,21 +121,20 @@ contract('ZeroOne', ([from, secondary]) => { data, } let amount = await zeroOne.getQuestionsAmount(); - try { - console.log(`amount = ${amount.toNumber()}`) - await zeroOne.startVoting(votingData); - const userBalance = await token.balanceOf(from); - await token.approve(zeroOne.address, userBalance); - await zeroOne.setVote(token.address, from, 1); - increase(web3, 320000); - const tx = await zeroOne.closeVoting(); - const {args:{data}} = tx.logs.find(element => element.event.match('Call')); - console.log(web3.eth.abi.decodeParameters(['tuple(bool, string, string, uint256, uint256, string[], string[], address, bytes4)'], data)); - amount = await zeroOne.getQuestionsAmount(); - //assert.strictEqual(amount.toNumber(), 5); - } catch({message}) { - console.log(message); - } + console.log(`amount = ${amount.toNumber()}`) + + await zeroOne.startVoting(votingData); + const userBalance = await token.balanceOf(from); + + await token.approve(zeroOne.address, userBalance); + await zeroOne.setVote(token.address, from, 1); + + const balance = await token.balanceOf(zeroOne.address); + increase(web3, 320000); + + await zeroOne.closeVoting(); + amount = await zeroOne.getQuestionsAmount(); + assert.strictEqual(amount.toNumber(), 5); }); }); }); \ No newline at end of file diff --git a/test/helpers/questions.js b/test/helpers/questions.js index 257bee8..176b09c 100644 --- a/test/helpers/questions.js +++ b/test/helpers/questions.js @@ -4,7 +4,7 @@ module.exports = { name: "Добавить Вопрос", description: "Добавление нового вопроса", timeLimit: 10 * 3600, - methodSelector: "0xea5d11ff", + methodSelector: "0x9c88d333", paramNames: [ "GroupId", "Name", @@ -48,7 +48,7 @@ module.exports = { name: "Добавить группу вопросов", description: "Добавить новую группу вопросов", timeLimit: 10 * 3600, - methodSelector: "0x9e26e4a0", + methodSelector: "0xb9253b2b", paramNames: [ "Name" ], @@ -61,7 +61,7 @@ module.exports = { name: "Установить администратора группы", description: "Установка администратора в группе кастомных токенов", timeLimit: 10 * 3600, - methodSelector: "0x704b6c02", + methodSelector: "0x9c88d333", paramNames: [ "Group Address", "New Admin Address" From b34994e357adb0830b06ae18329d921a1c57a295 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 25 Feb 2020 13:02:57 +0700 Subject: [PATCH 12/24] some docs fixes --- contracts/ZeroOne/Ballots/Ballots.sol | 46 ++++++++++++++++++------ contracts/ZeroOne/Ballots/lib/Ballot.sol | 5 ++- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 891b3bf..4d31ea5 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -80,7 +80,9 @@ contract Ballots is QuestionsWithGroups, UserGroups { function isProject() public pure returns (bool) { return true; } /** - * @dev creates new Ballot in list + * @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 @@ -101,6 +103,15 @@ contract Ballots is QuestionsWithGroups, UserGroups { /** * @dev getting the voting by id + * @param _id id of voting + * @return startTime + * @return endTime + * @return starterGroupId + * @return starterAddress + * @return questionId + * @return status + * @return result + * @return votingData */ function getVoting( uint _id @@ -135,7 +146,12 @@ contract Ballots is QuestionsWithGroups, UserGroups { } /** - * @dev set {_descision} of {_user} from {_group} with {_voteWeight} + * @dev set {_descision} of {_user} from {_group} + * method fetching balance of {_user} in {_group} and writing vote in voting struct + * @param _group address of group + * @param _user address of user + * @param _descision descision of {_user} + * @return success */ function setVote( address _group, @@ -166,26 +182,35 @@ contract Ballots is QuestionsWithGroups, UserGroups { /** - * @dev get userVote + * @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, + uint _votingId, address _group, address _user ) public view + ballotExist(_votingId) returns (BallotType.BallotResult descision) { - return ballots.list[votingId].votes[_group][_user]; + return ballots.list[_votingId].votes[_group][_user]; } /** - * @dev get user vote weight + * @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 weigth */ function getUserVoteWeight( - uint votingId, + uint _votingId, address _group, address _user ) @@ -193,13 +218,14 @@ contract Ballots is QuestionsWithGroups, UserGroups { view returns (uint256 weight) { - - return ballots.list[votingId].votesWeight[_group][_user]; + return ballots.list[_votingId].votesWeight[_group][_user]; } /** - * @dev returns confirming that this user is voted + * @dev returns confirming that this {_user} from {_group} is voted + * @param _group address of group + * @param _user address of user * @return confirm */ function isUserVoted ( diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index 6f608e9..a6221d6 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -16,12 +16,13 @@ library BallotType { uint endTime; uint starterGroupId; address starterAddress; - uint questionId; + 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; } /** @@ -81,6 +82,8 @@ library BallotType { { _self.votes[_group][_user] = _descision; _self.votesWeight[_group][_user] = _voteWeight; + _self.descisionWeights[_group][uint(_descision)] = _self.descisionWeights[_group][uint(_descision)] + _voteWeight; + return true; } From c67ed7dfdbabc7ff8ba59be37955a73afe15b2dc Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Wed, 26 Feb 2020 18:06:01 +0700 Subject: [PATCH 13/24] started using zeroone vm's --- contracts/Token/CustomToken.sol | 6 +- contracts/ZeroOne/Ballots/Ballots.sol | 49 +++++++++---- contracts/ZeroOne/Ballots/IBallots.sol | 17 +++++ contracts/ZeroOne/Ballots/lib/Ballot.sol | 60 ++++++++++++---- contracts/ZeroOne/Ballots/lib/BallotList.sol | 26 +++---- contracts/ZeroOne/IZeroOne.sol | 12 ---- contracts/ZeroOne/Questions/lib/Question.sol | 2 + contracts/ZeroOne/ZeroOne.sol | 75 ++------------------ package-lock.json | 25 +++++++ package.json | 4 +- test/11_ZeroOne.spec.js | 54 ++------------ test/3_Questions.spec.js | 2 + test/helpers/questions.js | 4 ++ 13 files changed, 158 insertions(+), 178 deletions(-) create mode 100644 contracts/ZeroOne/Ballots/IBallots.sol diff --git a/contracts/Token/CustomToken.sol b/contracts/Token/CustomToken.sol index ec0457e..c2e44a6 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,7 +191,7 @@ contract CustomToken is Ownable { { for (uint i = 1; i < projects.length - 1; i++) { if (isTokenLocked(projects[i], _user)) { - IZeroOne project = IZeroOne(projects[i]); + IBallots project = IBallots(projects[i]); project.updateUserVote(address(this), _user, balanceOf(_user)); } } @@ -357,7 +357,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 index 4d31ea5..f56e684 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -5,7 +5,9 @@ import "./lib/Ballot.sol"; import "./lib/BallotList.sol"; import "../Questions/QuestionsWithGroups.sol"; import "../UserGroups/UserGroups.sol"; -import "../../__vendor__/IERC20.sol"; +import "./IBallots.sol"; +import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; + /** * @title Ballots @@ -14,6 +16,7 @@ import "../../__vendor__/IERC20.sol"; contract Ballots is QuestionsWithGroups, UserGroups { using BallotList for BallotList.List; using BallotType for BallotType.Ballot; + using ZeroOneVM for ZeroOneVM.Ballot; BallotList.List ballots; @@ -24,6 +27,8 @@ contract Ballots is QuestionsWithGroups, UserGroups { event UserVote(address group, address user, BallotType.BallotResult descision); + event UpdatedUserVote(address group, address user); + /** * @notice reverts on non-existing ballot id * @param _id ballot id @@ -98,6 +103,10 @@ contract Ballots is QuestionsWithGroups, UserGroups { { _votingPrimary.endTime = block.timestamp + questions.list[_votingPrimary.questionId].timeLimit; id = ballots.add(_votingPrimary); + ballots.descriptors[id].executeDescriptors( + questions.list[_votingPrimary.questionId].formula, + groups.list[0].groupAddress + ); emit VotingStarted(id, _votingPrimary.questionId); } @@ -110,7 +119,6 @@ contract Ballots is QuestionsWithGroups, UserGroups { * @return starterAddress * @return questionId * @return status - * @return result * @return votingData */ function getVoting( @@ -126,7 +134,6 @@ contract Ballots is QuestionsWithGroups, UserGroups { address starterAddress, uint questionId, BallotType.BallotStatus status, - BallotType.BallotResult result, bytes memory votingData ) { @@ -166,20 +173,33 @@ contract Ballots is QuestionsWithGroups, UserGroups { returns (bool success) { uint votingId = ballots.list.length - 1; - require(ballots.list[votingId].endTime > block.timestamp, "Votes recieving are closed"); - require( - ballots.list[votingId].status != BallotType.BallotStatus.CLOSED, - "Voting is closed, you must start new voting before vote" - ); - IERC20 group = IERC20(_group); - uint256 voteWeight = group.balanceOf(_user); - group.transferFrom(_user, address(this), voteWeight); - ballots.list[votingId].setVote(_group, _user, _descision, voteWeight); + for (uint i = 0; i < 16; i++) { + if (ballots.descriptors[votingId].groups[i].groupAddress == _group) { + + } + } + emit UserVote(_group, _user, _descision); return true; } + /** + * @dev updates vote weight of {_user} from {_group} with {_newVoteWeight} + * calls when admin transfer tokens between users in custom token contract + * @param _group address of group + * @param _user address of user + * @param _newVoteWeight new vote weight of {_user} + * @return status + */ + function updateUserVote(address _group, address _user, uint256 _newVoteWeight) + public + returns(bool status) + { + uint votingId = ballots.list.length - 1; + status = ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); + emit UpdatedUserVote(_group, _user); + } /** * @dev returns descision of {_user} from {_group} in voting with {_votingId} @@ -207,7 +227,7 @@ contract Ballots is QuestionsWithGroups, UserGroups { * @param _votingId id of voting * @param _group address of group * @param _user address of user - * @return weigth + * @return weight */ function getUserVoteWeight( uint _votingId, @@ -221,14 +241,13 @@ contract Ballots is QuestionsWithGroups, UserGroups { 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 isUserVoted ( + function didUserVote ( address _group, address _user ) diff --git a/contracts/ZeroOne/Ballots/IBallots.sol b/contracts/ZeroOne/Ballots/IBallots.sol new file mode 100644 index 0000000..d847203 --- /dev/null +++ b/contracts/ZeroOne/Ballots/IBallots.sol @@ -0,0 +1,17 @@ +pragma solidity 0.6.1; + + +interface IBallots { + + function updateUserVote(address project, 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(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 index a6221d6..cbc6254 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -1,11 +1,16 @@ 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 } enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } @@ -18,7 +23,6 @@ library BallotType { address starterAddress; uint questionId; BallotStatus status; - BallotResult result; bytes votingData; mapping(address => mapping(address => BallotResult)) votes; mapping(address => mapping(address => uint256)) votesWeight; @@ -33,7 +37,6 @@ library BallotType { * @return starterAddress * @return questionId * @return status - * @return result * @return votingData */ function getPrimaryInfo( @@ -48,7 +51,6 @@ library BallotType { address starterAddress, uint questionId, BallotStatus status, - BallotResult result, bytes storage votingData ) { @@ -59,7 +61,6 @@ library BallotType { _self.starterAddress, _self.questionId, _self.status, - _self.result, _self.votingData ); } @@ -74,16 +75,48 @@ library BallotType { Ballot storage _self, address _group, address _user, - BallotResult _descision, - uint256 _voteWeight + BallotResult _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; + _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] = BallotResult.NOT_ACCEPTED; + } + } return true; } @@ -123,19 +156,16 @@ library BallotType { /** * @dev set result and status "Closed" to voting - * @param _result calculated result of voting * @return success */ function setResult( - Ballot storage _self, - BallotResult _result + Ballot storage _self ) internal returns (bool success) { require(setStatus(_self, BallotStatus.CLOSED), "Problem with setting status"); - _self.result = _result; - return _self.result == _result; + return true; } /** @@ -161,7 +191,7 @@ library BallotType { returns (BallotResult result) { BallotResult _result = calculateResult(); - setResult(_self, _result); + setResult(_self); return _result; } diff --git a/contracts/ZeroOne/Ballots/lib/BallotList.sol b/contracts/ZeroOne/Ballots/lib/BallotList.sol index 4cfce82..aead0bc 100644 --- a/contracts/ZeroOne/Ballots/lib/BallotList.sol +++ b/contracts/ZeroOne/Ballots/lib/BallotList.sol @@ -1,6 +1,8 @@ pragma solidity 0.6.1; import "./Ballot.sol"; +import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; + /** * @title BallotList @@ -11,6 +13,7 @@ library BallotList { struct List { BallotType.Ballot[] list; + mapping(uint => ZeroOneVM.Ballot) descriptors; } struct BallotSimple { @@ -33,20 +36,19 @@ library BallotList { internal returns (uint id) { - BallotType.Ballot memory _voting = BallotType.Ballot({ - startBlock: block.number, - startTime: block.timestamp, - endTime: _votingPrimary.endTime, - starterGroupId: _votingPrimary.starterGroupId, - starterAddress: _votingPrimary.starterAddress, - questionId: _votingPrimary.questionId, - status: BallotType.BallotStatus.ACTIVE, - result: BallotType.BallotResult.NOT_ACCEPTED, - votingData: _votingPrimary.data - }); + 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); - return _self.list.length - 1; + id = _self.list.length - 1; } diff --git a/contracts/ZeroOne/IZeroOne.sol b/contracts/ZeroOne/IZeroOne.sol index 7704b90..16499f8 100644 --- a/contracts/ZeroOne/IZeroOne.sol +++ b/contracts/ZeroOne/IZeroOne.sol @@ -19,16 +19,4 @@ interface IZeroOne { uint endBlock; Result 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/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/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 2ebc508..3913c78 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -7,7 +7,6 @@ import "../lib/Meta.sol"; import "./Ballots/Ballots.sol"; - /** * @title ZeroOne * @dev main ZeroOne contract @@ -34,7 +33,7 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { * @dev closes last voting in list * @return descision */ - function closeVoting() + function submitVoting() public returns ( BallotType.BallotResult descision @@ -153,82 +152,16 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { address _group, address _user ) - public + internal onlySelf() returns(uint ballotId) { - _group.call(abi.encodeWithSignature("setAdmin(address)", _user)); + _group.call(abi.encodeWithSignature("transferOwnership(address)", _user)); emit ZeroOneCall(_metaData); return _metaData.ballotId; } -} - 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) - { - return ballot.votesWeight[tokenAddr][user]; - } - - function didUserVote(address tokenAddr, address user) - public - override - returns(bool) - { - return ballot.votes[tokenAddr][user] != BallotResult.NOT_ACCEPTED; - } - - function submitVoting() - public - override - { - require(block.timestamp > ballot.endTime, "Time is not over"); - ballot.status = BallotStatus.CLOSED; - } - - function setGroupAdmin( - address tokenAddr, - address newOwner - ) - public - override - { - tokenAddr.call(abi.encodeWithSignature("transferOwnership(address)", newOwner)); - } - function disableUserGroup(address tokenAddr) public { + function disableUserGroup(address tokenAddr) internal { tokenAddr.call(abi.encodeWithSignature("removeFromProjects(address)", address(this))); } 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/11_ZeroOne.spec.js b/test/11_ZeroOne.spec.js index a1ec06d..b37f2fe 100644 --- a/test/11_ZeroOne.spec.js +++ b/test/11_ZeroOne.spec.js @@ -2,69 +2,25 @@ const ZeroOne = artifacts.require('./ZeroOne.sol'); const Controlled = artifacts.require('./ControlledMock.sol'); const ERC20 = artifacts.require('./ERC20.sol'); -const {questions} = require('./helpers/questions'); const increase = require('./helpers/increase-time'); +const { compile, compileDescriptors } = require('zeroone-translator'); +const { questions } = require('./helpers/questions'); contract('ZeroOne', ([from, secondary]) => { let zeroOne; let token; - let func = { - "inputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "ballotId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "questionId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "startBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "endBlock", - "type": "uint256" - }, - { - "internalType": "enum IZeroOne.Result", - "name": "result", - "type": "uint8" - } - ], - "internalType": "struct IZeroOne.MetaData", - "name": "_meta", - "type": "tuple" - }, - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }; beforeEach(async () => { token = await ERC20.new('test', 'tst', 1000); zeroOne = await ZeroOne.new(token.address, { from }); controlled = await Controlled.new(zeroOne.address, { from }); - console.log(web3.eth.abi.encodeFunctionSignature(func)); }); 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]); @@ -94,7 +50,7 @@ contract('ZeroOne', ([from, secondary]) => { await token.approve(zeroOne.address, userBalance); await zeroOne.setVote(token.address, from, 1); increase(web3, 320000); - await zeroOne.closeVoting(); + await zeroOne.submitVoting(); const group = await zeroOne.getQuestionGroup(1); assert.strictEqual(group.name, 'test'); @@ -132,7 +88,7 @@ contract('ZeroOne', ([from, secondary]) => { const balance = await token.balanceOf(zeroOne.address); increase(web3, 320000); - await zeroOne.closeVoting(); + await zeroOne.submitVoting(); amount = await zeroOne.getQuestionsAmount(); assert.strictEqual(amount.toNumber(), 5); }); 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/helpers/questions.js b/test/helpers/questions.js index 176b09c..ec75627 100644 --- a/test/helpers/questions.js +++ b/test/helpers/questions.js @@ -25,6 +25,7 @@ module.exports = { "string[]", "string[]" ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, { groupId: 0, @@ -42,6 +43,7 @@ module.exports = { "address", "string" ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, { groupId: 0, @@ -55,6 +57,7 @@ module.exports = { paramTypes: [ "string" ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, { groupId: 0, @@ -70,5 +73,6 @@ module.exports = { "address", "address" ], + rawFormula: "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }] } \ No newline at end of file From 813225cc4bcd6340c4f48473b77355d21758578e Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Thu, 27 Feb 2020 18:54:30 +0700 Subject: [PATCH 14/24] some fixes, remaked voting methods --- contracts/ZeroOne/Ballots/Ballots.sol | 168 ++++++++++++++------ contracts/ZeroOne/Ballots/lib/Ballot.sol | 34 ++-- contracts/ZeroOne/UserGroups/UserGroups.sol | 2 +- contracts/ZeroOne/ZeroOne.sol | 7 +- test/11_ZeroOne.spec.js | 66 +++----- 5 files changed, 174 insertions(+), 103 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index f56e684..5fd2788 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -6,6 +6,7 @@ import "./lib/BallotList.sol"; import "../Questions/QuestionsWithGroups.sol"; import "../UserGroups/UserGroups.sol"; import "./IBallots.sol"; +import "../../__vendor__/IERC20.sol"; import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; @@ -14,6 +15,7 @@ import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; * @dev stores Ballots */ contract Ballots is QuestionsWithGroups, UserGroups { + using BallotList for BallotList.List; using BallotType for BallotType.Ballot; using ZeroOneVM for ZeroOneVM.Ballot; @@ -23,9 +25,9 @@ contract Ballots is QuestionsWithGroups, UserGroups { event VotingStarted(uint votingId, uint questionId); - event VotingEnded(uint votingId, BallotType.BallotResult descision); + event VotingEnded(uint votingId, VM.Vote descision); - event UserVote(address group, address user, BallotType.BallotResult descision); + event UserVote(address user, VM.Vote descision); event UpdatedUserVote(address group, address user); @@ -53,19 +55,6 @@ contract Ballots is QuestionsWithGroups, UserGroups { _; } - modifier userNotVoted( - address _group, - address _user - ) { - uint _id = ballots.list.length - 1; - - require( - ballots.list[_id].votes[_group][_user] == BallotType.BallotResult.NOT_ACCEPTED, - "User already vote" - ); - _; - } - modifier groupIsAllowed( uint _questionId, uint _groupId @@ -77,7 +66,6 @@ contract Ballots is QuestionsWithGroups, UserGroups { ); _; } - constructor() public {} /** * @dev returns the confirmation that this is a project @@ -155,50 +143,130 @@ contract Ballots is QuestionsWithGroups, UserGroups { /** * @dev set {_descision} of {_user} from {_group} * method fetching balance of {_user} in {_group} and writing vote in voting struct - * @param _group address of group - * @param _user address of user * @param _descision descision of {_user} * @return success */ function setVote( - address _group, - address _user, - BallotType.BallotResult _descision + VM.Vote _descision ) public - userNotVoted( - _group, - _user - ) returns (bool success) { uint votingId = ballots.list.length - 1; for (uint i = 0; i < 16; i++) { - if (ballots.descriptors[votingId].groups[i].groupAddress == _group) { - + DescriptorVM.Group storage group = ballots.descriptors[votingId].groups[i]; + DescriptorVM.User storage user = ballots.descriptors[votingId].users[i]; + if (group.groupAddress != address(0)) { + bool excluded = isUserExcluded(group.exclude, msg.sender); + if (!excluded) { + bool userVoted = didUserVote(group.groupAddress, msg.sender); + if (!userVoted) { + ballots.list[votingId].setVote(group.groupAddress, msg.sender, _descision); + (uint256 positive, uint256 negative, uint256 totalSupply) = ballots.list[votingId].getGroupVotes(group.groupAddress); + group.positive = positive; + group.negative = negative; + group.totalSupply = totalSupply; + } + } + } + + if (user.groupAddress != address(0)) { + bool userVoted = didUserVote(user.groupAddress, msg.sender); + if (!userVoted) { + ballots.list[votingId].setVote(user.groupAddress, msg.sender, _descision); + } } } - emit UserVote(_group, _user, _descision); + emit UserVote(msg.sender, _descision); return true; } + 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))) { + bool excluded = isUserExcluded(group.exclude, _user); + if (!excluded) { + bool userVoted = didUserVote(group.groupAddress, msg.sender); + if (!userVoted) { + ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); + (uint256 positive, uint256 negative, uint256 totalSupply) = ballots.list[votingId].getGroupVotes(group.groupAddress); + group.positive = positive; + group.negative = negative; + group.totalSupply = totalSupply; + } + } + } + + if ((user.groupAddress != address(0)) && (user.groupAddress == _group)) { + bool userVoted = didUserVote(user.groupAddress, _user); + if (!userVoted) { + ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); + if (_newVoteWeight == 0) { + user.vote = VM.Vote.UNDEFINED; + } + } + } + } + emit UpdatedUserVote(_group, _user); + return true; + } + /** - * @dev updates vote weight of {_user} from {_group} with {_newVoteWeight} - * calls when admin transfer tokens between users in custom token contract + * @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 - * @param _user address of user - * @param _newVoteWeight new vote weight of {_user} - * @return status + * @return positive + * @return negative + * @return totalSupply */ - function updateUserVote(address _group, address _user, uint256 _newVoteWeight) - public - returns(bool status) + function getGroupVotes( + uint _votingId, + address _group + ) + public + view + returns ( + uint256 positive, + uint256 negative, + uint256 totalSupply + ) { - uint votingId = ballots.list.length - 1; - status = ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); - emit UpdatedUserVote(_group, _user); + (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; + } + } } /** @@ -216,7 +284,7 @@ contract Ballots is QuestionsWithGroups, UserGroups { public view ballotExist(_votingId) - returns (BallotType.BallotResult descision) + returns (VM.Vote descision) { return ballots.list[_votingId].votes[_group][_user]; @@ -247,15 +315,21 @@ contract Ballots is QuestionsWithGroups, UserGroups { * @param _user address of user * @return confirm */ - function didUserVote ( - address _group, - address _user - ) + function didUserVote ( + address _group, + address _user + ) public view returns(bool confirm) - { + { uint votingId = ballots.list.length - 1; - confirm = ballots.list[votingId].votes[_group][_user] != BallotType.BallotResult.NOT_ACCEPTED; - } + confirm = ballots.list[votingId].votes[_group][_user] != VM.Vote.UNDEFINED; + } + + function submitVoting() + public + { + uint votingId = ballots.list.length - 1; + } } diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index cbc6254..9bc3eb9 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -24,7 +24,7 @@ library BallotType { uint questionId; BallotStatus status; bytes votingData; - mapping(address => mapping(address => BallotResult)) votes; + mapping(address => mapping(address => VM.Vote)) votes; mapping(address => mapping(address => uint256)) votesWeight; mapping(address => mapping(uint => uint256)) descisionWeights; } @@ -75,7 +75,7 @@ library BallotType { Ballot storage _self, address _group, address _user, - BallotResult _descision + VM.Vote _descision ) internal returns (bool status) @@ -93,7 +93,6 @@ library BallotType { _self.votes[_group][_user] = _descision; _self.votesWeight[_group][_user] = voteWeight; _self.descisionWeights[_group][uint(_descision)] = _self.descisionWeights[_group][uint(_descision)] + voteWeight; - return true; } @@ -114,12 +113,29 @@ library BallotType { _self.votesWeight[_group][_user] = _newVoteWeight; _self.descisionWeights[_group][index] = oldDescisionWeight - oldVoteWeight + _newVoteWeight; if (_newVoteWeight == 0) { - _self.votes[_group][_user] = BallotResult.NOT_ACCEPTED; + _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 @@ -133,7 +149,7 @@ library BallotType { ) internal view - returns (BallotResult userVote) + returns (VM.Vote userVote) { userVote = _self.votes[_group][_user]; } @@ -175,9 +191,9 @@ library BallotType { function calculateResult() internal pure - returns (BallotResult) + returns (VM.Vote) { - return BallotResult.POSITIVE; + return VM.Vote.ACCEPTED; } /** @@ -188,9 +204,9 @@ library BallotType { Ballot storage _self ) internal - returns (BallotResult result) + returns (VM.Vote result) { - BallotResult _result = calculateResult(); + VM.Vote _result = calculateResult(); setResult(_self); return _result; } diff --git a/contracts/ZeroOne/UserGroups/UserGroups.sol b/contracts/ZeroOne/UserGroups/UserGroups.sol index 511b8d9..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 diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 3913c78..3e37f5e 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -5,6 +5,7 @@ import "./IZeroOne.sol"; import "./Notifier/Notifier.sol"; import "../lib/Meta.sol"; import "./Ballots/Ballots.sol"; +import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; /** @@ -18,6 +19,10 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { MetaData _meta ); + constructor(UserGroup.Group memory _group) public { + addUserGroup(_group); + } + /** * @notice for modified functions allows only self external call */ @@ -36,7 +41,7 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { function submitVoting() public returns ( - BallotType.BallotResult descision + VM.Vote descision ) { uint votingId = ballots.list.length - 1; diff --git a/test/11_ZeroOne.spec.js b/test/11_ZeroOne.spec.js index b37f2fe..6c210db 100644 --- a/test/11_ZeroOne.spec.js +++ b/test/11_ZeroOne.spec.js @@ -1,6 +1,8 @@ -const ZeroOne = artifacts.require('./ZeroOne.sol'); const Controlled = artifacts.require('./ControlledMock.sol'); const ERC20 = artifacts.require('./ERC20.sol'); +const ZeroOne = artifacts.require('./ZeroOne.sol'); +const BallotType = artifacts.require('./BallotType.sol'); +const ZeroOneVM = artifacts.require('zeroone-voting-vm/contracts/ZeroOneVM.sol'); const increase = require('./helpers/increase-time'); const { compile, compileDescriptors } = require('zeroone-translator'); @@ -10,10 +12,22 @@ contract('ZeroOne', ([from, secondary]) => { let zeroOne; let token; - beforeEach(async () => { token = await ERC20.new('test', 'tst', 1000); - zeroOne = await ZeroOne.new(token.address, { from }); + + 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 }); }); @@ -33,6 +47,7 @@ contract('ZeroOne', ([from, secondary]) => { 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]); @@ -48,49 +63,10 @@ contract('ZeroOne', ([from, secondary]) => { await zeroOne.startVoting(votingData); const userBalance = await token.balanceOf(from); await token.approve(zeroOne.address, userBalance); - await zeroOne.setVote(token.address, from, 1); + await zeroOne.setVote(1); increase(web3, 320000); - await zeroOne.submitVoting(); - const group = await zeroOne.getQuestionGroup(1); - assert.strictEqual(group.name, 'test'); - + const {positive, negative, totalSupply} = await zeroOne.getGroupVotes(0, token.address); + console.log(positive.toNumber(), negative.toNumber(), totalSupply.toNumber()); }); - it('should add question', async () => { - for (let i = 0; i < questions.length; i++) { - 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(bool, string, string, uint256, uint256, string[], string[], address, bytes4)'], - [ - [0, 0, 0, 0, 0], - [true, "test question", "some text", 0, 360000, ['test'], ['string'], secondary, "0x00000000"] - ] - ) - const votingData = { - questionId: 0, - starterAddress: from, - starterGroupId: 0, - endTime: 0, - data, - } - let amount = await zeroOne.getQuestionsAmount(); - console.log(`amount = ${amount.toNumber()}`) - - await zeroOne.startVoting(votingData); - const userBalance = await token.balanceOf(from); - - await token.approve(zeroOne.address, userBalance); - await zeroOne.setVote(token.address, from, 1); - - const balance = await token.balanceOf(zeroOne.address); - increase(web3, 320000); - - await zeroOne.submitVoting(); - amount = await zeroOne.getQuestionsAmount(); - assert.strictEqual(amount.toNumber(), 5); - }); }); }); \ No newline at end of file From e659e8bb1542ca74a926c7cad12a664f62bf4dde Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Fri, 28 Feb 2020 18:31:13 +0700 Subject: [PATCH 15/24] Ballots are working with VM's some test for votings --- contracts/ZeroOne/Ballots/Ballots.sol | 103 +++++++++++--- contracts/ZeroOne/Ballots/lib/Ballot.sol | 52 +------ contracts/ZeroOne/IZeroOne.sol | 4 +- contracts/ZeroOne/ZeroOne.sol | 23 ++-- test/11_1_ZeroOne.spec.js | 168 +++++++++++++++++++++++ test/11_ZeroOne.spec.js | 33 ++++- 6 files changed, 298 insertions(+), 85 deletions(-) create mode 100644 test/11_1_ZeroOne.spec.js diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 5fd2788..3310e87 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -157,24 +157,39 @@ contract Ballots is QuestionsWithGroups, UserGroups { 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)) { bool excluded = isUserExcluded(group.exclude, msg.sender); if (!excluded) { bool userVoted = didUserVote(group.groupAddress, msg.sender); if (!userVoted) { ballots.list[votingId].setVote(group.groupAddress, msg.sender, _descision); - (uint256 positive, uint256 negative, uint256 totalSupply) = ballots.list[votingId].getGroupVotes(group.groupAddress); - group.positive = positive; - group.negative = negative; - group.totalSupply = totalSupply; + (uint positive, uint negative, uint totalSupply) = ballots.list[votingId].getGroupVotes(group.groupAddress); + ballots.descriptors[votingId].groups[i].positive = positive; + ballots.descriptors[votingId].groups[i].negative = negative; + ballots.descriptors[votingId].groups[i].totalSupply = totalSupply; } } } - if (user.groupAddress != address(0)) { - bool userVoted = didUserVote(user.groupAddress, msg.sender); - if (!userVoted) { - ballots.list[votingId].setVote(user.groupAddress, msg.sender, _descision); + if (user.groupAddress != address(0)) { + + if((user.userAddress != address(0)) && (user.userAddress == msg.sender)) { + bool userVoted = didUserVote(user.groupAddress, user.userAddress); + if (!userVoted) { + ballots.list[votingId].setVote(user.groupAddress, user.userAddress, _descision); + } else { + user.vote = getUserVote(votingId, group.groupAddress, user.userAddress); + } + } else if ( (user.admin == true) && (IERC20(user.groupAddress).owner() == msg.sender) ) { + user.userAddress = msg.sender; + bool userVoted = didUserVote(user.groupAddress, msg.sender); + if (!userVoted) { + ballots.list[votingId].setVote(user.groupAddress, msg.sender, _descision); + user.vote = _descision; + } else { + user.vote = getUserVote(votingId, group.groupAddress, msg.sender); + } } } } @@ -183,6 +198,12 @@ contract Ballots is QuestionsWithGroups, UserGroups { 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, @@ -200,7 +221,7 @@ contract Ballots is QuestionsWithGroups, UserGroups { bool excluded = isUserExcluded(group.exclude, _user); if (!excluded) { bool userVoted = didUserVote(group.groupAddress, msg.sender); - if (!userVoted) { + if (userVoted) { ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); (uint256 positive, uint256 negative, uint256 totalSupply) = ballots.list[votingId].getGroupVotes(group.groupAddress); group.positive = positive; @@ -211,11 +232,21 @@ contract Ballots is QuestionsWithGroups, UserGroups { } if ((user.groupAddress != address(0)) && (user.groupAddress == _group)) { - bool userVoted = didUserVote(user.groupAddress, _user); - if (!userVoted) { - ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); - if (_newVoteWeight == 0) { - user.vote = VM.Vote.UNDEFINED; + if((user.userAddress != address(0)) && (user.userAddress == msg.sender)) { + bool userVoted = didUserVote(user.groupAddress, _user); + if (userVoted) { + ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); + if (_newVoteWeight == 0) { + user.vote = VM.Vote.UNDEFINED; + } + } + } else if ( (user.admin == true) && (IERC20(user.groupAddress).owner() == msg.sender) ) { + user.userAddress = msg.sender; + bool userVoted = didUserVote(user.groupAddress, msg.sender); + if (userVoted) { + if (_newVoteWeight == 0) { + user.vote = VM.Vote.UNDEFINED; + } } } } @@ -327,9 +358,49 @@ contract Ballots is QuestionsWithGroups, UserGroups { confirm = ballots.list[votingId].votes[_group][_user] != VM.Vote.UNDEFINED; } - function submitVoting() + /** + * @dev closes the voting by executing descriptors for result calculating + * and setting BallotStatus.CLOSED + * @return votingId + * @return questionId + * @return result + */ + function closeVoting() + public + returns ( + uint votingId, + uint questionId, + VM.Vote result + ) + { + votingId = ballots.list.length - 1; + questionId = ballots.list[votingId].questionId; + + require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); + bytes storage formula = questions.list[questionId].formula; + address owners = groups.list[0].groupAddress; + ballots.descriptors[votingId].executeResult(formula, owners); + ballots.list[votingId].close(); + + return ( + votingId, + questionId, + ballots.descriptors[votingId].result + ); + } + + /** + * @dev gets voting result by {_votingId} + * @param _votingId id of voting + * @return result + */ + function getVotingResult ( + uint _votingId + ) public + view + returns (VM.Vote result) { - uint votingId = ballots.list.length - 1; + result = ballots.descriptors[_votingId].result; } } diff --git a/contracts/ZeroOne/Ballots/lib/Ballot.sol b/contracts/ZeroOne/Ballots/lib/Ballot.sol index 9bc3eb9..51e4859 100644 --- a/contracts/ZeroOne/Ballots/lib/Ballot.sol +++ b/contracts/ZeroOne/Ballots/lib/Ballot.sol @@ -13,8 +13,6 @@ library BallotType { enum BallotStatus { CLOSED, ACTIVE } - enum BallotResult { NOT_ACCEPTED, POSITIVE, NEGATIVE } - struct Ballot { uint startBlock; uint startTime; @@ -154,64 +152,18 @@ library BallotType { userVote = _self.votes[_group][_user]; } - /** - * @dev set status in voting - * @param _status new Voting status - * @return success - */ - function setStatus( - Ballot storage _self, - BallotStatus _status - ) - internal - returns (bool success) - { - _self.status = _status; - return _self.status == _status; - } - - /** - * @dev set result and status "Closed" to voting - * @return success - */ - function setResult( - Ballot storage _self - ) - internal - returns (bool success) - { - require(setStatus(_self, BallotStatus.CLOSED), "Problem with setting status"); - return true; - } - - /** - * @dev calculates result of voting - */ - // TODO Implement this after ready formula parser - function calculateResult() - internal - pure - returns (VM.Vote) - { - return VM.Vote.ACCEPTED; - } - /** * @dev close ballot by calculating result and setting status "CLOSED" * @param _self ballot */ - function closeVoting( + function close( Ballot storage _self ) internal - returns (VM.Vote result) { - VM.Vote _result = calculateResult(); - setResult(_self); - return _result; + _self.status = BallotStatus.CLOSED; } - function validate( //Ballot memory _self ) diff --git a/contracts/ZeroOne/IZeroOne.sol b/contracts/ZeroOne/IZeroOne.sol index 16499f8..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,6 +19,6 @@ interface IZeroOne { uint questionId; uint startBlock; uint endBlock; - Result result; + VM.Vote result; } } diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 3e37f5e..1a9c18e 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -36,34 +36,33 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { /** * @dev closes last voting in list - * @return descision + * @return success */ function submitVoting() public - returns ( - VM.Vote descision - ) + returns (bool) { - uint votingId = ballots.list.length - 1; - require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); - descision = ballots.list[votingId].closeVoting(); - uint questionId = ballots.list[votingId].questionId; + + (uint votingId, uint questionId, VM.Vote result) = Ballots.closeVoting(); + 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.ACCEPTED + result: result }); makeCall( - questions.list[questionId].target, - questions.list[questionId].methodSelector, + question.target, + question.methodSelector, ballots.list[votingId].votingData, meta ); - emit VotingEnded(votingId, descision); + + emit VotingEnded(votingId, result); + return true; } /** diff --git a/test/11_1_ZeroOne.spec.js b/test/11_1_ZeroOne.spec.js new file mode 100644 index 0000000..b1068c8 --- /dev/null +++ b/test/11_1_ZeroOne.spec.js @@ -0,0 +1,168 @@ +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); + + console.log(`questions with formula --- ${formula}`); + + 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} --- 🚫`) + } + } + }); + + it('should finish voting with UNDEFINED result', async () => { + + }) + }); +}); \ No newline at end of file diff --git a/test/11_ZeroOne.spec.js b/test/11_ZeroOne.spec.js index 6c210db..85474ca 100644 --- a/test/11_ZeroOne.spec.js +++ b/test/11_ZeroOne.spec.js @@ -1,19 +1,35 @@ -const Controlled = artifacts.require('./ControlledMock.sol'); -const ERC20 = artifacts.require('./ERC20.sol'); const ZeroOne = artifacts.require('./ZeroOne.sol'); -const BallotType = artifacts.require('./BallotType.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, compileDescriptors } = require('zeroone-translator'); +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", @@ -65,8 +81,13 @@ contract('ZeroOne', ([from, secondary]) => { await token.approve(zeroOne.address, userBalance); await zeroOne.setVote(1); increase(web3, 320000); - const {positive, negative, totalSupply} = await zeroOne.getGroupVotes(0, token.address); - console.log(positive.toNumber(), negative.toNumber(), totalSupply.toNumber()); + 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 From a5f9816a3a7afd36d1fe37c6ffbde07ca3d5d9a5 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 10 Mar 2020 16:56:33 +0700 Subject: [PATCH 16/24] some changes in Ballots --- contracts/ZeroOne/Ballots/Ballots.sol | 77 +++----------------- test/10_Ballot.spec.js | 100 ++++++++------------------ 2 files changed, 39 insertions(+), 138 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 3310e87..d0b6889 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -3,22 +3,18 @@ pragma experimental ABIEncoderV2; import "./lib/Ballot.sol"; import "./lib/BallotList.sol"; -import "../Questions/QuestionsWithGroups.sol"; -import "../UserGroups/UserGroups.sol"; import "./IBallots.sol"; import "../../__vendor__/IERC20.sol"; -import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; /** * @title Ballots * @dev stores Ballots */ -contract Ballots is QuestionsWithGroups, UserGroups { +contract Ballots { using BallotList for BallotList.List; using BallotType for BallotType.Ballot; - using ZeroOneVM for ZeroOneVM.Ballot; BallotList.List ballots; @@ -55,46 +51,20 @@ contract Ballots is QuestionsWithGroups, UserGroups { _; } - 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 returns the confirmation that this is a project */ function isProject() public pure returns (bool) { return true; } - /** - * @dev creates new Ballot in list, emits {VotingStarted} - * @param _votingPrimary primary info about voting - * @return id of new voting - */ - function startVoting( + + function addVoting( BallotList.BallotSimple memory _votingPrimary - ) - public - noActiveVotings() - questionExists(_votingPrimary.questionId) - groupIsAllowed( - _votingPrimary.questionId, - _votingPrimary.starterGroupId - ) - returns (uint id) + ) + public + noActiveVotings + returns (uint id) { - _votingPrimary.endTime = block.timestamp + questions.list[_votingPrimary.questionId].timeLimit; id = ballots.add(_votingPrimary); - ballots.descriptors[id].executeDescriptors( - questions.list[_votingPrimary.questionId].formula, - groups.list[0].groupAddress - ); emit VotingStarted(id, _votingPrimary.questionId); } @@ -240,7 +210,7 @@ contract Ballots is QuestionsWithGroups, UserGroups { user.vote = VM.Vote.UNDEFINED; } } - } else if ( (user.admin == true) && (IERC20(user.groupAddress).owner() == msg.sender) ) { + } else if ( (user.admin == true) && (IERC20(user.groupAddress).owner() == msg.sender) ) { user.userAddress = msg.sender; bool userVoted = didUserVote(user.groupAddress, msg.sender); if (userVoted) { @@ -358,37 +328,6 @@ contract Ballots is QuestionsWithGroups, UserGroups { confirm = ballots.list[votingId].votes[_group][_user] != VM.Vote.UNDEFINED; } - /** - * @dev closes the voting by executing descriptors for result calculating - * and setting BallotStatus.CLOSED - * @return votingId - * @return questionId - * @return result - */ - function closeVoting() - public - returns ( - uint votingId, - uint questionId, - VM.Vote result - ) - { - votingId = ballots.list.length - 1; - questionId = ballots.list[votingId].questionId; - - require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); - bytes storage formula = questions.list[questionId].formula; - address owners = groups.list[0].groupAddress; - ballots.descriptors[votingId].executeResult(formula, owners); - ballots.list[votingId].close(); - - return ( - votingId, - questionId, - ballots.descriptors[votingId].result - ); - } - /** * @dev gets voting result by {_votingId} * @param _votingId id of voting diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index 3d55eea..eddbc76 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -35,7 +35,7 @@ contract('Ballot', ([from, secondary]) => { ballot = await Ballot.new({ from }); customToken = await CustomToken.new('test', 'tst', 1000, { from }); - await ballot.addQuestion(question) + // await ballot.addQuestion(question) }); describe('constructor()', () => { @@ -46,10 +46,10 @@ contract('Ballot', ([from, secondary]) => { }); }); - describe('startVoting()', () => { - it('should start voting', async () => { + describe('addVoting()', () => { + it('should add voting', async () => { - const tx = await ballot.startVoting(primaryInfo); + const tx = await ballot.addVoting(primaryInfo); const {args : {votingId, questionId}} = tx.logs.find(element => element.event.match('VotingStarted')); assert.strictEqual(votingId.toNumber(), 0); @@ -59,12 +59,12 @@ contract('Ballot', ([from, secondary]) => { assert.strictEqual(amount.toNumber(), 1); }); - it('should fail on start voting, while has active voting', async () => { + it('should fail on adding voting, while has active voting', async () => { let error = false; - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); try { - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); } catch ({ message }) { error = true; assert.strictEqual(message, getErrorMessage('You have active voting')); @@ -75,21 +75,19 @@ contract('Ballot', ([from, secondary]) => { describe('getVoting()', () => { it('should return information about voting', async () => { - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); const { endTime, starterGroupId, starterAddress, questionId, - status, - result + status } = await ballot.getVoting(0); assert.strictEqual(starterGroupId.toNumber(), 0); assert.strictEqual(starterAddress, secondary); assert.strictEqual(questionId.toNumber(), 0); assert.strictEqual(status.toNumber(), 1); - assert.strictEqual(result.toNumber(), 0); }); it('should fail on getting non-existing voting', async () => { @@ -109,7 +107,7 @@ contract('Ballot', ([from, secondary]) => { let amount = await ballot.getVotingsAmount(); assert.strictEqual(amount.toNumber(), 0); - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); amount = await ballot.getVotingsAmount(); assert.strictEqual(amount.toNumber(), 1); @@ -118,51 +116,48 @@ contract('Ballot', ([from, secondary]) => { describe('setVote()', () => { it('should successfully set Positive vote', async () => { - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); - const tx = await ballot.setVote(from, secondary, 1, 200); + const tx = await ballot.setVote(1); const {args : { - user, group, descision + user, descision }} = tx.logs.find(element => element.event.match('UserVote')); - assert.strictEqual(user, secondary); - assert.strictEqual(group, from); + assert.strictEqual(user, from); assert.strictEqual(descision.toNumber(), 1) }) it('should successfully set Negative vote', async () => { - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); - const tx = await ballot.setVote(from, secondary, 2, 200); + const tx = await ballot.setVote(2); const {args : { - user, group, descision + user, descision }} = tx.logs.find(element => element.event.match('UserVote')); - assert.strictEqual(user, secondary); - assert.strictEqual(group, from); + assert.strictEqual(user, from); assert.strictEqual(descision.toNumber(), 2) }) it('should successfully remove vote', async () => { - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); - const tx = await ballot.setVote(from, secondary, 0, 0); + const tx = await ballot.setVote(1); const {args : { - user, group, descision + user, descision }} = tx.logs.find(element => element.event.match('UserVote')); - assert.strictEqual(user, secondary); - assert.strictEqual(group, from); - assert.strictEqual(descision.toNumber(), 0) + assert.strictEqual(user, from); + assert.strictEqual(descision.toNumber(), 1) }) it('should fail on set new vote of user, which already voted', async () => { let error = false; - await ballot.startVoting(primaryInfo); - await ballot.setVote(from, secondary, 1, 1000); + await ballot.addVoting(primaryInfo); + await ballot.setVote(1); try { error = true; - await ballot.setVote(from, secondary, 2, 0); + await ballot.setVote(2); } catch ({message}) { assert.strictEqual(message, getErrorMessage('User already vote')); } @@ -170,57 +165,24 @@ contract('Ballot', ([from, secondary]) => { }) }) - describe('closeVoting()', () => { - it('should successfully close voting', async () => { - await ballot.startVoting(primaryInfo); - increaseTime(web3, 300000); - const tx = await ballot.closeVoting(); - const {args : {votingId, descision}} = tx.logs.find(element => element.event.match('VotingEnded')); - assert.strictEqual(votingId.toNumber(), 0); - assert.strictEqual(descision.toNumber(), 0); - }) - - it('should fail on close voting, when time is not over', async () => { - let error = false; - await ballot.startVoting(primaryInfo); - try { - await ballot.closeVoting(); - } 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.startVoting(primaryInfo); + const tx = await ballot.addVoting(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 VotingEnded event', async () => { - await ballot.startVoting(primaryInfo); - increaseTime(web3, 300000); - const tx = await ballot.closeVoting(); - const {args : {votingId, descision}} = tx.logs.find(element => element.event.match('VotingEnded')); - assert.strictEqual(votingId.toNumber(), 0); - assert.strictEqual(descision.toNumber(), 0); - }); - it('should fire UserVote event', async () => { - await ballot.startVoting(primaryInfo); + await ballot.addVoting(primaryInfo); - const tx = await ballot.setVote(from, secondary, 1, 200); + const tx = await ballot.setVote(1); const {args : { - user, group, descision + user, descision }} = tx.logs.find(element => element.event.match('UserVote')); - assert.strictEqual(user, secondary); - assert.strictEqual(group, from); + assert.strictEqual(user, from); assert.strictEqual(descision.toNumber(), 1) }); }) From 3fd8cae8c3bcbf1d86161d2649fae3ff446fcfc4 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 10 Mar 2020 17:12:10 +0700 Subject: [PATCH 17/24] small doc fix --- contracts/ZeroOne/Ballots/Ballots.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index d0b6889..f92fde9 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -56,7 +56,11 @@ contract Ballots { */ 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 + * @return id + */ function addVoting( BallotList.BallotSimple memory _votingPrimary ) From fa19497dbe8cc298d417d24c370aec9110390a86 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Tue, 10 Mar 2020 18:04:07 +0700 Subject: [PATCH 18/24] function closeVoting returned to Ballots --- contracts/ZeroOne/Ballots/Ballots.sol | 32 +++++++++++++++++++ contracts/ZeroOne/ZeroOne.sol | 46 +++++++++++++++++++++++++-- contracts/__mocks__/ZeroOneMock.sol | 45 -------------------------- test/11_1_ZeroOne.spec.js | 8 ----- 4 files changed, 76 insertions(+), 55 deletions(-) delete mode 100644 contracts/__mocks__/ZeroOneMock.sol diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index f92fde9..73008b6 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -72,6 +72,38 @@ contract Ballots { emit VotingStarted(id, _votingPrimary.questionId); } + /** + * @dev closes the voting by executing descriptors for result calculating + * and setting BallotStatus.CLOSED + * @return votingId + * @return questionId + * @return result + */ + function closeVoting() + public + returns ( + uint votingId, + uint questionId, + VM.Vote result + ) + { + votingId = ballots.list.length - 1; + questionId = ballots.list[votingId].questionId; + + require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); + bytes storage formula = questions.list[questionId].formula; + address owners = groups.list[0].groupAddress; + ballots.descriptors[votingId].executeResult(formula, owners); + ballots.list[votingId].close(); + + return ( + votingId, + questionId, + ballots.descriptors[votingId].result + ); + } + + /** * @dev getting the voting by id * @param _id id of voting diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 1a9c18e..6d5f91a 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -2,9 +2,11 @@ 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 "./Ballots/Ballots.sol"; import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; @@ -12,8 +14,9 @@ import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; * @title ZeroOne * @dev main ZeroOne contract */ -contract ZeroOne is Notifier, IZeroOne, Ballots { +contract ZeroOne is Notifier, IZeroOne, Ballots, UserGroups, QuestionsWithGroups { using Meta for bytes; + using ZeroOneVM for ZeroOneVM.Ballot; event ZeroOneCall( MetaData _meta @@ -34,6 +37,45 @@ contract ZeroOne is Notifier, IZeroOne, Ballots { _; } + 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); + ballots.descriptors[id].executeDescriptors( + questions.list[_votingPrimary.questionId].formula, + groups.list[0].groupAddress + ); + } + + /** * @dev closes last voting in list * @return success diff --git a/contracts/__mocks__/ZeroOneMock.sol b/contracts/__mocks__/ZeroOneMock.sol deleted file mode 100644 index bf022d5..0000000 --- a/contracts/__mocks__/ZeroOneMock.sol +++ /dev/null @@ -1,45 +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 { - - constructor(address _owners) public { - UserGroup.Group memory _group = UserGroup.Group({ - name: "Owners", - groupAddress: _owners, - groupType: UserGroup.Type.ERC20 - }); - addUserGroup(_group); - } - /** - * @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/test/11_1_ZeroOne.spec.js b/test/11_1_ZeroOne.spec.js index b1068c8..a10eeb6 100644 --- a/test/11_1_ZeroOne.spec.js +++ b/test/11_1_ZeroOne.spec.js @@ -57,8 +57,6 @@ contract('ZeroOne', ([from, secondary, third]) => { customToken.addToProjects(zeroOne.address); - - for (question of questions) { question.rawFormula = formula; question.formula = compile( @@ -120,8 +118,6 @@ contract('ZeroOne', ([from, secondary, third]) => { customToken.addToProjects(zeroOne.address); - console.log(`questions with formula --- ${formula}`); - for (question of questions) { question.rawFormula = formula; question.formula = compile( @@ -160,9 +156,5 @@ contract('ZeroOne', ([from, secondary, third]) => { } } }); - - it('should finish voting with UNDEFINED result', async () => { - - }) }); }); \ No newline at end of file From f503165e62fe1c35a0161b52c805f87cea25a486 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Wed, 11 Mar 2020 14:29:31 +0700 Subject: [PATCH 19/24] fixes for ballots --- contracts/ZeroOne/Ballots/Ballots.sol | 115 ++++++++++++++------------ contracts/ZeroOne/ZeroOne.sol | 9 +- test/10_Ballot.spec.js | 23 ++++++ 3 files changed, 90 insertions(+), 57 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 73008b6..8810638 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -15,6 +15,8 @@ contract Ballots { using BallotList for BallotList.List; using BallotType for BallotType.Ballot; + using ZeroOneVM for ZeroOneVM.Ballot; + BallotList.List ballots; @@ -75,30 +77,28 @@ contract Ballots { /** * @dev closes the voting by executing descriptors for result calculating * and setting BallotStatus.CLOSED - * @return votingId - * @return questionId + * @param votingId id of voting + * @param formula formula of voting + * @param owners address of * @return result */ - function closeVoting() - public + function closeVoting( + uint votingId, + bytes storage formula, + address owners + ) + internal returns ( - uint votingId, - uint questionId, VM.Vote result ) - { - votingId = ballots.list.length - 1; - questionId = ballots.list[votingId].questionId; - + { require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); - bytes storage formula = questions.list[questionId].formula; - address owners = groups.list[0].groupAddress; ballots.descriptors[votingId].executeResult(formula, owners); ballots.list[votingId].close(); + emit VotingEnded(votingId, ballots.descriptors[votingId].result); + return ( - votingId, - questionId, ballots.descriptors[votingId].result ); } @@ -146,6 +146,28 @@ contract Ballots { 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 @@ -165,36 +187,31 @@ contract Ballots { DescriptorVM.User storage user = ballots.descriptors[votingId].users[i]; if (group.groupAddress != address(0)) { - bool excluded = isUserExcluded(group.exclude, msg.sender); - if (!excluded) { - bool userVoted = didUserVote(group.groupAddress, msg.sender); - if (!userVoted) { + if (!isUserExcluded(group.exclude, msg.sender)) { + if (!didUserVote(group.groupAddress, msg.sender)) { ballots.list[votingId].setVote(group.groupAddress, msg.sender, _descision); - (uint positive, uint negative, uint totalSupply) = ballots.list[votingId].getGroupVotes(group.groupAddress); - ballots.descriptors[votingId].groups[i].positive = positive; - ballots.descriptors[votingId].groups[i].negative = negative; - ballots.descriptors[votingId].groups[i].totalSupply = totalSupply; + 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)) { - bool userVoted = didUserVote(user.groupAddress, user.userAddress); - if (!userVoted) { - ballots.list[votingId].setVote(user.groupAddress, user.userAddress, _descision); - } else { - user.vote = getUserVote(votingId, group.groupAddress, user.userAddress); - } - } else if ( (user.admin == true) && (IERC20(user.groupAddress).owner() == msg.sender) ) { + userAddress = msg.sender; + } else if (user.admin == true && IERC20(user.groupAddress).owner() == msg.sender) { user.userAddress = msg.sender; - bool userVoted = didUserVote(user.groupAddress, msg.sender); - if (!userVoted) { - ballots.list[votingId].setVote(user.groupAddress, msg.sender, _descision); + 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, group.groupAddress, msg.sender); + user.vote = getUserVote(votingId, groupAddress, userAddress); } } } @@ -223,33 +240,23 @@ contract Ballots { 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))) { - bool excluded = isUserExcluded(group.exclude, _user); - if (!excluded) { - bool userVoted = didUserVote(group.groupAddress, msg.sender); - if (userVoted) { + if (group.groupAddress != address(0) && group.groupAddress == _group) { + if (!isUserExcluded(group.exclude, _user)) { + if (didUserVote(group.groupAddress, msg.sender)) { ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); - (uint256 positive, uint256 negative, uint256 totalSupply) = ballots.list[votingId].getGroupVotes(group.groupAddress); - group.positive = positive; - group.negative = negative; - group.totalSupply = totalSupply; + setGroupVotes(group.groupAddress, votingId, i); } } } - if ((user.groupAddress != address(0)) && (user.groupAddress == _group)) { - if((user.userAddress != address(0)) && (user.userAddress == msg.sender)) { - bool userVoted = didUserVote(user.groupAddress, _user); - if (userVoted) { - ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); - if (_newVoteWeight == 0) { - user.vote = VM.Vote.UNDEFINED; - } - } - } else if ( (user.admin == true) && (IERC20(user.groupAddress).owner() == msg.sender) ) { + if ( user.groupAddress != address(0) && user.groupAddress == _group ) { + if ( user.admin == true && IERC20(user.groupAddress).owner() == msg.sender ) { user.userAddress = msg.sender; - bool userVoted = didUserVote(user.groupAddress, msg.sender); - if (userVoted) { + } + + if (user.userAddress != address(0)) { + if (didUserVote(user.groupAddress, msg.sender)) { + ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); if (_newVoteWeight == 0) { user.vote = VM.Vote.UNDEFINED; } diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 6d5f91a..8d7e184 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -16,7 +16,6 @@ import "zeroone-voting-vm/contracts/ZeroOneVM.sol"; */ contract ZeroOne is Notifier, IZeroOne, Ballots, UserGroups, QuestionsWithGroups { using Meta for bytes; - using ZeroOneVM for ZeroOneVM.Ballot; event ZeroOneCall( MetaData _meta @@ -84,8 +83,13 @@ contract ZeroOne is Notifier, IZeroOne, Ballots, UserGroups, QuestionsWithGroups public returns (bool) { + uint votingId = ballots.list.length - 1; + uint questionId = ballots.list[votingId].questionId; - (uint votingId, uint questionId, VM.Vote result) = Ballots.closeVoting(); + 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({ @@ -103,7 +107,6 @@ contract ZeroOne is Notifier, IZeroOne, Ballots, UserGroups, QuestionsWithGroups meta ); - emit VotingEnded(votingId, result); return true; } diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index eddbc76..f292896 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -165,6 +165,29 @@ contract('Ballot', ([from, secondary]) => { }) }) + describe('closeVoting()', () => { + it('should successfully close voting', async () => { + await ballot.addVoting(primaryInfo); + increaseTime(web3, 300000); + const tx = await ballot.closeVoting(); + const {args : {votingId, descision}} = tx.logs.find(element => element.event.match('VotingEnded')); + assert.strictEqual(votingId.toNumber(), 0); + assert.strictEqual(descision.toNumber(), 0); + }) + + it('should fail on close voting, when time is not over', async () => { + let error = false; + await ballot.addVoting(primaryInfo); + try { + await ballot.closeVoting(); + } 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.addVoting(primaryInfo); From a3b0a439e45b2baa9fb163fe2c2c83ff98c6b808 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Wed, 11 Mar 2020 16:56:38 +0700 Subject: [PATCH 20/24] some fixes, mock for ballots --- contracts/ZeroOne/Ballots/Ballots.sol | 14 +++++++--- contracts/ZeroOne/ZeroOne.sol | 5 ++-- contracts/__mocks__/BallotsMock.sol | 25 ++++++++++++++++++ test/10_Ballot.spec.js | 38 ++++++++++++++------------- 4 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 contracts/__mocks__/BallotsMock.sol diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 8810638..eeb6606 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -64,13 +64,20 @@ contract Ballots { * @return id */ function addVoting( - BallotList.BallotSimple memory _votingPrimary + BallotList.BallotSimple memory _votingPrimary, + bytes storage formula, + address owners ) - public + internal noActiveVotings returns (uint id) { id = ballots.add(_votingPrimary); + + ballots.descriptors[id].executeDescriptors( + formula, + owners + ); emit VotingStarted(id, _votingPrimary.questionId); } @@ -93,6 +100,7 @@ contract Ballots { ) { require(ballots.list[votingId].endTime < block.timestamp, "Time is not over yet"); + ballots.descriptors[votingId].executeResult(formula, owners); ballots.list[votingId].close(); @@ -207,7 +215,7 @@ contract Ballots { } if(userAddress != address(0)){ - if (!didUserVote(user.groupAddress, user.userAddress)) { + if (!didUserVote(user.groupAddress, user.userAddress)) { ballots.list[votingId].setVote(groupAddress, userAddress, _descision); user.vote = _descision; } else { diff --git a/contracts/ZeroOne/ZeroOne.sol b/contracts/ZeroOne/ZeroOne.sol index 8d7e184..5886ad6 100644 --- a/contracts/ZeroOne/ZeroOne.sol +++ b/contracts/ZeroOne/ZeroOne.sol @@ -67,8 +67,9 @@ contract ZeroOne is Notifier, IZeroOne, Ballots, UserGroups, QuestionsWithGroups returns (uint id) { _votingPrimary.endTime = block.timestamp + questions.list[_votingPrimary.questionId].timeLimit; - id = Ballots.addVoting(_votingPrimary); - ballots.descriptors[id].executeDescriptors( + + id = Ballots.addVoting( + _votingPrimary, questions.list[_votingPrimary.questionId].formula, groups.list[0].groupAddress ); diff --git a/contracts/__mocks__/BallotsMock.sol b/contracts/__mocks__/BallotsMock.sol new file mode 100644 index 0000000..5e2abbf --- /dev/null +++ b/contracts/__mocks__/BallotsMock.sol @@ -0,0 +1,25 @@ +pragma solidity 0.6.1; +pragma experimental ABIEncoderV2; + +import "../ZeroOne/Ballots/Ballots.sol"; + +contract BallotsMock is Ballots { + bytes formula = "0x0041c8a92378323f33fc30b0a10e3fd771eb8f6dff010006010032320704000904000000"; + + function testAddVoting( + BallotList.BallotSimple memory _votingPrimary + ) + public + { + addVoting( + _votingPrimary, + formula, + 0x41c8a92378323F33FC30B0a10E3fd771Eb8f6DFf + ); + } + function testCloseVoting() + public + { + closeVoting(0, formula, 0x41c8a92378323F33FC30B0a10E3fd771Eb8f6DFf); + } +} \ No newline at end of file diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index f292896..75f120d 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -1,6 +1,5 @@ -const Ballot = artifacts.require('Ballots.sol'); -const Questions = artifacts.require('QuestionsWithGroups.sol'); -const UserGroups = artifacts.require('UserGroups.sol'); +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'); @@ -29,9 +28,12 @@ contract('Ballot', ([from, secondary]) => { target: from, methodSelector: '0x12121212' }; + group = { name: 'group name' }; + zeroOneVM = await ZeroOneVM.new() + await Ballot.link("ZeroOneVM", zeroOneVM.address); ballot = await Ballot.new({ from }); customToken = await CustomToken.new('test', 'tst', 1000, { from }); @@ -49,7 +51,7 @@ contract('Ballot', ([from, secondary]) => { describe('addVoting()', () => { it('should add voting', async () => { - const tx = await ballot.addVoting(primaryInfo); + const tx = await ballot.testAddVoting(primaryInfo); const {args : {votingId, questionId}} = tx.logs.find(element => element.event.match('VotingStarted')); assert.strictEqual(votingId.toNumber(), 0); @@ -62,9 +64,9 @@ contract('Ballot', ([from, secondary]) => { it('should fail on adding voting, while has active voting', async () => { let error = false; - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); try { - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); } catch ({ message }) { error = true; assert.strictEqual(message, getErrorMessage('You have active voting')); @@ -75,7 +77,7 @@ contract('Ballot', ([from, secondary]) => { describe('getVoting()', () => { it('should return information about voting', async () => { - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); const { endTime, starterGroupId, @@ -107,7 +109,7 @@ contract('Ballot', ([from, secondary]) => { let amount = await ballot.getVotingsAmount(); assert.strictEqual(amount.toNumber(), 0); - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); amount = await ballot.getVotingsAmount(); assert.strictEqual(amount.toNumber(), 1); @@ -116,7 +118,7 @@ contract('Ballot', ([from, secondary]) => { describe('setVote()', () => { it('should successfully set Positive vote', async () => { - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); const tx = await ballot.setVote(1); const {args : { @@ -128,7 +130,7 @@ contract('Ballot', ([from, secondary]) => { }) it('should successfully set Negative vote', async () => { - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); const tx = await ballot.setVote(2); const {args : { @@ -140,7 +142,7 @@ contract('Ballot', ([from, secondary]) => { }) it('should successfully remove vote', async () => { - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); const tx = await ballot.setVote(1); const {args : { @@ -153,7 +155,7 @@ contract('Ballot', ([from, secondary]) => { it('should fail on set new vote of user, which already voted', async () => { let error = false; - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); await ballot.setVote(1); try { error = true; @@ -167,9 +169,9 @@ contract('Ballot', ([from, secondary]) => { describe('closeVoting()', () => { it('should successfully close voting', async () => { - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); increaseTime(web3, 300000); - const tx = await ballot.closeVoting(); + 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(), 0); @@ -177,9 +179,9 @@ contract('Ballot', ([from, secondary]) => { it('should fail on close voting, when time is not over', async () => { let error = false; - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); try { - await ballot.closeVoting(); + await ballot.testCloseVoting(); } catch ({message}) { error = true; assert.strictEqual(message, getErrorMessage('Time is not over yet')); @@ -190,7 +192,7 @@ contract('Ballot', ([from, secondary]) => { describe('events', () => { it('should fire VotingStarted event', async () => { - const tx = await ballot.addVoting(primaryInfo); + const tx = await ballot.testAddVoting(primaryInfo); const {args : {votingId, questionId}} = tx.logs.find(element => element.event.match('VotingStarted')); assert.strictEqual(votingId.toNumber(), 0); @@ -198,7 +200,7 @@ contract('Ballot', ([from, secondary]) => { }); it('should fire UserVote event', async () => { - await ballot.addVoting(primaryInfo); + await ballot.testAddVoting(primaryInfo); const tx = await ballot.setVote(1); const {args : { From 0317c209202a691b27ba3a8a508e6552c1eb3d88 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Wed, 11 Mar 2020 18:59:04 +0700 Subject: [PATCH 21/24] fixes --- contracts/ZeroOne/Ballots/Ballots.sol | 12 +++++++----- contracts/__mocks__/BallotsMock.sol | 9 ++++++--- test/10_Ballot.spec.js | 3 ++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index eeb6606..7205659 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -29,6 +29,8 @@ contract Ballots { event UpdatedUserVote(address group, address user); + event Formula(bytes formula); + /** * @notice reverts on non-existing ballot id * @param _id ballot id @@ -61,6 +63,8 @@ contract Ballots { /** * @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( @@ -74,10 +78,8 @@ contract Ballots { { id = ballots.add(_votingPrimary); - ballots.descriptors[id].executeDescriptors( - formula, - owners - ); + ballots.descriptors[id].executeDescriptors(formula, owners); + emit VotingStarted(id, _votingPrimary.questionId); } @@ -86,7 +88,7 @@ contract Ballots { * and setting BallotStatus.CLOSED * @param votingId id of voting * @param formula formula of voting - * @param owners address of + * @param owners address of owners * @return result */ function closeVoting( diff --git a/contracts/__mocks__/BallotsMock.sol b/contracts/__mocks__/BallotsMock.sol index 5e2abbf..d142bcd 100644 --- a/contracts/__mocks__/BallotsMock.sol +++ b/contracts/__mocks__/BallotsMock.sol @@ -4,22 +4,25 @@ pragma experimental ABIEncoderV2; import "../ZeroOne/Ballots/Ballots.sol"; contract BallotsMock is Ballots { - bytes formula = "0x0041c8a92378323f33fc30b0a10e3fd771eb8f6dff010006010032320704000904000000"; + bytes public formula = hex"0041c8a92378323f33fc30b0a10e3fd771eb8f6dff010006010032320704000904000000"; function testAddVoting( BallotList.BallotSimple memory _votingPrimary ) public { + _votingPrimary.endTime = block.timestamp + 36000; addVoting( _votingPrimary, formula, - 0x41c8a92378323F33FC30B0a10E3fd771Eb8f6DFf + msg.sender ); } + + function testCloseVoting() public { - closeVoting(0, formula, 0x41c8a92378323F33FC30B0a10E3fd771Eb8f6DFf); + closeVoting(0, formula, msg.sender); } } \ No newline at end of file diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index 75f120d..e05f14b 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -50,7 +50,8 @@ contract('Ballot', ([from, secondary]) => { describe('addVoting()', () => { it('should add voting', async () => { - + + // await ballot.setFormula("0x0041c8a92378323f33fc30b0a10e3fd771eb8f6dff010006010032320704000904000000"); const tx = await ballot.testAddVoting(primaryInfo); const {args : {votingId, questionId}} = tx.logs.find(element => element.event.match('VotingStarted')); From 48be965de148967497778550a8c0d04c74071711 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Thu, 12 Mar 2020 10:06:35 +0700 Subject: [PATCH 22/24] Ballots tested --- contracts/__mocks__/BallotsMock.sol | 51 ++++++++++++++++++----------- migrations/3_userGroups.js | 6 ++-- test/10_Ballot.spec.js | 24 +++++++++++--- test/8_CustomTokens.spec.js | 5 +-- 4 files changed, 56 insertions(+), 30 deletions(-) diff --git a/contracts/__mocks__/BallotsMock.sol b/contracts/__mocks__/BallotsMock.sol index d142bcd..6d8f8f2 100644 --- a/contracts/__mocks__/BallotsMock.sol +++ b/contracts/__mocks__/BallotsMock.sol @@ -3,26 +3,37 @@ pragma experimental ABIEncoderV2; import "../ZeroOne/Ballots/Ballots.sol"; +/** + * @title BallotsMock + * @dev Mock for testing Ballots contract + */ contract BallotsMock is Ballots { - bytes public formula = hex"0041c8a92378323f33fc30b0a10e3fd771eb8f6dff010006010032320704000904000000"; + bytes public formula = hex"00BEF946538A29B7c330F6a77f61c5C1c8735a8ace010006010032320704000904000000"; + address owners = 0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace; - function testAddVoting( - BallotList.BallotSimple memory _votingPrimary - ) - public - { - _votingPrimary.endTime = block.timestamp + 36000; - addVoting( - _votingPrimary, - formula, - msg.sender - ); - } + /** + * @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 + ); + } - - function testCloseVoting() - public - { - closeVoting(0, formula, msg.sender); - } -} \ No newline at end of file + /** + * @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/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/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index e05f14b..0037f32 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -7,6 +7,7 @@ const increaseTime = require('./helpers/increase-time'); contract('Ballot', ([from, secondary]) => { let ballot; + let token; const primaryInfo = { starterGroupId: 0, @@ -32,10 +33,10 @@ contract('Ballot', ([from, secondary]) => { group = { name: 'group name' }; - zeroOneVM = await ZeroOneVM.new() + + zeroOneVM = await ZeroOneVM.new(); await Ballot.link("ZeroOneVM", zeroOneVM.address); ballot = await Ballot.new({ from }); - customToken = await CustomToken.new('test', 'tst', 1000, { from }); // await ballot.addQuestion(question) }); @@ -50,8 +51,6 @@ contract('Ballot', ([from, secondary]) => { describe('addVoting()', () => { it('should add voting', async () => { - - // await ballot.setFormula("0x0041c8a92378323f33fc30b0a10e3fd771eb8f6dff010006010032320704000904000000"); const tx = await ballot.testAddVoting(primaryInfo); const {args : {votingId, questionId}} = tx.logs.find(element => element.event.match('VotingStarted')); @@ -119,6 +118,9 @@ contract('Ballot', ([from, secondary]) => { describe('setVote()', () => { it('should successfully set Positive vote', async () => { + token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + token.addToProjects(ballot.address); + await ballot.testAddVoting(primaryInfo); const tx = await ballot.setVote(1); @@ -131,6 +133,9 @@ contract('Ballot', ([from, secondary]) => { }) it('should successfully set Negative vote', async () => { + token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + token.addToProjects(ballot.address); + await ballot.testAddVoting(primaryInfo); const tx = await ballot.setVote(2); @@ -143,6 +148,9 @@ contract('Ballot', ([from, secondary]) => { }) it('should successfully remove vote', async () => { + token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + token.addToProjects(ballot.address); + await ballot.testAddVoting(primaryInfo); const tx = await ballot.setVote(1); @@ -155,6 +163,9 @@ contract('Ballot', ([from, secondary]) => { }) it('should fail on set new vote of user, which already voted', async () => { + token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + token.addToProjects(ballot.address); + let error = false; await ballot.testAddVoting(primaryInfo); await ballot.setVote(1); @@ -175,7 +186,7 @@ contract('Ballot', ([from, secondary]) => { 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(), 0); + assert.strictEqual(descision.toNumber(), 1); }) it('should fail on close voting, when time is not over', async () => { @@ -201,6 +212,9 @@ contract('Ballot', ([from, secondary]) => { }); 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); 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(); From 65157bc49126e40ae8dd00ef73d74bd591019a7e Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Thu, 12 Mar 2020 14:49:28 +0700 Subject: [PATCH 23/24] fixes and tests --- contracts/Token/CustomToken.sol | 5 +-- contracts/ZeroOne/Ballots/Ballots.sol | 22 +++++------- contracts/ZeroOne/Ballots/IBallots.sol | 4 +-- test/10_Ballot.spec.js | 46 +++++++++++++++++--------- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/contracts/Token/CustomToken.sol b/contracts/Token/CustomToken.sol index c2e44a6..cf6a0dd 100644 --- a/contracts/Token/CustomToken.sol +++ b/contracts/Token/CustomToken.sol @@ -192,7 +192,8 @@ contract CustomToken is Ownable { for (uint i = 1; i < projects.length - 1; i++) { if (isTokenLocked(projects[i], _user)) { IBallots project = IBallots(projects[i]); - project.updateUserVote(address(this), _user, balanceOf(_user)); + 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); diff --git a/contracts/ZeroOne/Ballots/Ballots.sol b/contracts/ZeroOne/Ballots/Ballots.sol index 7205659..6242675 100644 --- a/contracts/ZeroOne/Ballots/Ballots.sol +++ b/contracts/ZeroOne/Ballots/Ballots.sol @@ -17,19 +17,15 @@ contract Ballots { 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); - - event Formula(bytes formula); + event UpdatedUserVote(address group, address user, VM.Vote userVote); /** * @notice reverts on non-existing ballot id @@ -77,9 +73,7 @@ contract Ballots { returns (uint id) { id = ballots.add(_votingPrimary); - ballots.descriptors[id].executeDescriptors(formula, owners); - emit VotingStarted(id, _votingPrimary.questionId); } @@ -113,7 +107,6 @@ contract Ballots { ); } - /** * @dev getting the voting by id * @param _id id of voting @@ -252,7 +245,7 @@ contract Ballots { 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, msg.sender)) { + if (didUserVote(group.groupAddress, _user)) { ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); setGroupVotes(group.groupAddress, votingId, i); } @@ -260,12 +253,12 @@ contract Ballots { } if ( user.groupAddress != address(0) && user.groupAddress == _group ) { - if ( user.admin == true && IERC20(user.groupAddress).owner() == msg.sender ) { - user.userAddress = msg.sender; + if ( user.admin == true && IERC20(user.groupAddress).owner() == _user ) { + user.userAddress = _user; } if (user.userAddress != address(0)) { - if (didUserVote(user.groupAddress, msg.sender)) { + if (didUserVote(user.groupAddress, _user)) { ballots.list[votingId].updateUserVote(_group, _user, _newVoteWeight); if (_newVoteWeight == 0) { user.vote = VM.Vote.UNDEFINED; @@ -274,7 +267,9 @@ contract Ballots { } } } - emit UpdatedUserVote(_group, _user); + + VM.Vote userVote = getUserVote(votingId, _group, _user); + emit UpdatedUserVote(_group, _user, userVote); return true; } @@ -340,7 +335,6 @@ contract Ballots { ballotExist(_votingId) returns (VM.Vote descision) { - return ballots.list[_votingId].votes[_group][_user]; } diff --git a/contracts/ZeroOne/Ballots/IBallots.sol b/contracts/ZeroOne/Ballots/IBallots.sol index d847203..46d9fba 100644 --- a/contracts/ZeroOne/Ballots/IBallots.sol +++ b/contracts/ZeroOne/Ballots/IBallots.sol @@ -3,13 +3,13 @@ pragma solidity 0.6.1; interface IBallots { - function updateUserVote(address project, address user, uint256 newVoteWeight) external returns(bool); + 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(address tokenAddr, address user) external view returns(uint); + function getUserVote(uint votingId, address tokenAddr, address user) external view returns(uint); function submitVoting() external; diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index 0037f32..cba481f 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -37,7 +37,7 @@ contract('Ballot', ([from, secondary]) => { zeroOneVM = await ZeroOneVM.new(); await Ballot.link("ZeroOneVM", zeroOneVM.address); ballot = await Ballot.new({ from }); - + token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); // await ballot.addQuestion(question) }); @@ -118,7 +118,7 @@ contract('Ballot', ([from, secondary]) => { describe('setVote()', () => { it('should successfully set Positive vote', async () => { - token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + token.addToProjects(ballot.address); await ballot.testAddVoting(primaryInfo); @@ -133,7 +133,6 @@ contract('Ballot', ([from, secondary]) => { }) it('should successfully set Negative vote', async () => { - token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); token.addToProjects(ballot.address); await ballot.testAddVoting(primaryInfo); @@ -147,12 +146,29 @@ contract('Ballot', ([from, secondary]) => { assert.strictEqual(descision.toNumber(), 2) }) - it('should successfully remove vote', async () => { - token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + 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 @@ -160,22 +176,20 @@ contract('Ballot', ([from, secondary]) => { 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 fail on set new vote of user, which already voted', async () => { - token = await CustomToken.at('0xBEF946538A29B7c330F6a77f61c5C1c8735a8ace'); + it('should not set new vote of user, which already voted', async () => { token.addToProjects(ballot.address); - - let error = false; await ballot.testAddVoting(primaryInfo); await ballot.setVote(1); - try { - error = true; - await ballot.setVote(2); - } catch ({message}) { - assert.strictEqual(message, getErrorMessage('User already vote')); - } - assert.strictEqual(error, true) + await ballot.setVote(2); + const userVote = await ballot.getUserVote(0, token.address, from) + assert.strictEqual(userVote.toNumber(), 1); }) }) From dd5b02be46724a1e60d67a11a8aaf16eb0ad62a4 Mon Sep 17 00:00:00 2001 From: pavkahanov Date: Thu, 12 Mar 2020 14:50:06 +0700 Subject: [PATCH 24/24] uncommented one condition --- test/10_Ballot.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/10_Ballot.spec.js b/test/10_Ballot.spec.js index cba481f..cf243e8 100644 --- a/test/10_Ballot.spec.js +++ b/test/10_Ballot.spec.js @@ -161,7 +161,7 @@ contract('Ballot', ([from, secondary]) => { 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(userVote.toNumber(), 0); assert.strictEqual(voteWeight.toNumber(), 0); })