pragma solidity >=0.0; /* Copyright 2016, Jordi Baylina This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /// @title MilestoneTracker Contract /// @author Jordi Baylina /// @dev This contract tracks the /// is rules the relation between a donor and a recipient /// in order to guaranty to the donor that the job will be done and to guaranty /// to the recipient that he will be paid /// @dev We use the RLP library to decode RLP so that the donor can approve one /// set of milestone changes at a time. /// https://github.com/androlo/standard-contracts/blob/master/contracts/src/codec/RLP.sol import "RLP.sol"; /// @dev This contract allows for `recipient` to set and modify milestones contract MilestoneTracker { using RLP for RLP.RLPItem; using RLP for RLP.Iterator; using RLP for bytes; struct Milestone { string description; // Description of this milestone string url; // A link to more information (swarm gateway) uint minCompletionDate; // Earliest UNIX time the milestone can be paid uint maxCompletionDate; // Latest UNIX time the milestone can be paid address milestoneLeadLink; // Similar to `recipient`but for this milestone address reviewer; // Can reject the completion of this milestone uint reviewTime; // How many seconds the reviewer has to review address paymentSource; // Where the milestone payment is sent from bytes payData; // Data defining how much ether is sent where MilestoneStatus status; // Current status of the milestone // (Completed, AuthorizedForPayment...) uint doneTime; // UNIX time when the milestone was marked DONE } // The list of all the milestones. Milestone[] public milestones; address public recipient; // Calls functions in the name of the recipient address public donor; // Calls functions in the name of the donor address public arbitrator; // Calls functions in the name of the arbitrator enum MilestoneStatus { AcceptedAndInProgress, Completed, AuthorizedForPayment, Canceled } // True if the campaign has been canceled bool public campaignCanceled; // True if an approval on a change to `milestones` is a pending bool public changingMilestones; // The pending change to `milestones` encoded in RLP bytes public proposedMilestones; /// @dev The following modifiers only allow specific roles to call functions /// with these modifiers modifier onlyRecipient { if (msg.sender != recipient) revert(); _; } modifier onlyArbitrator { if (msg.sender != arbitrator) revert(); _; } modifier onlyDonor { if (msg.sender != donor) revert(); _; } /// @dev The following modifiers prevent functions from being called if the /// campaign has been canceled or if new milestones are being proposed modifier campaignNotCanceled { if (campaignCanceled) revert(); _; } modifier notChanging { if (changingMilestones) revert(); _; } // @dev Events to make the payment movements easy to find on the blockchain event NewMilestoneListProposed(); event NewMilestoneListUnproposed(); event NewMilestoneListAccepted(); event ProposalStatusChanged(uint idProposal, MilestoneStatus newProposal); event CampaignCanceled(); /////////// // Constructor /////////// /// @notice The Constructor creates the Milestone contract on the blockchain /// @param _arbitrator Address assigned to be the arbitrator /// @param _donor Address assigned to be the donor /// @param _recipient Address assigned to be the recipient constructor ( address _arbitrator, address _donor, address _recipient ) { arbitrator = _arbitrator; donor = _donor; recipient = _recipient; } ///////// // Helper functions ///////// /// @return The number of milestones ever created even if they were canceled function numberOfMilestones() public view returns (uint) { return milestones.length; } //////// // Change players //////// /// @notice `onlyArbitrator` Reassigns the arbitrator to a new address /// @param _newArbitrator The new arbitrator function changeArbitrator(address _newArbitrator) public onlyArbitrator { arbitrator = _newArbitrator; } /// @notice `onlyDonor` Reassigns the `donor` to a new address /// @param _newDonor The new donor function changeDonor(address _newDonor) public onlyDonor { donor = _newDonor; } /// @notice `onlyRecipient` Reassigns the `recipient` to a new address /// @param _newRecipient The new recipient function changeRecipient(address _newRecipient) public onlyRecipient { recipient = _newRecipient; } //////////// // Creation and modification of Milestones //////////// /// @notice `onlyRecipient` Proposes new milestones or changes old /// milestones, this will require a user interface to be built up to /// support this functionality as asks for RLP encoded bytecode to be /// generated, until this interface is built you can use this script: /// https://github.com/Giveth/milestonetracker/blob/master/js/milestonetracker_helper.js /// the functions milestones2bytes and bytes2milestones will enable the /// recipient to encode and decode a list of milestones, also see /// https://github.com/Giveth/milestonetracker/blob/master/README.md /// @param _newMilestones The RLP encoded list of milestones; each milestone /// has these fields: /// string description, /// string url, /// uint minCompletionDate, // seconds since 1/1/1970 (UNIX time) /// uint maxCompletionDate, // seconds since 1/1/1970 (UNIX time) /// address milestoneLeadLink, /// address reviewer, /// uint reviewTime /// address paymentSource, /// bytes payData, function proposeMilestones(bytes memory _newMilestones ) public onlyRecipient campaignNotCanceled { proposedMilestones = _newMilestones; changingMilestones = true; emit NewMilestoneListProposed(); } //////////// // Normal actions that will change the state of the milestones //////////// /// @notice `onlyRecipient` Cancels the proposed milestones and reactivates /// the previous set of milestones function unproposeMilestones() public onlyRecipient campaignNotCanceled { delete proposedMilestones; changingMilestones = false; emit NewMilestoneListUnproposed(); } /// @notice `onlyDonor` Approves the proposed milestone list /// @param _hashProposals The keccak256() of the proposed milestone list's /// bytecode; this confirms that the `donor` knows the set of milestones /// they are approving function acceptProposedMilestones(bytes32 _hashProposals ) public onlyDonor campaignNotCanceled { uint i; if (!changingMilestones) revert(); if (keccak256(proposedMilestones) != _hashProposals) revert(); // Cancel all the unfinished milestones for (i=0; i= milestones.length) revert(); Milestone storage milestone = milestones[_idMilestone]; if ( (msg.sender != milestone.milestoneLeadLink) &&(msg.sender != recipient)) revert(); if (milestone.status != MilestoneStatus.AcceptedAndInProgress) revert(); if (block.timestamp < milestone.minCompletionDate) revert(); if (block.timestamp > milestone.maxCompletionDate) revert(); milestone.status = MilestoneStatus.Completed; milestone.doneTime = block.timestamp; emit ProposalStatusChanged(_idMilestone, milestone.status); } /// @notice `onlyReviewer` Approves a specific milestone /// @param _idMilestone ID of the milestone that is approved function approveCompletedMilestone(uint _idMilestone) public campaignNotCanceled notChanging { if (_idMilestone >= milestones.length) revert(); Milestone storage milestone = milestones[_idMilestone]; if ((msg.sender != milestone.reviewer) || (milestone.status != MilestoneStatus.Completed)) revert(); authorizePayment(_idMilestone); } /// @notice `onlyReviewer` Rejects a specific milestone's completion and /// reverts the `milestone.status` back to the `AcceptedAndInProgress` /// state /// @param _idMilestone ID of the milestone that is being rejected function rejectMilestone(uint _idMilestone) public campaignNotCanceled notChanging { if (_idMilestone >= milestones.length) revert(); Milestone storage milestone = milestones[_idMilestone]; if ((msg.sender != milestone.reviewer) || (milestone.status != MilestoneStatus.Completed)) revert(); milestone.status = MilestoneStatus.AcceptedAndInProgress; emit ProposalStatusChanged(_idMilestone, milestone.status); } /// @notice `onlyRecipientOrLeadLink` Sends the milestone payment as /// specified in `payData`; the recipient can only call this after the /// `reviewTime` has elapsed /// @param _idMilestone ID of the milestone to be paid out function requestMilestonePayment(uint _idMilestone ) public campaignNotCanceled notChanging { if (_idMilestone >= milestones.length) revert(); Milestone storage milestone = milestones[_idMilestone]; if ( (msg.sender != milestone.milestoneLeadLink) &&(msg.sender != recipient)) revert(); if ((milestone.status != MilestoneStatus.Completed) || (block.timestamp < milestone.doneTime + milestone.reviewTime)) revert(); authorizePayment(_idMilestone); } /// @notice `onlyRecipient` Cancels a previously accepted milestone /// @param _idMilestone ID of the milestone to be canceled function cancelMilestone(uint _idMilestone) public onlyRecipient campaignNotCanceled notChanging { if (_idMilestone >= milestones.length) revert(); Milestone storage milestone = milestones[_idMilestone]; if ((milestone.status != MilestoneStatus.AcceptedAndInProgress) && (milestone.status != MilestoneStatus.Completed)) revert(); milestone.status = MilestoneStatus.Canceled; emit ProposalStatusChanged(_idMilestone, milestone.status); } /// @notice `onlyArbitrator` Forces a milestone to be paid out as long as it /// has not been paid or canceled /// @param _idMilestone ID of the milestone to be paid out function arbitrateApproveMilestone(uint _idMilestone ) public onlyArbitrator campaignNotCanceled notChanging { if (_idMilestone >= milestones.length) revert(); Milestone storage milestone = milestones[_idMilestone]; if ((milestone.status != MilestoneStatus.AcceptedAndInProgress) && (milestone.status != MilestoneStatus.Completed)) revert(); authorizePayment(_idMilestone); } /// @notice `onlyArbitrator` Cancels the entire campaign voiding all /// milestones. function arbitrateCancelCampaign() public onlyArbitrator campaignNotCanceled { campaignCanceled = true; emit CampaignCanceled(); } // @dev This internal function is executed when the milestone is paid out function authorizePayment(uint _idMilestone) internal { if (_idMilestone >= milestones.length) revert(); Milestone storage milestone = milestones[_idMilestone]; // Recheck again to not pay twice if (milestone.status == MilestoneStatus.AuthorizedForPayment) revert(); milestone.status = MilestoneStatus.AuthorizedForPayment; (bool success,) = milestone.paymentSource.call{value: 0}(milestone.payData); require(success); emit ProposalStatusChanged(_idMilestone, milestone.status); } }