Functions & Modifiers

Difficulty: Beginner

Functions are the executable units of a Solidity contract. They define the behavior of the contract - how it processes inputs, modifies state, and returns outputs. Every function has a visibility specifier, optional state mutability keywords, and may include custom modifiers. Mastering function design is essential for writing secure, gas-efficient smart contracts.

Solidity provides four visibility specifiers for functions: public, external, internal, and private. Public functions can be called both internally (from within the contract and derived contracts) and externally (via transactions and other contracts). External functions can only be called from outside the contract - they are more gas-efficient than public for functions that receive large arrays as parameters because they can read directly from calldata. Internal functions can only be called from within the contract and derived contracts (similar to 'protected' in other languages). Private functions are restricted to the contract that defines them and are not accessible to derived contracts.

State mutability keywords - view, pure, and payable - communicate and enforce how a function interacts with blockchain state. A view function can read state but cannot modify it (no storage writes, no event emissions, no ETH transfers). A pure function cannot read or modify state - it operates only on its inputs and returns computed values. Payable functions can receive ETH along with the transaction. Functions without any mutability keyword can both read and modify state. The compiler enforces these restrictions, and view/pure functions cost no gas when called externally (they are executed locally by the node).

Modifiers are reusable function guards that execute code before and/or after the function body. They are defined using the `modifier` keyword and applied to functions. The most common pattern is access control - for example, an `onlyOwner` modifier that checks whether the caller is the contract owner before allowing execution. Modifiers use the special `_` (underscore) placeholder to indicate where the function body should be inserted. Multiple modifiers can be chained on a single function, and they execute in the order they are declared.

Functions can also be overridden in derived contracts using the `virtual` and `override` keywords, enabling polymorphism. Solidity supports function overloading (multiple functions with the same name but different parameter types) but not default parameter values. Special functions include the constructor (runs once during deployment), the receive function (called when the contract receives plain ETH with no data), and the fallback function (called when no other function matches the call data or when the contract receives ETH with data). Understanding these patterns is key to writing contracts that are both secure and maintainable.

Code examples

Functions, Visibility, and Modifiers

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FunctionsAndModifiers {
    address public owner;
    mapping(address => uint256) public balances;
    bool private locked;

    // Events
    event Deposit(address indexed user, uint256 amount);
    event Withdrawal(address indexed user, uint256 amount);
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    // ============ Modifiers ============

    /// @notice Restricts function to the contract owner
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _; // Function body executes here
    }

    /// @notice Prevents reentrancy attacks
    modifier nonReentrant() {
        require(!locked, "Reentrant call");
        locked = true;
        _;
        locked = false;
    }

    /// @notice Validates that an amount is greater than zero
    modifier validAmount(uint256 _amount) {
        require(_amount > 0, "Amount must be > 0");
        _;
    }

    // ============ Constructor ============

    constructor() {
        owner = msg.sender;
    }

    // ============ Visibility Examples ============

    // PUBLIC: callable internally and externally
    function deposit() public payable validAmount(msg.value) {
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    // EXTERNAL: only callable from outside (gas-efficient for array params)
    function batchTransfer(
        address[] calldata recipients, // calldata = cheaper than memory for external
        uint256[] calldata amounts
    ) external onlyOwner {
        require(recipients.length == amounts.length, "Length mismatch");

        for (uint i = 0; i < recipients.length; i++) {
            _transfer(msg.sender, recipients[i], amounts[i]);
        }
    }

    // INTERNAL: callable from this contract and derived contracts
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal validAmount(amount) {
        require(balances[from] >= amount, "Insufficient balance");
        balances[from] -= amount;
        balances[to] += amount;
    }

    // PRIVATE: only callable from this contract (not derived)
    function _validateOwner(address _addr) private view returns (bool) {
        return _addr == owner;
    }

    // ============ State Mutability ============

    // VIEW: reads state but does not modify it (free when called externally)
    function getBalance(address _user) external view returns (uint256) {
        return balances[_user];
    }

    // PURE: does not read or modify state (free when called externally)
    function calculateFee(uint256 _amount, uint256 _feeBps) external pure returns (uint256) {
        return (_amount * _feeBps) / 10_000; // Basis points: 100 bps = 1%
    }

    // PAYABLE: can receive ETH
    function withdraw(uint256 _amount) external nonReentrant validAmount(_amount) {
        require(balances[msg.sender] >= _amount, "Insufficient balance");

        balances[msg.sender] -= _amount;

        // Send ETH to the caller
        (bool success, ) = payable(msg.sender).call{value: _amount}("");
        require(success, "Transfer failed");

        emit Withdrawal(msg.sender, _amount);
    }

    // ============ Chaining Multiple Modifiers ============

    function emergencyWithdrawAll() external onlyOwner nonReentrant {
        uint256 contractBalance = address(this).balance;
        require(contractBalance > 0, "No funds");

        (bool success, ) = payable(owner).call{value: contractBalance}("");
        require(success, "Transfer failed");
    }

    // ============ Ownership Transfer ============

    function transferOwnership(address _newOwner) external onlyOwner {
        require(_newOwner != address(0), "Invalid address");
        emit OwnershipTransferred(owner, _newOwner);
        owner = _newOwner;
    }

    // ============ Special Functions ============

    // Receive: called when contract receives plain ETH (no calldata)
    receive() external payable {
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    // Fallback: called when no function matches or ETH sent with data
    fallback() external payable {
        balances[msg.sender] += msg.value;
    }
}

This contract demonstrates all four visibility levels (public, external, internal, private), state mutability keywords (view, pure, payable), custom modifiers (onlyOwner, nonReentrant, validAmount), modifier chaining, and special functions (constructor, receive, fallback). Notice how internal functions use the underscore prefix convention, how modifiers use '_' as the function body placeholder, and how the nonReentrant modifier prevents reentrancy attacks by using a boolean lock.

Key points

Concepts covered

visibility, modifier, view, pure