Many people may agree that the public’s trust in nonprofit organizations has recently dropped. In fact, the percentage of Americans donating to charities has steadily decreased in the last twenty years, from 66 percent in 2000 to 53 percent in 2016. This seems to be the effect of a wider phenomenon of lost trust in government, business, NGOs, the media, and more.
An interesting (and in some ways sad) takeaway here is that diminishing public trust is not associated with increased trust in other institutions. Solutions to this cannot only be technological but will require a new approach to looking at these institutions.
Blockchain is, of course, a technological architecture, but it still contains new ways of handling trust relations between users. In general, a blockchain does not offer a surface for forms of centralization of power – everything happens in the open.
Data, and, more importantly, algorithms that handle this data, are collected in a public registry. Copies are freely inspectable and available to anybody. With this, a blockchain-based charity organization could foster a renewed trust among givers.
In this article, we will build a simple, functional charity organization that will collect funds in crypto and distribute them among receivers.
Our approach will be based on a smart contract that will host all the functionalities we want to use. We will take inspiration from popular crowdsourcing sites like GoFundMe and Kickstarter.
For our project, any user will be able to create a campaign to collect donations. A campaign will have a title, logo, short description, and deadline, and donations will be in ETH. The donation type is a strict requirement, and though an interesting part of the crypto-based economy is related to NFTs, we will use ETH for simplicity.
As usual, we’ll provide a ready-to-run repository. You can find the code in my GitHub repo here.
To keep the repository (and this article) simple, we will concentrate on smart contract logic more than implementing a full web app. If you really crave a complete web app experience, you can easily extract the code from the test suite and use it in your framework of choice.
This project uses Hardhat to support development. You can run tests and play with the methods without the need for a real Ethereum node.
The general idea of our system is to allow anybody to create a campaign. Once a campaign goes live, people can donate up until a certain deadline.
The withdrawal of the donated ETH is only possible when the campaign is no longer live. This is done in one of two ways: by explicitly requesting to terminate the campaign or when the set deadline is met.
A detail worth noting is that each of these two methods generates a proper event. Events in Solidity are the best solution to keep track of what is happening, but also achieve some asynchronous interaction between the blockchain and, as an example, the UI.
The creation of a campaign happens by invoking the method startCampaign
:
function startCampaign( string calldata title, string calldata description, string calldata imgUrl, uint256 deadline )
The method takes four parameters that will constitute the metadata for the campaign: a title, description, URL for an image, and deadline expressed in UTC. We can see an example of how to execute it in the test suite:
const tx = await contract.startCampaign( "Test campaign", "This is the description", "http://localhost:3000/conference-64.png", Math.round(new Date().getTime() / 1000) + 3600); await tx.wait();
In the example, you can see that we calculate the campaign deadline by adding 3,600 seconds (the equivalent of one hour) to the time we invoke the method.
Once the campaign is correctly created, that is after the transaction is actually collected in a block, the campaign will go live and will be identified by the triplet [owner account, title, description]
. If we look at the code of the contract in the /contracts
directory, you will notice the following method:
function generateCampaignId( address initiator, string calldata title, string calldata description ) public pure returns (bytes32)
This method takes the triplet described above as parameters and generates a unique campaign id
.
By looking at the parameters, you may notice that two campaigns from different initiators may have the exact same title and description, all while the same initiator cannot initiate the same campaign twice. Of course, there’s room for improvement in handling more complex policies and conditions while starting a campaign.
Once the campaign is live, it can receive donations. Each campaign has a balance
field. Every donation is tracked by increasing the field once a number of conditions are met.
The method donateToCampaign
is in charge of receiving the donations and updating the counters.
function donateToCampaign(bytes32 campaignId) public payable
As you may see from the method signature, the method is payable. This means that the transaction addressed to it will bring funds that can be transferred to the smart contract.
The donate
method takes the campaignId
, calculated with the method described above, as a parameter. The amount to transfer to the campaign is the content of the value
parameter in the transaction. Following this is an example of the call, once again taken from the test suite in the /test
directory.
await contract.connect(accounts[1]).donateToCampaign( campaignId, { value: ethers.utils.parseEther('0.5') });
As you can see, the invocation of the method is different than usual. This is because it contains the value that represents the amount of ETH we are going to donate.
An important data structure we’re updating here is the registry userCampaignDonation
that will track each campaign, the backer, and the amount they donated. If a backer donates more than once to the same campaign, the donations will be added to a sum.
As we mentioned before, a campaign ends either when the deadline is met or when the initiator explicitly calls the endCampaign()
method:
function endCampaign(bytes32 campaignId) public
The method does two simple things after checking the legitimacy of the call. It sets the .isLive
flag to false and sets the .deadline
field to the current block timestamp. This makes sure that no more donations are accepted by the campaign.
Both the mechanisms are checked in the method:
function withdrawCampaignFunds(bytes32 campaignId) public
When a campaign is no longer live, the withdrawal
method will move the funds collected to the account of the initiator.
uint256 amountToWithdraw = campaign.balance; campaign.balance = 0; payable(campaign.initiator).transfer(amountToWithdraw);
The payable()
function is just syntactic sugar to tell the Solidity compiler that it’s fine for this address to receive an ETH transfer.
This is exactly what the function transfer()
does. It transfers the specific amount of ETH the campaign has collected (the campaign.balance
field) to the initiator.
The functions above close the life cycle of a campaign via the creation, collection of donations, deadline setting, and withdrawal of funds.
We’ll briefly discuss some additional functions for more comfortably creating a complete system.
The following method will return the campaignId
in batches of five items. This is useful to implement a UI where we can show a paginated list of the available campaign:
function getCampaignsInBatch(uint256 _batchNumber) public view returns(bytes32[] memory)
The last method is getCampaign
, which returns the campaign’s metadata, such as the title, description, and balance, and takes the campaignId
as a parameter.
function getCampaign(bytes32 campaignId) public view
So where do we go from here? Once you have the contract up and running, you can start thinking about the user experience you intend to provide to your potential user base and, from this, can start designing and implementing the most suitable frontend for the various functions!
You could also implement a mechanism to let the owner of the smart contract keep a small fee on the fund transfers as it happens. This mechanism can be implemented in either the donateToCampaign
or withdrawCampaignFunds
methods.
Additionally, you could also focus on strengthening the smart contract. This smart contract handles counters and funds, but even simple mathematics may be prone to weakness. You can consider using OpenZeppelin Counters for handling the _campaignCount
.
All in all, this article began talking about trust and the big result of implementing a fundraising system.
Using a blockchain means that there are no secret mechanisms for handling donations and fund transfers. Everything within it, including the ledger of the donations and the algorithm used to manipulate them, is written in a smart contract that can be easily inspected.
Campaigns on the blockchain may be a solution to the public’s decreasing trust in fundraising organizations. They would be a huge, disruptive, paradigm shift in the way these systems are designed!
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.