Crowdfunded Product With Automatic Refunds

What it does: Backers fund a product, but money only releases when milestones are met — otherwise refunds trigger automatically.

Why it matters: Protects supporters from failed projects.

How it works:

  • Milestones defined upfront.

  • Third-party auditors verify progress.

  • Funds unlock progressively.

  • Refunds execute instantly if milestones fail.

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

/**
 * @title Crowdfunded Product With Automatic Refunds
 *
 * Model:
 * - Creator defines goal + deadline
 * - Users back the campaign
 * - Funds are locked (escrow)
 * - If goal is met -> creator withdraws
 * - If goal NOT met -> backers self-refund
 * - Optional milestones allow partial unlocks
 */

contract CrowdfundedProduct {
    enum CampaignStatus {
        Funding,
        Successful,
        Failed,
        Withdrawn
    }

    struct Milestone {
        string description;
        uint256 unlockAmount;
        bool released;
    }

    address public creator;

    string public productName;
    string public description;
    string public productUrl;

    uint256 public fundingGoal;
    uint256 public deadline;
    uint256 public totalRaised;

    CampaignStatus public status;

    mapping(address => uint256) public contributions;

    Milestone[] public milestones;

    event Backed(address indexed backer, uint256 amount);
    event Refunded(address indexed backer, uint256 amount);
    event CampaignSuccessful(uint256 totalRaised);
    event CampaignFailed();
    event MilestoneReleased(uint256 milestoneIndex, uint256 amount);
    event FullWithdrawal(uint256 amount);

    constructor(
        string memory _name,
        string memory _description,
        string memory _url,
        uint256 _fundingGoal,
        uint256 _durationSeconds
    ) {
        require(_fundingGoal > 0, "Goal must be > 0");
        require(_durationSeconds > 0, "Invalid duration");

        creator = msg.sender;
        productName = _name;
        description = _description;
        productUrl = _url;

        fundingGoal = _fundingGoal;
        deadline = block.timestamp + _durationSeconds;

        status = CampaignStatus.Funding;
    }

    modifier onlyCreator() {
        require(msg.sender == creator, "Not creator");
        _;
    }

    modifier inStatus(CampaignStatus expected) {
        require(status == expected, "Invalid campaign status");
        _;
    }

    // -------------------------------------
    // BACKING
    // -------------------------------------

    function back() external payable inStatus(CampaignStatus.Funding) {
        require(block.timestamp  0, "Must send ETH");

        contributions[msg.sender] += msg.value;
        totalRaised += msg.value;

        emit Backed(msg.sender, msg.value);

        if (totalRaised >= fundingGoal) {
            status = CampaignStatus.Successful;
            emit CampaignSuccessful(totalRaised);
        }
    }

    // -------------------------------------
    // STATUS RESOLUTION AFTER DEADLINE
    // -------------------------------------

    function finalize() external {
        require(block.timestamp >= deadline, "Not yet");
        require(
            status == CampaignStatus.Funding ||
                status == CampaignStatus.Successful,
            "Already finalized"
        );

        if (totalRaised  0, "Nothing to refund");

        contributions[msg.sender] = 0;
        payable(msg.sender).transfer(amount);

        emit Refunded(msg.sender, amount);
    }

    // -------------------------------------
    // OPTIONAL: MILESTONE CONTROLLED RELEASE
    // -------------------------------------

    function addMilestone(
        string calldata _description,
        uint256 _unlockAmount
    ) external onlyCreator inStatus(CampaignStatus.Funding) {
        require(_unlockAmount > 0, "Invalid amount");

        milestones.push(
            Milestone({
                description: _description,
                unlockAmount: _unlockAmount,
                released: false
            })
        );
    }

    function releaseMilestone(uint256 index)
        external
        onlyCreator
        inStatus(CampaignStatus.Successful)
    {
        require(index = m.unlockAmount, "Not enough funds");

        m.released = true;
        payable(creator).transfer(m.unlockAmount);

        emit MilestoneReleased(index, m.unlockAmount);
    }

    // -------------------------------------
    // FULL WITHDRAWAL (NO MILESTONES)
    // -------------------------------------

    function withdrawAll()
        external
        onlyCreator
        inStatus(CampaignStatus.Successful)
    {
        // Only if no milestones exist or all released
        bool allReleased = true;

        for (uint256 i = 0; i < milestones.length; i++) {
            if (!milestones[i].released) {
                allReleased = false;
                break;
            }
        }

        require(allReleased, "Milestones pending");

        status = CampaignStatus.Withdrawn;

        uint256 amount = address(this).balance;
        payable(creator).transfer(amount);

        emit FullWithdrawal(amount);
    }

    // -------------------------------------
    // VIEW HELPERS
    // -------------------------------------

    function milestoneCount() external view returns (uint256) {
        return milestones.length;
    }

    function getSummary()
        external
        view
        returns (
            string memory,
            uint256,
            uint256,
            uint256,
            CampaignStatus
        )
    {
        return (productName, fundingGoal, totalRaised, deadline, status);
    }
}