pragma solidity ^0.4.11; /// @title StandardBounty /// @dev Used to pay out individuals or groups for task fulfillment through /// stepwise work submission, acceptance, and payment /// @author Mark Beylin , Gonçalo Sá contract StandardBounty { uint constant public MAX_FULFILLMENTS = 254; /* * Events */ event BountyActivated(address issuer); event BountyFulfilled(address indexed fulfiller, uint256 indexed _fulfillmentId, uint256 indexed _milestoneId); event FulfillmentAccepted(address indexed fulfiller, uint256 indexed _fulfillmentId, uint256 indexed _milestoneId); event FulfillmentPaid(address indexed fulfiller, uint256 indexed _fulfillmentId, uint256 indexed _milestoneId); event BountyKilled(); event ContributionAdded(address indexed contributor, uint256 value); event DeadlineExtended(uint newDeadline); /* * Storage */ address public issuer; // the creator of the bounty string public issuerContact; // string of a contact method used to reach the issuer in case it is needed address public arbiter; // should mediate conflicts, has the power to accept work BountyStages public bountyStage; // the stage of the bounty uint public deadline; // unix timestamp before which all work must be submitted string public data; // a hash of data representing the requirements for each milestone of the bounty, along with any associated files uint public numMilestones; // the total number of milestones uint[] public fulfillmentAmounts; // the amount of wei to be rewarded to the user who fulfills the i'th milestone mapping(uint=>Fulfillment[]) public fulfillments; // all submitted fulfillments for each milestone mapping(uint=>uint) public numFulfillments; // the number of submitted fulfillments for each milestone mapping(uint=>uint) public numAccepted; // the number of accepted fulfillments for each milestone mapping(uint=>uint) public numPaid; // the number of paid fulfillments for each milestone /* * Enums */ enum BountyStages { Draft, Active, Dead } /* * Structs */ struct Fulfillment { bool paid; bool accepted; address fulfiller; string data; string dataType; } /* * Modifiers */ modifier onlyIssuer() { require(msg.sender == issuer); _; } modifier onlyIssuerOrArbiter() { require(msg.sender == issuer || msg.sender == arbiter); _; } modifier notIssuerOrArbiter() { require(msg.sender != issuer && msg.sender != arbiter); _; } modifier onlyFulfiller(uint _fulfillmentId, uint _milestoneId) { require(msg.sender == fulfillments[_milestoneId][_fulfillmentId].fulfiller); _; } modifier amountIsNotZero(uint amount) { require(amount != 0); _; } modifier amountsNotZero(uint[] amount) { for (uint i = 0; i < amount.length; i++){ require(amount[i] != 0); } _; } modifier amountEqualsValue(uint amount) { require((amount * 1 wei) == msg.value); _; } modifier isBeforeDeadline() { require(now < deadline); _; } modifier validateDeadline(uint newDeadline) { require(newDeadline > now); _; } modifier newDeadlineIsValid(uint newDeadline) { require(newDeadline > deadline); _; } modifier isAtStage(BountyStages desiredStage) { require(bountyStage == desiredStage); _; } modifier isNotDraft () { require(bountyStage != BountyStages.Draft); _; } modifier isNotDead() { require(bountyStage != BountyStages.Dead); _; } modifier checkFulfillmentsNumber(uint _milestoneId) { require(numFulfillments[_milestoneId] < MAX_FULFILLMENTS); _; } modifier validateFulfillmentArrayIndex(uint index, uint _milestoneId) { require(index < numFulfillments[_milestoneId]); _; } modifier validateMilestoneIndex(uint _milestoneId){ require(_milestoneId < numMilestones); _; } modifier checkFulfillmentIsApprovedAndUnpaid(uint _fulfillmentId, uint _milestoneId) { require(fulfillments[_milestoneId][_fulfillmentId].accepted && !fulfillments[_milestoneId][_fulfillmentId].paid); _; } modifier validateFunding() { uint total = 0; for (uint i = 0 ; i < numMilestones; i++){ total += fulfillmentAmounts[i]; } require (this.balance >= (total + unpaidAmount())); _; } modifier correctLengths(uint _numMilestones, uint _length) { require(_numMilestones == _length); _; } modifier unpaidAmountRemains(uint _milestoneId) { require((unpaidAmount() + fulfillmentAmounts[_milestoneId]) < this.balance); _; } /* * Public functions */ /// @dev StandardBounty(): instantiates a new draft bounty /// @param _deadline the unix timestamp after which fulfillments will no longer be accepted /// @param _contactInfo a string with contact info of the issuer, for them to be contacted if needed /// @param _data the requirements of the bounty /// @param _fulfillmentAmounts the amount of wei to be paid out for each successful fulfillment /// @param _numMilestones the total number of milestones which can be paid out /// @param _arbiter the address of the arbiter who can mediate claims function StandardBounty( uint _deadline, string _contactInfo, string _data, uint[] _fulfillmentAmounts, uint _numMilestones, address _arbiter ) amountsNotZero(_fulfillmentAmounts) validateDeadline(_deadline) correctLengths(_numMilestones, _fulfillmentAmounts.length) { issuer = tx.origin; //this allows for the issuance of a bounty using a factory issuerContact = _contactInfo; bountyStage = BountyStages.Draft; deadline = _deadline; data = _data; numMilestones = _numMilestones; arbiter = _arbiter; fulfillmentAmounts = _fulfillmentAmounts; } /// @dev contribute(): a function allowing anyone to contribute ether to a /// bounty, as long as it is still before its deadline. Shouldn't /// keep ether by accident (hence 'value'). /// @notice Please note you funds will be at the mercy of the issuer /// and can be drained at any moment. Be careful! /// @param value the amount being contributed in ether to prevent /// accidental deposits function contribute (uint value) payable isBeforeDeadline isNotDead amountIsNotZero(value) amountEqualsValue(value) { ContributionAdded(msg.sender, msg.value); } /// @notice Send funds to activate the bug bounty /// @dev activateBounty(): activate a bounty so it may pay out /// @param value the amount being contributed in ether to prevent /// accidental deposits function activateBounty(uint value) payable public isBeforeDeadline onlyIssuer amountEqualsValue(value) validateFunding { transitionToState(BountyStages.Active); ContributionAdded(msg.sender, msg.value); BountyActivated(msg.sender); } /// @dev fulfillBounty(): submit a fulfillment for the given bounty /// @param _data the data artifacts representing the fulfillment of the bounty /// @param _dataType a meaningful description of the type of data the fulfillment represents /// @param _milestoneId the id of the milestone being fulfilled function fulfillBounty(string _data, string _dataType, uint _milestoneId) public isAtStage(BountyStages.Active) isBeforeDeadline checkFulfillmentsNumber(_milestoneId) notIssuerOrArbiter { fulfillments[_milestoneId].push(Fulfillment(false, false, msg.sender, _data, _dataType)); BountyFulfilled(msg.sender, numFulfillments[_milestoneId]++, _milestoneId); } /// @dev acceptFulfillment(): accept a given fulfillment /// @param _fulfillmentId the index of the fulfillment being accepted /// @param _milestoneId the id of the milestone being accepted function acceptFulfillment(uint _fulfillmentId, uint _milestoneId) public onlyIssuerOrArbiter isAtStage(BountyStages.Active) validateMilestoneIndex(_milestoneId) validateFulfillmentArrayIndex(_fulfillmentId, _milestoneId) unpaidAmountRemains(_milestoneId) { fulfillments[_milestoneId][_fulfillmentId].accepted = true; numAccepted[_milestoneId]++; FulfillmentAccepted(msg.sender, _fulfillmentId, _milestoneId); } /// @dev fulfillmentPayment(): pay the fulfiller for their work /// @param _fulfillmentId the index of the fulfillment being accepted /// @param _milestoneId the id of the milestone being paid function fulfillmentPayment(uint _fulfillmentId, uint _milestoneId) public validateMilestoneIndex(_milestoneId) validateFulfillmentArrayIndex(_fulfillmentId, _milestoneId) onlyFulfiller(_fulfillmentId, _milestoneId) checkFulfillmentIsApprovedAndUnpaid(_fulfillmentId, _milestoneId) { fulfillments[_milestoneId][_fulfillmentId].fulfiller.transfer(fulfillmentAmounts[_milestoneId]); fulfillments[_milestoneId][_fulfillmentId].paid = true; numPaid[_milestoneId]++; FulfillmentPaid(msg.sender, _fulfillmentId, _milestoneId); } /// @dev killBounty(): drains the contract of it's remaining /// funds, and moves the bounty into stage 3 (dead) since it was /// either killed in draft stage, or never accepted any fulfillments function killBounty() public onlyIssuer { issuer.transfer(this.balance - unpaidAmount()); transitionToState(BountyStages.Dead); BountyKilled(); } /// @dev extendDeadline(): allows the issuer to add more time to the /// bounty, allowing it to continue accepting fulfillments /// @param _newDeadline the new deadline in timestamp format function extendDeadline(uint _newDeadline) public onlyIssuer newDeadlineIsValid(_newDeadline) { deadline = _newDeadline; DeadlineExtended(_newDeadline); } /// @dev changeBounty(): allows the issuer to change all bounty storage /// members simultaneously /// @param _newDeadline the new deadline for the bounty /// @param _newContactInfo the new contact information for the issuer /// @param _newData the new requirements of the bounty /// @param _newFulfillmentAmounts the new fulfillment amounts /// @param _newNumMilestones the number of milestones which can be fulfilled function changeBounty(uint _newDeadline, string _newContactInfo, string _newData, uint[] _newFulfillmentAmounts, uint _newNumMilestones, address _newArbiter) public onlyIssuer validateDeadline(_newDeadline) amountsNotZero(_newFulfillmentAmounts) correctLengths(_newNumMilestones, _newFulfillmentAmounts.length) isAtStage(BountyStages.Draft) { deadline = _newDeadline; issuerContact = _newContactInfo; data = _newData; fulfillmentAmounts = _newFulfillmentAmounts; numMilestones = _newNumMilestones; arbiter = _newArbiter; } /// @dev changeDeadline(): allows the issuer to change the deadline of the bounty /// @param _newDeadline the new deadline function changeDeadline(uint _newDeadline) public onlyIssuer validateDeadline(_newDeadline) isAtStage(BountyStages.Draft) { deadline = _newDeadline; } /// @dev changeData(): allows the issuer to change the data of the bounty /// @param _newData the new data function changeData(string _newData) public onlyIssuer isAtStage(BountyStages.Draft) { data = _newData; } /// @dev changeContact(): allows the issuer to change the contact of the bounty /// @param _newContact the new data function changeContact(string _newContact) public onlyIssuer isAtStage(BountyStages.Draft) { issuerContact = _newContact; } /// @dev changeArbiter(): allows the issuer to change the arbiter of the bounty /// @param _newArbiter the new data function changeArbiter(address _newArbiter) public onlyIssuer isAtStage(BountyStages.Draft) { arbiter = _newArbiter; } /// @dev changeFulfillmentAmounts(): allows the issuer to change the /// fulfillment amounts of the bounty /// @param _newFulfillmentAmounts the new fulfillment amounts /// @param _numMilestones the number of milestones which can be fulfilled function changeFulfillmentAmounts(uint[] _newFulfillmentAmounts, uint _numMilestones) public onlyIssuer amountsNotZero(_newFulfillmentAmounts) correctLengths(_numMilestones, _newFulfillmentAmounts.length) isAtStage(BountyStages.Draft) { fulfillmentAmounts = _newFulfillmentAmounts; } /// @dev getFulfillment(): Returns the fulfillment at a given index /// @param _fulfillmentId the index of the fulfillment to return /// @param _milestoneId the index of the milestone to return function getFulfillment(uint _fulfillmentId, uint _milestoneId) public constant returns (bool, bool, address, string, string) { return (fulfillments[_milestoneId][_fulfillmentId].paid, fulfillments[_milestoneId][_fulfillmentId].accepted, fulfillments[_milestoneId][_fulfillmentId].fulfiller, fulfillments[_milestoneId][_fulfillmentId].data, fulfillments[_milestoneId][_fulfillmentId].dataType); } /// @dev getBounty(): Returns the details of the bounty function getBounty() public constant returns (address, string, uint, uint, string, uint) { return (issuer, issuerContact, uint(bountyStage), deadline, data, numMilestones); } /// @dev unpaidAmount(): calculates the amount which /// the bounty has yet to pay out function unpaidAmount() public constant returns (uint amount) { for (uint i = 0; i < numMilestones; i++){ amount += fulfillmentAmounts[i] * (numAccepted[i] - numPaid[i]); } } /* * Internal functions */ /// @dev transitionToState(): transitions the contract to the /// state passed in the parameter `_newStage` given the /// conditions stated in the body of the function /// @param _newStage the new stage to transition to function transitionToState(BountyStages _newStage) internal { bountyStage = _newStage; } }