Solidity ^0.8.20

Crowdfund contract

Production-ready crowdfunding contract used by this dApp. Deploy to a testnet (Sepolia, Base Sepolia, etc.), then point the frontend ABI at the address.

Name

Crowdfund

License

MIT

Demo address

0xOpenFund0000000000000000000000000001

Interface

Public surface

createCampaign

title, desc, goal, duration

contribute

id (payable)

claim

id

refund

id

getCampaign

id → view

campaignCount

→ view

Crowdfund.sol

Full source — copy into Remix or Foundry

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

/**
 * @title Crowdfund
 * @notice Simple on-chain crowdfunding: create campaigns, contribute ETH,
 *         claim when the goal is met, or refund if the deadline passes short.
 * @dev Demo contract for the OpenFund dApp. Deploy to any EVM testnet.
 */
contract Crowdfund {
    struct Campaign {
        address creator;
        string title;
        string description;
        uint256 goal;       // wei
        uint256 raised;     // wei
        uint256 deadline;   // unix timestamp
        bool claimed;
        bool exists;
    }

    uint256 public campaignCount;
    mapping(uint256 => Campaign) public campaigns;
    mapping(uint256 => mapping(address => uint256)) public contributions;

    event CampaignCreated(
        uint256 indexed id,
        address indexed creator,
        string title,
        uint256 goal,
        uint256 deadline
    );
    event Contributed(
        uint256 indexed id,
        address indexed contributor,
        uint256 amount
    );
    event Claimed(uint256 indexed id, address indexed creator, uint256 amount);
    event Refunded(uint256 indexed id, address indexed contributor, uint256 amount);

    error EmptyTitle();
    error InvalidGoal();
    error InvalidDeadline();
    error CampaignNotFound();
    error CampaignEnded();
    error CampaignActive();
    error GoalNotMet();
    error GoalMet();
    error AlreadyClaimed();
    error NothingToRefund();
    error ZeroContribution();
    error NotCreator();
    error TransferFailed();

    function createCampaign(
        string calldata title,
        string calldata description,
        uint256 goal,
        uint256 durationSeconds
    ) external returns (uint256 id) {
        if (bytes(title).length == 0) revert EmptyTitle();
        if (goal == 0) revert InvalidGoal();
        if (durationSeconds < 60) revert InvalidDeadline();

        id = campaignCount;
        campaignCount = id + 1;

        uint256 deadline = block.timestamp + durationSeconds;
        campaigns[id] = Campaign({
            creator: msg.sender,
            title: title,
            description: description,
            goal: goal,
            raised: 0,
            deadline: deadline,
            claimed: false,
            exists: true
        });

        emit CampaignCreated(id, msg.sender, title, goal, deadline);
    }

    function contribute(uint256 id) external payable {
        Campaign storage c = campaigns[id];
        if (!c.exists) revert CampaignNotFound();
        if (block.timestamp >= c.deadline) revert CampaignEnded();
        if (msg.value == 0) revert ZeroContribution();

        c.raised += msg.value;
        contributions[id][msg.sender] += msg.value;

        emit Contributed(id, msg.sender, msg.value);
    }

    function claim(uint256 id) external {
        Campaign storage c = campaigns[id];
        if (!c.exists) revert CampaignNotFound();
        if (msg.sender != c.creator) revert NotCreator();
        if (c.claimed) revert AlreadyClaimed();
        if (c.raised < c.goal) revert GoalNotMet();

        c.claimed = true;
        uint256 amount = c.raised;

        (bool ok, ) = payable(c.creator).call{value: amount}("");
        if (!ok) revert TransferFailed();

        emit Claimed(id, c.creator, amount);
    }

    function refund(uint256 id) external {
        Campaign storage c = campaigns[id];
        if (!c.exists) revert CampaignNotFound();
        if (block.timestamp < c.deadline) revert CampaignActive();
        if (c.raised >= c.goal) revert GoalMet();
        if (c.claimed) revert AlreadyClaimed();

        uint256 amount = contributions[id][msg.sender];
        if (amount == 0) revert NothingToRefund();

        contributions[id][msg.sender] = 0;
        c.raised -= amount;

        (bool ok, ) = payable(msg.sender).call{value: amount}("");
        if (!ok) revert TransferFailed();

        emit Refunded(id, msg.sender, amount);
    }

    function getCampaign(uint256 id)
        external
        view
        returns (
            address creator,
            string memory title,
            string memory description,
            uint256 goal,
            uint256 raised,
            uint256 deadline,
            bool claimed,
            bool exists
        )
    {
        Campaign storage c = campaigns[id];
        return (
            c.creator,
            c.title,
            c.description,
            c.goal,
            c.raised,
            c.deadline,
            c.claimed,
            c.exists
        );
    }
}

ABI (JSON)

Minimal ABI for frontend binding

[
  {
    "type": "function",
    "name": "createCampaign",
    "stateMutability": "nonpayable",
    "inputs": [
      {
        "name": "title",
        "type": "string"
      },
      {
        "name": "description",
        "type": "string"
      },
      {
        "name": "goal",
        "type": "uint256"
      },
      {
        "name": "durationSeconds",
        "type": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "id",
        "type": "uint256"
      }
    ]
  },
  {
    "type": "function",
    "name": "contribute",
    "stateMutability": "payable",
    "inputs": [
      {
        "name": "id",
        "type": "uint256"
      }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "name": "claim",
    "stateMutability": "nonpayable",
    "inputs": [
      {
        "name": "id",
        "type": "uint256"
      }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "name": "refund",
    "stateMutability": "nonpayable",
    "inputs": [
      {
        "name": "id",
        "type": "uint256"
      }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "name": "campaignCount",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "uint256"
      }
    ]
  },
  {
    "type": "function",
    "name": "getCampaign",
    "stateMutability": "view",
    "inputs": [
      {
        "name": "id",
        "type": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "creator",
        "type": "address"
      },
      {
        "name": "title",
        "type": "string"
      },
      {
        "name": "description",
        "type": "string"
      },
      {
        "name": "goal",
        "type": "uint256"
      },
      {
        "name": "raised",
        "type": "uint256"
      },
      {
        "name": "deadline",
        "type": "uint256"
      },
      {
        "name": "claimed",
        "type": "bool"
      },
      {
        "name": "exists",
        "type": "bool"
      }
    ]
  },
  {
    "type": "function",
    "name": "contributions",
    "stateMutability": "view",
    "inputs": [
      {
        "name": "",
        "type": "uint256"
      },
      {
        "name": "",
        "type": "address"
      }
    ],
    "outputs": [
      {
        "name": "",
        "type": "uint256"
      }
    ]
  },
  {
    "type": "event",
    "name": "CampaignCreated",
    "inputs": [
      {
        "name": "id",
        "type": "uint256",
        "indexed": true
      },
      {
        "name": "creator",
        "type": "address",
        "indexed": true
      },
      {
        "name": "title",
        "type": "string",
        "indexed": false
      },
      {
        "name": "goal",
        "type": "uint256",
        "indexed": false
      },
      {
        "name": "deadline",
        "type": "uint256",
        "indexed": false
      }
    ]
  },
  {
    "type": "event",
    "name": "Contributed",
    "inputs": [
      {
        "name": "id",
        "type": "uint256",
        "indexed": true
      },
      {
        "name": "contributor",
        "type": "address",
        "indexed": true
      },
      {
        "name": "amount",
        "type": "uint256",
        "indexed": false
      }
    ]
  },
  {
    "type": "event",
    "name": "Claimed",
    "inputs": [
      {
        "name": "id",
        "type": "uint256",
        "indexed": true
      },
      {
        "name": "creator",
        "type": "address",
        "indexed": true
      },
      {
        "name": "amount",
        "type": "uint256",
        "indexed": false
      }
    ]
  },
  {
    "type": "event",
    "name": "Refunded",
    "inputs": [
      {
        "name": "id",
        "type": "uint256",
        "indexed": true
      },
      {
        "name": "contributor",
        "type": "address",
        "indexed": true
      },
      {
        "name": "amount",
        "type": "uint256",
        "indexed": false
      }
    ]
  }
]

Deploy checklist

1Open Remix, paste Crowdfund.sol, compile with 0.8.20+.

2Deploy to a testnet with a funded wallet (MetaMask).

3Set the deployed address in the dApp config and wire CROWDFUND_ABI via viem/ethers.

4Call createCampaign / contribute / claim / refund from this UI against the live chain.