Building a Celo-based crowdfunding platform with smart contracts and web3.js

Introduction

In a world increasingly driven by digital innovation, crowdfunding has become a popular method for raising funds for various causes, ideas, and projects. But, there’s an even more exciting innovation in the crowdfunding scene—decentralized crowdfunding, powered by blockchain technology. Celo, with its fast transaction times, low fees, and user-friendly approach, makes an excellent blockchain for building a crowdfunding platform.

In this tutorial, we’re going to construct a Celo-based crowdfunding platform using smart contracts and web3.js. By the end of this guide, you’ll have built a secure and transparent platform to raise funds, cutting out intermediaries and bringing power back to the people.

Prerequisites

Before diving into this tutorial, there are a few things you need to take care of. Firstly, you should have a solid understanding of JavaScript and Solidity, as we’ll be using these languages extensively. Familiarity with blockchain concepts and the Ethereum ecosystem will also be beneficial, given that Celo is a fork of Ethereum and shares many similarities.

Requirements​

For this tutorial, you would need to have the following tools installed on your computer:

  • Node.js and NPM: To install, follow the instructions at Node.js.
  • Truffle: This is a popular development framework for building Ethereum-based applications. It allows you to test and deploy on celo. You can install it using npm by running npm install -g truffle.
  • A text editor of your choice (e.g., Visual Studio Code).
  • A Celo wallet with some testnet CELO and cUSD tokens. You can follow the Setting Up a Celo Wallet section here.

Setting up the Development Environment

Before we start coding, let’s set up our development environment. We’ll be using Truffle, a development environment, testing framework, and asset pipeline for Ethereum.

  1. Install Truffle:Open your terminal and install Truffle globally on your system using the following command:

    npm install -g truffle
    
  2. Initialize a new Truffle project: Navigate to the directory where you want to create your project and initialize a new Truffle project using the following command:

    mkdir celo-crowdfunding
    cd celo-crowdfunding
    truffle init
    

    This will create a new directory called celo-crowdfunding and initialize an empty Truffle project inside it.

  3. Next, let’s install truffle/hdwallet-provider. This allows you to sign transactions for addresses derived from a mnemonic. You’ll use this to connect to Celo in your truffle configuration file.
    Run the command below in your terminal.

    npm install @truffle/hdwallet-provider --save
    

    Ensure you run the command above in the root directory of your project.

Creating the Smart Contract

Our next step is to create a Solidity smart contract for our crowdfunding platform.

  1. Create a new file for the smart contract: Inside the contracts directory of your Truffle project, create a new file named Crowdfunding.sol.
    cd contracts
    touch Crowdfunding.sol
    
  2. Add the contract code: Open Crowdfunding.sol in your text editor and add the following Solidity code:
    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract Crowdfunding {
        // Contract code goes here
    }
    
    This is just the skeleton of our contract. We will fill in the details later on.

Developing the Smart Contract

  1. Defining the Project Structure:In our Crowdfunding contract, we’ll have a Project struct that will represent each crowdfunding project. This struct will contain details like the creator of the project, the target amount of funds to be raised, the deadline, and the amount of funds currently raised.

    struct Project {
        address payable creator;
        uint targetAmount;
        uint deadline;
        uint numFunders;
        uint amountRaised;
    }
    
  2. Defining the Funder Structure:Similarly, we’ll have a Funder struct representing each person funding the projects.

    struct Funder {
        address payable addr;
        uint amount;
    }
    

    Add the above definitions inside your Crowdfunding contract. The full contract should look something like this:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract Crowdfunding {
        struct Project {
            address payable creator;
            uint targetAmount;
            uint deadline;
            uint numFunders;
            uint amountRaised;
        }
    
        struct Funder {
            address payable addr;
            uint amount;
        }
    }
    

    We’ll continue developing the contract in the next part, where we’ll add the functions for creating and funding projects.

Adding Functions to the Smart Contract

  1. Creating a New Project:We’ll add a function named createProject that will allow users to create new projects. Each project will have a target amount and a deadline.

    function createProject(uint targetAmount, uint deadline) public {
        Project newProject = Project(payable(msg.sender), targetAmount, deadline, 0, 0);
        projects.push(newProject);
    }
    

    This function takes the target amount and deadline as arguments, creates a new Project struct with the sender of the transaction as the creator, and adds the new project to an array of all projects.

  2. Funding a Project:Next, we’ll add a function named fundProject that will allow users to send funds to a project.

    function fundProject(uint projectId) public payable {
        Project storage project = projects[projectId];
        require(block.timestamp < project.deadline, "Project funding period has ended");
        require(msg.value <= project.targetAmount - project.amountRaised, "Project has reached its funding goal");
    
        project.amountRaised += msg.value;
        project.numFunders++;
    }
    

    This function takes the ID of the project as an argument. It then retrieves the project from the array and checks if the project’s deadline has not passed and if the project has not already reached its funding goal. If both checks pass, the function adds the sent amount (msg.value) to the project’s amountRaised and increments numFunders.

    Your Crowdfunding contract should now look something like this:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract Crowdfunding {
       struct Project {
           address payable creator;
           uint targetAmount;
           uint deadline;
           uint numFunders;
           uint amountRaised;
       }
    
       struct Funder {
           address payable addr;
           uint amount;
       }
    
       Project[] public projects;
    
       function createProject(uint targetAmount, uint deadline) public {
           Project memory newProject = Project(payable(msg.sender), targetAmount, deadline, 0, 0);
           projects.push(newProject);
       }
    
       function fundProject(uint projectId) public payable {
           Project storage project = projects[projectId];
           require(block.timestamp < project.deadline, "Project funding period has ended");
           require(msg.value <= project.targetAmount - project.amountRaised, "Project has reached its funding goal");
    
           project.amountRaised += msg.value;
           project.numFunders++;
       }
    }
    

    Create a file named 2_deploy_contracts.js in the migrations folder and populate it with the code below.

    var Voting = artifacts.require("Crowdfunding");
    
    module.exports = function (deployer) {
      deployer.deploy(Crowdfunding);
    };
    

    The code defines a deployment script for the Crowdfunding contract, using the Truffle framework’s deployer function. The script specifies that the Crowdfunding contract will be deployed, and then calls the deploy function with Crowdfunding as its argument to actually deploy the contract.

    In the next part, we will compile and deploy our contract to the Celo Alfajores Testnet.

Deploying the Smart Contract

To deploy the smart contract, we need to compile it first. In your terminal, run the following command in the root directory of the project:

truffle compile

This will compile the smart contract and generate the artifacts in the /build/contracts directory.

Next, let’s configure Truffle to deploy the smart contract to the Celo network. In the truffle-config.js file, add the following code:

const HDWalletProvider = require("@truffle/hdwallet-provider");
const mnemonic = "client great south good cement bucket rank free legend green"; // replace with your MNEMONIC

module.exports = {
  networks: {
    local: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*",
    },
    alfajores: {
      provider: function () {
        return new HDWalletProvider(
          mnemonic,
          "https://alfajores-forno.celo-testnet.org"
        );
      },
      network_id: 44787,
      gas: 20000000,
      deployTimeout: 300000,
      networkCheckTimeout: 300000,
    },
    mainnet: {
      provider: function () {
        return new HDWalletProvider(mnemonic, "https://forno.celo.org");
      },
      network_id: 42220,
      gas: 4000000,
      deployTimeout: 300000,
      networkCheckTimeout: 300000,
    },
  },
  // Configure your compilers
  compilers: {
    solc: {
      version: "0.5.16", // Fetch exact version from solc-bin (default: truffle's version)
    },
  },
};

Replace the mnemonic in the code above with your own Celo account’s mnemonic. You can find your mnemonic in your Celo account’s settings.

Finally, run the following command in your terminal to deploy the smart contract to the Alfajores celo network:

truffle deploy --network alfajores

This will deploy your smart contract to the Celo network and output the contract address once the deployment is complete. The terminal output should be similar to this:

Interacting with the Smart Contract

Now, let’s interact with the smart contract. We will create a simple command-line interface (CLI) that will allow users to create and fund a project.

First, let’s install the web3 and readline-sync packages:

npm install web3
npm install readline-sync

Next, we will create a new file called crowdfunding-cli.js in the project directory and add the following code:

const Web3 = require("web3");

const Crowdfunding = require("./build/contracts/Crowdfunding.json");
const readlineSync = require("readline-sync");
const web3 = new Web3("https://alfajores-forno.celo-testnet.org");

async function createProject(contract) {
  const targetAmount = readlineSync.question("Enter the target amount: ");
  const deadline = readlineSync.question(
    "Enter the deadline (UNIX timestamp): "
  );
  const privateKey = "YOUR_PRIVATE_KEY"; // replace with the private key of your celo account
  const celoAccount = web3.eth.accounts.privateKeyToAccount(privateKey);
  web3.eth.accounts.wallet.add(celoAccount);
  await contract.methods
    .createProject(targetAmount, deadline)
    .send({
      from: celoAccount.address,
      gas: 3000000,
      gasPrice: "10000000000",
    })
    .then((result) => {
      console.log(
        `Project created with ID ${result.events.ProjectCreated.returnValues.projectId}`
      );
    })
    .catch((error) => {
      console.error(error);
    });
}

async function fundProject(contract) {
  const projectId = readlineSync.question("Enter the project ID: ");
  const amount = readlineSync.question("Enter the amount to fund: ");
  const privateKey = "YOUR_PRIVATE_KEY"; // replace with the private key of your celo account
  const celoAccount = web3.eth.accounts.privateKeyToAccount(privateKey);
  web3.eth.accounts.wallet.add(celoAccount);
  await contract.methods
    .fundProject(projectId)
    .send({
      from: celoAccount.address,
      value: web3.utils.toWei(amount, "ether"),
      gas: 3000000,
      gasPrice: "10000000000",
    })
    .then((result) => {
      console.log(`Funded project ${projectId} with amount ${amount}`);
    })
    .catch((error) => {
      console.error(error);
    });
}

async function main() {
  const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Change to you your contract address

  // Create an instance of the contract
  const contract = new web3.eth.Contract(Crowdfunding.abi, contractAddress);

  const action = readlineSync.question(
    "Enter action (createProject/fundProject): "
  );
  switch (action) {
    case "createProject":
      await createProject(contract);
      break;
    case "fundProject":
      await fundProject(contract);
      break;
    default:
      console.log("Invalid action");
  }
}

main();

In this code, we imported the web3 and readline-sync libraries. We also create a new instance of web3 and point it to the Celo network node hosted at https://alfajores-forno.celo-testnet.org.

The code above exposes three main functions:

  1. createProject(contract): This function interacts with the smart contract to create a new project. It first asks the user for the necessary input, such as the target amount and deadline. Then, it creates a new Celo account using the provided private key. It adds this account to web3’s local wallet (this is a way to manage accounts locally, it doesn’t involve any external wallet like Metamask). Finally, it sends a transaction to the smart contract, calling the createProject function with the input provided by the user. Once the transaction is mined and the function is executed, it will log the ID of the project created.

  2. fundProject(contract): This function allows a user to fund a specific project. It first asks the user for the account address, project ID, and the amount to fund. Similar to the createProject function, it creates a new Celo account using the provided private key and adds it to web3’s local wallet. Then, it sends a transaction to the smart contract, calling the fundProject function with the provided project ID and sends the provided amount as value. Once the transaction is mined and the function is executed, it will log a success message.

  3. main(): This is the main function that gets executed when the program starts. It first creates an instance of the Crowdfunding contract using its ABI and deployed address. Then, it asks the user what action they want to perform, either “createProject” or “fundProject”. Depending on the user’s input, it will call either the createProject function or the fundProject function.

Please replace “YOUR_PRIVATE_KEY” with your private key and “YOUR_CONTRACT_ADDRESS” with the address of your deployed Crowdfunding contract.

You can run the application using the command:

node crowdfunding-cli.js

This will prompt the user for an action (createProject or fundProject) and then execute the corresponding function.

Common Errors

  • Invalid account address or private key: Make sure the account address and private key you’re using are correct.
  • Insufficient funds: The account you’re using to create or fund a project must have enough funds to cover the gas fees and the amount you’re funding.
  • Project funding period has ended: Make sure you’re not trying to fund a project after its deadline. You can check the project’s deadline by calling the projects function on the contract with the project ID as the argument.
  • Project has reached its funding goal: If a project has already reached its funding goal, you won’t be able to fund it anymore. You can check the current amount raised by a project by calling the projects function on the contract with the project ID as the argument.

Conclusion

Congratulations! You’ve just built a decentralized crowdfunding platform on the Celo blockchain using smart contracts and web3.js. Through this tutorial, you’ve learned how to create and interact with a smart contract on Celo, how to create a simple command-line interface for your DApp, and how to handle common errors.

Next Steps

Now that you’ve built a basic crowdfunding platform, you can take it to the next level by adding more features. For example, you could implement a reward system where funders receive tokens in return for their contribution, or add a voting mechanism for project approval. You could also build a frontend for your DApp to make it more user-friendly.

About the Author

Abdur-Rahman Olawale is a blockchain developer and a technical writer. Specializing in creating decentralized applications and insightful technical content, Abdur-Rahman Olawale aims to foster understanding and drive innovation within the blockchain community.

References

  1. Celo documentation
  2. Web3.js documentation
  3. Solidity documentation
  4. Source Code
10 Likes

Hi @joenyzio @Celo_Academy .
This tutorial belongs to me .
Thanks

4 Likes

This is a nice topic idea

3 Likes

Anticipating

1 Like

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

5 Likes

The topic idea is interesting. :rocket:

@Celo_Academy We’ve got a couple of tutorials with this title all building crowdfunding platforms such as those itemized below. Please confirm if this should pass:

5 Likes

I will be reviewing this