diff --git a/contracts/extensions/VotingBase.sol b/contracts/extensions/VotingBase.sol new file mode 100644 index 0000000000..6400876580 --- /dev/null +++ b/contracts/extensions/VotingBase.sol @@ -0,0 +1,57 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.5.8; +pragma experimental ABIEncoderV2; + +import "../../lib/dappsys/math.sol"; +import "../IColony.sol"; +import "../IColonyNetwork.sol"; + + +contract VotingBase is DSMath { + + uint64 constant REVEAL_PERIOD = 2 days; + + enum PollState { Open, Reveal, Closed } + + IColony colony; + IColonyNetwork colonyNetwork; + + constructor(address _colony) public { + colony = IColony(_colony); + colonyNetwork = IColonyNetwork(colony.getColonyNetwork()); + } + + function getVoteSecret(bytes32 _salt, uint64 _vote) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_salt, _vote)); + } + + function getPollState(uint64 _pollCloseTime) public view returns (PollState) { + if (_pollCloseTime < uint64(now)) { + return PollState.Open; + } else if (add64(_pollCloseTime, REVEAL_PERIOD) < uint64(now)) { + return PollState.Reveal; + } else { + return PollState.Closed; + } + } + + function add64(uint64 x, uint64 y) internal pure returns (uint64 z) { + require((z = x + y) >= x, "voting-base-add64-overflow"); + } +} diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol new file mode 100644 index 0000000000..3be0c19c56 --- /dev/null +++ b/contracts/extensions/VotingReputation.sol @@ -0,0 +1,119 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.5.8; +pragma experimental ABIEncoderV2; + +import "../PatriciaTree/PatriciaTreeProofs.sol"; +import "./VotingBase.sol"; + + +contract VotingReputation is VotingBase, PatriciaTreeProofs { + + struct Poll { + uint64 numOutcomes; + uint64 pollCloses; + bytes32 rootHash; + uint256 skillId; + mapping (uint64 => uint256) voteCounts; + } + + // The UserVote type here is just the bytes32 voteSecret + + uint256 pollCount; + mapping (uint256 => Poll) polls; + mapping (address => mapping (uint256 => bytes32)) userVotes; + + function createPoll(uint64 _numOutcomes, uint64 _duration, uint256 _skillId) public { + pollCount += 1; + polls[pollCount] = Poll({ + numOutcomes: _numOutcomes, + pollCloses: add64(uint64(now), _duration), + rootHash: colonyNetwork.getReputationRootHash(), + skillId: _skillId + }); + } + + function submitVote(uint256 _pollId, bytes32 _voteSecret) public { + uint64 pollCloses = polls[_pollId].pollCloses; + require(getPollState(pollCloses) == PollState.Open, "colony-rep-voting-poll-not-open"); + + userVotes[msg.sender][_pollId] = _voteSecret; + } + + function revealVote( + uint256 _pollId, + bytes32 _salt, + uint64 _vote, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + uint64 pollCloses = polls[_pollId].pollCloses; + require(getPollState(pollCloses) != PollState.Open, "colony-rep-voting-poll-still-open"); + + bytes32 voteSecret = getVoteSecret(_salt, _vote); + require(userVotes[msg.sender][_pollId] == voteSecret, "colony-rep-voting-secret-no-match"); + + // Validate proof and get reputation value + uint256 userReputation = checkReputation(_pollId, _key, _value, _branchMask, _siblings); + + // Remove the secret + delete userVotes[msg.sender][_pollId]; + + // Increment the vote if poll in reveal, otherwise skip + // NOTE: since there's no locking, we could just `require` PollState.Reveal + if (getPollState(pollCloses) == PollState.Reveal) { + polls[_pollId].voteCounts[_vote] += userReputation; + } + } + + function checkReputation( + uint256 _pollId, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + internal view returns (uint256) + { + bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); + require(polls[_pollId].rootHash == impliedRoot, "colony-rep-voting-invalid-root-hash"); + + uint256 reputationValue; + address keyColonyAddress; + uint256 keySkill; + address keyUserAddress; + + assembly { + reputationValue := mload(add(_value, 32)) + keyColonyAddress := mload(add(_key, 20)) + keySkill := mload(add(_key, 52)) + keyUserAddress := mload(add(_key, 72)) + } + + require(keyColonyAddress == address(colony), "colony-rep-voting-invalid-colony-address"); + require(keySkill == polls[_pollId].skillId, "colony-rep-voting-invalid-skill-id"); + require(keyUserAddress == msg.sender, "colony-rep-voting-invalid-user-address"); + + return reputationValue; + } + +} diff --git a/contracts/extensions/VotingToken.sol b/contracts/extensions/VotingToken.sol new file mode 100644 index 0000000000..c62e57cded --- /dev/null +++ b/contracts/extensions/VotingToken.sol @@ -0,0 +1,115 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.5.8; +pragma experimental ABIEncoderV2; + +import "../ITokenLocking.sol"; +import "./VotingBase.sol"; + + +contract VotingToken is VotingBase { + + struct Poll { + uint64 numOutcomes; + uint64 pollCloses; + mapping (uint64 => uint256) voteCounts; + } + + struct UserVote { + uint256 pollId; + bytes32 voteSecret; + uint64 prevPollCloses; + uint64 nextPollCloses; + } + + uint256 pollCount; + mapping (uint256 => Poll) polls; + mapping (address => mapping (uint64 => UserVote)) userVotes; + + function createPoll(uint64 _numOutcomes, uint64 _duration) public { + pollCount += 1; + polls[pollCount] = Poll({ + numOutcomes: _numOutcomes, + pollCloses: add64(uint64(now), _duration) + }); + } + + // TODO: Implement inner linked list + function submitVote(uint256 _pollId, bytes32 _voteSecret, uint64 _prevPollCloses, uint256 _prevPollId) public { + uint64 pollCloses = polls[_pollId].pollCloses; + require(getPollState(pollCloses) == PollState.Open, "colony-token-voting-poll-not-open"); + + UserVote storage prev = userVotes[msg.sender][_prevPollCloses]; + UserVote storage next = userVotes[msg.sender][prev.nextPollCloses]; + + // Check we are inserting at the correct location + require(pollCloses > _prevPollCloses, "colony-token-voting-insert-too-soon"); + require(pollCloses < prev.nextPollCloses || prev.nextPollCloses == 0, "colony-token-voting-insert-too-late"); + + userVotes[msg.sender][pollCloses] = UserVote({ + pollId: _pollId, + voteSecret: _voteSecret, + prevPollCloses: _prevPollCloses, + nextPollCloses: prev.nextPollCloses + }); + + prev.nextPollCloses = pollCloses; + next.prevPollCloses = pollCloses; + } + + function revealVote(uint256 _pollId, bytes32 _salt, uint64 _vote) public { + uint64 pollCloses = polls[_pollId].pollCloses; + require(getPollState(pollCloses) != PollState.Open, "colony-token-voting-poll-still-open"); + + UserVote storage curr = userVotes[msg.sender][pollCloses]; + UserVote storage prev = userVotes[msg.sender][curr.prevPollCloses]; + UserVote storage next = userVotes[msg.sender][curr.nextPollCloses]; + + bytes32 voteSecret = getVoteSecret(_salt, _vote); + require(curr.voteSecret == voteSecret, "colony-token-voting-secret-no-match"); + + // Remove the secret + prev.nextPollCloses = curr.nextPollCloses; + next.prevPollCloses = curr.prevPollCloses; + delete userVotes[msg.sender][pollCloses]; + + // Increment the vote if poll in reveal + if (getPollState(pollCloses) == PollState.Reveal) { + address token = colony.getToken(); + address tokenLocking = colonyNetwork.getTokenLocking(); + uint256 userBalance = ITokenLocking(tokenLocking).getUserLock(token, msg.sender).balance; + polls[_pollId].voteCounts[_vote] += userBalance; + } + } + + function isAddressLocked(address _address) public view returns (bool) { + uint64 nextPollCloses = userVotes[_address][0].nextPollCloses; + if (nextPollCloses == 0) { + // The list is empty, no unrevealed votes for this address + return false; + } else if (now < nextPollCloses) { + // The poll is still open for voting and tokens transfer + return false; + } else { + // The poll is closed for voting and is in the reveal period, during which all votes' tokens are locked until reveal + // Note: even after the poll is resolved, tokens remain locked until reveal + return true; + } + } + +}