What it does: Clients post tasks (design, coding, writing). Funds are escrowed on-chain until verified completion.
Why it matters: Eliminates platform middlemen and payment fraud.
How it works:
Job posted with milestone rules.
Funds locked in escrow.
Deliverables verified by decentralized arbiters.
Release happens automatically on acceptance.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* Skill-Based Bounty Marketplace
*
* Entities:
* - Client: creates job and funds escrow
* - Worker: accepts job and delivers work
* - Arbiter: resolves disputes
*
* Features:
* - Escrowed funds with milestones
* - Submission + review flow
* - Refunds when work is not delivered
* - Dispute resolution
*/
contract SkillBounty {
enum JobState {
Open, // Posted, not yet accepted
Accepted, // Worker locked in
Submitted, // Work delivered, awaiting review
Approved, // Work accepted, payout in progress
Disputed, // Arbiter intervention
Refunded, // Client refunded
Completed // Worker paid
}
struct Milestone {
string description;
uint256 amount;
bool released;
}
struct Job {
address client;
address worker;
address arbiter;
JobState state;
uint256 createdAt;
uint256 deadline; // seconds (optional)
uint256 totalFunded;
uint256 totalReleased;
Milestone[] milestones;
string briefURI; // IPFS or https job brief
string deliveryURI; // work submission link
}
uint256 public nextJobId;
mapping(uint256 => Job) public jobs;
// Config
uint256 public arbiterFeeBps = 200; // 2% from funded amount when dispute handled
// Events
event JobCreated(uint256 indexed jobId, address indexed client);
event JobFunded(uint256 indexed jobId, uint256 amount);
event JobAccepted(uint256 indexed jobId, address indexed worker);
event WorkSubmitted(uint256 indexed jobId, string deliveryURI);
event MilestoneReleased(uint256 indexed jobId, uint256 milestoneIndex, uint256 amount);
event JobApproved(uint256 indexed jobId);
event JobRefunded(uint256 indexed jobId, uint256 refunded);
event DisputeOpened(uint256 indexed jobId);
event DisputeResolved(uint256 indexed jobId, uint256 toWorker, uint256 toClient, uint256 arbiterFee);
event JobCompleted(uint256 indexed jobId);
modifier onlyClient(uint256 jobId) {
require(msg.sender == jobs[jobId].client, "Not client");
_;
}
modifier onlyWorker(uint256 jobId) {
require(msg.sender == jobs[jobId].worker, "Not worker");
_;
}
modifier onlyArbiter(uint256 jobId) {
require(msg.sender == jobs[jobId].arbiter, "Not arbiter");
_;
}
modifier inState(uint256 jobId, JobState s) {
require(jobs[jobId].state == s, "Invalid job state");
_;
}
// -------- CREATE JOB --------
function createJob(
address arbiter,
uint256 deadline,
string calldata briefURI,
string[] calldata milestoneDescriptions,
uint256[] calldata milestoneAmounts
) external payable returns (uint256) {
require(arbiter != address(0), "Arbiter required");
require(milestoneDescriptions.length > 0, "Milestones required");
require(milestoneDescriptions.length == milestoneAmounts.length, "Length mismatch");
uint256 total;
for (uint256 i = 0; i < milestoneAmounts.length; i++) {
total += milestoneAmounts[i];
}
require(msg.value == total, "Escrow must match milestones");
uint256 jobId = ++nextJobId;
Job storage j = jobs[jobId];
j.client = msg.sender;
j.arbiter = arbiter;
j.state = JobState.Open;
j.createdAt = block.timestamp;
j.deadline = deadline;
j.totalFunded = msg.value;
j.briefURI = briefURI;
for (uint256 i = 0; i < milestoneDescriptions.length; i++) {
j.milestones.push(
Milestone({
description: milestoneDescriptions[i],
amount: milestoneAmounts[i],
released: false
})
);
}
emit JobCreated(jobId, msg.sender);
emit JobFunded(jobId, msg.value);
return jobId;
}
// -------- ACCEPT JOB --------
function acceptJob(uint256 jobId)
external
inState(jobId, JobState.Open)
{
Job storage j = jobs[jobId];
require(j.worker == address(0), "Already accepted");
j.worker = msg.sender;
j.state = JobState.Accepted;
emit JobAccepted(jobId, msg.sender);
}
// -------- SUBMIT WORK --------
function submitWork(uint256 jobId, string calldata deliveryURI)
external
onlyWorker(jobId)
inState(jobId, JobState.Accepted)
{
Job storage j = jobs[jobId];
j.deliveryURI = deliveryURI;
j.state = JobState.Submitted;
emit WorkSubmitted(jobId, deliveryURI);
}
// -------- APPROVE & RELEASE --------
function approveAndRelease(uint256 jobId)
external
onlyClient(jobId)
inState(jobId, JobState.Submitted)
{
Job storage j = jobs[jobId];
// release all remaining unreleased milestones
for (uint256 i = 0; i < j.milestones.length; i++) {
if (!j.milestones[i].released) {
j.milestones[i].released = true;
j.totalReleased += j.milestones[i].amount;
payable(j.worker).transfer(j.milestones[i].amount);
emit MilestoneReleased(jobId, i, j.milestones[i].amount);
}
}
j.state = JobState.Completed;
emit JobApproved(jobId);
emit JobCompleted(jobId);
}
// -------- PARTIAL MILESTONE RELEASE --------
function releaseMilestone(uint256 jobId, uint256 index)
external
onlyClient(jobId)
inState(jobId, JobState.Submitted)
{
Job storage j = jobs[jobId];
require(index 0) {
require(block.timestamp > j.deadline, "Deadline not passed");
}
uint256 refundable = j.totalFunded - j.totalReleased;
require(refundable > 0, "Nothing refundable");
j.state = JobState.Refunded;
payable(j.client).transfer(refundable);
emit JobRefunded(jobId, refundable);
}
// -------- DISPUTE --------
function openDispute(uint256 jobId)
external
{
Job storage j = jobs[jobId];
require(
msg.sender == j.client || msg.sender == j.worker,
"Not participant"
);
require(
j.state == JobState.Submitted || j.state == JobState.Accepted,
"Cannot dispute"
);
j.state = JobState.Disputed;
emit DisputeOpened(jobId);
}
function resolveDispute(
uint256 jobId,
uint256 toWorker,
uint256 toClient
) external onlyArbiter(jobId) inState(jobId, JobState.Disputed) {
Job storage j = jobs[jobId];
uint256 remaining = j.totalFunded - j.totalReleased;
require(toWorker + toClient <= remaining, "Over-allocate");
uint256 fee = (remaining * arbiterFeeBps) / 10000;
require(toWorker + toClient + fee 0) payable(j.worker).transfer(toWorker);
if (toClient > 0) payable(j.client).transfer(toClient);
if (fee > 0) payable(j.arbiter).transfer(fee);
j.totalReleased += (toWorker + toClient + fee);
j.state = JobState.Completed;
emit DisputeResolved(jobId, toWorker, toClient, fee);
emit JobCompleted(jobId);
}
// -------- ADMIN-LIKE PARAMS (OPTIONAL OWNERLESS) --------
function setArbiterFee(uint256 newFeeBps) external {
// simple governance placeholder — in production use multisig/DAO
require(newFeeBps <= 500, "Too high");
arbiterFeeBps = newFeeBps;
}
// -------- VIEW HELPERS --------
function getMilestones(uint256 jobId)
external
view
returns (Milestone[] memory)
{
return jobs[jobId].milestones;
}
}
Build and Grow By Nam Le Thanh