Skip to content

Solidity Functions, Modifiers and Events

DodaTech Updated 2026-06-22 7 min read

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

  1. What is the difference between view and pure functions? view functions can read but not modify state. pure functions cannot read or modify state. pure functions only operate on their parameters.

  2. When should you use custom errors over require with 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.

  3. 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.

  4. 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).

  5. 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

Can modifiers accept arguments?

Yes, modifiers can accept arguments just like functions. For example, modifier costs(uint256 amount) requires msg.value >= amount. Arguments are evaluated at the call site and passed to the modifier logic.

Are events stored on the blockchain?

Event logs are stored in transaction receipts and are part of the blockchain history. However, they are not accessible from within smart contracts. Events are designed for off-chain applications to index and query.

What happens if a function throws an exception?

All state changes in the transaction are reverted, but gas spent is not refunded (except for the remaining gas after the revert). Events emitted before the revert are not included in the receipt.

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