Podcast Sponsorship Escrow

What it does:
Holds sponsorship funds in escrow and releases payments to podcasters only when agreed sponsorship deliverables are completed and verified on-chain.

Why it matters:
Protects both sponsors and podcasters, reduces payment disputes, enforces sponsorship terms transparently, and removes the need for trusted intermediaries or manual invoicing.

How it works:

  • Sponsors create a sponsorship deal with budget, deliverables, and deadlines

  • Funds are deposited into an on-chain escrow contract

  • Podcasters submit proof of sponsorship delivery (episode link, timestamp, reference hash)

  • Sponsors or designated verifiers approve completed deliverables

  • Smart contract releases payment automatically upon approval

  • Partial payments can be unlocked per milestone or episode

  • All agreements, approvals, and payouts are transparent and auditable on-chain

      // SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title PodcastSponsorshipEscrow
 * @author Nam
 * @notice Escrow contract for podcast sponsorships with milestone-based releases
 */
contract PodcastSponsorshipEscrow {

    uint256 public dealCount;

    // -------------------- STRUCTS --------------------

    struct Milestone {
        string description;      // e.g. "Mid-roll ad in Episode #42"
        uint256 amount;          // payment amount
        bool approved;
        bool paid;
        string proofURI;         // episode link / IPFS hash
    }

    struct SponsorshipDeal {
        address sponsor;
        address payable podcaster;
        uint256 totalBudget;
        uint256 deadline;
        bool cancelled;
        Milestone[] milestones;
    }

    // -------------------- STORAGE --------------------

    mapping(uint256 => SponsorshipDeal) public deals;

    // -------------------- EVENTS --------------------

    event DealCreated(
        uint256 indexed dealId,
        address indexed sponsor,
        address indexed podcaster,
        uint256 totalBudget
    );

    event ProofSubmitted(uint256 indexed dealId, uint256 milestoneIndex, string proofURI);
    event MilestoneApproved(uint256 indexed dealId, uint256 milestoneIndex);
    event MilestonePaid(uint256 indexed dealId, uint256 milestoneIndex, uint256 amount);
    event DealCancelled(uint256 indexed dealId);

    // -------------------- DEAL CREATION --------------------

    function createDeal(
        address payable _podcaster,
        uint256 _deadline,
        string[] calldata _milestoneDescriptions,
        uint256[] calldata _milestoneAmounts
    ) external payable returns (uint256) {
        require(_podcaster != address(0), "Invalid podcaster");
        require(_milestoneDescriptions.length == _milestoneAmounts.length, "Length mismatch");
        require(_milestoneDescriptions.length > 0, "No milestones");

        uint256 total;
        for (uint256 i = 0; i < _milestoneAmounts.length; i++) {
            total += _milestoneAmounts[i];
        }
        require(msg.value == total, "Budget mismatch");

        dealCount += 1;
        SponsorshipDeal storage d = deals[dealCount];
        d.sponsor = msg.sender;
        d.podcaster = _podcaster;
        d.totalBudget = msg.value;
        d.deadline = _deadline;

        for (uint256 i = 0; i < _milestoneDescriptions.length; i++) {
            d.milestones.push(
                Milestone({
                    description: _milestoneDescriptions[i],
                    amount: _milestoneAmounts[i],
                    approved: false,
                    paid: false,
                    proofURI: ""
                })
            );
        }

        emit DealCreated(dealCount, msg.sender, _podcaster, msg.value);
        return dealCount;
    }

    // -------------------- PODCASTER ACTIONS --------------------

    function submitProof(
        uint256 _dealId,
        uint256 _milestoneIndex,
        string calldata _proofURI
    ) external {
        SponsorshipDeal storage d = deals[_dealId];
        require(msg.sender == d.podcaster, "Not podcaster");
        require(_milestoneIndex < d.milestones.length, "Invalid milestone");

        Milestone storage m = d.milestones[_milestoneIndex];
        require(!m.paid, "Already paid");

        m.proofURI = _proofURI;
        emit ProofSubmitted(_dealId, _milestoneIndex, _proofURI);
    }

    // -------------------- SPONSOR ACTIONS --------------------

    function approveMilestone(uint256 _dealId, uint256 _milestoneIndex) external {
        SponsorshipDeal storage d = deals[_dealId];
        require(msg.sender == d.sponsor, "Not sponsor");
        require(_milestoneIndex  0, "No proof submitted");

        m.approved = true;
        emit MilestoneApproved(_dealId, _milestoneIndex);

        _releasePayment(_dealId, _milestoneIndex);
    }

    // -------------------- INTERNAL PAYMENT LOGIC --------------------

    function _releasePayment(uint256 _dealId, uint256 _milestoneIndex) internal {
        SponsorshipDeal storage d = deals[_dealId];
        Milestone storage m = d.milestones[_milestoneIndex];
        require(!m.paid, "Already paid");

        m.paid = true;
        d.podcaster.transfer(m.amount);

        emit MilestonePaid(_dealId, _milestoneIndex, m.amount);
    }

    // -------------------- DEAL CANCELLATION --------------------

    function cancelDeal(uint256 _dealId) external {
        SponsorshipDeal storage d = deals[_dealId];
        require(msg.sender == d.sponsor, "Not sponsor");
        require(!d.cancelled, "Already cancelled");

        d.cancelled = true;

        uint256 refund;
        for (uint256 i = 0; i  0) {
            payable(d.sponsor).transfer(refund);
        }

        emit DealCancelled(_dealId);
    }
}