How To Use Ethereum Events Properly: An Solidity Dapp Tutorial
How to create an efficient crowdfunding dapp on the Ethereum blockchain using Solidity and web3 js, and why using smart contract events is superior to storage variables.
Join the DZone community and get the full member experience.
Join For FreeIn this tutorial, I will show you how to create a crowdfunding smart contract with a frontend showing the list of donations and learn an advanced technique to make the contract more efficient. If you are new to Solidity, you can check out my free solidity tutorial.
Let’s start with a naïve solution:
contract Crowdfunding is Ownable {
struct Donor {
address: donor;
uint256: amount;
}
Donor[] private donors;
receive() external payable {
donors.push(Donor({
donor: msg.sender,
amount: msg.value
}));
}
function withdraw() external onlyOnwer {
uint256 balance = address(this).balance;
(bool ok, ) = msg.sender.call{value: balance}("");
require(ok, "withdraw failed");
}
function getDonors() external view returns (Donor[] memory) {
return donors;
}
}
Every time someone sends Ether to the contract, the receive function will get triggered, and a new donor is appended to the list of donors. The struct contains their address and the amount of Ether they sent.
The frontend can then call the getDonors()
function to display the donations.
But before we code the frontend, we want to point out a problem in the above design.
Storage Variables are Expensive
The variable donors
is a storage variable. This means it is stored in the blockchain’s state in such a way other smart contracts can access it if they call the getDonors()
function.
However, making data accessible to other smart contracts causes a significant overhead in storage costs.
Our intent is not for the data to be accessed by other smart contracts but by our Dapp (decentralized application).
This is a common mistake among Solidity developers: using storage variables for information that is only intended to be consumed by the frontend, not other smart contracts.
How Ethereum prices gas costs is a large topic, but to summarize, setting one struct in the example above will have the same cost as two Ethereum transfers. Since a single Ethereum transfer can cost a few dollars, this would not make the users happy.
We want the transaction cost the user incurs to be not much more than simply transferring Ether.
Use Logs Instead
Let’s rewrite the contract to be more efficient.
contract Crowdfunding is Ownable {
event Donation(address donor, uint256 amont);
fallback() external payable {
emit Donation(msg.sender, msg.value);
}
function withdraw() external onlyOnwer {
uint256 balance = address(this).balance;
(bool ok, ) = msg.sender.call{value: balance}("");
require(ok, "withdraw failed");
}
}
How To Retrieve Logs
Solidity events (logs) are not ephemeral items like logs in a shell we might be accustomed to. They are permanently stored in the blockchain state. Because they aren’t intended to be retrieved by other smart contracts, they can be stored in a more lightweight fashion compared to storage variables, making our crowdfunding smart contract smaller and more cost-efficient.
We don’t have to “watch” the smart contract to get all the events that occurred, as we will show later. We don’t have to worry about “missing” a log. It will always be there for us to retrieve.
Deploy the Contract
I’ve already deployed the contract to the Sepolia Testnet here, but if you want to deploy it yourself, follow this tutorial for how to deploy a smart contract using remix.
<!DOCTYPE html>
<html>
<head>
<title>Donation Events</title>
<script src="https://cdn.jsdelivr.net/npm/web3@1.3.4/dist/web3.min.js"></script>
</head>
<body>
<h1>Donation Events</h1>
<table id="eventsTable">
<tr>
<th>Address</th>
<th>Amount</th>
</tr>
</table>
<script src="events.js"></script>
</body>
</html>
And here is events.js. You can leave the address as is if you want to see the website work with an already deployed contract, or you can deploy the contract yourself and update the address.
if (window.ethereum) {
window.web3 = new Web3(ethereum);
ethereum.enable();
} else if (window.web3) {
window.web3 = new Web3(web3.currentProvider);
} else {
alert("no browser wallet found")
}
// Replace this with your contract
const contractAddress = '0x020b0bcdffeea5dbaba66bed36e677ccc2342465';
// This is how web3 js knows how to parse the event
const contractABI = [{
"constant": false,
"inputs": [],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}, {
"anonymous": false,
"inputs": [{"indexed": false,"name": "donor","type": "address"},{"indexed": false,"name": "amount","type": "uint256"}],
"name": "Donation",
"type": "event"
}, {
"payable": true,
"stateMutability": "payable",
"type": "fallback"
}];
let contract = new web3.eth.Contract(contractABI, contractAddress);
contract.events.Donation({
fromBlock: 0
}, function(error, event) {
if (!error) {
addEventToTable(event.returnValues);
} else {
console.error(error);
}
});
function addEventToTable(returnValues) {
let table = document.getElementById('eventsTable');
let row = table.insertRow();
let cell1 = row.insertCell(0);
let cell2 = row.insertCell(1);
cell1.innerHTML = returnValues.donor;
cell2.innerHTML = web3.utils.fromWei(returnValues.amount, 'ether') + " ETH";
}
You can run put both of these files into a folder called web3-site
then cd
to that directory. Your folder structure should look like this:
- web3-site
- index.html
- events.js
Inside that folder, run the following
python -m http.server --bind 127.0.0.1
To start the server.
If your events.js
file uses the same address in the code, and you are on the Sepolia network in Metamask, you will see the following:
The JavaScript code is not waiting for events to be emitted; it scans the blockchain for all events from that smart contract and lists them in the HTML table. The table contains test donations I made to the contract earlier.
To make a donation, you simply transfer ether directly to the smart contract’s address, and the receive function will be triggered, and the event containing the address of the sender and the amount they sent will be emitted, and the table will be updated.
(I advise you not to transfer anything to the contract in this example, as you will not be able to get the Ethereum back; deploy the contract yourself).
As donations happen, the website will update automatically as the web3 js client will fire the appropriate JavaScript event; you don’t need to refresh the page.
The Cost
Transferring Ethereum incurs 21,000 units of cost (which Ethereum calls gas).
If we look at our donation transactions, we see it is only slightly more expensive:
If we had used the storage variable solution presented first, the transaction cost would have been three times higher.
Conclusion
Solidity events are a superior way to communicate data to the frontend application compared to storage variables. Storage variables cost more and force the frontend to poll the smart contract for updates. Ethereum logs can trigger javascript events more conveniently. Most importantly, they are cheaper than storage variables leading to lower costs for the user.
Opinions expressed by DZone contributors are their own.
Comments