Skill-Based Bounty Marketplace

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;
    }
}