Crowdfunding has become a revolutionary way to pool resources for projects, causes, or businesses, it enables creators to bypass traditional funding barriers. With blockchain technology, crowdfunding takes a leap forward, offering transparency, decentralization, and immutability to ensure fairness for both creators and contributors.
This article introduces a Solidity-based Crowdfunding Smart Contract designed to operate on the Linea blockchain. This contract allows users to create campaigns, contribute funds securely, withdraw contributions when goals are met, and even request refunds when campaigns fail. The contract emphasizes accountability by leveraging smart contract features to automate processes, eliminate intermediaries, and ensure funds are used as intended.
This article aims to provide developers with a robust foundation for building decentralized crowdfunding platforms by walking through the contract's implementation, features, and functionality. Whether you're a blockchain enthusiast, developer, or entrepreneur, this guide will demonstrate how smart contracts can transform crowdfunding into a secure, efficient, and trustless experience.
This contract enables:
Create a crowdfunding.sol file in the src folder and add the code below to the file.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract Crowdfunding {
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address => uint256) contributions;
}
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
event FundsWithdrawn(uint256 campaignId, uint256 amount);
event RefundIssued(uint256 campaignId, address contributor, uint256 amount);
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
modifier onlyCreator(uint256 campaignId) {
require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
_;
}
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
// Create a new crowdfunding campaign
function createCampaign(uint256 goal, uint256 duration) external {
require(goal > 0, "Goal must be greater than zero");
require(duration > 0, "Duration must be greater than zero");
Campaign storage newCampaign = campaigns[campaignCount];
newCampaign.creator = msg.sender;
newCampaign.goal = goal;
newCampaign.deadline = block.timestamp + duration;
emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
campaignCount++;
}
// Contribute funds to a campaign
function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
require(msg.value > 0, "Contribution must be greater than zero");
Campaign storage campaign = campaigns[campaignId];
campaign.amountRaised += msg.value;
campaign.contributions[msg.sender] += msg.value;
emit ContributionMade(campaignId, msg.sender, msg.value);
}
// Withdraw funds if the campaign meets its goal
function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised >= campaign.goal, "Funding goal not reached");
require(!campaign.isWithdrawn, "Funds already withdrawn");
campaign.isWithdrawn = true;
(bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
require(sent, "Failed to send Ether");
emit FundsWithdrawn(campaignId, campaign.amountRaised);
}
// Request a refund if the campaign fails
function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
uint256 contribution = campaign.contributions[msg.sender];
require(contribution > 0, "No contributions found for this campaign");
campaign.contributions[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: contribution}("");
require(sent, "Failed to send Ether");
emit RefundIssued(campaignId, msg.sender, contribution);
}
}
The contract utilizes a struct
named Campaign
to store campaign details.
struct Campaign {
address creator; // Campaign creator
uint256 goal; // Fundraising goal
uint256 deadline; // Deadline to achieve the goal
uint256 amountRaised; // Total contributions raised
bool isWithdrawn; // Tracks if funds were withdrawn
mapping(address => uint256) contributions; // Tracks contributions by users
}
Each campaign is uniquely identified by an incremental campaignId
. This ensures scalability and makes managing multiple campaigns straightforward.
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
The createCampaign
function allows a user to initiate a campaign. The function takes in two parameter, goal and duration, the goal is the target amount that the user wants to raise, and the duration is the time in seconds that the user wants to use to raise the goal specified.
The function ensures that the goal and duration are greater than zero to prevent users from entering zero values. The campaign struct details are updated and the campaignId
is linked to the Campaign struct.
The campaignCount is incremented to make sure that new campaign created do not have the same campaignId
as old campaigns.
function createCampaign(uint256 goal, uint256 duration) external {
require(goal > 0, "Goal must be greater than zero");
require(duration > 0, "Duration must be greater than zero");
Campaign storage newCampaign = campaigns[campaignCount];
newCampaign.creator = msg.sender;
newCampaign.goal = goal;
newCampaign.deadline = block.timestamp + duration;
emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
campaignCount++;
}
The contribute
function allows users to fund active campaigns. The function takes in one parameter, the campaignId, this is the unique identifier for each campaign, it determines which campaign users have decided to contribute into.
The function ensures that users do not contribute zero amount to the campaign, it tracks the amountRaised in the campaign and the amount each user has contributed to the campaign.
function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
require(msg.value > 0, "Contribution must be greater than zero");
Campaign storage campaign = campaigns[campaignId];
campaign.amountRaised += msg.value;
campaign.contributions[msg.sender] += msg.value;
emit ContributionMade(campaignId, msg.sender, msg.value);
}
The campaignExists
modifier on the contribute function checks that the campaignId
exists and throws an error if the campaignId
does not exist.
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
The beforeDeadline
modifier checks if the deadline of the campaign has passed and throws an error if the deadline has passed.
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
Campaign creators can withdraw funds using withdrawFunds
. The function takes in one parameter, the campaignId, which is the unique identifier for each campaign.
The funds can only be withdrawn by the campaign creator, if any other user tries to withdraw the fund, the function will throw an error.
The function ensures that the funding goal was met and that the funds haven't already been withdrawn. If the funding goal is not met, the function will throw an error and if the funds have been withdrawn, the function will throw an error.
The function changes the isWithdrawn status to true and sends the funds to the campaign creator.
function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised >= campaign.goal, "Funding goal not reached");
require(!campaign.isWithdrawn, "Funds already withdrawn");
campaign.isWithdrawn = true;
(bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
require(sent, "Failed to send Ether");
emit FundsWithdrawn(campaignId, campaign.amountRaised);
}
The onlyCreator
modifier checks if the user calling the function is the campaign creator and throws an error if the user is not the campaign creator.
modifier onlyCreator(uint256 campaignId) {
require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
_;
}
The afterDeadline
modifier checks if the deadline of the campaign has not passed and throws an error if the deadline has not passed.
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
Refunds for Failed Campaigns
Contributors can retrieve their contributions if the campaign fails. The function takes in one parameter, the campaignId, which is the unique identifier for each campaign.
The function ensures that the campaign failed to meet its goal and that the user contributed to the campaign. It resets the user contribution to the campaign to zero and refunds the user contribution.
function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
uint256 contribution = campaign.contributions[msg.sender];
require(contribution > 0, "No contributions found for this campaign");
campaign.contributions[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: contribution}("");
require(sent, "Failed to send Ether");
emit RefundIssued(campaignId, msg.sender, contribution);
}
Events
The contract emits events for key actions:
CampaignCreated
: Logs details of a new campaign.ContributionMade
: Logs contributions to a campaign.FundsWithdrawn
: Logs successful fund withdrawals.RefundIssued
: Logs refunds issued to contributors.
event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
event FundsWithdrawn(uint256 campaignId, uint256 amount);
event RefundIssued(uint256 campaignId, address contributor, uint256 amount);
Create a crowdfunding.t.sol file in the test folder and add the code below to the file.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Crowdfunding} from "../src/Crowdfunding.sol";
contract CounterTest is Test {
Crowdfunding public crowd;
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
function setUp() public {
crowd = new Crowdfunding();
}
function test_createCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
}
function test_contributeCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 6 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 6 ether}(0);
}
function test_withdrawCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 6 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 6 ether}(0);
skip(11 hours);
console.log("b4", user1.balance);
vm.startPrank(user1);
crowd.withdrawFunds(0);
console.log("after",user1.balance);
}
function test_requestCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 2 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 2 ether}(0);
skip(11 hours);
vm.startPrank(user2);
crowd.requestRefund(0);
vm.startPrank(user3);
crowd.requestRefund(0);
console.log("after",user2.balance);
console.log("after",user3.balance);
}
}
The test suite above ensures that the Crowdfunding contract operates as intended across its core functionalities. Here’s a breakdown of the tests:
This test verifies that a user can successfully create a crowdfunding campaign. In the test, the campaign was created and no errors were thrown.
This test verifies that multiple users can contribute to a specific campaign. In the test, user1 creates a campaign, and user2 and user3 get 10 ether each with the foundry deal function. user2 and user3 contributed 6 ether to the campaign. Both contributions were accepted and the contract updates the campaign’s total amount raised.
This test verifies that the campaign creator can withdraw funds once the campaign meets its funding goal and the deadline has passed. After user2 and user3 has contributed to campaign and 11 hours has passed (done through simulation with skip). User1 calls withdrawFunds to withdraw the raised funds. The funds are transferred to user1 and the balance after withdrawal reflects this change.
This test verifies that the contributors can request refunds if the campaign fails to meet its funding goal. After user2 and user3 has contributed to campaign and 11 hours has passed (done through simulation with skip). User2 and user3 call requestRefund, each user receives their contributed ether back.
forge test
Add the necessary variables to .env, the .env file should be at the root level:
LINEA_RPC_URL=https://linea-sepolia.blockpi.network/v1/rpc/public
PRIVATE_KEY=*************************************************
LINEA_API_KEY=*************************
Add the details below to foundry.toml:
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Create a Makefile and add the details below:
deploy:
forge create src/Crowdfunding.sol:Crowdfunding --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)
verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Crowdfunding.sol:Crowdfunding
Deployment
make deploy
Verification
make verify
This article demonstrates the transformative potential of blockchain in decentralized fundraising. By offering transparency, immutability, and autonomy, it empowers campaign creators and contributors with a system free of intermediaries.
This contract serves as an excellent foundation for real-world crowdfunding applications and highlights the possibilities for building more robust and feature-rich decentralized systems. With Web3 adoption on the rise, such smart contracts will likely become pivotal in democratizing access to funding globally. Developers and businesses can leverage this solution to innovate further and expand its usability for a wide range of industries.