This post walks through cfxcrowd.sol, a compact crowdfunding contract that supports:
- campaign creation with a goal and deadline
- user contributions
- creator withdrawal when funding succeeds
- contributor refunds when funding fails
The contract is intentionally simple, making it great for learning core Solidity patterns.
Contract Overview
The contract (SimpleCrowdfund) tracks:
-
creator: wallet that deployed the contract -
goal: target amount to raise (in wei) -
deadline: UNIX timestamp when campaign ends -
totalRaised: total amount contributed -
contributions: per-user contribution mapping
Core flow:
- Creator deploys with goal + duration.
- Users call
contribute()withmsg.value. - After deadline:
- creator calls
withdraw()if goal reached - contributors call
refund()if goal not reached
- creator calls
Full Contract Template
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleCrowdfund {
address public creator;
uint public goal;
uint public deadline;
uint public totalRaised;
mapping(address => uint) public contributions;
constructor(uint _goalInCFX, uint _durationInMinutes) {
creator = msg.sender;
goal = _goalInCFX * 1e18;
deadline = block.timestamp + (_durationInMinutes * 1 minutes);
}
function contribute() public payable {
require(block.timestamp < deadline, "Campaign ended");
require(msg.value > 0, "Send some CFX");
contributions[msg.sender] += msg.value;
totalRaised += msg.value;
}
function withdraw() public {
require(msg.sender == creator, "Not creator");
require(block.timestamp >= deadline, "Campaign not ended");
require(totalRaised >= goal, "Goal not reached");
payable(creator).transfer(address(this).balance);
}
function refund() public {
require(block.timestamp >= deadline, "Campaign not ended");
require(totalRaised < goal, "Goal was reached");
uint amount = contributions[msg.sender];
require(amount > 0, "No contribution");
contributions[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
Important Code Templates (Reusable Patterns)
1) Constructor: initialize ownership and timeline
constructor(uint _goalInCFX, uint _durationInMinutes) {
creator = msg.sender;
goal = _goalInCFX * 1e18; // convert CFX to wei
deadline = block.timestamp + (_durationInMinutes * 1 minutes);
}
Use this when you need:
- a contract owner/deployer identity
- a target amount
- a time-boxed campaign
2) Contribution gate checks
function contribute() public payable {
require(block.timestamp < deadline, "Campaign ended");
require(msg.value > 0, "Send some CFX");
contributions[msg.sender] += msg.value;
totalRaised += msg.value;
}
Pattern highlights:
- validate state before mutating
- track both per-user and global totals
3) Creator-only withdrawal after success
function withdraw() public {
require(msg.sender == creator, "Not creator");
require(block.timestamp >= deadline, "Campaign not ended");
require(totalRaised >= goal, "Goal not reached");
payable(creator).transfer(address(this).balance);
}
Pattern highlights:
- role-based access control (
msg.sender == creator) - post-deadline execution
- success-only payout
4) Safe refund pattern (checks-effects-interactions)
function refund() public {
require(block.timestamp >= deadline, "Campaign not ended");
require(totalRaised < goal, "Goal was reached");
uint amount = contributions[msg.sender];
require(amount > 0, "No contribution");
contributions[msg.sender] = 0; // effects first
payable(msg.sender).transfer(amount); // external interaction last
}
Pattern highlights:
- only allow refunds on failure
- zero-out state before transfer to reduce reentrancy risk
Why This Design Works Well for MVPs
- Minimal storage and easy-to-read flow
- Clear outcome rules (success withdraw vs failure refund)
- Straightforward UX for contributors and creators
- Good educational baseline before adding advanced features
Suggested Next Enhancements
If you want to evolve this into production-grade crowdfunding:
- emit events (
ContributionReceived,Withdrawn,Refunded) - support campaign metadata (title, description, image URI)
- add a paused/cancelled campaign state
- switch from
transferto low-levelcallwith return checks - add tests for deadline edge cases and multiple contributors
Quick Testing Checklist
- Deploy with goal
10CFX and duration5minutes. - Contribute from two different wallets.
- Before deadline, verify
contribute()works andwithdraw()fails. - After deadline:
- if
totalRaised >= goal, creator can withdraw - if
totalRaised < goal, contributors can refund exactly their own amount
- if
This gives a complete end-to-end validation of the campaign lifecycle.
Resources;
Demo Video: https://www.youtube.com/watch?v=Q6Zmz76nyNk&feature=youtu.be
GitHub: https://github.com/Vikash-8090-Yadav/cfxcrowd