From bf0ecdbea69d00ce9435fa1f52e2ae4660f87c61 Mon Sep 17 00:00:00 2001 From: orenyodfat Date: Thu, 5 Dec 2019 10:47:43 +0200 Subject: [PATCH] Competition contract + ContributionRewardExt scheme (#697) * first version * comp version * wip : competition scheme * add redeem to competition. * remove experimental pragma * wip add contributionrewardext test * tests * tests * fixes and cosmetics * fix refreshTopSuggestions * add initilize function * update tests * naming address payable _suggester * remove redundent line * update pr comments * add sendLeftOverFunds * naming + tests * comments * bump version to 0.0.1-rc.35 increase GAS_LIMIT to 62000000 due to new openzeppeling-solidity * better test timing setup * suggestionEndTime test --- contracts/schemes/Competition.sol | 395 ++++++++++++ contracts/schemes/ContributionRewardExt.sol | 465 ++++++++++++++ contracts/utils/Redeemer.sol | 107 +++- package-lock.json | 15 +- package.json | 6 +- test/competition.js | 632 ++++++++++++++++++ test/constants.js | 2 +- test/contributionrewardext.js | 670 ++++++++++++++++++++ 8 files changed, 2279 insertions(+), 13 deletions(-) create mode 100644 contracts/schemes/Competition.sol create mode 100644 contracts/schemes/ContributionRewardExt.sol create mode 100644 test/competition.js create mode 100644 test/contributionrewardext.js diff --git a/contracts/schemes/Competition.sol b/contracts/schemes/Competition.sol new file mode 100644 index 00000000..9d6f1a19 --- /dev/null +++ b/contracts/schemes/Competition.sol @@ -0,0 +1,395 @@ +pragma solidity 0.5.13; + +import "./ContributionRewardExt.sol"; + + +contract Competition { + using SafeMath for uint256; + + uint256 constant public MAX_NUMBER_OF_WINNERS = 100; + + event NewCompetitionProposal( + bytes32 indexed _proposalId, + uint256 _numberOfWinners, + uint256[] _rewardSplit, + uint256 _startTime, + uint256 _votingStartTime, + uint256 _suggestionsEndTime, + uint256 _endTime, + uint256 _maxNumberOfVotesPerVoter, + address payable _contributionRewardExt //address of the contract to redeem from. + ); + + event Redeem( + bytes32 indexed _proposalId, + uint256 indexed _suggestionId, + uint256 _rewardPercentage + ); + + event NewSuggestion( + bytes32 indexed _proposalId, + uint256 indexed _suggestionId, + string indexed _descriptionHash, + address payable _suggester + ); + + event NewVote( + bytes32 indexed _proposalId, + uint256 indexed _suggestionId, + address indexed _voter, + uint256 _reputation + ); + + event SnapshotBlock( + bytes32 indexed _proposalId, + uint256 _snapshotBlock + ); + + // A struct holding the data for a competition proposal + struct Proposal { + uint256 numberOfWinners; + uint256[] rewardSplit; + uint256 startTime; + uint256 votingStartTime; + uint256 suggestionsEndTime; + uint256 endTime; + uint256 maxNumberOfVotesPerVoter; + address payable contributionRewardExt; + uint256 snapshotBlock; + uint256 reputationReward; + uint256 ethReward; + uint256 nativeTokenReward; + uint256 externalTokenReward; + uint256[] topSuggestions; + //mapping from suggestions totalVotes to the number of suggestions with the same totalVotes. + mapping(uint256=>uint256) suggestionsPerVote; + mapping(address=>uint256) votesPerVoter; + } + + struct Suggestion { + uint256 totalVotes; + bytes32 proposalId; + address payable suggester; + mapping(address=>uint256) votes; + } + + //mapping from proposalID to Proposal + mapping(bytes32=>Proposal) public proposals; + //mapping from suggestionId to Suggestion + mapping(uint256=>Suggestion) public suggestions; + uint256 public suggestionsCounter; + address payable public contributionRewardExt; //address of the contract to redeem from. + + /** + * @dev initialize + * @param _contributionRewardExt the contributionRewardExt scheme which + * manage and allocate the rewards for the competition. + */ + function initialize(address payable _contributionRewardExt) external { + require(contributionRewardExt == address(0), "can be called only one time"); + require(_contributionRewardExt != address(0), "contributionRewardExt cannot be zero"); + contributionRewardExt = _contributionRewardExt; + } + + /** + * @dev Submit a competion proposal + * @param _descriptionHash A hash of the proposal's description + * @param _reputationChange - Amount of reputation change requested .Can be negative. + * @param _rewards rewards array: + * rewards[0] - Amount of tokens requested per period + * rewards[1] - Amount of ETH requested per period + * rewards[2] - Amount of external tokens requested per period + * @param _externalToken Address of external token, if reward is requested there + * @param _rewardSplit an array of precentages which specify how to split the rewards + * between the winning suggestions + * @param _competitionParams competition parameters : + * _competitionParams[0] - competition startTime + * _competitionParams[1] - _votingStartTime competition voting start time + * _competitionParams[2] - _endTime competition end time + * _competitionParams[3] - _maxNumberOfVotesPerVoter on how many suggestions a voter can vote + * _competitionParams[4] - _suggestionsEndTime suggestion submition end time + * @return proposalId the proposal id. + */ + function proposeCompetition( + string calldata _descriptionHash, + int256 _reputationChange, + uint[3] calldata _rewards, + IERC20 _externalToken, + uint256[] calldata _rewardSplit, + uint256[5] calldata _competitionParams + ) + external + returns(bytes32 proposalId) { + uint256 numberOfWinners = _rewardSplit.length; + uint256 startTime = _competitionParams[0]; + if (startTime == 0) { + // solhint-disable-next-line not-rely-on-time + startTime = now; + } + // solhint-disable-next-line not-rely-on-time + require(startTime >= now, "startTime should be greater than proposing time"); + require(numberOfWinners <= MAX_NUMBER_OF_WINNERS, "number of winners greater than max allowed"); + require(_competitionParams[1] < _competitionParams[2], "voting start time greater than end time"); + require(_competitionParams[1] >= startTime, "voting start time smaller than start time"); + require(_competitionParams[3] > 0, "maxNumberOfVotesPerVoter should be greater than 0"); + require(_competitionParams[4] <= _competitionParams[2], + "suggestionsEndTime should be earlier than proposal end time"); + require(_competitionParams[4] > startTime, "suggestionsEndTime should be later than proposal start time"); + uint256 totalRewardSplit; + for (uint256 i = 0; i < numberOfWinners; i++) { + totalRewardSplit = totalRewardSplit.add(_rewardSplit[i]); + } + require(totalRewardSplit == 100, "total rewards split is not 100%"); + proposalId = ContributionRewardExt(contributionRewardExt).proposeContributionReward( + _descriptionHash, + _reputationChange, + _rewards, + _externalToken, + contributionRewardExt, + msg.sender); + proposals[proposalId].numberOfWinners = numberOfWinners; + proposals[proposalId].rewardSplit = _rewardSplit; + proposals[proposalId].startTime = startTime; + proposals[proposalId].votingStartTime = _competitionParams[1]; + proposals[proposalId].endTime = _competitionParams[2]; + proposals[proposalId].maxNumberOfVotesPerVoter = _competitionParams[3]; + proposals[proposalId].suggestionsEndTime = _competitionParams[4]; + proposals[proposalId].reputationReward = uint256(_reputationChange); + proposals[proposalId].nativeTokenReward = _rewards[0]; + proposals[proposalId].ethReward = _rewards[1]; + proposals[proposalId].externalTokenReward = _rewards[2]; + + emit NewCompetitionProposal( + proposalId, + numberOfWinners, + proposals[proposalId].rewardSplit, + startTime, + proposals[proposalId].votingStartTime, + proposals[proposalId].suggestionsEndTime, + proposals[proposalId].endTime, + proposals[proposalId].maxNumberOfVotesPerVoter, + contributionRewardExt + ); + } + + /** + * @dev submit a competion suggestion + * @param _proposalId the proposal id this suggestion is referring to. + * @param _descriptionHash a descriptionHash of the suggestion. + * @return suggestionId the suggestionId. + */ + function suggest( + bytes32 _proposalId, + string calldata _descriptionHash + ) + external + returns(uint256) + { + // solhint-disable-next-line not-rely-on-time + require(proposals[_proposalId].startTime <= now, "competition not started yet"); + // solhint-disable-next-line not-rely-on-time + require(proposals[_proposalId].suggestionsEndTime > now, "suggestions submition time is over"); + suggestionsCounter = suggestionsCounter.add(1); + suggestions[suggestionsCounter].proposalId = _proposalId; + suggestions[suggestionsCounter].suggester = msg.sender; + emit NewSuggestion(_proposalId, suggestionsCounter, _descriptionHash, msg.sender); + return suggestionsCounter; + } + + /** + * @dev vote on a suggestion + * @param _suggestionId suggestionId + * @return bool + */ + function vote(uint256 _suggestionId) + external + returns(bool) + { + bytes32 proposalId = suggestions[_suggestionId].proposalId; + require(proposalId != bytes32(0), "suggestion does not exist"); + setSnapshotBlock(proposalId); + Avatar avatar = ContributionRewardExt(contributionRewardExt).avatar(); + uint256 reputation = avatar.nativeReputation().balanceOfAt(msg.sender, proposals[proposalId].snapshotBlock); + require(reputation > 0, "voter had no reputation when snapshot was taken"); + Proposal storage proposal = proposals[proposalId]; + // solhint-disable-next-line not-rely-on-time + require(proposal.endTime > now, "competition ended"); + Suggestion storage suggestion = suggestions[_suggestionId]; + require(suggestion.votes[msg.sender] == 0, "already voted on this suggestion"); + require(proposal.votesPerVoter[msg.sender] < proposal.maxNumberOfVotesPerVoter, + "exceed number of votes allowed"); + proposal.votesPerVoter[msg.sender] = proposal.votesPerVoter[msg.sender].add(1); + if (suggestion.totalVotes > 0) { + proposal.suggestionsPerVote[suggestion.totalVotes] = + proposal.suggestionsPerVote[suggestion.totalVotes].sub(1); + } + suggestion.totalVotes = suggestion.totalVotes.add(reputation); + proposal.suggestionsPerVote[suggestion.totalVotes] = proposal.suggestionsPerVote[suggestion.totalVotes].add(1); + suggestion.votes[msg.sender] = reputation; + refreshTopSuggestions(proposalId, _suggestionId); + emit NewVote(proposalId, _suggestionId, msg.sender, reputation); + return true; + } + + /** + * @dev redeem a winning suggestion reward + * @param _suggestionId suggestionId + * @param _beneficiary - the reward beneficiary. + * this parameter is take into account only if the msg.sender is the suggestion's suggester, + * otherwise the _beneficiary param is ignored and the beneficiary is suggestion's suggester. + */ + function redeem(uint256 _suggestionId, address payable _beneficiary) external { + address payable beneficiary = suggestions[_suggestionId].suggester; + if ((msg.sender == suggestions[_suggestionId].suggester) && + (_beneficiary != address(0))) { + //only suggester can redeem to other address + beneficiary = _beneficiary; + } + _redeem(_suggestionId, beneficiary); + } + + /** + * @dev setSnapshotBlock set the block for the reputaion snapshot + * @param _proposalId the proposal id + */ + function setSnapshotBlock(bytes32 _proposalId) public { + // solhint-disable-next-line not-rely-on-time + require(proposals[_proposalId].votingStartTime < now, "voting period not started yet"); + if (proposals[_proposalId].snapshotBlock == 0) { + proposals[_proposalId].snapshotBlock = block.number; + emit SnapshotBlock(_proposalId, block.number); + } + } + + /** + * @dev sendLeftOverFund send letf over funds back to the dao. + * @param _proposalId the proposal id + */ + function sendLeftOverFunds(bytes32 _proposalId) public { + // solhint-disable-next-line not-rely-on-time + require(proposals[_proposalId].endTime < now, "competition is still on"); + uint256[] memory topSuggestions = proposals[_proposalId].topSuggestions; + for (uint256 i; i < topSuggestions.length; i++) { + require(suggestions[topSuggestions[i]].suggester == address(0), "not all winning suggestions redeemed"); + } + + (, , , , , , + uint256 nativeTokenRewardLeft, , + uint256 ethRewardLeft, + uint256 externalTokenRewardLeft,) + = ContributionRewardExt(contributionRewardExt).organizationProposals(_proposalId); + + Avatar avatar = ContributionRewardExt(contributionRewardExt).avatar(); + + ContributionRewardExt(contributionRewardExt).redeemExternalTokenByRewarder( + _proposalId, address(avatar), externalTokenRewardLeft); + + ContributionRewardExt(contributionRewardExt).redeemEtherByRewarder( + _proposalId, address(avatar), ethRewardLeft); + + ContributionRewardExt(contributionRewardExt).redeemNativeTokenByRewarder( + _proposalId, address(avatar), nativeTokenRewardLeft); + } + + /** + * @dev getOrderedIndexOfSuggestion return the index of specific suggestion in the winners list. + * @param _suggestionId suggestion id + */ + function getOrderedIndexOfSuggestion(uint256 _suggestionId) + public + view + returns(uint256 index) { + bytes32 proposalId = suggestions[_suggestionId].proposalId; + require(proposalId != bytes32(0), "suggestion does not exist"); + uint256[] memory topSuggestions = proposals[proposalId].topSuggestions; + /** get how many elements are greater than a given element*/ + for (uint256 i; i < topSuggestions.length; i++) { + if (suggestions[topSuggestions[i]].totalVotes > suggestions[_suggestionId].totalVotes) { + index++; + } + } + } + + /** + * @dev refreshTopSuggestions this function maintain a winners list array. + * it will check if the given suggestion is among the top suggestions, and if so, + * update the list of top suggestions + * @param _proposalId proposal id + * @param _suggestionId suggestion id + */ + function refreshTopSuggestions(bytes32 _proposalId, uint256 _suggestionId) private { + uint256[] storage topSuggestions = proposals[_proposalId].topSuggestions; + if (topSuggestions.length < proposals[_proposalId].numberOfWinners) { + topSuggestions.push(_suggestionId); + } else { + /** get the index of the smallest element **/ + uint256 smallest = 0; + for (uint256 i; i < proposals[_proposalId].numberOfWinners; i++) { + if (suggestions[topSuggestions[i]].totalVotes < + suggestions[topSuggestions[smallest]].totalVotes) { + smallest = i; + } else if (topSuggestions[i] == _suggestionId) { + //the suggestion is already in the topSuggestions list + return; + } + } + + if (suggestions[topSuggestions[smallest]].totalVotes < suggestions[_suggestionId].totalVotes) { + topSuggestions[smallest] = _suggestionId; + } + } + } + + /** + * @dev redeem a winning suggestion reward + * @param _suggestionId suggestionId + * @param _beneficiary - the reward beneficiary + */ + function _redeem(uint256 _suggestionId, address payable _beneficiary) private { + bytes32 proposalId = suggestions[_suggestionId].proposalId; + Proposal storage proposal = proposals[proposalId]; + // solhint-disable-next-line not-rely-on-time + require(proposal.endTime < now, "competition is still on"); + require(suggestions[_suggestionId].suggester != address(0), + "suggestion was already redeemed"); + uint256 orderIndex = getOrderedIndexOfSuggestion(_suggestionId); + require(orderIndex < proposal.topSuggestions.length, "suggestion is not in winners list"); + suggestions[_suggestionId].suggester = address(0); + uint256 rewardPercentage = 0; + uint256 numberOfTieSuggestions = proposal.suggestionsPerVote[suggestions[_suggestionId].totalVotes]; + uint256 j; + //calc the reward percentage for this suggestion + for (j = orderIndex; j < (orderIndex+numberOfTieSuggestions) && j < proposal.rewardSplit.length; j++) { + rewardPercentage = rewardPercentage.add(proposal.rewardSplit[j]); + } + rewardPercentage = rewardPercentage.div(numberOfTieSuggestions); + uint256 rewardPercentageLeft = 0; + if (proposal.topSuggestions.length < proposal.numberOfWinners) { + //if there are less winners than the proposal number of winners so divide the pre allocated + //left reward equally between the winners + for (j = proposal.topSuggestions.length; j < proposal.numberOfWinners; j++) { + rewardPercentageLeft = rewardPercentageLeft.add(proposal.rewardSplit[j]); + } + rewardPercentage = + rewardPercentage.add(rewardPercentageLeft.div(proposal.topSuggestions.length)); + } + uint256 amount; + amount = proposal.externalTokenReward.mul(rewardPercentage).div(100); + ContributionRewardExt(contributionRewardExt).redeemExternalTokenByRewarder( + proposalId, _beneficiary, amount); + + amount = proposal.reputationReward.mul(rewardPercentage).div(100); + ContributionRewardExt(contributionRewardExt).redeemReputationByRewarder( + proposalId, _beneficiary, amount); + + amount = proposal.ethReward.mul(rewardPercentage).div(100); + ContributionRewardExt(contributionRewardExt).redeemEtherByRewarder( + proposalId, _beneficiary, amount); + + amount = proposal.nativeTokenReward.mul(rewardPercentage).div(100); + ContributionRewardExt(contributionRewardExt).redeemNativeTokenByRewarder( + proposalId, _beneficiary, amount); + emit Redeem(proposalId, _suggestionId, rewardPercentage); + } + +} diff --git a/contracts/schemes/ContributionRewardExt.sol b/contracts/schemes/ContributionRewardExt.sol new file mode 100644 index 00000000..d921a1b5 --- /dev/null +++ b/contracts/schemes/ContributionRewardExt.sol @@ -0,0 +1,465 @@ +pragma solidity 0.5.13; + +import "@daostack/infra/contracts/votingMachines/IntVoteInterface.sol"; +import "@daostack/infra/contracts/votingMachines/VotingMachineCallbacksInterface.sol"; +import "../votingMachines/VotingMachineCallbacks.sol"; +import "../libs/SafeERC20.sol"; + + +/** + * @title A scheme for proposing and rewarding contributions to an organization + * @dev An agent can ask an organization to recognize a contribution and reward + * him with token, reputation, ether or any combination. + * This scheme extend the functionality of the ContributionReward scheme. + * It enable to assign a rewarder, which, after the contributionreward has been accepted, + * can then later distribute the assets as it would like. + */ +contract ContributionRewardExt is VotingMachineCallbacks, ProposalExecuteInterface { + using SafeMath for uint; + using SafeERC20 for address; + + event NewContributionProposal( + address indexed _avatar, + bytes32 indexed _proposalId, + address indexed _intVoteInterface, + string _descriptionHash, + int256 _reputationChange, + uint[3] _rewards, + IERC20 _externalToken, + address _beneficiary, + address _proposer + ); + + event ProposalExecuted(address indexed _avatar, bytes32 indexed _proposalId, int256 _param); + + event RedeemReputation( + address indexed _avatar, + bytes32 indexed _proposalId, + address indexed _beneficiary, + int256 _amount); + + event RedeemEther(address indexed _avatar, + bytes32 indexed _proposalId, + address indexed _beneficiary, + uint256 _amount); + + event RedeemNativeToken(address indexed _avatar, + bytes32 indexed _proposalId, + address indexed _beneficiary, + uint256 _amount); + + event RedeemExternalToken(address indexed _avatar, + bytes32 indexed _proposalId, + address indexed _beneficiary, + uint256 _amount); + + // A struct holding the data for a contribution proposal + struct ContributionProposal { + uint256 nativeTokenReward; // Reward asked in the native token of the organization. + int256 reputationChange; // Organization reputation reward requested. + uint256 ethReward; + IERC20 externalToken; + uint256 externalTokenReward; + address payable beneficiary; + uint256 nativeTokenRewardLeft; + uint256 reputationChangeLeft; + uint256 ethRewardLeft; + uint256 externalTokenRewardLeft; + bool acceptedByVotingMachine; + } + + modifier onlyRewarder() { + if (rewarder != address(0)) { + require(msg.sender == rewarder, "msg.sender is not authorized"); + } + _; + } + + mapping(bytes32=>ContributionProposal) public organizationProposals; + + IntVoteInterface public votingMachine; + bytes32 public voteParams; + Avatar public avatar; + address public rewarder; + + /** + * @dev enables this contract to receive ethers + */ + function() external payable { + } + + /** + * @dev initialize + * @param _avatar the avatar to mint reputation from + * @param _votingMachine the voting machines address + * @param _voteParams voting machine parameters + * @param _rewarder an address which allowed to redeem the contribution. + if _rewarder is 0 this param is agnored. + */ + function initialize( + Avatar _avatar, + IntVoteInterface _votingMachine, + bytes32 _voteParams, + address _rewarder + ) + external + { + require(avatar == Avatar(0), "can be called only one time"); + require(_avatar != Avatar(0), "avatar cannot be zero"); + avatar = _avatar; + votingMachine = _votingMachine; + voteParams = _voteParams; + rewarder = _rewarder; + } + + /** + * @dev execution of proposals, can only be called by the voting machine in which the vote is held. + * @param _proposalId the ID of the proposal in the voting machine + * @param _decision a parameter of the voting result, 1 yes and 2 is no. + */ + function executeProposal(bytes32 _proposalId, int256 _decision) + external + onlyVotingMachine(_proposalId) + returns(bool) { + require(organizationProposals[_proposalId].acceptedByVotingMachine == false); + require(organizationProposals[_proposalId].beneficiary != address(0)); + if (_decision == 1) { + organizationProposals[_proposalId].acceptedByVotingMachine = true; + } + emit ProposalExecuted(address(avatar), _proposalId, _decision); + return true; + } + + /** + * @dev Submit a proposal for a reward for a contribution: + * @param _descriptionHash A hash of the proposal's description + * @param _reputationChange - Amount of reputation change requested .Can be negative. + * @param _rewards rewards array: + * rewards[0] - Amount of tokens requested + * rewards[1] - Amount of ETH requested + * rewards[2] - Amount of external tokens + * @param _externalToken Address of external token, if reward is requested there + * @param _beneficiary Who gets the rewards. if equal to 0 the beneficiary will be msg.sender. + * @param _proposer proposer . if equal to 0 the proposer will be msg.sender. + */ + function proposeContributionReward( + string memory _descriptionHash, + int256 _reputationChange, + uint[3] memory _rewards, + IERC20 _externalToken, + address payable _beneficiary, + address _proposer + ) + public + returns(bytes32 proposalId) + { + address proposer = _proposer; + if (proposer == address(0)) { + proposer = msg.sender; + } + proposalId = votingMachine.propose(2, voteParams, proposer, address(avatar)); + address payable beneficiary = _beneficiary; + if (beneficiary == address(0)) { + beneficiary = msg.sender; + } + + ContributionProposal memory proposal = ContributionProposal({ + nativeTokenReward: _rewards[0], + reputationChange: _reputationChange, + ethReward: _rewards[1], + externalToken: _externalToken, + externalTokenReward: _rewards[2], + beneficiary: beneficiary, + nativeTokenRewardLeft: 0, + reputationChangeLeft: 0, + ethRewardLeft: 0, + externalTokenRewardLeft: 0, + acceptedByVotingMachine: false + }); + organizationProposals[proposalId] = proposal; + + emit NewContributionProposal( + address(avatar), + proposalId, + address(votingMachine), + _descriptionHash, + _reputationChange, + _rewards, + _externalToken, + beneficiary, + proposer + ); + + proposalsInfo[address(votingMachine)][proposalId] = ProposalInfo({ + blockNumber:block.number, + avatar:avatar + }); + } + + /** + * @dev RedeemReputation reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @return reputation the redeemed reputation. + */ + function redeemReputation(bytes32 _proposalId) public returns(int256 reputation) { + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + + //if the beneficiary is the current contract, we are not minting the rep to it + //but instead refer to a mechanism in which the rep can be minted by the current contract + //per request of the rewarder + if (proposal.beneficiary == address(this)) { + if (proposal.reputationChangeLeft == 0) {//for now only mint(not burn) rep allowed from ext contract. + proposal.reputationChangeLeft = uint256(proposal.reputationChange); + } + } else { + reputation = proposal.reputationChange; + //set proposal reward to zero to prevent reentrancy attack. + proposal.reputationChange = 0; + + if (reputation > 0) { + require( + Controller( + avatar.owner()).mintReputation(uint(reputation), proposal.beneficiary, address(avatar))); + } else if (reputation < 0) { + require( + Controller( + avatar.owner()).burnReputation(uint(reputation*(-1)), proposal.beneficiary, address(avatar))); + } + if (reputation != 0) { + emit RedeemReputation(address(avatar), _proposalId, proposal.beneficiary, reputation); + } + } + } + + /** + * @dev RedeemNativeToken reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @return amount the redeemed nativeToken. + */ + function redeemNativeToken(bytes32 _proposalId) public returns(uint256 amount) { + + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + + if (proposal.beneficiary == address(this)) { + //ensure nativeTokenRewardLeft can be set only one time + if (proposal.nativeTokenRewardLeft == 0) { + proposal.nativeTokenRewardLeft = proposal.nativeTokenReward; + } + } + amount = proposal.nativeTokenReward; + //set proposal rewards to zero to prevent reentrancy attack. + proposal.nativeTokenReward = 0; + if (amount > 0) { + require(Controller(avatar.owner()).mintTokens(amount, proposal.beneficiary, address(avatar))); + emit RedeemNativeToken(address(avatar), _proposalId, proposal.beneficiary, amount); + } + } + + /** + * @dev RedeemEther reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @return amount ether redeemed amount + */ + function redeemEther(bytes32 _proposalId) public returns(uint256 amount) { + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + + if (proposal.beneficiary == address(this)) { + if (proposal.ethRewardLeft == 0) { + proposal.ethRewardLeft = proposal.ethReward; + } + } + amount = proposal.ethReward; + //set proposal rewards to zero to prevent reentrancy attack. + proposal.ethReward = 0; + if (amount > 0) { + require(Controller(avatar.owner()).sendEther(amount, proposal.beneficiary, avatar)); + emit RedeemEther(address(avatar), _proposalId, proposal.beneficiary, amount); + } + } + + /** + * @dev RedeemNativeToken reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @return amount the external token redeemed amount + */ + function redeemExternalToken(bytes32 _proposalId) public returns(uint256 amount) { + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + + + if (proposal.beneficiary == address(this)) { + if (proposal.externalTokenRewardLeft == 0) { + proposal.externalTokenRewardLeft = proposal.externalTokenReward; + } + } + + if (proposal.externalToken != IERC20(0) && proposal.externalTokenReward > 0) { + amount = proposal.externalTokenReward; + //set proposal rewards to zero to prevent reentrancy attack. + proposal.externalTokenReward = 0; + require( + Controller( + avatar.owner()) + .externalTokenTransfer(proposal.externalToken, proposal.beneficiary, amount, avatar)); + emit RedeemExternalToken(address(avatar), _proposalId, proposal.beneficiary, amount); + } + } + + /** + * @dev redeemReputationByRewarder redeem reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @param _beneficiary the beneficiary to mint reputation to. + * @param _reputation the reputation amount to mint + * note: burn reputation is not supported via this function + */ + function redeemReputationByRewarder(bytes32 _proposalId, address _beneficiary, uint256 _reputation) + public + onlyRewarder + { + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + //this will ensure sum zero of reputation + //and that there was a privious call to redeemReputation function. + proposal.reputationChangeLeft = + proposal.reputationChangeLeft.sub(_reputation, + "cannot redeem more reputation than allocated for this proposal or no redeemReputation was called"); + require( + Controller( + avatar.owner()).mintReputation(_reputation, _beneficiary, address(avatar))); + if (_reputation != 0) { + emit RedeemReputation(address(avatar), _proposalId, _beneficiary, int256(_reputation)); + } + } + + /** + * @dev redeemNativeTokenByRewarder redeem reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @param _beneficiary the beneficiary to mint tokens to. + * @param _amount the tokens amount to mint + */ + function redeemNativeTokenByRewarder(bytes32 _proposalId, address _beneficiary, uint256 _amount) + public + onlyRewarder + { + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + //this will ensure sum zero of reputation + //and that there was a privious call to redeemNativeToken function. + proposal.nativeTokenRewardLeft = + proposal.nativeTokenRewardLeft.sub(_amount, + "cannot redeem more tokens than allocated for this proposal or no redeemNativeToken was called"); + + if (_amount > 0) { + address(avatar.nativeToken()).safeTransfer(_beneficiary, _amount); + emit RedeemNativeToken(address(avatar), _proposalId, _beneficiary, _amount); + } + } + + /** + * @dev redeemEtherByRewarder redeem reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @param _beneficiary the beneficiary to send eth to. + * @param _amount eth amount to send + */ + function redeemEtherByRewarder(bytes32 _proposalId, address payable _beneficiary, uint256 _amount) + public + onlyRewarder + { + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + //this will ensure sum zero of reputation. + //and that there was a privious call to redeemEther function. + proposal.ethRewardLeft = proposal.ethRewardLeft.sub(_amount, + "cannot redeem more Ether than allocated for this proposal or no redeemEther was called"); + + if (_amount > 0) { + _beneficiary.transfer(_amount); + emit RedeemEther(address(avatar), _proposalId, _beneficiary, _amount); + } + } + + /** + * @dev redeemExternalTokenByRewarder redeem reward for proposal + * @param _proposalId the ID of the voting in the voting machine + * @param _beneficiary the beneficiary to send the external token to. + * @param _amount the amount of external token to send + */ + function redeemExternalTokenByRewarder(bytes32 _proposalId, address _beneficiary, uint256 _amount) + public + onlyRewarder { + ContributionProposal storage proposal = organizationProposals[_proposalId]; + require(proposal.acceptedByVotingMachine, "proposal was not accepted by the voting machine"); + //this will ensure sum zero of reputation. + //and that there was a privious call to redeemExternalToken function. + proposal.externalTokenRewardLeft = + proposal.externalTokenRewardLeft.sub(_amount, + "cannot redeem more tokens than allocated for this proposal or no redeemExternalToken was called"); + + if (proposal.externalToken != IERC20(0)) { + if (_amount > 0) { + address(proposal.externalToken).safeTransfer(_beneficiary, _amount); + emit RedeemExternalToken(address(avatar), _proposalId, _beneficiary, _amount); + } + } + } + + /** + * @dev redeem rewards for proposal + * @param _proposalId the ID of the voting in the voting machine + * @param _whatToRedeem whatToRedeem array of boolean values: + * whatToRedeem[0] - reputation + * whatToRedeem[1] - nativeTokenReward + * whatToRedeem[2] - Ether + * whatToRedeem[3] - ExternalToken + * @return result boolean array for each redeem type. + */ + function redeem(bytes32 _proposalId, bool[4] memory _whatToRedeem) + public + returns(int256 reputationReward, uint256 nativeTokenReward, uint256 etherReward, uint256 externalTokenReward) + { + + if (_whatToRedeem[0]) { + reputationReward = redeemReputation(_proposalId); + } + + if (_whatToRedeem[1]) { + nativeTokenReward = redeemNativeToken(_proposalId); + } + + if (_whatToRedeem[2]) { + etherReward = redeemEther(_proposalId); + } + + if (_whatToRedeem[3]) { + externalTokenReward = redeemExternalToken(_proposalId); + } + } + + function getProposalEthReward(bytes32 _proposalId) public view returns (uint256) { + return organizationProposals[_proposalId].ethReward; + } + + function getProposalExternalTokenReward(bytes32 _proposalId) public view returns (uint256) { + return organizationProposals[_proposalId].externalTokenReward; + } + + function getProposalExternalToken(bytes32 _proposalId) public view returns (address) { + return address(organizationProposals[_proposalId].externalToken); + } + + function getProposalReputationReward(bytes32 _proposalId) public view returns (int256) { + return organizationProposals[_proposalId].reputationChange; + } + + function getProposalNativeTokenReward(bytes32 _proposalId) public view returns (uint256) { + return organizationProposals[_proposalId].nativeTokenReward; + } + + function getProposalAcceptedByVotingMachine(bytes32 _proposalId) public view returns (bool) { + return organizationProposals[_proposalId].acceptedByVotingMachine; + } + +} diff --git a/contracts/utils/Redeemer.sol b/contracts/utils/Redeemer.sol index f59d144e..231e8ad4 100644 --- a/contracts/utils/Redeemer.sol +++ b/contracts/utils/Redeemer.sol @@ -1,6 +1,7 @@ pragma solidity 0.5.13; import "../universalSchemes/ContributionReward.sol"; +import "../schemes/ContributionRewardExt.sol"; import "@daostack/infra/contracts/votingMachines/GenesisProtocol.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; @@ -53,6 +54,82 @@ contract Redeemer { uint256 crEthReward, uint256 crExternalTokenReward) { + bool callContributionReward; + (gpRewards, gpDaoBountyReward, executed, winningVote, callContributionReward) = + genesisProtocolRedeem(_genesisProtocol, _proposalId, _beneficiary); + if (callContributionReward) { + //redeem from contributionReward only if it executed + if (_contributionReward.getProposalExecutionTime(_proposalId, address(_avatar)) > 0) { + (crReputationReward, crNativeTokenReward, crEthReward, crExternalTokenReward) = + contributionRewardRedeem(_contributionReward, _proposalId, _avatar); + } + } + } + + /** + * @dev helper to redeem rewards for a proposal + * It calls execute on the proposal if it is not yet executed. + * It tries to redeem reputation and stake from the GenesisProtocol. + * It tries to redeem proposal rewards from the contribution rewards scheme. + * This function does not emit events. + * A client should listen to GenesisProtocol and ContributionReward redemption events + * to monitor redemption operations. + * @param _contributionRewardExt contributionRewardExt + * @param _genesisProtocol genesisProtocol + * @param _proposalId the ID of the voting in the voting machine + * @param _beneficiary beneficiary + * @return gpRewards array + * gpRewards[0] - stakerTokenAmount + * gpRewards[1] - voterReputationAmount + * gpRewards[2] - proposerReputationAmount + * @return gpDaoBountyReward array + * gpDaoBountyReward[0] - staker dao bounty reward - + * will be zero for the case there is not enough tokens in avatar for the reward. + * gpDaoBountyReward[1] - staker potential dao bounty reward. + * @return executed bool true or false + * @return winningVote + * 1 - executed or closed and the winning vote is YES + * 2 - executed or closed and the winning vote is NO + * @return int256 crReputationReward Reputation - from ContributionReward + * @return int256 crNativeTokenReward NativeTokenReward - from ContributionReward + * @return int256 crEthReward Ether - from ContributionReward + * @return int256 crExternalTokenReward ExternalToken - from ContributionReward + */ + function redeemFromCRExt(ContributionRewardExt _contributionRewardExt, + GenesisProtocol _genesisProtocol, + bytes32 _proposalId, + address _beneficiary) + external + returns(uint[3] memory gpRewards, + uint[2] memory gpDaoBountyReward, + bool executed, + uint256 winningVote, + int256 crReputationReward, + uint256 crNativeTokenReward, + uint256 crEthReward, + uint256 crExternalTokenReward) + { + bool callContributionReward; + (gpRewards, gpDaoBountyReward, executed, winningVote, callContributionReward) = + genesisProtocolRedeem(_genesisProtocol, _proposalId, _beneficiary); + if (callContributionReward) { + //redeem from contributionReward only if it executed + if (_contributionRewardExt.getProposalAcceptedByVotingMachine(_proposalId)) { + (crReputationReward, crNativeTokenReward, crEthReward, crExternalTokenReward) = + contributionRewardExtRedeem(_contributionRewardExt, _proposalId); + } + } + } + + function genesisProtocolRedeem(GenesisProtocol _genesisProtocol, + bytes32 _proposalId, + address _beneficiary) + private + returns(uint[3] memory gpRewards, + uint[2] memory gpDaoBountyReward, + bool executed, + uint256 winningVote, + bool callContributionReward) { GenesisProtocol.ProposalState pState = _genesisProtocol.state(_proposalId); if ((pState == GenesisProtocolLogic.ProposalState.Queued)|| @@ -70,11 +147,7 @@ contract Redeemer { _genesisProtocol.redeemDaoBounty(_proposalId, _beneficiary); } winningVote = _genesisProtocol.winningVote(_proposalId); - //redeem from contributionReward only if it executed - if (_contributionReward.getProposalExecutionTime(_proposalId, address(_avatar)) > 0) { - (crReputationReward, crNativeTokenReward, crEthReward, crExternalTokenReward) = - contributionRewardRedeem(_contributionReward, _proposalId, _avatar); - } + callContributionReward = true; } } @@ -105,4 +178,28 @@ contract Redeemer { } (reputation, nativeToken, eth, externalToken) = _contributionReward.redeem(_proposalId, _avatar, whatToRedeem); } + + function contributionRewardExtRedeem(ContributionRewardExt _contributionRewardExt, bytes32 _proposalId) + private + returns (int256 reputation, uint256 nativeToken, uint256 eth, uint256 externalToken) + { + bool[4] memory whatToRedeem; + whatToRedeem[0] = true; //reputation + whatToRedeem[1] = true; //nativeToken + uint256 ethReward = _contributionRewardExt.getProposalEthReward(_proposalId); + uint256 externalTokenReward = _contributionRewardExt.getProposalExternalTokenReward(_proposalId); + address externalTokenAddress = _contributionRewardExt.getProposalExternalToken(_proposalId); + if ((ethReward == 0) || (address(_contributionRewardExt.avatar()).balance < ethReward)) { + whatToRedeem[2] = false; + } else { + whatToRedeem[2] = true; + } + if ((externalTokenReward == 0) || + (IERC20(externalTokenAddress).balanceOf(address(_contributionRewardExt.avatar())) < externalTokenReward)) { + whatToRedeem[3] = false; + } else { + whatToRedeem[3] = true; + } + (reputation, nativeToken, eth, externalToken) = _contributionRewardExt.redeem(_proposalId, whatToRedeem); + } } diff --git a/package-lock.json b/package-lock.json index 1e69956d..ac351d89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.34", + "version": "0.0.1-rc.35", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -145,6 +145,13 @@ "requires": { "ethereumjs-abi": "^0.6.5", "openzeppelin-solidity": "2.3.0" + }, + "dependencies": { + "openzeppelin-solidity": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.3.0.tgz", + "integrity": "sha512-QYeiPLvB1oSbDt6lDQvvpx7k8ODczvE474hb2kLXZBPKMsxKT1WxTCHBYrCU7kS7hfAku4DcJ0jqOyL+jvjwQw==" + } } }, "@resolver-engine/core": { @@ -5132,9 +5139,9 @@ } }, "openzeppelin-solidity": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.3.0.tgz", - "integrity": "sha512-QYeiPLvB1oSbDt6lDQvvpx7k8ODczvE474hb2kLXZBPKMsxKT1WxTCHBYrCU7kS7hfAku4DcJ0jqOyL+jvjwQw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.4.0.tgz", + "integrity": "sha512-533gc5jkspxW5YT0qJo02Za5q1LHwXK9CJCc48jNj/22ncNM/3M/3JfWLqfpB90uqLwOKOovpl0JfaMQTR+gXQ==" }, "optionator": { "version": "0.8.3", diff --git a/package.json b/package.json index b2eea1ef..eadf0321 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.34", + "version": "0.0.1-rc.35", "description": "A platform for building DAOs", "files": [ "contracts/", @@ -12,7 +12,7 @@ "tsconfig.json" ], "config": { - "gasLimit": "6100000" + "gasLimit": "6200000" }, "scripts": { "test": "cross-conf-env run-with-ganache --ganache-cmd 'npm run ganache' 'npm run truffle compile && npm run truffle migrate && npm run truffle test'", @@ -77,7 +77,7 @@ "dependencies": { "@daostack/infra": "0.0.1-rc.15", "math": "0.0.3", - "openzeppelin-solidity": "2.3.0", + "openzeppelin-solidity": "2.4.0", "truffle-flattener": "^1.4.2" }, "peerDependencies": { diff --git a/test/competition.js b/test/competition.js new file mode 100644 index 00000000..c78299b2 --- /dev/null +++ b/test/competition.js @@ -0,0 +1,632 @@ +import * as helpers from './helpers'; +const constants = require('./constants'); +const ContributionRewardExt = artifacts.require("./ContributionRewardExt.sol"); +const ERC20Mock = artifacts.require('./test/ERC20Mock.sol'); +const DaoCreator = artifacts.require("./DaoCreator.sol"); +const ControllerCreator = artifacts.require("./ControllerCreator.sol"); +const DAOTracker = artifacts.require("./DAOTracker.sol"); +const Competition = artifacts.require("./Competition.sol"); + +export class ContributionRewardParams { + constructor() { + } +} + +const setupContributionRewardParams = async function( + contributionReward, + accounts, + genesisProtocol, + token, + avatar, + redeemer = helpers.NULL_ADDRESS + ) { + var contributionRewardParams = new ContributionRewardParams(); + if (genesisProtocol === true) { + contributionRewardParams.votingMachine = await helpers.setupGenesisProtocol(accounts,token,avatar,helpers.NULL_ADDRESS); + await contributionReward.initialize( avatar.address, + contributionRewardParams.votingMachine.genesisProtocol.address, + contributionRewardParams.votingMachine.params, + redeemer); + } else { + contributionRewardParams.votingMachine = await helpers.setupAbsoluteVote(helpers.NULL_ADDRESS,50,contributionReward.address); + await contributionReward.initialize( + avatar.address, + contributionRewardParams.votingMachine.absoluteVote.address, + contributionRewardParams.votingMachine.params, + redeemer + ); + } + return contributionRewardParams; +}; + +const setup = async function (accounts,genesisProtocol = false,tokenAddress=0,service=helpers.NULL_ADDRESS) { + var testSetup = new helpers.TestSetup(); + testSetup.standardTokenMock = await ERC20Mock.new(accounts[1],100000); + testSetup.contributionRewardExt = await ContributionRewardExt.new(); + var controllerCreator = await ControllerCreator.new({gas: constants.ARC_GAS_LIMIT}); + var daoTracker = await DAOTracker.new({gas: constants.ARC_GAS_LIMIT}); + testSetup.daoCreator = await DaoCreator.new(controllerCreator.address,daoTracker.address,{gas:constants.ARC_GAS_LIMIT}); + if (genesisProtocol) { + testSetup.reputationArray = [1000,100,0]; + } else { + testSetup.reputationArray = [2000,5000,7000]; + } + testSetup.org = await helpers.setupOrganizationWithArrays(testSetup.daoCreator,[accounts[0],accounts[1],accounts[2]],[1000,0,0],testSetup.reputationArray); + testSetup.contributionRewardExtParams= await setupContributionRewardParams( + testSetup.contributionRewardExt, + accounts,genesisProtocol, + tokenAddress, + testSetup.org.avatar, + service); + var permissions = "0x00000000"; + await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address, + [testSetup.contributionRewardExt.address], + [helpers.NULL_HASH],[permissions],"metaData"); + + testSetup.competition = await Competition.new(); + testSetup.competition.initialize(testSetup.contributionRewardExt.address); + return testSetup; +}; + +const proposeCompetition = async function( + _testSetup, + _descriptionHash = "description-hash", + _reputationChange = 10, + _rewards = [1,2,3], + _rewardSplit = [50,25,15,10], + _startTime = 10, + _votingStartTime = 600, + _endTime = 1200, + _maxNumberOfVotesPerVoter = 3, + _suggestionsEndTime = 1200 + ) { + + var block = await web3.eth.getBlock("latest"); + _testSetup.startTime = block.timestamp + _startTime; + _testSetup.votingStartTime = block.timestamp + _votingStartTime; + _testSetup.endTime = block.timestamp + _endTime; + _testSetup.suggestionsEndTime = block.timestamp + _suggestionsEndTime; + var tx = await _testSetup.competition.proposeCompetition( + _descriptionHash, + _reputationChange, + _rewards, + _testSetup.standardTokenMock.address, + _rewardSplit, + [_testSetup.startTime, + _testSetup.votingStartTime, + _testSetup.endTime, + _maxNumberOfVotesPerVoter, + _testSetup.suggestionsEndTime] + ); + + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "NewCompetitionProposal"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._numberOfWinners,_rewardSplit.length); + assert.equal(tx.logs[0].args._rewardSplit[0],_rewardSplit[0]); + assert.equal(tx.logs[0].args._rewardSplit[1],_rewardSplit[1]); + assert.equal(tx.logs[0].args._rewardSplit[2],_rewardSplit[2]); + assert.equal(tx.logs[0].args._rewardSplit[3],_rewardSplit[3]); + assert.equal(tx.logs[0].args._startTime,_testSetup.startTime); + assert.equal(tx.logs[0].args._votingStartTime,_testSetup.votingStartTime); + assert.equal(tx.logs[0].args._endTime,_testSetup.endTime); + assert.equal(tx.logs[0].args._maxNumberOfVotesPerVoter,_maxNumberOfVotesPerVoter); + assert.equal(tx.logs[0].args._contributionRewardExt,_testSetup.contributionRewardExt.address); + assert.equal(tx.logs[0].args._suggestionsEndTime,_testSetup.suggestionsEndTime); + + return proposalId; +}; + + +contract('Competition', accounts => { + + it("proposeCompetition log", async function() { + var testSetup = await setup(accounts); + await proposeCompetition(testSetup); + + var descriptionHash = "description-hash"; + var reputationChange = 10; + var rewards = [1,2,3]; + var rewardSplit = new Array(101).fill(0); + var startTime = 0; + var votingStartTime = 600; + var endTime = 1200; + rewardSplit[0]= 100; + try { + + await proposeCompetition(testSetup, + descriptionHash, + reputationChange, + rewards, + rewardSplit); + assert(false, 'number of winners should be <= 100'); + } catch (ex) { + helpers.assertVMException(ex); + } + rewardSplit = [50,25,15,0]; + try { + + await proposeCompetition(testSetup, + descriptionHash, + reputationChange, + rewards, + rewardSplit); + assert(false, 'total reward split should be 100%'); + } catch (ex) { + helpers.assertVMException(ex); + } + rewardSplit = [50,25,15,10]; + + try { + + await proposeCompetition(testSetup, + descriptionHash, + reputationChange, + rewards, + rewardSplit, + startTime, + endTime);//votingStartTime + assert(false, '_votingStartTime < _endTime'); + } catch (ex) { + helpers.assertVMException(ex); + } + + try { + + await proposeCompetition(testSetup, + descriptionHash, + reputationChange, + rewards, + rewardSplit, + votingStartTime,//startTime + votingStartTime-1);//votingStartTime + assert(false, '_votingStartTime >= _startTime,'); + } catch (ex) { + helpers.assertVMException(ex); + } + + try { + + await proposeCompetition(testSetup, + descriptionHash, + reputationChange, + rewards, + rewardSplit, + startTime,//startTime + votingStartTime, + endTime, + 0); + assert(false, 'maxNumberOfVotesPerVoter > 0'); + } catch (ex) { + helpers.assertVMException(ex); + } + }); + + it("suggest", async function() { + var testSetup = await setup(accounts); + var proposalId = await proposeCompetition(testSetup); + await helpers.increaseTime(20); + var tx = await testSetup.competition.suggest(proposalId,"suggestion"); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "NewSuggestion"); + assert.equal(tx.logs[0].args._suggestionId,1); + }); + + it("cannot suggest after suggestionEndTime", async function() { + var descriptionHash = "description-hash"; + var reputationChange = 10; + var rewards = [1,2,3]; + var rewardSplit = [100]; + var startTime = 10; + var votingStartTime = 600; + var endTime = 1200; + var maxNumberOfVotesPerVoter = 3; + var testSetup = await setup(accounts); + var proposalId = await proposeCompetition(testSetup, + descriptionHash, + reputationChange, + rewards, + rewardSplit, + startTime, + votingStartTime, + endTime, + maxNumberOfVotesPerVoter, + 200);//suggestionEndTime + await helpers.increaseTime(20);//increase time for suggestion + await testSetup.competition.suggest(proposalId,"suggestion"); + //increase time after suggestion end time + await helpers.increaseTime(250); + try { + + await testSetup.competition.suggest(proposalId,"suggestion"); + assert(false, 'cannot suggest after suggestionEndTime'); + } catch (ex) { + helpers.assertVMException(ex); + } + + }); + + it("cannot suggest before start time", async function() { + var testSetup = await setup(accounts); + var descriptionHash = "description-hash"; + var reputationChange = 10; + var rewards = [1,2,3]; + var rewardSplit = [0,50,25,25]; + var proposalId = await proposeCompetition(testSetup, + descriptionHash, + reputationChange, + rewards, + rewardSplit, + 20//startTime + );//votingStartTime + try { + + await testSetup.competition.suggest(proposalId,"suggestion"); + assert(false, 'cannot suggest before start time'); + } catch (ex) { + helpers.assertVMException(ex); + } + await helpers.increaseTime(20); + await testSetup.competition.suggest(proposalId,"suggestion"); + }); + + it("cannot suggest after competition end", async function() { + var testSetup = await setup(accounts); + var proposalId = await proposeCompetition(testSetup);//votingStartTime + await helpers.increaseTime(20); + await testSetup.competition.suggest(proposalId,"suggestion"); + await helpers.increaseTime(1200+100); + try { + await testSetup.competition.suggest(proposalId,"suggestion"); + assert(false, 'cannot suggest after competition end'); + } catch (ex) { + helpers.assertVMException(ex); + } + }); + + it("vote", async function() { + var testSetup = await setup(accounts); + var proposalId = await proposeCompetition(testSetup); + await helpers.increaseTime(20); + var tx = await testSetup.competition.suggest(proposalId,"suggestion"); + var suggestionId = tx.logs[0].args._suggestionId; + + try { + await testSetup.competition.vote(suggestionId); + assert(false, 'vote before voting start time should fail'); + } catch (ex) { + helpers.assertVMException(ex); + } + await helpers.increaseTime(650); + + try { + await testSetup.competition.vote(suggestionId+1); + assert(false, 'vote on none valid suggestion'); + } catch (ex) { + helpers.assertVMException(ex); + } + var proposal = await testSetup.competition.proposals(proposalId); + tx = await testSetup.competition.vote(suggestionId); + + try { + await testSetup.competition.vote(suggestionId); + assert(false, 'can vote only one time on each suggestion'); + } catch (ex) { + helpers.assertVMException(ex); + } + + assert.equal(tx.logs.length, 2); + assert.equal(tx.logs[0].event, "SnapshotBlock"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._snapshotBlock,tx.logs[0].blockNumber); + + assert.equal(tx.logs[1].event, "NewVote"); + assert.equal(tx.logs[1].args._suggestionId,1); + assert.equal(tx.logs[1].args._reputation,testSetup.reputationArray[0]); + + //first vote set the snapshotBlock + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.vote(2); + proposal = await testSetup.competition.proposals(proposalId); + assert.equal(proposal.snapshotBlock, tx.logs[0].blockNumber); + + //3rd suggestion + await testSetup.competition.suggest(proposalId,"suggestion"); + //4th suggestion + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.vote(3); + + try { + await testSetup.competition.vote(4); + assert(false, 'cannot vote more than allowed per voter'); + } catch (ex) { + helpers.assertVMException(ex); + } + + }); + + + it("total votes", async function() { + var testSetup = await setup(accounts); + var proposalId = await proposeCompetition(testSetup); + await helpers.increaseTime(20); + var tx = await testSetup.competition.suggest(proposalId,"suggestion"); + var suggestionId = tx.logs[0].args._suggestionId; + await helpers.increaseTime(650); + await testSetup.competition.vote(suggestionId); + await testSetup.competition.vote(suggestionId,{from:accounts[1]}); + await testSetup.competition.vote(suggestionId,{from:accounts[2]}); + var suggestion = await testSetup.competition.suggestions(suggestionId); + assert.equal(suggestion.totalVotes, testSetup.reputationArray[0] +testSetup.reputationArray[1]+testSetup.reputationArray[2]); + }); + + it("getOrderedIndexOfSuggestion", async function() { + var testSetup = await setup(accounts); + var proposalId = await proposeCompetition(testSetup); + await helpers.increaseTime(20); + for (var i=0;i<20;i++) { + //submit 20 suggestion + await testSetup.competition.suggest(proposalId,"suggestion"); + } + await helpers.increaseTime(650); + await testSetup.competition.vote(10,{from:accounts[0]}); + await testSetup.competition.vote(16,{from:accounts[2]}); + await testSetup.competition.vote(5,{from:accounts[1]}); + + assert.equal(await testSetup.competition.getOrderedIndexOfSuggestion(10),2); + assert.equal(await testSetup.competition.getOrderedIndexOfSuggestion(5),1); + assert.equal(await testSetup.competition.getOrderedIndexOfSuggestion(16),0); + + }); + + it("getOrderedIndexOfSuggestion equality case", async function() { + var testSetup = await setup(accounts); + var proposalId = await proposeCompetition(testSetup); + await helpers.increaseTime(20); + for (var i=0;i<20;i++) { + //submit 20 suggestion + await testSetup.competition.suggest(proposalId,"suggestion"); + } + await helpers.increaseTime(650); + await testSetup.competition.vote(10,{from:accounts[1]}); + await testSetup.competition.vote(16,{from:accounts[1]}); + await testSetup.competition.vote(5,{from:accounts[0]}); + + assert.equal(await testSetup.competition.getOrderedIndexOfSuggestion(10),0); + assert.equal(await testSetup.competition.getOrderedIndexOfSuggestion(16),0); + assert.equal(await testSetup.competition.getOrderedIndexOfSuggestion(5),2); + try { + await testSetup.competition.getOrderedIndexOfSuggestion(0); + assert(false, 'revert if suggestion does not exist'); + } catch (ex) { + helpers.assertVMException(ex); + } + try { + await testSetup.competition.getOrderedIndexOfSuggestion(21); + assert(false, 'revert if suggestion does not exist'); + } catch (ex) { + helpers.assertVMException(ex); + } + assert.equal(await testSetup.competition.getOrderedIndexOfSuggestion(1),3); + }); + + it("redeem", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address,30,{from:accounts[1]}); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var proposalId = await proposeCompetition(testSetup); + await helpers.increaseTime(20); + await testSetup.competition.suggest(proposalId,"suggestion"); + try { + await testSetup.competition.redeem(1,accounts[0]); + assert(false, 'cannot redeem if no vote'); + } catch (ex) { + helpers.assertVMException(ex); + } + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[0]}); + await testSetup.contributionRewardExt.redeem(proposalId,[true,true,true,true]); + await helpers.increaseTime(650); + await testSetup.competition.vote(1,{from:accounts[1]}); + try { + await testSetup.competition.redeem(1,accounts[0]); + assert(false, 'cannot redeem if competion not ended yet'); + } catch (ex) { + helpers.assertVMException(ex); + } + await helpers.increaseTime(650); + var tx = await testSetup.competition.redeem(1,accounts[0]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Redeem"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._rewardPercentage,100); + + await testSetup.contributionRewardExt.getPastEvents('RedeemReputation', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemReputation"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,10); + }); + + await testSetup.contributionRewardExt.getPastEvents('RedeemEther', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemEther"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,2); + }); + + await testSetup.contributionRewardExt.getPastEvents('RedeemNativeToken', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemNativeToken"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,1); + }); + + await testSetup.contributionRewardExt.getPastEvents('RedeemExternalToken', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemExternalToken"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,3); + }); + + }); + + it("redeem multipe suggestions", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address,3000,{from:accounts[1]}); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:2000}); + var proposalId = await proposeCompetition(testSetup,"description-hash",1000,[1000,2000,3000]); + await helpers.increaseTime(20); + + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[0]}); + await testSetup.contributionRewardExt.redeem(proposalId,[true,true,true,true]); + await helpers.increaseTime(650); + await testSetup.competition.vote(1,{from:accounts[0]}); + await testSetup.competition.vote(2,{from:accounts[1]}); + await testSetup.competition.vote(3,{from:accounts[2]}); + + await helpers.increaseTime(650); + var tx = await testSetup.competition.redeem(1,accounts[0]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Redeem"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._rewardPercentage,18); + + await testSetup.contributionRewardExt.getPastEvents('RedeemReputation', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemReputation"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,180); + }); + + await testSetup.contributionRewardExt.getPastEvents('RedeemEther', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemEther"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,(2000*18/100)); + }); + + await testSetup.contributionRewardExt.getPastEvents('RedeemNativeToken', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemNativeToken"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,(1000*18/100)); + }); + + await testSetup.contributionRewardExt.getPastEvents('RedeemExternalToken', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemExternalToken"); + assert.equal(events[0].args._beneficiary,accounts[0]); + assert.equal(events[0].args._amount,(3000*18/100)); + }); + tx = await testSetup.competition.redeem(2,accounts[0]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Redeem"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._rewardPercentage,28); + + try { + await testSetup.competition.sendLeftOverFunds(proposalId); + assert(false, 'cannot sendLeftOverFunds because not all proposals redeemed yet'); + } catch (ex) { + helpers.assertVMException(ex); + } + + tx = await testSetup.competition.redeem(3,accounts[0]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Redeem"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._rewardPercentage,53); + + var proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + + tx = await testSetup.competition.sendLeftOverFunds(proposalId); + await testSetup.contributionRewardExt.getPastEvents('RedeemExternalToken', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"RedeemExternalToken"); + assert.equal(events[0].args._beneficiary,testSetup.org.avatar.address); + assert.equal(events[0].args._amount.toNumber(),proposal.externalTokenRewardLeft.toNumber()); + }); + + }); + + it("redeem multipe suggestions - multiple smallers suggestion", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address,3000,{from:accounts[1]}); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:2000}); + var proposalId = await proposeCompetition(testSetup,"description-hash",1000,[1000,2000,3000]); + await helpers.increaseTime(20); + + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + await testSetup.competition.suggest(proposalId,"suggestion"); + + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[0]}); + await testSetup.contributionRewardExt.redeem(proposalId,[true,true,true,true]); + await helpers.increaseTime(650); + await testSetup.competition.vote(1,{from:accounts[2]}); + await testSetup.competition.vote(2,{from:accounts[1]}); + await testSetup.competition.vote(3,{from:accounts[1]}); + await testSetup.competition.vote(4,{from:accounts[0]}); + await testSetup.competition.vote(5,{from:accounts[0]}); + await testSetup.competition.vote(6,{from:accounts[0]}); + await helpers.increaseTime(650); + var tx = await testSetup.competition.redeem(4,accounts[0]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Redeem"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._rewardPercentage,3); + + tx = await testSetup.competition.redeem(5,accounts[0]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Redeem"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._rewardPercentage,3); + + tx = await testSetup.competition.redeem(6,accounts[0]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Redeem"); + assert.equal(tx.logs[0].args._proposalId,proposalId); + assert.equal(tx.logs[0].args._rewardPercentage,3); + }); +}); diff --git a/test/constants.js b/test/constants.js index ab71e6ac..04c7b377 100644 --- a/test/constants.js +++ b/test/constants.js @@ -1,3 +1,3 @@ -const ARC_GAS_LIMIT = 6100000; +const ARC_GAS_LIMIT = 6200000; module.exports = { ARC_GAS_LIMIT }; diff --git a/test/contributionrewardext.js b/test/contributionrewardext.js new file mode 100644 index 00000000..55cd3e3e --- /dev/null +++ b/test/contributionrewardext.js @@ -0,0 +1,670 @@ +import * as helpers from './helpers'; +const constants = require('./constants'); +const ContributionRewardExt = artifacts.require("./ContributionRewardExt.sol"); +const ERC20Mock = artifacts.require('./test/ERC20Mock.sol'); +const DaoCreator = artifacts.require("./DaoCreator.sol"); +const ControllerCreator = artifacts.require("./ControllerCreator.sol"); +const DAOTracker = artifacts.require("./DAOTracker.sol"); +const Avatar = artifacts.require("./Avatar.sol"); +const Redeemer = artifacts.require("./Redeemer.sol"); + + + +export class ContributionRewardParams { + constructor() { + } +} + +const setupContributionRewardParams = async function( + contributionReward, + accounts, + genesisProtocol, + token, + avatar, + redeemer = helpers.NULL_ADDRESS + ) { + var contributionRewardParams = new ContributionRewardParams(); + if (genesisProtocol === true) { + contributionRewardParams.votingMachine = await helpers.setupGenesisProtocol(accounts,token,avatar,helpers.NULL_ADDRESS); + await contributionReward.initialize( avatar.address, + contributionRewardParams.votingMachine.genesisProtocol.address, + contributionRewardParams.votingMachine.params, + redeemer); + } else { + contributionRewardParams.votingMachine = await helpers.setupAbsoluteVote(helpers.NULL_ADDRESS,50,contributionReward.address); + await contributionReward.initialize( + avatar.address, + contributionRewardParams.votingMachine.absoluteVote.address, + contributionRewardParams.votingMachine.params, + redeemer + ); + } + return contributionRewardParams; +}; + +const setup = async function (accounts,genesisProtocol = false,tokenAddress=0,service=helpers.NULL_ADDRESS) { + var testSetup = new helpers.TestSetup(); + testSetup.standardTokenMock = await ERC20Mock.new(accounts[1],100); + testSetup.contributionRewardExt = await ContributionRewardExt.new(); + var controllerCreator = await ControllerCreator.new({gas: constants.ARC_GAS_LIMIT}); + var daoTracker = await DAOTracker.new({gas: constants.ARC_GAS_LIMIT}); + testSetup.daoCreator = await DaoCreator.new(controllerCreator.address,daoTracker.address,{gas:constants.ARC_GAS_LIMIT}); + if (genesisProtocol) { + testSetup.reputationArray = [1000,100,0]; + } else { + testSetup.reputationArray = [2000,4000,7000]; + } + testSetup.org = await helpers.setupOrganizationWithArrays(testSetup.daoCreator,[accounts[0],accounts[1],accounts[2]],[1000,0,0],testSetup.reputationArray); + testSetup.contributionRewardExtParams= await setupContributionRewardParams( + testSetup.contributionRewardExt, + accounts,genesisProtocol, + tokenAddress, + testSetup.org.avatar, + service); + var permissions = "0x00000000"; + await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address, + [testSetup.contributionRewardExt.address], + [helpers.NULL_HASH],[permissions],"metaData"); + return testSetup; +}; +contract('ContributionRewardExt', accounts => { + + it("initialize", async function() { + var testSetup = await setup(accounts); + assert.equal(await testSetup.contributionRewardExt.votingMachine(),testSetup.contributionRewardExtParams.votingMachine.absoluteVote.address); + }); + + it("proposeContributionReward log", async function() { + var testSetup = await setup(accounts); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + "description-hash", + 10, + [1,2,3], + testSetup.standardTokenMock.address, + accounts[0], + helpers.NULL_ADDRESS); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "NewContributionProposal"); + assert.equal(await helpers.getValueFromLogs(tx, '_avatar',0), testSetup.org.avatar.address, "Wrong log: _avatar"); + assert.equal(await helpers.getValueFromLogs(tx, '_intVoteInterface',0), testSetup.contributionRewardExtParams.votingMachine.absoluteVote.address, "Wrong log: _intVoteInterface"); + assert.equal(await helpers.getValueFromLogs(tx, '_descriptionHash',15), "description-hash", "Wrong log: _contributionDescription"); + assert.equal(await helpers.getValueFromLogs(tx, '_reputationChange',0), 10, "Wrong log: _reputationChange"); + var arr = await helpers.getValueFromLogs(tx, '_rewards',0); + assert.equal(arr[0].words[0], 1, "Wrong log: _rewards"); + assert.equal(arr[1].words[0], 2, "Wrong log: _rewards"); + assert.equal(arr[2].words[0], 3, "Wrong log: _rewards"); + assert.equal(await helpers.getValueFromLogs(tx, '_externalToken',0), testSetup.standardTokenMock.address, "Wrong log: _externalToken"); + assert.equal(await helpers.getValueFromLogs(tx, '_beneficiary',0), accounts[0], "Wrong log: _beneficiary"); + }); + + it("proposeContributionReward check beneficiary==0", async() => { + var testSetup = await setup(accounts); + var beneficiary = helpers.NULL_ADDRESS; + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + 0, + [0,0,0], + testSetup.standardTokenMock.address, + beneficiary, + helpers.NULL_ADDRESS + ); + assert.equal(await helpers.getValueFromLogs(tx, '_beneficiary'),accounts[0]); + }); + + it("execute proposeContributionReward yes ", async function() { + var testSetup = await setup(accounts); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + 0, + [0,0,0], + testSetup.standardTokenMock.address, + accounts[0], + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + var organizationProposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.notEqual(organizationProposal.acceptedByVotingMachine,0);//acceptedByVotingMachine + }); + + it("execute proposeContributionReward mint reputation ", async function() { + var testSetup = await setup(accounts); + var reputationReward = 12; + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [0,0,0], + testSetup.standardTokenMock.address, + accounts[1], + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + tx = await testSetup.contributionRewardExt.redeem(proposalId,[true,false,false,false]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "RedeemReputation"); + assert.equal(tx.logs[0].args._amount, reputationReward); + var rep = await testSetup.org.reputation.balanceOf(accounts[1]); + assert.equal(rep.toNumber(),testSetup.reputationArray[1]+reputationReward); + }); + + it("execute proposeContributionReward mint tokens ", async function() { + var testSetup = await setup(accounts); + var reputationReward = 12; + var nativeTokenReward = 12; + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,0,0], + testSetup.standardTokenMock.address, + accounts[1], + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + tx = await testSetup.contributionRewardExt.redeem(proposalId,[false,true,false,false]); + var tokens = await testSetup.org.token.balanceOf(accounts[1]); + assert.equal(tokens.toNumber(),nativeTokenReward); + }); + + it("execute proposeContributionReward send ethers ", async function() { + var testSetup = await setup(accounts); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + //send some ether to the org avatar + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,0], + testSetup.standardTokenMock.address, + otherAvatar.address, + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.contributionRewardExt.redeem(proposalId,[false,false,true,false]); + var eth = await web3.eth.getBalance(otherAvatar.address); + assert.equal(eth,ethReward); + }); + + it("execute proposeContributionReward send externalToken ", async function() { + var testSetup = await setup(accounts); + //give some tokens to organization avatar + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address,30,{from:accounts[1]}); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + var externalTokenReward = 12; + //send some ether to the org avatar + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,externalTokenReward], + testSetup.standardTokenMock.address, + otherAvatar.address, + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.contributionRewardExt.redeem(proposalId,[false,false,false,true]); + var tokens = await testSetup.standardTokenMock.balanceOf(otherAvatar.address); + assert.equal(tokens.toNumber(),externalTokenReward); + }); + + it("execute proposeContributionReward proposal decision=='no' send externalToken ", async function() { + var testSetup = await setup(accounts); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + var externalTokenReward = 12; + + //send some ether to the org avatar + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,externalTokenReward], + testSetup.standardTokenMock.address, + otherAvatar.address, + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + var organizationProposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(organizationProposal[5],otherAvatar.address);//beneficiary + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,0,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + + try { + await testSetup.contributionRewardExt.redeem(proposalId,[true,true,true,true]); + assert(false, 'redeem should revert because there was no positive voting'); + } catch (ex) { + helpers.assertVMException(ex); + } + }); + + it("execute proposeContributionReward mint negative reputation ", async function() { + var testSetup = await setup(accounts); + var reputationReward = -12; + + + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [0,0,0], + testSetup.standardTokenMock.address, + accounts[0], + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + + tx = await testSetup.contributionRewardExt.redeem(proposalId,[true,false,false,false]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "RedeemReputation"); + assert.equal(tx.logs[0].args._amount, reputationReward); + var rep = await testSetup.org.reputation.balanceOf(accounts[0]); + assert.equal(rep.toNumber(),testSetup.reputationArray[0]+reputationReward); + }); + + + it("call execute should revert ", async function() { + var testSetup = await setup(accounts); + var reputationReward = -12; + + + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [0,0,0], + testSetup.standardTokenMock.address, + accounts[0], + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + try { + await testSetup.contributionRewardExt.executeProposal(proposalId,1); + assert(false, 'only voting machine can call execute'); + } catch (ex) { + helpers.assertVMException(ex); + } + + }); + + it("execute proposeContributionReward via genesisProtocol and redeem using Redeemer", async function() { + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,true,standardTokenMock.address); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + + + //send some ether to the org avatar + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,0], + testSetup.standardTokenMock.address, + otherAvatar.address, + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[0]}); + + var arcUtils = await Redeemer.new(); + var redeemRewards = await arcUtils.redeemFromCRExt.call(testSetup.contributionRewardExt.address, + testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + assert.equal(redeemRewards[0][1],100); //redeemRewards[0] gpRewards + assert.equal(redeemRewards[0][2],60); + assert.equal(redeemRewards[1][0],0); //daoBountyRewards + assert.equal(redeemRewards[1][1],0); //daoBountyRewards + assert.equal(redeemRewards[2],false); //isExecuted + assert.equal(redeemRewards[3],1); //winningVote + assert.equal(redeemRewards[4],reputationReward); //crReputationReward + assert.equal(redeemRewards[5],nativeTokenReward); //crNativeTokenReward + assert.equal(redeemRewards[6],ethReward); //crEthReward + assert.equal(redeemRewards[7],0); //crExternalTokenReward + + await arcUtils.redeemFromCRExt(testSetup.contributionRewardExt.address, + testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + + var eth = await web3.eth.getBalance(otherAvatar.address); + assert.equal(eth,ethReward); + assert.equal(await testSetup.org.reputation.balanceOf(otherAvatar.address),reputationReward); + assert.equal(await testSetup.org.token.balanceOf(otherAvatar.address),nativeTokenReward); + var reputation = await testSetup.org.reputation.balanceOf(accounts[0]); + var reputationGainAsVoter = 0; + var proposingRepRewardConstA=60; + var reputationGainAsProposer = proposingRepRewardConstA; + assert.equal(reputation, 1000+reputationGainAsVoter + reputationGainAsProposer); + }); + + it("execute proposeContributionReward via genesisProtocol and redeem using Redeemer for un excuted boosted proposal", async function() { + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,true,standardTokenMock.address); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + //send some ether to the org avatar + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,0], + testSetup.standardTokenMock.address, + otherAvatar.address, + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[1]}); + + await standardTokenMock.approve(testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.address,1000); + await testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.stake(proposalId,1,1000); + await helpers.increaseTime(60+1); + var arcUtils = await Redeemer.new(); + + var redeemRewards = await arcUtils.redeemFromCRExt.call(testSetup.contributionRewardExt.address, + testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + + assert.equal(redeemRewards[0][1],0); //redeemRewards[0] gpRewards + assert.equal(redeemRewards[0][2],60); + assert.equal(redeemRewards[1][0],0); //daoBountyRewards + assert.equal(redeemRewards[1][1],15); //daoBountyRewards + assert.equal(redeemRewards[2],true); //isExecuted + assert.equal(redeemRewards[3],1); //winningVote + assert.equal(redeemRewards[4],reputationReward); //crReputationReward + assert.equal(redeemRewards[5],nativeTokenReward); //crNativeTokenReward + assert.equal(redeemRewards[6],ethReward); //crEthReward + assert.equal(redeemRewards[7],0); //crExternalTokenReward + + await arcUtils.redeemFromCRExt(testSetup.contributionRewardExt.address, + testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + + var eth = await web3.eth.getBalance(otherAvatar.address); + assert.equal(eth,ethReward); + assert.equal(await testSetup.org.reputation.balanceOf(otherAvatar.address),reputationReward); + assert.equal(await testSetup.org.token.balanceOf(otherAvatar.address),nativeTokenReward); + var reputation = await testSetup.org.reputation.balanceOf(accounts[0]); + var reputationGainAsVoter = 0; + var proposingRepRewardConstA=60; + var reputationGainAsProposer = proposingRepRewardConstA; + assert.equal(reputation, 1000+reputationGainAsVoter + reputationGainAsProposer); + }); + + it("execute proposeContributionReward via genesisProtocol and redeem using Redeemer for negative proposal", async function() { + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,true,standardTokenMock.address); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + + + //send some ether to the org avatar + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,0], + testSetup.standardTokenMock.address, + otherAvatar.address, + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.vote(proposalId,2,0,helpers.NULL_ADDRESS,{from:accounts[0]}); + + var arcUtils = await Redeemer.new(); + await arcUtils.redeemFromCRExt(testSetup.contributionRewardExt.address, + testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + var eth = await web3.eth.getBalance(otherAvatar.address); + assert.equal(eth,0); + assert.equal(await testSetup.org.reputation.balanceOf(otherAvatar.address),0); + assert.equal(await testSetup.org.token.balanceOf(otherAvatar.address),0); + var reputation = await testSetup.org.reputation.balanceOf(accounts[0]); + //no reputation reward for proposer for negative proposal. + //reputation reward for a single voter = 0 + assert.equal(reputation, 1000); + }); + + it("execute proposeContributionReward via genesisProtocol and redeem using Redeemer ExpiredInQueue", async function() { + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,true,standardTokenMock.address); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + + + //send some ether to the org avatar + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,0], + testSetup.standardTokenMock.address, + otherAvatar.address, + helpers.NULL_ADDRESS + ); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + + await testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[1]}); + await helpers.increaseTime(60+1); + var arcUtils = await Redeemer.new(); + await arcUtils.redeemFromCRExt(testSetup.contributionRewardExt.address, + testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[1]); + var proposal = await testSetup.contributionRewardExtParams.votingMachine.genesisProtocol.proposals(proposalId); + assert.equal(proposal.state,1); //ExpiredInQueue + var reputation = await testSetup.org.reputation.balanceOf(accounts[1]); + //accounts[1] redeems its deposit rep. + assert.equal(reputation.toNumber(), 100); + }); + + it("execute proposeContributionReward mint reputation with period 0 ", async function() { + var testSetup = await setup(accounts); + var reputationReward = 12; + + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [0,0,0], + testSetup.standardTokenMock.address, + accounts[1], + helpers.NULL_ADDRESS, + {from:accounts[2]} + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + tx = await testSetup.contributionRewardExt.redeem(proposalId,[true,false,false,false]); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "RedeemReputation"); + assert.equal(tx.logs[0].args._amount, reputationReward); + var rep = await testSetup.org.reputation.balanceOf(accounts[1]); + assert.equal(rep.toNumber(),testSetup.reputationArray[1]+reputationReward); + //try to redeem again. + tx = await testSetup.contributionRewardExt.redeem(proposalId,[true,false,false,false]); + assert.equal(tx.logs.length, 0); + rep = await testSetup.org.reputation.balanceOf(accounts[1]); + assert.equal(rep.toNumber(),testSetup.reputationArray[1]+reputationReward); + }); + + it("cannot initialize twice", async function() { + var testSetup = await setup(accounts); + try { + await testSetup.contributionRewardExt.initialize( + testSetup.org.avatar.address, + testSetup.contributionRewardExtParams.votingMachine.absoluteVote.address, + testSetup.contributionRewardExtParams.votingMachine.absoluteVote.address, + helpers.NULL_ADDRESS + ); + assert(false, 'cannot initialize twice'); + } catch (ex) { + helpers.assertVMException(ex); + } + }); + it("execute proposeContributionReward to self and redeem from external contract ", async function() { + var testSetup = await setup(accounts,false,0,accounts[0]); + var reputationReward = 12; + var nativeTokenReward = 12; + var ethReward = 12; + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address,30,{from:accounts[1]}); + var externalTokenReward = 12; + //send some ether to the org avatar + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value:20}); + var tx = await testSetup.contributionRewardExt.proposeContributionReward( + web3.utils.asciiToHex("description"), + reputationReward, + [nativeTokenReward,ethReward,externalTokenReward], + testSetup.standardTokenMock.address, + testSetup.contributionRewardExt.address, + helpers.NULL_ADDRESS + ); + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.contributionRewardExtParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.contributionRewardExt.redeem(proposalId,[true,true,true,true]); + var eth = await web3.eth.getBalance(testSetup.contributionRewardExt.address); + assert.equal(eth,ethReward); + var otherAvatar = await Avatar.new('otheravatar', helpers.NULL_ADDRESS, helpers.NULL_ADDRESS); + + //redeem ether + try { + await testSetup.contributionRewardExt.redeemEtherByRewarder(proposalId,otherAvatar.address,1,{from:accounts[1]}); + assert(false, 'only service contract can redeem'); + } catch (ex) { + helpers.assertVMException(ex); + } + tx = await testSetup.contributionRewardExt.redeemEtherByRewarder(proposalId,otherAvatar.address,1); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "RedeemEther"); + assert.equal(tx.logs[0].args._amount, 1); + assert.equal(await web3.eth.getBalance(otherAvatar.address),1); + //cannot redeem more than the proposal reward + var proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.ethRewardLeft, ethReward - 1); + try { + await testSetup.contributionRewardExt.redeemEtherByRewarder(proposalId,otherAvatar.address,proposal.ethRewardLeft+1); + assert(false, 'cannot redeem more than the proposal reward'); + } catch (ex) { + helpers.assertVMException(ex); + } + await testSetup.contributionRewardExt.redeemEtherByRewarder(proposalId,otherAvatar.address,proposal.ethRewardLeft); + assert.equal(await web3.eth.getBalance(otherAvatar.address),ethReward); + proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.ethRewardLeft, 0); + + //redeem nativeToken + try { + await testSetup.contributionRewardExt.redeemNativeTokenByRewarder(proposalId,otherAvatar.address,1,{from:accounts[1]}); + assert(false, 'only service contract can redeem'); + } catch (ex) { + helpers.assertVMException(ex); + } + tx = await testSetup.contributionRewardExt.redeemNativeTokenByRewarder(proposalId,otherAvatar.address,1); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "RedeemNativeToken"); + assert.equal(tx.logs[0].args._amount, 1); + + assert.equal(await testSetup.org.token.balanceOf(otherAvatar.address),1); + //cannot redeem more than the proposal reward + proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.nativeTokenRewardLeft, nativeTokenReward - 1); + try { + await testSetup.contributionRewardExt.redeemNativeTokenByRewarder(proposalId,otherAvatar.address,proposal.nativeTokenRewardLeft+1); + assert(false, 'cannot redeem more than the proposal reward'); + } catch (ex) { + helpers.assertVMException(ex); + } + await testSetup.contributionRewardExt.redeemNativeTokenByRewarder(proposalId,otherAvatar.address,proposal.nativeTokenRewardLeft); + assert.equal(await testSetup.org.token.balanceOf(otherAvatar.address),nativeTokenReward); + proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.nativeTokenRewardLeft, 0); + + + //redeem externalToken + try { + await testSetup.contributionRewardExt.redeemExternalTokenByRewarder(proposalId,otherAvatar.address,1,{from:accounts[1]}); + assert(false, 'only service contract can redeem'); + } catch (ex) { + helpers.assertVMException(ex); + } + tx = await testSetup.contributionRewardExt.redeemExternalTokenByRewarder(proposalId,otherAvatar.address,1); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "RedeemExternalToken"); + assert.equal(tx.logs[0].args._amount, 1); + + assert.equal(await testSetup.standardTokenMock.balanceOf(otherAvatar.address),1); + //cannot redeem more than the proposal reward + proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.externalTokenRewardLeft, externalTokenReward - 1); + try { + await testSetup.contributionRewardExt.redeemExternalTokenByRewarder(proposalId,otherAvatar.address,proposal.externalTokenRewardLeft+1); + assert(false, 'cannot redeem more than the proposal reward'); + } catch (ex) { + helpers.assertVMException(ex); + } + await testSetup.contributionRewardExt.redeemExternalTokenByRewarder(proposalId,otherAvatar.address,proposal.externalTokenRewardLeft); + assert.equal(await testSetup.standardTokenMock.balanceOf(otherAvatar.address),externalTokenReward); + proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.externalTokenRewardLeft, 0); + + + //redeem reputation + try { + await testSetup.contributionRewardExt.redeemReputationByRewarder(proposalId,otherAvatar.address,1,{from:accounts[1]}); + assert(false, 'only service contract can redeem'); + } catch (ex) { + helpers.assertVMException(ex); + } + tx = await testSetup.contributionRewardExt.redeemReputationByRewarder(proposalId,otherAvatar.address,1); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "RedeemReputation"); + assert.equal(tx.logs[0].args._amount, 1); + + assert.equal(await testSetup.org.reputation.balanceOf(otherAvatar.address),1); + //cannot redeem more than the proposal reward + proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.reputationChangeLeft, reputationReward - 1); + try { + await testSetup.contributionRewardExt.redeemReputationByRewarder(proposalId,otherAvatar.address,proposal.reputationChangeLeft+1); + assert(false, 'cannot redeem more than the proposal reward'); + } catch (ex) { + helpers.assertVMException(ex); + } + await testSetup.contributionRewardExt.redeemReputationByRewarder(proposalId,otherAvatar.address,proposal.reputationChangeLeft); + assert.equal(await testSetup.org.reputation.balanceOf(otherAvatar.address),reputationReward); + proposal = await testSetup.contributionRewardExt.organizationProposals(proposalId); + assert.equal(proposal.reputationChangeLeft, 0); + + }); + +});