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);
}
}
Build and Grow By Nam Le Thanh