Build and Deploy a Decentralized Crowdfunding Smart Contract on Linea
Learn how to build and deploy a decentralized crowdfunding smart contract on Linea.
![Build and Deploy a Decentralized Crowdfunding Smart Contract on the Linea Feature image](https://images.ctfassets.net/9sy2a0egs6zh/5bBHB7DzVnhA9bbUhn3MhG/9856d0c7a279741dc6b8d956af4f3535/25-02-05_Decentralized_Crowdfunding_Smart_Contracts_on_Linea.png?w=1920&h=1080&q=80&fm=png)
Crowdfunding has changed how projects, causes, and businesses raise funds by allowing creators to connect directly with supporters and bypass traditional financial institutions. Blockchain technology enhances this model by adding transparency, decentralization, and immutability, ensuring a fairer system for both creators and contributors.
This article will guide you through building a Solidity-based Crowdfunding Smart Contract designed for EVM-compatible blockchains like Linea. The contract will allow users to create campaigns, securely contribute funds, withdraw funds upon successful goal achievement, and request refunds if a campaign falls short. By leveraging smart contract automation, the platform eliminates intermediaries and enforces accountability.
By exploring the contract's implementation, features, and functionality, developers will gain valuable insights into building decentralized crowdfunding platforms. Whether you are a blockchain enthusiast, developer, or entrepreneur, this guide will demonstrate how smart contracts can revolutionize crowdfunding by making it more secure, efficient, and trustless.
Prerequisite
- You should have a basic understanding of Solidity.
- You should have Nodejs and Foundry installed on your PC.
Overview of the contract
This contract enables:
- Campaign creation: Users can initiate fundraising campaigns by setting a goal and duration.
- Contributions: Users can contribute funds to active campaigns.
- Fund withdrawal: Campaign creators can withdraw funds if the fundraising goal is met after the campaign deadline.
- Refunds: Contributors can reclaim their contributions if the goal is not met within the deadline.
Project setup
Run the command below to initiate a Foundry project. We will be using the Foundry framework to build the smart contract.
forge init crowdfunding
Open the crowdfunding folder in VS Code or your favorite code editor, and delete the scripts/counter.s.sol, src/counter.sol, and test/counter.t.sol.
Install all dependencies
forge install foundry-rs/forge-std --no-commit
Full code of the crowdfunding smart contract
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);
}
}
Key features of the code
Campaign structure
The contract uses 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
}
Campaign count Each campaign is uniquely identified by an incrementing campaignId, ensuring scalability and simplifying the management of multiple campaigns.
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
Campaign creation
The createCampaign
function allows a user to initiate a campaign. It takes in two parameters: goal
and duration
. The goal
represents the target amount the user wants to raise, while the duration
specifies the time in seconds allotted to reach that goal.
The function ensures that both the goal
and duration
are greater than zero to prevent invalid entries. The campaign details are then stored in the Campaign
struct, and the campaignId
is assigned accordingly.
The campaignCount
is incremented to ensure that newly created campaigns have unique campaignId
s and do not overlap with previous 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++;
}
Contribution mechanism
The contribute
function allows users to fund active campaigns. It takes in one parameter, campaignId
, which serves as the unique identifier for each campaign and determines where the contribution will be allocated.
The function ensures that users do not contribute a zero amount. It tracks the amountRaised
for the campaign and records the total contribution made by each user.
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 in the contribute
function verifies whether the campaignId
is valid and throws an error if the campaign does not exist.
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
The beforeDeadline
modifier verifies whether the campaign's deadline has passed and throws an error if it has.
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
Withdrawing funds
Campaign creators can withdraw funds using the withdrawFunds
function, which takes in one parameter, campaignId
, serving as the unique identifier for each campaign.
Only the campaign creator can withdraw funds. If any other user attempts to do so, the function will throw an error.
The function ensures that the funding goal has been met and that the funds have not already been withdrawn. If the goal is not met or the funds have already been withdrawn, the function will throw an error.
Upon successful withdrawal, the function updates the isWithdrawn
status to true
and transfers the funds to the campaign creator.
``Solidity 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 verifies whether the caller is the campaign creator and throws an error if they are not.
``Solidity
modifier onlyCreator(uint256 campaignId) {
require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
_;
}
The afterDeadline
modifier ensures that the campaign's deadline has passed and throws an error if it has not.
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
Refunds for failed campaigns
Contributors can withdraw their contributions if the campaign fails. The function takes in one parameter, campaignId
, which uniquely identifies each campaign.
It verifies that the campaign did not meet its funding goal and that the user has made a contribution. If both conditions are met, the function resets the user's contribution to zero and processes the refund.
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);
Tests
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 ensures that the Crowdfunding contract functions correctly across its core features. Below is a breakdown of the tests:
test_createCampaign
This test verifies that a user can successfully create a crowdfunding campaign. The campaign is created without errors, confirming that the function works as expected.
test_contributeCampaign
This test verifies that multiple users can contribute to a specific campaign. In the test, user1
creates a campaign, while user2
and user3
each receive 10 ether using Foundry's deal
function. Both user2
and user3
contribute 6 ether to the campaign, and the contract correctly updates the campaign’s total amount raised.
test_withdrawCampaign
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
contribute to the campaign and 11 hours have passed (simulated using skip
), user1
calls withdrawFunds
to withdraw the raised funds. The funds are successfully transferred, and the contract’s balance reflects the withdrawal.
test_requestCampaign
This test verifies that contributors can request refunds if the campaign fails to meet its funding goal. After user2
and user3
contribute to the campaign and 11 hours have passed (simulated using skip
), both user2
and user3
call requestRefund
. Each user successfully receives their contributed ether back.
Run test
forge test
Deployment and verification on Linea
Add the required environment variables to the .env
file, ensuring it is placed at the root level of the project. These variables store sensitive credentials needed for blockchain interactions.
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/ // Infura RPC endpoint for connecting to Linea
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
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
Conclusion
This guide covered the fundamentals of building and deploying a decentralized crowdfunding smart contract on the Linea blockchain. We explored key contract functionalities such as campaign creation, contributions, withdrawals, and refunds, along with writing and running tests to validate the contract’s behavior. Finally, we walked through the deployment and verification process to make the contract live on Linea.
For more hands-on tutorials and deep dives into blockchain development, check out the MetaMask Developer YouTube channel, and stay updated with our latest insights by following us on Twitter.
Keep reading our latest stories
Developers, security news, and more