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 or arbiter 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 the issuer.
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 the issuer or the arbiter 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 the issuer 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 the issuer to change the issuer, arbiter, data, and deadline fields of their bounty.
changeIssuer(address _issuer): This may be called by the issuer to change to a new issuer if need be
changeArbiter(address _arbiter): This may be called by the issuer to change to a new arbiter if need be
changeData(string _data): This may be called by the issuer to change just the data
changeDeadline(uint _deadline): This may be called by the issuer to change just the deadline
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’s issuer 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): For issuers 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 the refundableContribute 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 issuers 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.sol
Although 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.