Solidity Functions, Modifiers and Events
In this tutorial, you'll learn Solidity functions, modifiers, events, and error handling patterns for writing secure and gas-efficient smart contracts. Why it matters: proper function visibility and event usage are critical for contract security and dApp frontend integration, preventing costly exploits and enabling efficient data indexing. By the end, you'll implement robust contracts with professional error handling.
Solidity functions define the executable logic of smart contracts, with visibility modifiers controlling access, events enabling off-chain communication, and error handling preventing invalid state transitions.
Function Visibility and Mutability
Every Solidity function has both a visibility level (who can call it) and a mutability modifier (whether it reads or writes state).
| Modifier | Reads State | Writes State | Gas Cost | Example |
|---|---|---|---|---|
view |
Yes | No | Low (free if external) | Get balance |
pure |
No | No | Lowest | Math helper |
| (default) | Yes | Yes | High | Update state |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract FunctionTypes {
uint256 public value;
// pure: no state access at all
function addPure(uint256 a, uint256 b) external pure returns (uint256) {
return a + b;
}
// view: reads state but doesn't modify
function getDouble() external view returns (uint256) {
return value * 2;
}
// default (non-payable): modifies state
function setValue(uint256 _newValue) external {
value = _newValue;
}
// payable: receives ETH
function deposit() external payable {
value += msg.value;
}
}
Expected behavior: addPure(3, 5) returns 8 with zero gas cost. getDouble() returns value * 2. setValue(10) updates value. deposit() accepts ETH and adds to value.
Understanding Modifiers
Modifiers are reusable condition checks that execute before or after a function. They reduce code duplication and improve security.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ModifierExample {
address public owner;
mapping(address => bool) public whitelist;
bool public paused;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyWhitelisted() {
require(whitelist[msg.sender], "Not whitelisted");
_;
}
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
modifier costs(uint256 _amount) {
require(msg.value >= _amount, "Insufficient payment");
_;
}
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
}
function addToWhitelist(address _user) external onlyOwner {
whitelist[_user] = true;
}
function restrictedAction() external onlyWhitelisted whenNotPaused costs(0.01 ether) {
// Combines three modifiers
}
}
Expected behavior: restrictedAction() requires the caller to be whitelisted, the contract unpaused, and at least 0.01 ETH sent. Multiple modifiers execute in order.
flowchart TD A[Function Call] --> B[Modifier 1: onlyOwner] B -->|Pass| C[Modifier 2: whenNotPaused] B -->|Fail| D[Revert: Not owner] C -->|Pass| E[Modifier 3: costs] C -->|Fail| F[Revert: Contract paused] E -->|Pass| G[Function Body Executes] E -->|Fail| H[Revert: Insufficient payment]
Events and Logging
Events allow contracts to communicate with off-chain applications. They are stored in transaction logs and are significantly cheaper than storage operations.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract EventsDemo {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event Deposit(address indexed user, uint256 amount);
event Withdrawal(address indexed user, uint256 amount, uint256 timestamp);
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 _amount) external {
require(balances[msg.sender] >= _amount, "Insufficient balance");
balances[msg.sender] -= _amount;
payable(msg.sender).transfer(_amount);
emit Withdrawal(msg.sender, _amount, block.timestamp);
}
function transfer(address _to, uint256 _amount) external {
require(balances[msg.sender] >= _amount, "Insufficient balance");
require(_to != address(0), "Invalid recipient");
balances[msg.sender] -= _amount;
balances[_to] += _amount;
emit Transfer(msg.sender, _to, _amount);
}
}
Expected behavior: Each deposit, withdrawal, or transfer emits a corresponding event. The indexed keyword allows filtering events by address in JavaScript frontends. Events cost approximately 375 gas plus 8 gas per byte of data.
Error Handling: require, revert, and assert
Solidity provides three ways to handle errors, each with different use cases and gas behavior.
| Method | Refunds Gas | Use Case | Example |
|---|---|---|---|
require |
Yes | Input validation, access control | require(msg.sender == owner) |
revert |
Yes | Complex conditions | if(x) { revert("msg"); } |
assert |
No | Internal invariants, bugs | assert(balance > 0) |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ErrorHandling {
address public owner;
uint256 public totalSupply;
constructor() {
owner = msg.sender;
}
function withdraw(uint256 _amount) external {
// require: validates inputs and permissions
require(msg.sender == owner, "Caller is not owner");
require(_amount > 0, "Amount must be positive");
require(address(this).balance >= _amount, "Insufficient contract balance");
payable(owner).transfer(_amount);
}
function customRevert(uint256 _value) external pure {
if (_value > 100) {
revert("Value exceeds maximum");
}
if (_value == 0) {
revert ZeroValueNotAllowed();
}
}
// Custom error (gas efficient, Solidity 0.8.4+)
error ZeroValueNotAllowed();
error UnauthorizedAccess(address caller);
function secureAction() external {
if (msg.sender != owner) {
revert UnauthorizedAccess(msg.sender);
}
}
// assert: for invariants that should never fail
function burn(uint256 _amount) external {
require(_amount > 0, "Amount must be positive");
require(totalSupply >= _amount, "Insufficient supply");
totalSupply -= _amount;
// Invariant: total supply should never underflow
assert(totalSupply >= 0);
}
}
Expected behavior: withdraw() reverts if caller is not owner or amount exceeds balance. customRevert(150) reverts with "Value exceeds maximum". secureAction() uses the gas-efficient custom error pattern.
Function Overloading and Fallbacks
Solidity supports function overloading (same name, different parameters) and special fallback/receive functions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract OverloadExample {
event ValueSet(uint256 value);
event ValueSet(string value);
// Overloaded functions
function set(uint256 _value) external {
emit ValueSet(_value);
}
function set(string calldata _value) external {
emit ValueSet(_value);
}
// Receive: called on plain ETH transfers
receive() external payable {
emit ValueSet(msg.value);
}
// Fallback: called when no function matches
fallback() external {
emit ValueSet("fallback called");
}
}
Expected behavior: set(100) emits ValueSet(100). set("hello") emits ValueSet("hello"). Sending ETH triggers receive(). Calling an unknown function triggers fallback().
Common Errors and Misunderstandings
1. Missing _ in Modifiers
Forgetting the _ inside a modifier prevents the function body from executing. The underscore represents the function continuation point.
2. Not Using indexed for Event Filtering
Without indexed, off-chain code cannot efficiently filter events by parameter. Each event can have up to three indexed parameters.
3. Using assert for Input Validation
assert should only check invariants. It consumes all remaining gas on failure. Use require for input and access validation.
4. Forgetting payable on ETH-Receiving Functions
Functions that accept ETH must be marked payable. Without it, ETH transfers revert.
5. Modifier Order Matters
Modifiers execute in the order they are listed. If onlyOwner is after whenNotPaused, the pause check runs even if the caller is not the owner.
Practice Questions
What is the difference between
viewandpurefunctions?viewfunctions can read but not modify state.purefunctions cannot read or modify state.purefunctions only operate on their parameters.When should you use custom errors over
requirewith strings? Custom errors (Solidity 0.8.4+) are more gas-efficient and can include parameters. Use them in production contracts to save gas and provide better error context.How many indexed parameters can an event have? A maximum of three. Indexed parameters are searchable by off-chain code. Non-indexed parameters are stored in the log data but not searchable.
What happens when a modifier reverts? The entire transaction reverts, including any state changes made before the modifier check. Gas is consumed but remaining gas is refunded (except with
assert).What is the purpose of the
receive()function?receive()is called when the contract receives ETH without calldata (plain ETH transfers). It must be payable. Without it, direct ETH transfers revert.
Challenge
Build an access-controlled vault contract that uses modifiers for time locks, multi-sig approvals, and withdrawal limits. Implement events for every state change and use custom errors for all revert conditions. Test with Hardhat.
Real-World Task
Create a subscription payment contract that charges users ETH periodically, emits payment events, uses modifiers to restrict admin functions, and allows users to cancel subscriptions with proper error handling. Integrate this with a React frontend that listens to events for real-time updates.
Frequently Asked Questions
Next Steps
After mastering Solidity functions and events, proceed to Smart Contract Design Patterns for production patterns. Then learn to deploy and test with Hardhat.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro