paint-brush
How to Build and Deploy a Decentralized Crowdfunding Smart Contract on the Linea Blockchainby@heydamali

How to Build and Deploy a Decentralized Crowdfunding Smart Contract on the Linea Blockchain

by Kingsley OkonkwoNovember 22nd, 2024
Read on Terminal Reader
tldt arrow

Too Long; Didn't Read

Crowdfunding has become a revolutionary way to pool resources for projects, causes, or businesses. 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 **C 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.
featured image - How to Build and Deploy a Decentralized Crowdfunding Smart Contract on the Linea Blockchain
Kingsley Okonkwo HackerNoon profile picture


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.

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:


  1. Campaign Creation: Users can initiate fundraising campaigns by setting a goal and duration.
  2. Contributions: Users can contribute funds to active campaigns.
  3. Fund Withdrawal: Campaign creators can withdraw funds if the fundraising goal is met after the campaign deadline.
  4. Refunds: Contributors can reclaim their contributions if the goal is not met within the deadline.

Project Setup

  1. Run the command below to initiate a foundry project, we will be using foundry framework to build the smart contract.
  2. forge init crowdfunding
  3. Open the crowdfunding folder on Vscode or your favorite code editor, and delete the scripts/counter.s.sol, src/counter.sol, and test/counter.t.sol.
  4. Install all dependencies
  5. 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 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
   }


CampaignCount

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;

Campaign Creation

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++;
   }


Contribution Mechanism

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");
           _;
       }


Withdrawing Funds

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);


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 above ensures that the Crowdfunding contract operates as intended across its core functionalities. Here’s a breakdown of the tests:


test_createCampaign

This test verifies that a user can successfully create a crowdfunding campaign. In the test, the campaign was created and no errors were thrown.


test_contributeCampaign

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.


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 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.


test_requestCampaign

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.


How to run Test


forge test


Deployment and Verification on Linea

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


Conclusion

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.