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:
-
Validation: The
require
statement ensures that the transfer of Celo Dollars from the campaign creator to the admin address is successful. We use thetransferFrom
function of theIERC20Token
interface to transfer 1 cUSD (1e18 represents 1 cUSD in its smallest unit) from themsg.sender
(campaign creator) to theadminAddress
. If the transfer fails, the function will revert and display the error message âTransaction could not be performedâ. -
Storing the Campaign Details: We store the details of the donation campaign in the
donations
mapping. We use thedonateLength
variable as the index to store the campaign. The campaign details include theowner
(campaign creator),title
,description
,image
,goal
,amountDonated
(initially set to 0), andgoalReached
(initially set to false). -
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 aspayable
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:
- CeloExtensionWallet
- Celo Faucet
- 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!