Building a Decentralized Fundraiser on the Celo blockchain Part 1

Building a Decentralized Fundraiser on the Celo blockchain Part 1 https://celo.academy/uploads/default/optimized/2X/9/9b89a4a38c1270abfa36c8cf5f0acd6b489e1243_2_1024x576.png
none 0.0 0

Introduction

Aloha Dear Friends😊, In this tutorial, we would be going through how to build a fundraising dapp on the celo blockchain. This is part 1 of a two part series and I cant wait. Hold on to your seats!!!

Celo, The Bedrock of Innovation

Celo is a blockchain platform designed to facilitate mobile-first financial services. It shares its codebase with Ethereum and utilizes a proof-of-stake consensus mechanism for validating transactions. What sets Celo apart is its emphasis on usability and accessibility, with the goal of building a more inclusive and decentralized financial ecosystem.

Prerequisites

  • Prior knowledge of javascript
  • Familiarity with the command line
  • Basic understanding of blockchain concepts
  • Have some knowledge of solidity and its concepts
  • Have a basic understanding of React. Knowledge of JSX, props, state, and hooks.

Requirements

  • NodeJS from V12.or higher
  • A code editor or text editor. VSCode is recommended
  • A terminal. Git Bash is recommended
  • An Internet Browser and a good internet connection

Smart Contract Development

To get started, let’s create a new Solidity smart contract project. Open your favorite code editor and create a new file called Donater.sol. This will be our main contract file where we will define the functionality for the donation platform.

In the beginning of the Donater.sol file, let’s import the necessary dependencies. For our donation platform, we will need to interact with the Celo Dollar (cUSD) token. Add the following import statement to import the IERC20Token interface:

import "./IERC20Token.sol";

Here, we import the IERC20Token interface, which defines the functions and events for interacting with ERC20 tokens.

Next, let’s define the structure of our donation contract. Inside the Donater contract, create a struct called Donate that represents a single donation campaign. It should include the following fields:

struct Donate {
    address payable owner;
    string title;
    string description;
    string image;
    uint goal;
    uint amountDonated;
    bool goalReached;
}

This struct will hold information about each donation campaign, such as the owner’s address, title, description, image URL, fundraising goal, amount donated, and whether the goal has been reached.

After defining the struct, let’s declare the state variables and mappings we’ll use in our contract. Add the following code:

address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
address internal adminAddress = 0xE2a0411465fd913502A8390Fe22DC7004AC47A04;

mapping(uint256 => Donate) internal donations;
uint256 donateLength = 0;

Here, we declare two addresses: cUsdTokenAddress represents the address of the cUSD token contract, and adminAddress represents the address that will receive transaction fees for creating donation campaigns. We also define the donations mapping to store donation campaigns, and donateLength to keep track of the number of campaigns.

Now, let’s implement the functionality to add a new donation campaign. Add the following code to the contract:

function addDonation(
    string memory _title,
    string memory _description,
    string memory _image,
    uint _goal
) public payable {

In this part of the function, we define the function signature and input parameters. The addDonation function allows anyone to create a new donation campaign by providing the necessary details: title, description, image, and goal.

  • The string memory data type is used to store text data such as the campaign title, description, and image URL.
  • The uint data type is used to store unsigned integer values, representing the fundraising goal.

The public keyword indicates that the function can be called by any external account, and the payable keyword allows the function to receive value (in the form of Celo Dollars) along with the function call.

    require(
        IERC20Token(cUsdTokenAddress).transferFrom(
            msg.sender,
            adminAddress,
            1e18
        ),
        "Transaction could not be performed"
    );

    donations[donateLength] = Donate(
        payable(msg.sender),
        _title,
        _description,
        _image,
        _goal,
        0,
        false
    );

    donateLength++;
}

In this part of the function, we perform several important operations:

  1. Validation: The require statement ensures that the transfer of Celo Dollars from the campaign creator to the admin address is successful. We use the transferFrom function of the IERC20Token interface to transfer 1 cUSD (1e18 represents 1 cUSD in its smallest unit) from the msg.sender (campaign creator) to the adminAddress. If the transfer fails, the function will revert and display the error message “Transaction could not be performed”.

  2. Storing the Campaign Details: We store the details of the donation campaign in the donations mapping. We use the donateLength variable as the index to store the campaign. The campaign details include the owner (campaign creator), title, description, image, goal, amountDonated (initially set to 0), and goalReached (initially set to false).

  3. Updating Campaign Count: We increment the donateLength variable by “1” to keep track of the number of donation campaigns.

Now that we have implemented the functionality to add a donation campaign, let’s move on to retrieving the details of a specific campaign. We will create a function called getDonation that allows users to fetch the details of a donation campaign based on its index.

Add the following code to your contract:

function getDonation(uint _index) public view returns (
    address payable,
    string memory,
    string memory,
    string memory,
    uint,
    uint,
    bool
) {
    Donate storage _donation = donations[_index];
    return (
        _donation.owner,
        _donation.title,
        _donation.description,
        _donation.image,
        _donation.goal,
        _donation.amountDonated,
        _donation.goalReached
    );
}

In this function, we define the return types and access modifiers. The getDonation function takes an _index parameter, representing the index of the donation campaign to retrieve. It returns the following information about the campaign:

  • owner: The address of the campaign creator (marked as payable to indicate it can receive funds).
  • title: The title of the campaign.
  • description: A description of the campaign.
  • image: The URL of an image associated with the campaign.
  • goal: The fundraising goal of the campaign.
  • amountDonated: The current amount donated to the campaign.
  • goalReached: A boolean indicating whether the campaign’s goal has been reached or not.

To retrieve the campaign details, we access the corresponding Donate struct using the provided index _index in the donations mapping. We use the storage keyword to create a reference to the struct in storage, allowing us to read its properties. Finally, we return the campaign details as a tuple.

Next Up, To enable users to contribute funds to a specific donation campaign, we implement the donate function. This function takes the campaign index and the donation amount as parameters.

The first step in making a donation is to validate the goal status of the campaign. We want to ensure that users can only contribute funds to campaigns that haven’t reached their fundraising goal yet. Here’s the code for this step:

function donate(uint _index, uint amount) public payable {
    // Validate the campaign's goal status
    require(donations[_index].amountDonated < donations[_index].goal);
    
    // ...
}

In this code snippet, we use a require statement to check if the amount donated to the campaign is less than the fundraising goal. If the condition is not met, the function will revert and the donation will not proceed. This prevents users from contributing to campaigns that have already reached their goal.

The second step is to validate the donation amount and transfer the Celo Dollars to the campaign owner. Let’s take a closer look at this part:

function donate(uint _index, uint amount) public payable {
    // Validate the campaign's goal status
    require(donations[_index].amountDonated < donations[_index].goal);
    
    // Validate and transfer Celo Dollars
    require(
        IERC20Token(cUsdTokenAddress).transferFrom(
            msg.sender,
            donations[_index].owner,
            amount
        ),
        "Transaction could not be performed"
    );
    
    // ...
}

In this code, we first validate the donation amount by using the transferFrom function of the IERC20Token interface. This function transfers the specified amount of Celo Dollars from the msg.sender (the person making the donation) to the campaign owner’s address (donations[_index].owner). If the transfer is successful, the donation amount is deducted from the sender’s account and added to the campaign owner’s account.

The transferFrom function returns a boolean value to indicate whether the transfer was successful or not. If the transfer fails, the function will revert and display the specified error message.

After successfully transferring the funds, we need to update the donation details such as the amount donated and the goal achievement status. Let’s add that to our code:

function donate(uint _index, uint amount) public payable {
    // Validate the campaign's goal status
    require(donations[_index].amountDonated < donations[_index].goal);
    
    // Validate and transfer Celo Dollars
    require(
        IERC20Token(cUsdTokenAddress).transferFrom(
            msg.sender,
            donations[_index].owner,
            amount
        ),
        "Transaction could not be performed"
    );
    
    // Update the donation amount and goal achievement
    donations[_index].amountDonated += amount;
    if (donations[_index].amountDonated >= donations[_index].goal) {
        donations[_index].goalReached = true;
    }
}

In this code, we first increment the amountDonated property of the donation campaign by adding the donated amount. This keeps track of the total funds collected for the campaign.

Next, we check if the updated donation amount is equal to or greater than the fundraising goal. If it is, we set the goalReached flag to true to indicate that the campaign has reached its goal.

For the final function, we implement the getDonationLength function to retrieve the total number of donation campaigns created.

function getDonationLength() public view returns (uint) {
    return donateLength;
}

The getDonationLength function simply returns the value of the donateLength variable, which represents the total count of donation campaigns.

And there you have it!

Full Code :

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.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 Donater{
    struct Donate{
        address payable owner;
        string title;
        string  description;
        string  image;
        uint goal;
        uint amountDonated;
        bool goalReached;
    }


    mapping(uint256=>Donate) internal donations;
    uint256 donateLength = 0;

    address internal cUsdTokenAddress =
        0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
    address internal adminAddress = 0xE2a0411465fd913502A8390Fe22DC7004AC47A04;

    function addDonation(
        string memory _title,
        string memory _description,
        string memory _image,
        uint _goal 
    )public payable{
        require(
             IERC20Token(cUsdTokenAddress).transferFrom(
                msg.sender,
                adminAddress,
                1e18
            ),
            "Transaction could not be performed"
        );
        donations[donateLength] = Donate(
            payable(msg.sender),
            _title,
            _description,
            _image,
            _goal,
            0,
            false
        );

        donateLength++;
    }

    function getDonation(uint _index)public view returns(
        address payable,
        string memory,
        string memory,
        string memory,
        uint,
        uint,
        bool
    ){
        Donate storage _donations = donations[_index];
        return(
            _donations.owner,
            _donations.title,
            _donations.description,
            _donations.image,
            _donations.goal,
            _donations.amountDonated,
            _donations.goalReached
        );
    }

    function donate(uint _index, uint amount)public payable {
        require(donations[_index].amountDonated < donations[_index].goal);
        require(
             IERC20Token(cUsdTokenAddress).transferFrom(
                msg.sender,
                donations[_index].owner,
                amount
            ),
            "Transaction could not be performed"
        );
        donations[_index].amountDonated+=amount;
        if(donations[_index].amountDonated >= donations[_index].goal){
            donations[_index].goalReached = true;
        }
    }

    function getDonationLength() public view returns (uint){
        return donateLength;
    }
}

Contract Deployment

To deploy the contract, we would need:

  1. CeloExtensionWallet
  2. Celo Faucet
  3. Celo Remix Plugin

Download the Celo Extension Wallet from the Google chrome store using the link above. After doing that, create a wallet, and store your key phrase in a very safe place to avoid permanently losing your funds.

After downloading and creating your wallet, you will need to fund it using the Celo Faucet. Copy the address to your wallet, click the link to the faucet above and paste the address into the text field, and confirm.

Next up, on remix, download and activate the celo plugin from the plugin manager. Connect your wallet and deploy your contract.

Conclusion

In this tutorial, we have built a donation platform on the Celo blockchain using Solidity smart contracts. This tutorial provides a foundation for building more advanced features and integrating the donation platform into larger decentralized applications.

Remember to thoroughly test your smart contract and consider security measures and best practices before deploying it to the Celo blockchain mainnet.

Next Steps

For an additional challenge, you can consider implementing a frontend to interact with the smart contract. If you’re confident in your React skills and want to dive deeper into the development process, I encourage you to take on this task. You can use the provided link as a reference for the smart contract implementation. By creating a frontend, you’ll be able to provide a user-friendly interface for users to view donation campaigns, make contributions, and track the progress of fundraising goals.

About the Author

Daniel Ogbuti is a web3 developer with a passion for teaching as well as learning. I would love to connect on Twitter @daniel_ogbuti and linkedin @ Daniel Ogbuti

See you soon!

7 Likes

Congratulations on your proposal being chosen as a standout this week at Celo Academy! As you prepare your tutorial, please ensure you follow our guidelines found here: Celo Sage Guidelines and Best Practices. Thank you for helping improve the experience of the developers learning at Celo Academy.

4 Likes

I will be reviewing this @danielogbuti

2 Likes

Source code link broken, please check such things before you move this in publish section

1 Like

Done this @ishan.pathak2711 , didn’t know the repo was private

1 Like

Grateful for this knowledge shared with us.