Appearance
Simple Summary ​
A standard contract and interface for issuing bounties on Ethereum, usable for any type of task, paying in any ERC20 token or in ETH.
Abstract ​
In order to encourage cross-platform interoperability of bounties on Ethereum, and for easier reputational tracking, StandardBounties can facilitate the administration of funds in exchange for deliverables corresponding to a completed task, in a publicly auditable and immutable fashion.
Motivation ​
In the absence of a standard for bounties on Ethereum, it would be difficult for platforms to collaborate and share the bounties which users create (thereby recreating the walled gardens which currently exist on Web2.0 task outsourcing platforms). A standardization of these interactions across task types also makes it far easier to track various reputational metrics (such as how frequently you pay for completed submissions, or how frequently your work gets accepted).
Specification ​
After studying bounties as they've existed for thousands of years (and after implementing and processing over 300 of them on main-net in beta), we've discovered that there are 3 core steps to every bounty:
- a bounty is issued: an
issuer
specifies the requirements for the task, describing the desired outcome, and how much they would be willing to pay for the completion of that task (denoted in one or several tokens). - a bounty is fulfilled: a bounty
fulfiller
may see the bounty, complete the task, and produce a deliverable which is itself the desired outcome of the task, or simply a record that it was completed. Hashes of these deliverables should be stored immutably on-chain, to serve as proof after the fact. - a fulfillment is accepted: a bounty
issuer
orarbiter
may select one or more submissions to be accepted, thereby releasing payment to the bounty fulfiller(s), and transferring ownership over the given deliverable to theissuer
.
To implement these steps, a number of functions are needed:
initializeBounty(address _issuer, address _arbiter, string _data, uint _deadline)
: This is used when deploying a new StandardBounty contract, and is particularly useful when applying the proxy design pattern, whereby bounties cannot be initialized in their constructors. Here, the data string should represent an IPFS hash, corresponding to a JSON object which conforms to the schema (described below).fulfillBounty(address[] _fulfillers, uint[] _numerators, uint _denomenator, string _data)
: This is called to submit a fulfillment, submitting a string representing an IPFS hash which contains the deliverable for the bounty. Initially fulfillments could only be submitted by one individual at a time, however users consistently told us they desired to be able to collaborate on fulfillments, thereby allowing the credit for submissions to be shared by several parties. The lines along which eventual payouts are split are determined by the fractions of the submission credited to each fulfiller (using the array of numerators and single denominator). Here, a bounty platform may also include themselves as a collaborator to collect a small fee for matching the bounty with fulfillers.acceptFulfillment(uint _fulfillmentId, StandardToken[] _payoutTokens, uint[] _tokenAmounts)
: This is called by theissuer
or thearbiter
to pay out a given fulfillment, using an array of tokens, and an array of amounts of each token to be split among the contributors. This allows for the bounty payout amount to move as it needs to based on incoming contributions (which may be transferred directly to the contract address). It also allows for the easy splitting of a given bounty's balance among several fulfillments, if the need should arise.drainBounty(StandardToken[] _payoutTokens)
: This may be called by theissuer
to drain a bounty of it's funds, if the need should arise.
changeBounty(address _issuer, address _arbiter, string _data, uint _deadline)
: This may be called by theissuer
to change theissuer
,arbiter
,data
, anddeadline
fields of their bounty.changeIssuer(address _issuer)
: This may be called by theissuer
to change to a newissuer
if need bechangeArbiter(address _arbiter)
: This may be called by theissuer
to change to a newarbiter
if need bechangeData(string _data)
: This may be called by theissuer
to change just thedata
changeDeadline(uint _deadline)
: This may be called by theissuer
to change just thedeadline
Optional Functions:
acceptAndFulfill(address[] _fulfillers, uint[] _numerators, uint _denomenator, string _data, StandardToken[] _payoutTokens, uint[] _tokenAmounts)
: During the course of the development of this standard, we discovered the desire for fulfillers to avoid paying gas fees on their own, entrusting the bounty'sissuer
to make the submission for them, and at the same time accept it. This is useful since it still immutably stores the exchange of tokens for completed work, but avoids the need for new bounty fulfillers to have any ETH to pay for gas costs in advance of their earnings.changeMasterCopy(StandardBounty _masterCopy)
: Forissuer
s to be able to change the masterCopy which their proxy contract relies on, if the proxy design pattern is being employed.refundableContribute(uint[] _amounts, StandardToken[] _tokens)
: While non-refundable contributions may be sent to a bounty simply by transferring those tokens to the address where it resides, one may also desire to contribute to a bounty with the option to refund their contribution, should the bounty never receive a correct submission which is paid out.refundContribution(uint _contributionId)
: If a bounty hasn't yet paid out to any correct submissions and is past it's deadline, those individuals who employed therefundableContribute
function may retrieve their funds from the contract.
Schemas Persona Schema:
{
name: // optional - A string representing the name of the persona
email: // optional - A string representing the preferred contact email of the persona
githubUsername: // optional - A string representing the github username of the persona
address: // required - A string web3 address of the persona
}
Bounty issuance data
Schema:
{
payload: {
title: // A string representing the title of the bounty
description: // A string representing the description of the bounty, including all requirements
issuer: {
// persona for the issuer of the bounty
},
funders:[
// array of personas of those who funded the issue.
],
categories: // an array of strings, representing the categories of tasks which are being requested
tags: // an array of tags, representing various attributes of the bounty
created: // the timestamp in seconds when the bounty was created
tokenSymbol: // the symbol for the token which the bounty pays out
tokenAddress: // the address for the token which the bounty pays out (0x0 if ETH)
// ------- add optional fields here -------
sourceFileName: // A string representing the name of the file
sourceFileHash: // The IPFS hash of the file associated with the bounty
sourceDirectoryHash: // The IPFS hash of the directory which can be used to access the file
webReferenceURL: // The link to a relevant web reference (ie github issue)
},
meta: {
platform: // a string representing the original posting platform (ie 'gitcoin')
schemaVersion: // a string representing the version number (ie '0.1')
schemaName: // a string representing the name of the schema (ie 'standardSchema' or 'gitcoinSchema')
}
}
Bounty fulfillment
data Schema:
{
payload: {
description: // A string representing the description of the fulfillment, and any necessary links to works
sourceFileName: // A string representing the name of the file being submitted
sourceFileHash: // A string representing the IPFS hash of the file being submitted
sourceDirectoryHash: // A string representing the IPFS hash of the directory which holds the file being submitted
fulfillers: {
// personas for the individuals whose work is being submitted
}
// ------- add optional fields here -------
},
meta: {
platform: // a string representing the original posting platform (ie 'gitcoin')
schemaVersion: // a string representing the version number (ie '0.1')
schemaName: // a string representing the name of the schema (ie 'standardSchema' or 'gitcoinSchema')
}
}
Rationale ​
The development of this standard began a year ago, with the goal of encouraging interoperability among bounty implementations on Ethereum. The initial version had significantly more restrictions: a bounty's data
could not be changed after issuance (it seemed unfair for bounty issuer
s to change the requirements after work is underway), and the bounty payout could not be changed (all funds needed to be deposited in the bounty contract before it could accept submissions).
The initial version was also far less extensible, and only allowed for fixed payments to a given set of fulfillments. This new version makes it possible for funds to be split among several correct submissions, for submissions to be shared among several contributors, and for payouts to not only be in a single token as before, but in as many tokens as the issuer
of the bounty desires. These design decisions were made after the 8+ months which Gitcoin, the Bounties Network, and Status Open Bounty have been live and meaningfully facilitating bounties for repositories in the Web3.0 ecosystem.
Test Cases ​
Tests for our implementation can be found here: https://github.com/Bounties-Network/StandardBounties/tree/develop/test
Implementation ​
A reference implementation can be found here: https://github.com/Bounties-Network/StandardBounties/blob/develop/contracts/StandardBounty.solAlthough this code has been tested, it has not yet been audited or bug-bountied, so we cannot make any assertions about it's correctness, nor can we presently encourage it's use to hold funds on the Ethereum mainnet.
Copyright ​
Copyright and related rights waived via CC0.