Building a Minimal Crowdfunding Smart Contract in Solidity (CFX)

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:

  1. Creator deploys with goal + duration.
  2. Users call contribute() with msg.value.
  3. After deadline:
    • creator calls withdraw() if goal reached
    • contributors call refund() if goal not reached

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 transfer to low-level call with return checks
  • add tests for deadline edge cases and multiple contributors

Quick Testing Checklist

  • Deploy with goal 10 CFX and duration 5 minutes.
  • Contribute from two different wallets.
  • Before deadline, verify contribute() works and withdraw() fails.
  • After deadline:
    • if totalRaised >= goal, creator can withdraw
    • if totalRaised < goal, contributors can refund exactly their own amount

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