Module 5: Events Logging
Module 5: Events and Logging in Solidity
5.1 Understanding Events in Solidity
Events in Solidity are a way for your contract to communicate that something has happened on the blockchain to your front-end application or other listening contracts. They are primarily used for logging and are an important part of the Ethereum ecosystem.
What Are Events?
Events are inheritable members of contracts. When you call an event, it causes the arguments to be stored in the transaction's log - a special data structure in the blockchain. These logs are associated with the address of the contract, are incorporated into the blockchain, and stay there as long as a block is accessible.
Events are not accessible from within contracts (not even the contract that created them), but they can be efficiently accessed from outside the blockchain.
Why Use Events?
Events serve several important purposes in Ethereum smart contracts:
- Logging: Events provide a way to log important actions or state changes in your contract.
- Front-end Notifications: They allow front-end applications to listen for specific events and update their UI accordingly.
- Cheap Storage: Events are a cost-effective way to store data that doesn't need to be accessed by smart contracts.
- Historical Record: They create a permanent, searchable history of actions on the blockchain.
5.2 Declaring and Emitting Events
Declaring Events
Events are declared using the event keyword, followed by the event name and parameters:
// Simple event with no parameters
event LogMessage();
// Event with parameters
event Transfer(address indexed from, address indexed to, uint256 value);
// Event with multiple parameters, some indexed
event Approval(address indexed owner, address indexed spender, uint256 value);
The indexed Keyword
The indexed keyword is used to mark parameters as topics, which allows for efficient filtering of events. Up to three parameters can be marked as indexed:
- Indexed parameters: Stored as topics in the event log, making them searchable.
- Non-indexed parameters: Stored in the data part of the log, which cannot be searched but can store more complex data types.
Emitting Events
Events are emitted using the emit keyword followed by the event name and its arguments:
function transfer(address to, uint256 value) public {
// Perform transfer logic
balances[msg.sender] -= value;
balances[to] += value;
// Emit the event
emit Transfer(msg.sender, to, value);
}
In older versions of Solidity (before 0.4.21), the emit keyword was not required, but it's now recommended to always use it for clarity.
5.3 Event Logging and Gas Costs
Gas Costs of Events
Events are one of the more gas-efficient ways to store data on the blockchain:
- Basic event cost: ~375 gas
- Each indexed parameter: ~375 gas
- Each non-indexed parameter: Depends on size, but generally cheaper than indexed parameters
Compared to storing data in contract storage: - Setting a storage variable from zero to non-zero: 20,000 gas - Modifying a storage variable: 5,000 gas
This makes events an economical choice for data that doesn't need to be accessed by contracts.
Event Size Limitations
While events are efficient, they do have limitations:
- Maximum event size: There's no hard limit, but very large events can cause issues.
- Maximum indexed parameters: Only 3 parameters can be indexed.
- Complex types: Complex types like arrays and structs can only be used in non-indexed parameters.
5.4 Listening for Events in DApps
Web3.js Example
// Create contract instance
const contractABI = [...]; // Contract ABI
const contractAddress = '0x...'; // Contract address
const contract = new web3.eth.Contract(contractABI, contractAddress);
// Listen for Transfer events
contract.events.Transfer({
fromBlock: 0
})
.on('data', (event) => {
console.log('Transfer event:', event.returnValues);
})
.on('error', (error) => {
console.error('Error:', error);
});
// Filter events by indexed parameters
contract.getPastEvents('Transfer', {
filter: {
from: '0x123...', // Filter by 'from' address
to: '0x456...' // Filter by 'to' address
},
fromBlock: 0,
toBlock: 'latest'
})
.then((events) => {
console.log('Past Transfer events:', events);
})
.catch((error) => {
console.error('Error:', error);
});
Ethers.js Example
// Create contract instance
const contractABI = [...]; // Contract ABI
const contractAddress = '0x...'; // Contract address
const contract = new ethers.Contract(contractAddress, contractABI, provider);
// Listen for Transfer events
contract.on('Transfer', (from, to, value, event) => {
console.log(`Transfer from ${from} to ${to}: ${value.toString()}`);
console.log('Event data:', event);
});
// Filter events by indexed parameters
const filter = contract.filters.Transfer('0x123...', '0x456...');
contract.queryFilter(filter, 0, 'latest')
.then((events) => {
console.log('Past Transfer events:', events);
})
.catch((error) => {
console.error('Error:', error);
});
5.5 Event Design Patterns
Event-Driven Architecture
Events can be used to create an event-driven architecture where different parts of your system react to events emitted by your contracts:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EventDrivenContract {
event ActionRequested(uint256 indexed actionId, address requester, string actionType);
event ActionCompleted(uint256 indexed actionId, address executor, uint256 timestamp);
uint256 public nextActionId = 1;
function requestAction(string memory actionType) public {
uint256 actionId = nextActionId++;
emit ActionRequested(actionId, msg.sender, actionType);
}
function completeAction(uint256 actionId) public {
emit ActionCompleted(actionId, msg.sender, block.timestamp);
}
}
Event Sourcing
Event sourcing is a pattern where you use events as the primary source of truth for your application state:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EventSourcingExample {
struct Account {
uint256 balance;
bool exists;
}
mapping(address => Account) public accounts;
event AccountCreated(address indexed owner, uint256 initialBalance);
event Deposited(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
event Transferred(address indexed from, address indexed to, uint256 amount);
function createAccount() public {
require(!accounts[msg.sender].exists, "Account already exists");
accounts[msg.sender] = Account({
balance: 0,
exists: true
});
emit AccountCreated(msg.sender, 0);
}
function deposit(uint256 amount) public {
require(accounts[msg.sender].exists, "Account does not exist");
accounts[msg.sender].balance += amount;
emit Deposited(msg.sender, amount);
}
function withdraw(uint256 amount) public {
require(accounts[msg.sender].exists, "Account does not exist");
require(accounts[msg.sender].balance >= amount, "Insufficient balance");
accounts[msg.sender].balance -= amount;
emit Withdrawn(msg.sender, amount);
}
function transfer(address to, uint256 amount) public {
require(accounts[msg.sender].exists, "Sender account does not exist");
require(accounts[to].exists, "Recipient account does not exist");
require(accounts[msg.sender].balance >= amount, "Insufficient balance");
accounts[msg.sender].balance -= amount;
accounts[to].balance += amount;
emit Transferred(msg.sender, to, amount);
}
}
Notification System
Events can be used to create a notification system for your DApp:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract NotificationSystem {
enum NotificationType { Info, Warning, Error, Success }
event Notification(
address indexed user,
NotificationType notificationType,
string message,
uint256 timestamp
);
function notify(address user, NotificationType notificationType, string memory message) public {
emit Notification(user, notificationType, message, block.timestamp);
}
function notifyInfo(address user, string memory message) public {
notify(user, NotificationType.Info, message);
}
function notifyWarning(address user, string memory message) public {
notify(user, NotificationType.Warning, message);
}
function notifyError(address user, string memory message) public {
notify(user, NotificationType.Error, message);
}
function notifySuccess(address user, string memory message) public {
notify(user, NotificationType.Success, message);
}
}
5.6 Best Practices for Events
Naming Conventions
Follow these naming conventions for events:
- Use PascalCase for event names (e.g.,
Transfer,Approval) - Use descriptive names that indicate what happened
- Use past tense for event names (e.g.,
Transferred,Approved)
When to Use Events
Use events in the following situations:
- State Changes: Emit events when important state changes occur.
- User Actions: Emit events when users interact with your contract.
- Asynchronous Operations: Use events to signal the start and completion of asynchronous operations.
- Error Reporting: Emit events to provide additional context for errors.
What to Include in Events
Consider including the following information in your events:
- Who: The address that triggered the action.
- What: The details of what happened.
- When: The timestamp or block number.
- Identifiers: IDs or references to relevant entities.
Indexed Parameters
Choose indexed parameters carefully:
- Index parameters that you'll want to filter by.
- Index unique identifiers (IDs, addresses).
- Don't index large or complex data types.
5.7 Practical Example: Auction Contract with Events
Let's create an auction contract that uses events to track the auction lifecycle:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Auction {
address payable public beneficiary;
uint public auctionEndTime;
// Current state of the auction
address public highestBidder;
uint public highestBid;
// Allowed withdrawals of previous bids
mapping(address => uint) public pendingReturns;
// Set to true at the end, disallows any change
bool public ended;
// Events
event AuctionCreated(address beneficiary, uint duration);
event BidPlaced(address indexed bidder, uint amount);
event BidIncreased(address indexed bidder, uint amount);
event HighestBidIncreased(address indexed bidder, uint amount);
event AuctionEnded(address winner, uint amount);
constructor(uint _biddingTime, address payable _beneficiary) {
beneficiary = _beneficiary;
auctionEndTime = block.timestamp + _biddingTime;
emit AuctionCreated(_beneficiary, _biddingTime);
}
function bid() public payable {
// Check if the auction has ended
require(block.timestamp <= auctionEndTime, "Auction already ended");
// Check if the bid is higher than the current highest bid
require(msg.value > highestBid, "There already is a higher bid");
// Track if this is a new bid or an increase
bool isNewBid = pendingReturns[msg.sender] == 0;
// Refund the previous highest bidder
if (highestBid != 0) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
if (isNewBid) {
emit BidPlaced(msg.sender, msg.value);
} else {
emit BidIncreased(msg.sender, msg.value);
}
emit HighestBidIncreased(msg.sender, msg.value);
}
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
if (!payable(msg.sender).send(amount)) {
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
function endAuction() public {
// Check if the auction has already ended
require(!ended, "Auction already ended");
// Check if the auction end time has been reached
require(block.timestamp >= auctionEndTime, "Auction not yet ended");
// Mark the auction as ended
ended = true;
// Send the highest bid to the beneficiary
beneficiary.transfer(highestBid);
emit AuctionEnded(highestBidder, highestBid);
}
}
Front-end Integration
Here's how you might integrate this auction contract with a front-end application using ethers.js:
const { ethers } = require('ethers');
// Connect to the Ethereum network
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// Contract details
const auctionAddress = '0x...'; // Your deployed auction contract address
const auctionABI = [...]; // Your auction contract ABI
// Create contract instance
const auctionContract = new ethers.Contract(auctionAddress, auctionABI, signer);
// Listen for auction events
auctionContract.on('BidPlaced', (bidder, amount, event) => {
console.log(`New bid placed by ${bidder}: ${ethers.utils.formatEther(amount)} ETH`);
updateUI();
});
auctionContract.on('HighestBidIncreased', (bidder, amount, event) => {
console.log(`New highest bid: ${ethers.utils.formatEther(amount)} ETH by ${bidder}`);
updateUI();
});
auctionContract.on('AuctionEnded', (winner, amount, event) => {
console.log(`Auction ended! Winner: ${winner}, Final bid: ${ethers.utils.formatEther(amount)} ETH`);
showAuctionEndedNotification(winner, amount);
updateUI();
});
// Function to place a bid
async function placeBid(amount) {
try {
const tx = await auctionContract.bid({
value: ethers.utils.parseEther(amount.toString())
});
await tx.wait();
console.log('Bid placed successfully!');
} catch (error) {
console.error('Error placing bid:', error);
}
}
// Function to end the auction
async function endAuction() {
try {
const tx = await auctionContract.endAuction();
await tx.wait();
console.log('Auction ended successfully!');
} catch (error) {
console.error('Error ending auction:', error);
}
}
// Function to update the UI
async function updateUI() {
const highestBid = await auctionContract.highestBid();
const highestBidder = await auctionContract.highestBidder();
const endTime = await auctionContract.auctionEndTime();
const ended = await auctionContract.ended();
document.getElementById('highest-bid').textContent = ethers.utils.formatEther(highestBid);
document.getElementById('highest-bidder').textContent = highestBidder;
document.getElementById('end-time').textContent = new Date(endTime * 1000).toLocaleString();
document.getElementById('auction-status').textContent = ended ? 'Ended' : 'Active';
// Show/hide buttons based on auction state
document.getElementById('bid-button').disabled = ended;
document.getElementById('end-auction-button').disabled = ended || (Date.now() / 1000 < endTime);
}
// Initialize the UI
updateUI();
5.8 Exercise: Event-Driven Token Contract
Task: Create a token contract that uses events to track token transfers, approvals, and other important actions.
Solution:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EventDrivenToken {
string public name = "EventToken";
string public symbol = "EVT";
uint8 public decimals = 18;
uint256 public totalSupply = 1000000 * 10**18;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
// Events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event Burn(address indexed from, uint256 value);
event Mint(address indexed to, uint256 value);
event TransferFailed(address indexed from, address indexed to, uint256 value, string reason);
constructor() {
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 value) public returns (bool success) {
if (to == address(0)) {
emit TransferFailed(msg.sender, to, value, "Cannot transfer to zero address");
return false;
}
if (balanceOf[msg.sender] < value) {
emit TransferFailed(msg.sender, to, value, "Insufficient balance");
return false;
}
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) public returns (bool success) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value) public returns (bool success) {
if (to == address(0)) {
emit TransferFailed(from, to, value, "Cannot transfer to zero address");
return false;
}
if (balanceOf[from] < value) {
emit TransferFailed(from, to, value, "Insufficient balance");
return false;
}
if (allowance[from][msg.sender] < value) {
emit TransferFailed(from, to, value, "Insufficient allowance");
return false;
}
balanceOf[from] -= value;
balanceOf[to] += value;
allowance[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
function burn(uint256 value) public returns (bool success) {
if (balanceOf[msg.sender] < value) {
return false;
}
balanceOf[msg.sender] -= value;
totalSupply -= value;
emit Burn(msg.sender, value);
emit Transfer(msg.sender, address(0), value);
return true;
}
function mint(address to, uint256 value) public returns (bool success) {
totalSupply += value;
balanceOf[to] += value;
emit Mint(to, value);
emit Transfer(address(0), to, value);
return true;
}
}
This token contract uses events to track all important actions:
Transfer: Emitted when tokens are transferred from one address to another.Approval: Emitted when an address approves another address to spend tokens on its behalf.Burn: Emitted when tokens are burned (removed from circulation).Mint: Emitted when new tokens are minted (added to circulation).TransferFailed: Emitted when a transfer fails, with a reason for the failure.
Try implementing this contract in Remix and use the JavaScript console to listen for events!
Conclusion
Events and logging are powerful features in Solidity that allow your smart contracts to communicate with the outside world. They provide an efficient way to store historical data and enable front-end applications to react to changes on the blockchain.
Key takeaways from this module:
- Events are used to log important actions and state changes in your contract.
- Events are cost-effective compared to storing data in contract storage.
- Indexed parameters allow for efficient filtering of events.
- Events can be used to implement various design patterns, such as event-driven architecture and event sourcing.
- Front-end applications can listen for events to update their UI and provide notifications to users.
In the next module, we'll explore inheritance and interfaces in Solidity, which are essential for building modular and reusable smart contracts.