Build A Full Stack Decentralized Fund Raising Dapp On The Celo Blockchain

Build A Full Stack Decentralized Fund Raising Dapp On The Celo Blockchain https://celo.academy/uploads/default/optimized/2X/8/8b9b87aae70fe5b19194bd240f7a4e69a7cd01a8_2_1024x576.png
none 0.0 0

Introduction

In this tutorial, we will be exploring a Solidity smart contract that allows users to create fundraising campaigns and receive donations in cUSD token. The contract will have two main functions: createCampaign and fundCampaign. With the createCampaign function, a user can create a new fundraising campaign by providing the campaign details, including the beneficiary’s address and the campaign’s goal. The fundCampaign function allows users to fund an existing campaign by sending cUSD tokens to the campaign’s beneficiary address.

We will explain the code in detail, including the Campaign struct, the mapping used to store the campaigns, and the events emitted during the contract’s execution. By the end of this tutorial, you will have a good understanding of how to create a fundraising smart contract in Solidity. So let’s get started!

Here’s a demo link of what you’ll be creating.

And a screenshot.

Prerequisites

To fully follow up with these tutorials, you should have a basic understanding of the following technologies.

Solidity, smart-contract and blockchain concepts.
React.
Basic web Development.

Requirements

  • Solidity.
  • React.
  • Bootstrap.
  • NodeJS 12.0.1 upwards installed.
  • Celo Extension Wallet.
  • Remix IDE

SmartContract

Let’s begin writing our smart contract in Remix IDE

The completed code Should look like this.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20Token {
    function transfer(address, uint256) external returns (bool);
    function approve(address, uint256) external returns (bool);
    function transferFrom(address, address, uint256) external returns (bool);
    function totalSupply() external view returns (uint256);
    function balanceOf(address) external view returns (uint256);
    function allowance(address, address) external view returns (uint256);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract Fundraising {

    address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
    uint internal totalCampaigns = 0;

    struct Campaign {
        string image;
        string description;
        address beneficiary;
        uint totalRaised;
        uint goal;
    }

    mapping(uint => Campaign) public campaigns;

    event CampaignCreated(address indexed beneficiary, uint goal);
    event CampaignFunded(address indexed beneficiary, uint amount);
    event CampaignGoalReached(address indexed beneficiary);

    function createCampaign(
        string memory _image,
        string memory _description,
        address _beneficiary,
        uint _goal
        ) public {
        require(msg.sender == _beneficiary);

        Campaign storage campaign = campaigns[totalCampaigns];
        campaign.image = _image;
        campaign.description = _description;
        campaign.beneficiary = _beneficiary;
        campaign.totalRaised = 0;
        campaign.goal = _goal;
        totalCampaigns ++;

        emit CampaignCreated(_beneficiary, _goal);
    }

    function fundCampaign(uint _campaignId, uint _amount) public payable {
        Campaign storage campaign = campaigns[_campaignId];

        require(msg.sender != campaign.beneficiary);
        require(campaign.totalRaised < campaign.goal, "Campaign goal reached");

        campaign.totalRaised += _amount;

         require(
            IERC20Token(cUsdTokenAddress).transferFrom(
                msg.sender,
                campaign.beneficiary,
                _amount
            ),
            "Transfer failed."
        );


        emit CampaignFunded(campaign.beneficiary, _amount);

        if (campaign.totalRaised >= campaign.goal) {
            emit CampaignGoalReached(campaign.beneficiary);
        }
    }

    function getCampaign(uint256 _campaignId)
        public
        view
        returns (
            string memory,
            string memory,
            address,
            uint,
            uint
        )
    {
         Campaign storage campaign = campaigns[_campaignId];
        return (
            campaign.image,
            campaign.description,
            campaign.beneficiary,
            campaign.totalRaised,
            campaign.goal
        );
    }

     function getCampaignLength() public view returns (uint256){
         return (totalCampaigns);
     }
}

Break down

First, we declared our license and the solidity version.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

Next, let’s declare our cUSD ERC20 token interface.

interface IERC20Token {
  function transfer(address, uint256) external returns (bool);
  function approve(address, uint256) external returns (bool);
  function transferFrom(address, address, uint256) external returns (bool);
  function totalSupply() external view returns (uint256);
  function balanceOf(address) external view returns (uint256);
  function allowance(address, address) external view returns (uint256);

  event Transfer(address indexed from, address indexed to, uint256 value);
  event Approval(address indexed owner, address indexed spender, uint256 value);
}

The IERC20Token interface provides functions to transfer tokens, approve tokens, and view the total supply and balance of tokens. To create the interface, we need to define each of the required functions:

transfer – This function is used to transfer tokens from one address to another. It takes two parameters: the address to send the tokens to and the amount of tokens to send.

approve – This function is used to approve another address to transfer tokens from the sender’s account. It takes two parameters: the address to approve and the amount of tokens to approve.

transferFrom – This function is used to transfer tokens from one address to another. It takes three parameters: the address to transfer tokens from, the address to transfer tokens to, and the amount of tokens to transfer.

totalSupply – This function is used to view the total number of tokens in circulation. It takes no parameters and returns a uint256 value.

balanceOf – This function is used to view the balance of tokens for a given address. It takes one parameter: the address to check the balance for. It returns a uint256 value.

allowance – This function is used to view the approved allowance for a given address. It takes two parameters: the owner address and the spender address. It returns a uint256 value.

Transfer and Approval events – These events are used to emit Transfer and Approval events when tokens are transferred or approved.

Now let’s start building our fundraising smart contract

contract Fundraising {

    address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
    uint internal totalCampaigns = 0;

    struct Campaign {
        string image;
        string description;
        address beneficiary;
        uint totalRaised;
        uint goal;
    }

    mapping(uint => Campaign) public campaigns;

    event CampaignCreated(address indexed beneficiary, uint goal);
    event CampaignFunded(address indexed beneficiary, uint amount);
    event CampaignGoalReached(address indexed beneficiary);
}

The cUsdTokenAddress is the address of the ERC-20 token contract that is used to accept donations. The totalCampaigns variable is used to keep track of the total number of campaigns that have been created.

Next we declare a struct called Campaign which is used to store information about each campaign. It contains the image and description of the campaign, the address of the beneficiary, the total amount raised so far, and the goal amount that needs to be raised.

Then we added the campaigns mapping which is used to store the Campaign struct for each campaign.

The CampaignCreated, CampaignFunded, and CampaignGoalReached events are used to notify external applications when a campaign is created, funded, or when its goal is reached.

Let’s take a look at the createCampaign function.

function createCampaign(
        string memory _image,
        string memory _description,
        address _beneficiary, uint _goal
        ) public {
        require(msg.sender == _beneficiary);

        Campaign storage campaign = campaigns[totalCampaigns];
        campaign.image = _image;
        campaign.description = _description;
        campaign.beneficiary = _beneficiary;
        campaign.totalRaised = 0;
        campaign.goal = _goal;
        totalCampaigns ++;

        emit CampaignCreated(_beneficiary, _goal);
    }

The createCampaign function takes four parameters: image, description, beneficiary, and goal. These parameters define the fundraising campaign’s details. The function first verifies that the caller is the beneficiary and then creates a new Campaign struct to store the campaign details. The function also emits a CampaignCreated event with the beneficiary address and the campaign’s goal.

Next is the fundCampaign function.

function fundCampaign(uint _campaignId, uint _amount) public payable {
        Campaign storage campaign = campaigns[_campaignId];

        require(msg.sender != campaign.beneficiary);
        require(campaign.totalRaised < campaign.goal, "Campaign goal reached");

        campaign.totalRaised += _amount;

         require(
            IERC20Token(cUsdTokenAddress).transferFrom(
                msg.sender,
                campaign.beneficiary,
                _amount
            ),
            "Transfer failed."
        );

        emit CampaignFunded(campaign.beneficiary, _amount);

        if (campaign.totalRaised >= campaign.goal) {
            emit CampaignGoalReached(campaign.beneficiary);
        }
    }

The fundCampaign function is used to fund an existing campaign. It takes two parameters: campaignId and amount. The function first gets the campaign details using the campaignId parameter. It then verifies that the caller is not the beneficiary and that the campaign has not reached its goal. If these conditions are met, the function adds the amount to the campaign’s totalRaised variable and transfers the amount of cUSD tokens from the caller to the campaign beneficiary. The function then emits a CampaignFunded event with the beneficiary address and the funded amount.

If the campaign’s totalRaised variable is greater than or equal to the campaign’s goal after the funding, the function emits a CampaignGoalReached event with the beneficiary address.

Next up is the getCampain function

 function getCampaign(uint256 _campaignId)
        public
        view
        returns (
            string memory,
            string memory,
            address,
            uint,
            uint
        )
    {
         Campaign storage campaign = campaigns[_campaignId];
        return (
            campaign.image,
            campaign.description,
            campaign.beneficiary,
            campaign.totalRaised,
            campaign.goal
        );
    }

The getCampaign function is used to get the details of a campaign using the campaignId parameter. It returns a tuple with the campaign’s image, description, beneficiary address, totalRaised, and goal.

And finally the getCampaignLength function

 function getCampaignLength() public view returns (uint256){
         return (totalCampaigns);
     }

The getCampaignLength function returns the total number of campaigns created so far by returning the current value of the totalCampaigns variable.

Deployment

To deploy our smart contract successfully, we need the celo extention wallet which can be downloaded from here

Next, we need to fund our newly created wallet which can done using the celo alfojares faucet Here

You can now fund your wallet and deploy your contract using the celo plugin in remix.

Frontend

Click on this repo from your github.

  • Clone the repo to your computer.
  • open the project from from vscode.
  • Run npm install command to install all the dependencies required to run the app locally.

App.js

The completed code should look like this.

import "./App.css";
import Home from "./components/home";
import { Campaigns } from "./components/Campaigns";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@Celo_Academy/contractkit";
import fundraising from "./contracts/fundraising.abi.json";
import IERC from "./contracts/IERC.abi.json";

const ERC20_DECIMALS = 18;
const contractAddress = "0xadB1C74ce3b79344D3587BB2BC8530d95cDEEAa2";
const cUSDContractAddress = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";

function App() {
  const [contract, setcontract] = useState(null);
  const [address, setAddress] = useState(null);
  const [kit, setKit] = useState(null);
  const [cUSDBalance, setcUSDBalance] = useState(0);
  const [campaigns, setCampaigns] = useState([]);

  const connectToWallet = async () => {
    if (window.celo) {
      try {
        await window.celo.enable();
        const web3 = new Web3(window.celo);
        let kit = newKitFromWeb3(web3);

        const accounts = await kit.web3.eth.getAccounts();
        const user_address = accounts[0];
        kit.defaultAccount = user_address;

        await setAddress(user_address);
        await setKit(kit);
      } catch (error) {
        console.log(error);
      }
    } else {
      alert("Error Occurred");
    }
  };

  const getBalance = useCallback(async () => {
    try {
      const balance = await kit.getTotalBalance(address);
      const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);

      const contract = new kit.web3.eth.Contract(fundraising, contractAddress);
      setcontract(contract);
      setcUSDBalance(USDBalance);
    } catch (error) {
      console.log(error);
    }
  }, [address, kit]);

  const getCampaigns = useCallback(async () => {
    const campaignsLength = await contract.methods.getCampaignLength().call();
    const campaigns = [];
    for (let index = 0; index < campaignsLength; index++) {
      let _campaigns = new Promise(async (resolve, reject) => {
        let campaign = await contract.methods.getCampaign(index).call();

        resolve({
          index: index,
          image: campaign[0],
          description: campaign[1],
          beneficiary: campaign[2],
          totalRaised: campaign[3],
          goal: campaign[4],
        });
      });
      campaigns.push(_campaigns);
    }

    const _campaigns = await Promise.all(campaigns);
    setCampaigns(_campaigns);
  }, [contract]);

  const addCampaign = async (_image, _description, _beneficiary, _goal) => {
    try {
      await contract.methods
        .createCampaign(_image, _description, _beneficiary, _goal)
        .send({ from: address });
      getCampaigns();
    } catch (error) {
      alert(error);
    }
  };

  const fundCampaign = async (_index, _ammount) => {
    try {
      const cUSDContract = new kit.web3.eth.Contract(IERC, cUSDContractAddress);

      await cUSDContract.methods
        .approve(contractAddress, _ammount)
        .send({ from: address });
      await contract.methods
        .fundCampaign(_index, _ammount)
        .send({ from: address });
      getCampaigns();
      getBalance();
      alert("you have successfully sent cusd to this user");
    } catch (error) {
      alert(error);
    }
  };

  useEffect(() => {
    connectToWallet();
  }, []);

  useEffect(() => {
    if (kit && address) {
      getBalance();
    }
  }, [kit, address, getBalance]);

  useEffect(() => {
    if (contract) {
      getCampaigns();
    }
  }, [contract, getCampaigns]);

  return (
    <div className="App">
      <Home cUSDBalance={cUSDBalance} addCampaign={addCampaign} />
      <Campaigns
        campaigns={campaigns}
        fundCampaign={fundCampaign}
        walletAddress={address}
      />
    </div>
  );
}

export default App;

Break down

Let’s break down the App.js file.

import "./App.css";
import Home from "./components/home";
import { Campaigns } from "./components/Campaigns";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@Celo_Academy/contractkit";
import fundraising from "./contracts/fundraising.abi.json";
import IERC from "./contracts/IERC.abi.json";

const ERC20_DECIMALS = 18;
const contractAddress = "0xadB1C74ce3b79344D3587BB2BC8530d95cDEEAa2";
const cUSDContractAddress = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";

In the first few lines, the necessary dependencies and contracts are imported. Here, we are importing the Home and Campaigns components, React hooks, the Web3 library, the ContractKit library, and the ABIs of the smart contracts.

The ERC20_DECIMALS, contractAddress, and cUSDContractAddress are constants that represent the number of decimal places for the ERC20 token, the address of the fundraising smart contract, and the address of the cUSD ERC20 token contract on the Celo Blockchain.

function App() {
  const [contract, setcontract] = useState(null);
  const [address, setAddress] = useState(null);
  const [kit, setKit] = useState(null);
  const [cUSDBalance, setcUSDBalance] = useState(0);
  const [campaigns, setCampaigns] = useState([]);

Next, we define a functional component called App. This component holds the state variables of the contract instance, the wallet address, the ContractKit instance, the cUSD balance, and the campaigns.

const connectToWallet = async () => {
  if (window.celo) {
    try {
      await window.celo.enable();
      const web3 = new Web3(window.celo);
      let kit = newKitFromWeb3(web3);

      const accounts = await kit.web3.eth.getAccounts();
      const user_address = accounts[0];
      kit.defaultAccount = user_address;

      await setAddress(user_address);
      await setKit(kit);
    } catch (error) {
      console.log(error);
    }
  } else {
    alert("Error Occurred");
  }
};

The connectToWallet function enables the user’s wallet on the Celo network and sets the kit instance and user’s address as the default account for the ContractKit instance.

const getBalance = useCallback(async () => {
  try {
    const balance = await kit.getTotalBalance(address);
    const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);

    const contract = new kit.web3.eth.Contract(fundraising, contractAddress);
    setcontract(contract);
    setcUSDBalance(USDBalance);
  } catch (error) {
    console.log(error);
  }
}, [address, kit]);

The getBalance function uses the ContractKit instance to get the user’s total balance, converts it to the cUSD balance, and sets the contract and cUSDBalance state variables.

const getCampaigns = useCallback(async () => {
  const campaignsLength = await contract.methods.getCampaignLength().call();
  const campaigns = [];
  for (let index = 0; index < campaignsLength; index++) {
    let _campaigns = new Promise(async (resolve, reject) => {
      let campaign = await contract.methods.getCampaign(index).call();

      resolve({
        index: index,
        image: campaign[0],
        description: campaign[1],
        beneficiary: campaign[2],
        totalRaised: campaign[3],
        goal: campaign[4],
      });
    });
    campaigns.push(_campaigns);
  }

  const _campaigns = await Promise.all(campaigns);
  setCampaigns(_campaigns);
}, [contract]);

Next, we have the getCampaigns function which uses the contract object to retrieve a list of campaigns. It does this by calling the getCampaignLength method of the contract to get the number of campaigns and then iterating through them to get their details using the getCampaign method. The details are then added to an array and returned as a promise. Once all promises are resolved using Promise.all, the campaigns are set using setCampaigns.

const addCampaign = async (_image, _description, _beneficiary, _goal) => {
  try {
    await contract.methods
      .createCampaign(_image, _description, _beneficiary, _goal)
      .send({ from: address });
    getCampaigns();
  } catch (error) {
    alert(error);
  }
};

After that, we have the addCampaign function, which takes in the required parameters to create a new campaign and sends a transaction using the createCampaign method of the contract object. It then calls the getCampaigns function to update the list of campaigns.

const fundCampaign = async (_index, _ammount) => {
  try {
    const cUSDContract = new kit.web3.eth.Contract(IERC, cUSDContractAddress);

    await cUSDContract.methods
      .approve(contractAddress, _ammount)
      .send({ from: address });
    await contract.methods
      .fundCampaign(_index, _ammount)
      .send({ from: address });
    getCampaigns();
    getBalance();
    alert("you have successfully sent cusd to this user");
  } catch (error) {
    alert(error);
  }
};

Lastly, we have the fundCampaign function which takes in the index of the campaign and the amount of cUSD to be sent. It first approves the contract to spend the specified amount of cUSD using the approve method of the cUSDContract object. It then sends a transaction to the contract to fund the campaign using the fundCampaign method. Finally, it calls the getCampaigns and getBalance functions to update the list of campaigns and the user’s cUSD balance.

useEffect(() => {
  connectToWallet();
}, []);

useEffect(() => {
  if (kit && address) {
    getBalance();
  }
}, [kit, address, getBalance]);

useEffect(() => {
  if (contract) {
    getCampaigns();
  }
}, [contract, getCampaigns]);

We use useEffect to call the connectToWallet, getBalance, and getCampaigns functions at the appropriate times.

return (
    <div className="App">
      <Home cUSDBalance={cUSDBalance} addCampaign={addCampaign} />
      <Campaigns
        campaigns={campaigns}
        fundCampaign={fundCampaign}
        walletAddress={address}
      />
    </div>
  );
}

export default App;

And finally, we render the App component and return the Home and Campaigns components with the necessary props.

Next Steps

I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.

About the author

I’m Jonathan Iheme, A full stack block-chain Developer from Nigeria. let’s connect on linkedIn

Thank You!!

4 Likes

Great! I will try to build this in the coming days!

2 Likes

Great tutorial! The step-by-step instructions and clear explanations made it easy for me to follow along and understand the process of building a decentralized fundraising DApp on the Celo blockchain. The code examples were helpful and the integration with the Celo Extension Wallet was a valuable addition. Kudos to the writer for creating such a comprehensive and beginner-friendly tutorial. Keep up the excellent work, and I look forward to more tutorials from you in the future!

2 Likes

Great work, a must-follow guide for developers looking to make a positive impact in the world of fundraising