pragma solidity 0.5.17;






/**
 * @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() {
        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");
        require(_votingMachine != IntVoteInterface(0), "votingMachine 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;
        }
        if (beneficiary == address(this)) {
            require(_reputationChange >= 0, "negative rep change not allowed for this case");
        }

        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);
                proposal.reputationChange = 0;
            }
        } 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;
    }

}
[{"constant":false,"inputs":[{"internalType":"contract ContributionReward","name":"_contributionReward","type":"address"},{"internalType":"contract GenesisProtocol","name":"_genesisProtocol","type":"address"},{"internalType":"bytes32","name":"_proposalId","type":"bytes32"},{"internalType":"contract Avatar","name":"_avatar","type":"address"},{"internalType":"address","name":"_beneficiary","type":"address"}],"name":"redeem","outputs":[{"internalType":"uint256[3]","name":"gpRewards","type":"uint256[3]"},{"internalType":"uint256[2]","name":"gpDaoBountyReward","type":"uint256[2]"},{"internalType":"bool","name":"executed","type":"bool"},{"internalType":"uint256","name":"winningVote","type":"uint256"},{"internalType":"int256","name":"crReputationReward","type":"int256"},{"internalType":"uint256","name":"crNativeTokenReward","type":"uint256"},{"internalType":"uint256","name":"crEthReward","type":"uint256"},{"internalType":"uint256","name":"crExternalTokenReward","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ContributionRewardExt","name":"_contributionRewardExt","type":"address"},{"internalType":"contract GenesisProtocol","name":"_genesisProtocol","type":"address"},{"internalType":"bytes32","name":"_proposalId","type":"bytes32"},{"internalType":"address","name":"_beneficiary","type":"address"}],"name":"redeemFromCRExt","outputs":[{"internalType":"uint256[3]","name":"gpRewards","type":"uint256[3]"},{"internalType":"uint256[2]","name":"gpDaoBountyReward","type":"uint256[2]"},{"internalType":"bool","name":"executed","type":"bool"},{"internalType":"uint256","name":"winningVote","type":"uint256"},{"internalType":"int256","name":"crReputationReward","type":"int256"},{"internalType":"uint256","name":"crNativeTokenReward","type":"uint256"},{"internalType":"uint256","name":"crEthReward","type":"uint256"},{"internalType":"uint256","name":"crExternalTokenReward","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]