Introduction
The reality is that a single individual or small group of people may not have the financial strength to fund a particular goal or agenda. This is where collective power becomes more effective as they invite more people to contribute towards the goal.
This process is known as crowdfunding. Reports have it that while the crowdfunding industry is valued at around $1 billion in 2022, it will hit almost $5 billion in 2025.
The most exciting fact is that more crowdfunding campaigns are run on the blockchain with the advent of blockchain technology and its innovations.
Thus, as a competent contract developer, you should know how to build, test, and deploy secure crowdfunding contracts. By the time you finish this article, you would have built your crowdfunding contract and deployed it on Celo.
Let’s jump into it.
Prerequisites​
These are the prerequisites you will need for this tutorial:
- A fair understanding of Solidity and Javascript
- An adequate understanding of the Celo blockchain and Hardhat framework
Requirements​
- Have a Metamask extension
- Configure your Metamask to Celo
- Fund your Celo account on Metamask
A Brief Overview of Celo
It is a good practice in blockchain in blockchain engineering to have a fair understanding of an ecosystem as a developer before attempting to build smart contracts or DApps on it.
Thus, we shall examine Celo briefly:
First of all, Celo is a self-sufficient layer-1 blockchain protocol that uses a battle-tested proof-of-stake consensus mechanism and is compatible with the Ethereum Virtual Machine. That means developers can use the same languages and frameworks used to build on Ethereum to build on Celo. The main thing that will change is switching the configuration when it is time to deploy.
But while they are both interoperable, Celo is distinct in terms of its architecture, ecological friendliness, economic model, and transaction processing technicalities.
One of the most distinguishing qualities of Celo is that it is a DeFi-focused blockchain with a unique identity system that can point a regular phone number to a wallet address.
Thus, Celo is one of the leading blockchains with the best user experience and is on track to banking those who are unbanked.
Having laid this brief foundation, let us roll our sleeves and proceed to write our contract.
Writing a Crowdfunding Contract with Solidity
The general idea behind the contract we are about to write is that people should be able to donate ERC-20 tokens to the campaign. If the benefactors can donate up to the target, the beneficiary can withdraw the funds.
But if it were to be the case that the campaign was not eventually successful, the smart contract would automatically refund the benefactors.
Let us go over it step-by-step:
## Step 1: Importing the IERC-20 Dependency
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
The first thing we did was to specify the license of the contract to MIT; this is the license that allows you to use this for educational and general open-source purposes.
Then we declared the version of Solidity compiler we want to use. We imported an IERC20 interface. For that purpose, please create another file named IERC20.sol
and paste this interface:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function allowance(address owner, address spender)
external
view
returns (uint256 remaining);
function approve(address spender, uint256 value)
external
returns (bool success);
function balanceOf(address owner) external view returns (uint256 balance);
function decimals() external view returns (uint8 decimalPlaces);
function decreaseApproval(address spender, uint256 addedValue)
external
returns (bool success);
function increaseApproval(address spender, uint256 subtractedValue)
external;
function name() external view returns (string memory tokenName);
function symbol() external view returns (string memory tokenSymbol);
function totalSupply() external view returns (uint256 totalTokensIssued);
function transfer(address to, uint256 value)
external
returns (bool success);
function transferFrom(
address from,
address to,
uint256 value
) external returns (bool success);
}
Step 2: The Campaign and Benefactors Struct
struct Campaign{
address payable beneficiary;
uint moneyRaised;
uint target;
uint beginning;
uint ending;
bool withdrawn;
Benefactors[] benefactorsInfo;
}
struct Benefactors {
address benefactor;
uint amount;
}
Just like other cousin languages, struct which is a variable container is also available in Solidity. For this contract, we created two separate structs for packing two sets of details.
Foremost, we declared a struct for the Campaign itself. Here, we inputted the address of the beneficiary, the money that has been raised so far, and the target of the campaign.
For the sake of time, we put in variables to track the beginning and end of the campaign. Since we can have several counts of the campaign, we would need to ascertain if the beneficiary of a particular campaign has withdrawn or not.
That is the reason we declared a boolean data type for withdrawal. Finally, we would need to pack in the array of the entire benefactors who gave towards a campaign in the struct.
Moving on to the second struct, its use is to track those who contributed to the crowdfunding. We are tracking two details about everyone of them; their addresses and the amount each of them gave.
Step 3: The Rest of The State Variables
IERC20 public immutable token;
mapping(uint256 => Campaign) public campaigns;
// this mapping will be useful for ERC-20 transferFrom
mapping(uint256 => mapping(address => uint256)) public trackRaisedMoney;
uint256 public campaignCount;
constructor(address _token) {
if (_token == address(0)) revert();
token = IERC20(_token);
}
We introduced the ERC-20 token to this contract. Then we created two mappings. The first simple mapping will store the instances or lots of each campaign that will be created through this contract.
On the other hand, the following nested mapping will be useful for the transferFrom
function, which we will eventually work on. Since we can have more than one campaign lots, we created a variable called campaignCount
to track the campaigns.
The basic thing we did in the constructor was to initialize our token address.
Step 4: The getEndDate Function
function getEndDate(uint8 _days) private pure returns (uint256) {
if (_days < 0) revert();
return uint256(_days * 86400);
We need to create this function first because we will need it during the launch. This function contains an if statement that a hypothetical day is less than 0, the EVM should return the day the campaign ends.
Step 5: The kickOff Function
function kickOff(
address _beneficiary,
uint256 _target,
uint8 _endingDays
) external returns (uint256) {
// do this for auto-incrementation
campaignCount++;
campaigns[campaignCount].beneficiary = payable(_beneficiary);
campaigns[campaignCount].moneyRaised = 0;
campaigns[campaignCount].target = _target;
campaigns[campaignCount].beginning = block.timestamp;
campaigns[campaignCount].ending =
campaigns[campaignCount].beginning +
getEndDate(_endingDays);
uint256 endDate = campaigns[campaignCount].ending;
campaigns[campaignCount].withdrawn = false; // because the default of bool is false
require(
endDate < block.timestamp + 30 days,
"Campaign must end in 30 days"
);
emit Start(
campaignCount,
_beneficiary,
_target,
block.timestamp,
endDate
);
return campaignCount;
}
The kickOff function, as the name implies, is the function that will launch the contract. We incremented the campaignCount
and set the members of the campaign struct. While most of the settings were done in the normal way and are straightforward, pay closer attention to how we set the ending
member.
We added the beginning
to the result of the getEndDate
function we defined earlier. After this, we put a check that the day the campaign will end must be less than or equal to 30 days.
Moving on, we emitted some events into the log and return the campaignCount
.
Step 6: The Give Function
function give(uint256 _benefactorsId, uint256 _amount) external {
require(
campaigns[_benefactorsId].moneyRaised <=
campaigns[_benefactorsId].target,
"the target is reached already"
);
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only give when the campaign has not ended"
);
token.transferFrom(msg.sender, address(this), _amount);
campaigns[_benefactorsId].moneyRaised += _amount;
trackRaisedMoney[_benefactorsId][msg.sender] += _amount;
Campaign storage campaign = campaigns[_benefactorsId];
campaign.benefactorsInfo.push(Benefactors(msg.sender, _amount));
emit Give(_benefactorsId, msg.sender, _amount);
}
This is the function that the benefactors will use to donate money towards the success of a campaign. We put in two checks before a benefactor can donate.
First, it must be the case that the total funds that have been raised so far have not reached the desired goal. Otherwise, there will be no need for donations any longer.
Second, a benefactor can only donate when the contract has not ended.
Once a benefactor has passed these checks, they can donate any amount to the beneficiary. After making the transfer call, we incremented the total amount in the pool.
For record purposes, we push each benefactor into the benefactor’s array along with their addresses and the amount they donated.
Step 7: The unGive Function
function undoGiving(uint256 _benefactorsId, uint256 _amount) external {
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only ungive when the campaign has not ended"
);
// check that user indeed has token balance using the TRACKTOKENRAISED MAPPING
require(
trackRaisedMoney[_benefactorsId][msg.sender] >= _amount,
"Insufficient Balance"
);
campaigns[_benefactorsId].moneyRaised -= _amount;
trackRaisedMoney[_benefactorsId][msg.sender] -= _amount;
token.transfer(msg.sender, _amount);
// to remove msg.sender from benefactors
Campaign storage campaign = campaigns[_benefactorsId];
uint256 len = campaign.benefactorsInfo.length;
for (uint256 i = 0; i < len; ++i) {
Benefactors memory person = campaign.benefactorsInfo[i];
if (person.benefactor == msg.sender) {
campaign.benefactorsInfo[i] = campaign.benefactorsInfo[len - 1];
}
}
campaign.benefactorsInfo.pop();
emit UnGive(_benefactorsId, msg.sender, _amount);
}
Well, the reality is that sometimes, the donors might change their minds and conclude to take their donation back. Thus, there should be a function for this to allow fairness.
The first requirement we put in was that no donor can get a refund once the campaign has ended. Secondly, the said benefactor must have actually donated earlier.
Then we deducted the amount from the pool and sent the money back to the donor.
Now, we need to go a step further to remove such a donor from the benefactor’s array.
Thus, we will need to carry out a for loop, and for composability, we stored what should have been campaign.benefactorsInfo.length
variable. Afterward, we created a logic that such an earlier investor should be the last person in the benefactors’ array.
Then we called a .pop
, a method that removes the last person in an array.
Step 8: The Withdrawal Function
function withdrawal(uint256 _Id) external {
require(
campaigns[_Id].beneficiary == msg.sender,
"Error, only the beneficiary can withdraw!"
);
require(
block.timestamp > campaigns[_Id].ending,
"cannot withdraw before ending"
);
require(campaigns[_Id].moneyRaised >= campaigns[_Id].target); // should be greater than or equal to
require(!campaigns[_Id].withdrawn, "Withdrawn already"); // recall that the default of bool is false
campaigns[_Id].withdrawn = true;
token.transfer(campaigns[_Id].beneficiary, campaigns[_Id].moneyRaised);
emit Withdrawal(_Id);
}
This is a vital function in the contract. We set out four conditions that are native to withdrawing:
- Only the beneficiary can withdraw
- The beneficiary can only withdraw when the campaign has ended
- The money that has been raised should have hit the target
- The funds in the current lot of the campaign must not have been withdrawn before
Before we can make the transfer call, we have to set the boolean withdrawal time from its false default to true. Then we made a transfer call where we sent the money raised to the beneficiary.
Step 9: The Refund Function
// if the goal of the campaign is not met, everyone who donated should be refunded
function refund(uint256 _benefactorsId) external {
require(
block.timestamp > campaigns[_benefactorsId].ending,
"cannot withdraw before ending"
);
require(
campaigns[_benefactorsId].moneyRaised <
campaigns[_benefactorsId].target
);
uint256 bal = trackRaisedMoney[_benefactorsId][msg.sender];
// reset the balance
trackRaisedMoney[_benefactorsId][msg.sender] = 0;
token.transfer(msg.sender, bal);
emit Refund(_benefactorsId, msg.sender, bal);
}
What happens to all the money that has been raised if the campaign turns out to be unsuccessful? The purpose of this function is that the donors can call it to get their funds back if it were to be the case that they did not meet the target at the end of the day.
Step 10: The Detail Getter Functions
function checkSuccess(uint256 _campaignCount)
external
view
returns (bool success)
{
if (
campaigns[_campaignCount].moneyRaised >=
campaigns[_campaignCount].target
// should be greater than or equal to
) {
success = true;
}
}
function getContributorAmount(uint256 _benefactorsInfo)
external
view
returns (uint256)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
return trackRaisedMoney[_benefactorsInfo][msg.sender];
}
// this function fetches us the data of everyone who contributed to the campaign
// first their number and the addresses of each of them
function getBenefactors(uint256 _benefactorsInfo)
external
view
returns (Benefactors[] memory)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
// generally unhealthy to use an array
Campaign memory campaign = campaigns[_benefactorsInfo];
// return campaigns[_benefactorsInfo].length;
return campaign.benefactorsInfo; // originally not initialized
// abeg help me fix this --Done my chief!
}
The rest of the functions in this contract are getter functions for some important details:
- The
checkSuccess
function checks if any particular campaign was successful functions - The
getContributorAmount
function returns the amount that each donor gave - The
getBenefactors
returns all the donors that gave towards a particular outreach
Eventually, your full code base should appear like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
contract RaiseMoney {
event Start(
uint256 id,
address benefactor,
uint256 target,
uint256 beginning,
uint256 ending
);
event Give(uint256 id, address benefactors, uint256 amount);
event UnGive(uint256 id, address benefactor, uint256 amount);
event Withdrawal(uint256 id);
event Refund(uint256 id, address benefactor, uint256 balance);
// struct to pack the variables of the campaign
struct Benefactors {
address benefactor;
uint256 amount;
}
// struct to pack the variables of the campaign
struct Campaign {
address payable beneficiary;
uint256 moneyRaised;
uint256 target;
uint256 beginning;
uint256 ending;
bool withdrawn;
Benefactors[] benefactorsInfo;
}
IERC20 public immutable token;
mapping(uint256 => Campaign) public campaigns;
// this mapping will be useful for ERC-20 transferFrom
mapping(uint256 => mapping(address => uint256)) public trackRaisedMoney;
uint256 public campaignCount;
constructor(address _token) {
if (_token == address(0)) revert();
token = IERC20(_token);
}
function getEndDate(uint8 _days) private pure returns (uint256) {
if (_days < 0) revert();
return uint256(_days * 86400);
}
/*
*@dev the _beginning param in the kickOff function
* was modifed to block.timestamp
*/
function kickOff(
address _beneficiary,
uint256 _target,
uint8 _endingDays
) external returns (uint256) {
// do this for auto-incrementation
campaignCount++;
campaigns[campaignCount].beneficiary = payable(_beneficiary);
campaigns[campaignCount].moneyRaised = 0;
campaigns[campaignCount].target = _target;
campaigns[campaignCount].beginning = block.timestamp;
campaigns[campaignCount].ending =
campaigns[campaignCount].beginning +
getEndDate(_endingDays);
uint256 endDate = campaigns[campaignCount].ending;
campaigns[campaignCount].withdrawn = false; // because the default of bool is false
require(
endDate < block.timestamp + 30 days,
"Campaign must end in 30 days"
);
emit Start(
campaignCount,
_beneficiary,
_target,
block.timestamp,
endDate
);
return campaignCount;
}
function give(uint256 _benefactorsId, uint256 _amount) external {
require(
campaigns[_benefactorsId].moneyRaised <=
campaigns[_benefactorsId].target,
"the target is reached already"
);
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only give when the campaign has not ended"
);
token.transferFrom(msg.sender, address(this), _amount);
campaigns[_benefactorsId].moneyRaised += _amount;
trackRaisedMoney[_benefactorsId][msg.sender] += _amount;
Campaign storage campaign = campaigns[_benefactorsId];
campaign.benefactorsInfo.push(Benefactors(msg.sender, _amount));
emit Give(_benefactorsId, msg.sender, _amount);
}
function undoGiving(uint256 _benefactorsId, uint256 _amount) external {
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only ungive when the campaign has not ended"
);
// check that user indeed has token balance using the TRACKTOKENRAISED MAPPING
require(
trackRaisedMoney[_benefactorsId][msg.sender] >= _amount,
"Insufficient Balance"
);
campaigns[_benefactorsId].moneyRaised -= _amount;
trackRaisedMoney[_benefactorsId][msg.sender] -= _amount;
token.transfer(msg.sender, _amount);
// to remove msg.sender from benefactors
Campaign storage campaign = campaigns[_benefactorsId];
uint256 len = campaign.benefactorsInfo.length;
for (uint256 i = 0; i < len; ++i) {
Benefactors memory person = campaign.benefactorsInfo[i];
if (person.benefactor == msg.sender) {
campaign.benefactorsInfo[i] = campaign.benefactorsInfo[len - 1];
}
}
campaign.benefactorsInfo.pop();
emit UnGive(_benefactorsId, msg.sender, _amount);
}
function withdrawal(uint256 _Id) external {
require(
campaigns[_Id].beneficiary == msg.sender,
"Error, only the beneficiary can withdraw!"
);
require(
block.timestamp > campaigns[_Id].ending,
"cannot withdraw before ending"
);
require(campaigns[_Id].moneyRaised >= campaigns[_Id].target); // should be greater than or equal to
require(!campaigns[_Id].withdrawn, "Withdrawn already"); // recall that the default of bool is false
campaigns[_Id].withdrawn = true;
token.transfer(campaigns[_Id].beneficiary, campaigns[_Id].moneyRaised);
emit Withdrawal(_Id);
}
// if the goal of the campaign is not met, everyone who donated should be refunded
function refund(uint256 _benefactorsId) external {
require(
block.timestamp > campaigns[_benefactorsId].ending,
"cannot withdraw before ending"
);
require(
campaigns[_benefactorsId].moneyRaised <
campaigns[_benefactorsId].target
);
uint256 bal = trackRaisedMoney[_benefactorsId][msg.sender];
// reset the balance
trackRaisedMoney[_benefactorsId][msg.sender] = 0;
token.transfer(msg.sender, bal);
emit Refund(_benefactorsId, msg.sender, bal);
}
// to check if a particular count of fundraising has been successful
function checkSuccess(uint256 _campaignCount)
external
view
returns (bool success)
{
if (
campaigns[_campaignCount].moneyRaised >=
campaigns[_campaignCount].target
// should be greater than or equal to
) {
success = true;
}
}
function getContributorAmount(uint256 _benefactorsInfo)
external
view
returns (uint256)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
return trackRaisedMoney[_benefactorsInfo][msg.sender];
}
// this function fetches us the data of everyone who contributed to the campaign
// first their number and the addresses of each of them
function getBenefactors(uint256 _benefactorsInfo)
external
view
returns (Benefactors[] memory)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
// generally unhealthy to use an array
Campaign memory campaign = campaigns[_benefactorsInfo];
// return campaigns[_benefactorsInfo].length;
return campaign.benefactorsInfo;
}
}