Module 7: Function Visibility

Module 7: Function Visibility and Modifiers in Solidity

7.1 Function Visibility

Function visibility in Solidity determines who can call a function and from where. Understanding visibility is crucial for writing secure smart contracts.

Types of Function Visibility

Solidity provides four visibility specifiers for functions:

1. Public

Public functions can be called internally (within the contract) and externally (from other contracts or transactions):

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

contract VisibilityExample {
    uint public data = 10;

    function publicFunction() public returns (uint) {
        return data;
    }

    function callPublicFunction() public returns (uint) {
        // Can call public function internally
        return publicFunction();
    }
}

contract ExternalCaller {
    VisibilityExample public example;

    constructor(address _example) {
        example = VisibilityExample(_example);
    }

    function callPublicFunction() public returns (uint) {
        // Can call public function externally
        return example.publicFunction();
    }
}

Public functions are automatically created for public state variables, allowing external access to their values.

2. External

External functions can only be called from outside the contract (from other contracts or transactions):

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

contract VisibilityExample {
    uint public data = 10;

    function externalFunction() external returns (uint) {
        return data;
    }

    function callExternalFunction() public returns (uint) {
        // Cannot call external function internally
        // return externalFunction(); // This would cause a compilation error

        // But can call it using this
        return this.externalFunction();
    }
}

contract ExternalCaller {
    VisibilityExample public example;

    constructor(address _example) {
        example = VisibilityExample(_example);
    }

    function callExternalFunction() public returns (uint) {
        // Can call external function from another contract
        return example.externalFunction();
    }
}

External functions are more gas-efficient when dealing with large arrays as arguments because they can read directly from calldata instead of copying to memory.

3. Internal

Internal functions can only be called internally (within the current contract or contracts that inherit from it):

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

contract VisibilityExample {
    uint public data = 10;

    function internalFunction() internal returns (uint) {
        return data;
    }

    function callInternalFunction() public returns (uint) {
        // Can call internal function within the same contract
        return internalFunction();
    }
}

contract InheritingContract is VisibilityExample {
    function callParentInternalFunction() public returns (uint) {
        // Can call internal function from parent contract
        return internalFunction();
    }
}

contract ExternalCaller {
    VisibilityExample public example;

    constructor(address _example) {
        example = VisibilityExample(_example);
    }

    function callInternalFunction() public returns (uint) {
        // Cannot call internal function from another contract
        // return example.internalFunction(); // This would cause a compilation error
        return 0;
    }
}

Internal functions are useful for helper functions that should only be accessible within the contract or its derivatives.

4. Private

Private functions can only be called internally within the current contract (not even from derived contracts):

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

contract VisibilityExample {
    uint public data = 10;

    function privateFunction() private returns (uint) {
        return data;
    }

    function callPrivateFunction() public returns (uint) {
        // Can call private function within the same contract
        return privateFunction();
    }
}

contract InheritingContract is VisibilityExample {
    function callParentPrivateFunction() public returns (uint) {
        // Cannot call private function from parent contract
        // return privateFunction(); // This would cause a compilation error
        return 0;
    }
}

Private functions are useful for internal helper functions that should not be exposed to derived contracts.

Visibility for State Variables

State variables can have three visibility specifiers:

  1. Public: Creates an automatic getter function
  2. Internal: Accessible within the contract and derived contracts (default)
  3. Private: Accessible only within the contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VariableVisibility {
    uint public publicVar = 1;       // Accessible from anywhere
    uint internal internalVar = 2;   // Accessible within this contract and derived contracts
    uint private privateVar = 3;     // Accessible only within this contract
}

contract DerivedContract is VariableVisibility {
    function accessVariables() public view returns (uint, uint) {
        // Can access public and internal variables
        return (publicVar, internalVar);

        // Cannot access private variable
        // return privateVar; // This would cause a compilation error
    }
}

Visibility Best Practices

  1. Default to private/internal: Start with the most restrictive visibility and only increase it when necessary.
  2. Use external for public functions: If a function is only called externally, mark it as external for gas optimization.
  3. Be careful with public state variables: Public state variables automatically create getter functions, which might expose more information than intended.
  4. Document visibility choices: Clearly document why certain functions have specific visibility.

7.2 Function Modifiers

Function modifiers in Solidity allow you to change the behavior of functions in a declarative way. They are commonly used for access control, input validation, and other cross-cutting concerns.

Basic Modifiers

A basic modifier looks like this:

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

contract ModifierExample {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // Modifier to check if caller is owner
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _; // This represents the function body
    }

    // Function using the modifier
    function changeOwner(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

The _ symbol in the modifier represents the function body. When the function is called, the code in the modifier is executed, and when _ is reached, the function body is executed.

Modifiers with Parameters

Modifiers can take parameters:

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

contract ModifierExample {
    // Modifier to check if value is greater than a minimum
    modifier minimum(uint value, uint minValue) {
        require(value >= minValue, "Value below minimum");
        _;
    }

    // Function using the modifier with parameters
    function processPayment(uint amount) public payable minimum(amount, 100) {
        // Process payment logic
    }
}

Multiple Modifiers

Functions can have multiple modifiers, which are applied in the order they are listed:

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

contract MultiModifierExample {
    address public owner;
    bool public paused;

    constructor() {
        owner = msg.sender;
        paused = false;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    // Function with multiple modifiers
    function changeOwner(address newOwner) public onlyOwner whenNotPaused {
        owner = newOwner;
    }

    function pause() public onlyOwner {
        paused = true;
    }

    function unpause() public onlyOwner {
        paused = false;
    }
}

In this example, changeOwner will only execute if both modifiers pass: the caller must be the owner, and the contract must not be paused.

Modifier Execution Flow

Understanding the execution flow of modifiers is important:

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

contract ModifierFlowExample {
    modifier logBefore() {
        console.log("Before function execution");
        _;
        console.log("After function execution");
    }

    modifier checkValue(uint value) {
        console.log("Checking value");
        require(value > 0, "Value must be positive");
        _;
    }

    function doSomething(uint value) public logBefore checkValue(value) {
        console.log("Function body execution");
    }
}

When doSomething(10) is called, the execution flow is: 1. "Before function execution" (from logBefore) 2. "Checking value" (from checkValue) 3. "Function body execution" (from the function itself) 4. "After function execution" (from logBefore)

Common Modifier Patterns

Access Control

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

contract AccessControl {
    address public owner;
    mapping(address => bool) public admins;

    constructor() {
        owner = msg.sender;
        admins[msg.sender] = true;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    modifier onlyAdmin() {
        require(admins[msg.sender], "Not an admin");
        _;
    }

    function addAdmin(address admin) public onlyOwner {
        admins[admin] = true;
    }

    function removeAdmin(address admin) public onlyOwner {
        admins[admin] = false;
    }

    function ownerFunction() public onlyOwner {
        // Only owner can call this
    }

    function adminFunction() public onlyAdmin {
        // Owner and admins can call this
    }
}

State Validation

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

contract Auction {
    enum State { Created, Bidding, Ended }
    State public state;

    constructor() {
        state = State.Created;
    }

    modifier inState(State _state) {
        require(state == _state, "Invalid state");
        _;
    }

    function startBidding() public inState(State.Created) {
        state = State.Bidding;
    }

    function placeBid() public inState(State.Bidding) {
        // Bidding logic
    }

    function endAuction() public inState(State.Bidding) {
        state = State.Ended;
    }
}

Input Validation

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

contract InputValidation {
    modifier validAddress(address addr) {
        require(addr != address(0), "Zero address not allowed");
        _;
    }

    modifier validAmount(uint amount) {
        require(amount > 0, "Amount must be greater than zero");
        _;
    }

    function transfer(address to, uint amount) public validAddress(to) validAmount(amount) {
        // Transfer logic
    }
}

Reentrancy Guard

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

contract ReentrancyGuard {
    bool private locked;

    modifier noReentrancy() {
        require(!locked, "Reentrant call");
        locked = true;
        _;
        locked = false;
    }

    function withdraw(uint amount) public noReentrancy {
        // This function cannot be called recursively
        // Withdraw logic
    }
}

Modifiers vs. Functions

While modifiers and functions can sometimes achieve the same result, they have different use cases:

7.3 View and Pure Functions

Solidity provides special function modifiers view and pure to indicate that a function doesn't modify or doesn't read from the state.

View Functions

View functions promise not to modify the state:

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

contract ViewExample {
    uint public value;

    constructor(uint _value) {
        value = _value;
    }

    // View function - reads from state but doesn't modify it
    function getValue() public view returns (uint) {
        return value;
    }

    function calculateDouble() public view returns (uint) {
        return value * 2;
    }
}

View functions cannot: - Modify state variables - Emit events - Create other contracts - Use selfdestruct - Send Ether - Call non-view/non-pure functions - Use low-level calls - Use certain opcodes

Pure Functions

Pure functions promise not to read from or modify the state:

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

contract PureExample {
    uint public value;

    constructor(uint _value) {
        value = _value;
    }

    // Pure function - doesn't read from or modify state
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }

    function multiply(uint a, uint b) public pure returns (uint) {
        return a * b;
    }
}

Pure functions cannot: - Read from state variables - Access address(this).balance or <address>.balance - Access block, tx, or msg members (except msg.sig and msg.data) - Call non-pure functions - Use certain opcodes

Benefits of View and Pure Functions

  1. Gas efficiency: View and pure functions don't cost gas when called externally (from outside the blockchain).
  2. Security: They clearly indicate their behavior, making the code more predictable.
  3. Readability: They signal the intent of the function to readers of the code.

7.4 Function Overloading

Solidity supports function overloading, which allows multiple functions with the same name but different parameter types:

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

contract OverloadingExample {
    // Overloaded functions
    function getValue(uint id) public pure returns (uint) {
        return id;
    }

    function getValue(string memory name) public pure returns (string memory) {
        return name;
    }

    function getValue(uint id, string memory name) public pure returns (string memory) {
        return string(abi.encodePacked("ID: ", uint2str(id), ", Name: ", name));
    }

    // Helper function to convert uint to string
    function uint2str(uint _i) internal pure returns (string memory) {
        if (_i == 0) {
            return "0";
        }
        uint j = _i;
        uint len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint k = len;
        while (_i != 0) {
            k = k-1;
            uint8 temp = (48 + uint8(_i - _i / 10 * 10));
            bytes1 b1 = bytes1(temp);
            bstr[k] = b1;
            _i /= 10;
        }
        return string(bstr);
    }
}

Function overloading is resolved at compile time based on the types of the arguments. The return type is not considered for overload resolution.

Overloading Restrictions

  1. You cannot overload functions based on return type alone.
  2. You cannot overload functions with different visibility.
  3. You cannot overload functions with different state mutability (view/pure).

7.5 Function Selectors and Fallback Functions

Function Selectors

In Ethereum, functions are identified by their selector, which is the first 4 bytes of the keccak256 hash of the function signature:

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

contract SelectorExample {
    // Get the function selector for a function signature
    function getSelector(string memory _func) public pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }

    function transfer(address to, uint amount) public {
        // Function logic
    }

    // This will return the selector for "transfer(address,uint256)"
    function transferSelector() public pure returns (bytes4) {
        return this.transfer.selector;
    }
}

Fallback and Receive Functions

Solidity provides special functions to handle calls that don't match any function signature:

Fallback Function

The fallback function is executed when a function call doesn't match any function signature or when plain Ether is sent to a contract with data:

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

contract FallbackExample {
    event FallbackCalled(bytes data);

    // Fallback function - called when no other function matches
    fallback() external payable {
        emit FallbackCalled(msg.data);
    }

    // Regular function
    function doSomething() public pure returns (string memory) {
        return "Something done";
    }
}

Receive Function

The receive function is executed when plain Ether is sent to a contract without data:

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

contract ReceiveExample {
    event EtherReceived(address sender, uint amount);

    // Receive function - called when Ether is sent with empty calldata
    receive() external payable {
        emit EtherReceived(msg.sender, msg.value);
    }
}

Execution Flow for Incoming Calls

When a contract receives a call, the following logic determines which function is executed:

  1. If the call data matches a function signature, that function is called.
  2. If no function matches and Ether is sent with empty call data, the receive() function is called.
  3. If no function matches and there's no receive() function or call data is not empty, the fallback() function is called.
  4. If none of the above conditions are met, the transaction reverts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract PaymentProcessor {
    event Received(address sender, uint amount);
    event FallbackCalled(address sender, uint amount, bytes data);

    // Regular function
    function processPayment(string memory reference) public payable {
        // Process payment logic
    }

    // Receive function - called when Ether is sent with empty calldata
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    // Fallback function - called when no other function matches
    fallback() external payable {
        emit FallbackCalled(msg.sender, msg.value, msg.data);
    }
}

7.6 Practical Example: Access Control Contract

Let's create a comprehensive access control contract that uses various function visibility and modifiers:

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

contract AccessControl {
    // Role definitions
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE");
    bytes32 public constant USER_ROLE = keccak256("USER_ROLE");

    // Mapping for role assignments
    mapping(bytes32 => mapping(address => bool)) private _roles;

    // Contract owner
    address private _owner;

    // Contract state
    bool private _paused;

    // Events
    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event Paused(address account);
    event Unpaused(address account);

    constructor() {
        _owner = msg.sender;
        _grantRole(ADMIN_ROLE, msg.sender);
        _paused = false;
    }

    // Modifiers
    modifier onlyOwner() {
        require(msg.sender == _owner, "AccessControl: caller is not the owner");
        _;
    }

    modifier onlyRole(bytes32 role) {
        require(hasRole(role, msg.sender), "AccessControl: caller does not have the required role");
        _;
    }

    modifier whenNotPaused() {
        require(!_paused, "AccessControl: contract is paused");
        _;
    }

    modifier whenPaused() {
        require(_paused, "AccessControl: contract is not paused");
        _;
    }

    // View functions
    function owner() public view returns (address) {
        return _owner;
    }

    function paused() public view returns (bool) {
        return _paused;
    }

    function hasRole(bytes32 role, address account) public view returns (bool) {
        return _roles[role][account];
    }

    // Role management functions
    function grantRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
        _grantRole(role, account);
    }

    function revokeRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
        _revokeRole(role, account);
    }

    function renounceRole(bytes32 role) public {
        _revokeRole(role, msg.sender);
    }

    // Ownership management
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "AccessControl: new owner is the zero address");
        address oldOwner = _owner;
        _owner = newOwner;
        _grantRole(ADMIN_ROLE, newOwner);
        emit OwnershipTransferred(oldOwner, newOwner);
    }

    function renounceOwnership() public onlyOwner {
        address oldOwner = _owner;
        _owner = address(0);
        _revokeRole(ADMIN_ROLE, oldOwner);
        emit OwnershipTransferred(oldOwner, address(0));
    }

    // Pause/unpause functions
    function pause() public onlyRole(ADMIN_ROLE) whenNotPaused {
        _paused = true;
        emit Paused(msg.sender);
    }

    function unpause() public onlyRole(ADMIN_ROLE) whenPaused {
        _paused = false;
        emit Unpaused(msg.sender);
    }

    // Internal functions
    function _grantRole(bytes32 role, address account) internal {
        if (!_roles[role][account]) {
            _roles[role][account] = true;
            emit RoleGranted(role, account, msg.sender);
        }
    }

    function _revokeRole(bytes32 role, address account) internal {
        if (_roles[role][account]) {
            _roles[role][account] = false;
            emit RoleRevoked(role, account, msg.sender);
        }
    }

    // Example functions with different roles
    function adminFunction() public onlyRole(ADMIN_ROLE) {
        // Only admins can call this
    }

    function moderatorFunction() public onlyRole(MODERATOR_ROLE) whenNotPaused {
        // Only moderators can call this, and only when not paused
    }

    function userFunction() public onlyRole(USER_ROLE) whenNotPaused {
        // Only users can call this, and only when not paused
    }

    function publicFunction() public whenNotPaused {
        // Anyone can call this, but only when not paused
    }

    // Fallback and receive functions
    receive() external payable {
        // Handle incoming Ether
    }

    fallback() external payable {
        // Handle unknown function calls
    }
}

This contract demonstrates:

  1. Different function visibility (public, internal)
  2. Various modifiers for access control and state validation
  3. View functions for reading state
  4. Role-based access control
  5. Ownership management
  6. Pause/unpause functionality
  7. Fallback and receive functions

7.7 Exercise: Building a Multisignature Wallet

Task: Create a multisignature wallet contract that requires multiple approvals for transactions, using function visibility and modifiers for security.

Solution:

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

contract MultiSigWallet {
    // Events
    event Deposit(address indexed sender, uint amount);
    event SubmitTransaction(address indexed owner, uint indexed txIndex, address indexed to, uint value, bytes data);
    event ConfirmTransaction(address indexed owner, uint indexed txIndex);
    event RevokeConfirmation(address indexed owner, uint indexed txIndex);
    event ExecuteTransaction(address indexed owner, uint indexed txIndex);

    // State variables
    address[] public owners;
    mapping(address => bool) public isOwner;
    uint public numConfirmationsRequired;

    struct Transaction {
        address to;
        uint value;
        bytes data;
        bool executed;
        uint numConfirmations;
    }

    // Mapping from tx index => owner => confirmed
    mapping(uint => mapping(address => bool)) public isConfirmed;

    Transaction[] public transactions;

    // Modifiers
    modifier onlyOwner() {
        require(isOwner[msg.sender], "Not an owner");
        _;
    }

    modifier txExists(uint _txIndex) {
        require(_txIndex < transactions.length, "Transaction does not exist");
        _;
    }

    modifier notExecuted(uint _txIndex) {
        require(!transactions[_txIndex].executed, "Transaction already executed");
        _;
    }

    modifier notConfirmed(uint _txIndex) {
        require(!isConfirmed[_txIndex][msg.sender], "Transaction already confirmed");
        _;
    }

    constructor(address[] memory _owners, uint _numConfirmationsRequired) {
        require(_owners.length > 0, "Owners required");
        require(
            _numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length,
            "Invalid number of confirmations"
        );

        for (uint i = 0; i < _owners.length; i++) {
            address owner = _owners[i];

            require(owner != address(0), "Invalid owner");
            require(!isOwner[owner], "Owner not unique");

            isOwner[owner] = true;
            owners.push(owner);
        }

        numConfirmationsRequired = _numConfirmationsRequired;
    }

    // Fallback function to receive Ether
    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    // Submit a transaction
    function submitTransaction(address _to, uint _value, bytes memory _data) public onlyOwner {
        uint txIndex = transactions.length;

        transactions.push(Transaction({
            to: _to,
            value: _value,
            data: _data,
            executed: false,
            numConfirmations: 0
        }));

        emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
    }

    // Confirm a transaction
    function confirmTransaction(uint _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notExecuted(_txIndex)
        notConfirmed(_txIndex)
    {
        Transaction storage transaction = transactions[_txIndex];
        transaction.numConfirmations += 1;
        isConfirmed[_txIndex][msg.sender] = true;

        emit ConfirmTransaction(msg.sender, _txIndex);
    }

    // Execute a transaction
    function executeTransaction(uint _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notExecuted(_txIndex)
    {
        Transaction storage transaction = transactions[_txIndex];

        require(
            transaction.numConfirmations >= numConfirmationsRequired,
            "Not enough confirmations"
        );

        transaction.executed = true;

        (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
        require(success, "Transaction failed");

        emit ExecuteTransaction(msg.sender, _txIndex);
    }

    // Revoke a confirmation
    function revokeConfirmation(uint _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notExecuted(_txIndex)
    {
        require(isConfirmed[_txIndex][msg.sender], "Transaction not confirmed");

        Transaction storage transaction = transactions[_txIndex];
        transaction.numConfirmations -= 1;
        isConfirmed[_txIndex][msg.sender] = false;

        emit RevokeConfirmation(msg.sender, _txIndex);
    }

    // View functions
    function getOwners() public view returns (address[] memory) {
        return owners;
    }

    function getTransactionCount() public view returns (uint) {
        return transactions.length;
    }

    function getTransaction(uint _txIndex)
        public
        view
        returns (
            address to,
            uint value,
            bytes memory data,
            bool executed,
            uint numConfirmations
        )
    {
        Transaction storage transaction = transactions[_txIndex];

        return (
            transaction.to,
            transaction.value,
            transaction.data,
            transaction.executed,
            transaction.numConfirmations
        );
    }

    function getConfirmationCount(uint _txIndex) public view returns (uint) {
        return transactions[_txIndex].numConfirmations;
    }

    function isTransactionConfirmed(uint _txIndex, address _owner) public view returns (bool) {
        return isConfirmed[_txIndex][_owner];
    }
}

This multisignature wallet demonstrates:

  1. Function visibility (public, private)
  2. Modifiers for access control and validation
  3. View functions for reading state
  4. Complex transaction management with confirmations
  5. Fallback function for receiving Ether
  6. Event emission for tracking actions

Conclusion

Function visibility and modifiers are essential concepts in Solidity that help you control access to your contract's functions and implement reusable code patterns. By understanding these concepts, you can write more secure, maintainable, and gas-efficient smart contracts.

Key takeaways from this module:

  1. Function visibility (public, external, internal, private) determines who can call a function and from where.
  2. Modifiers allow you to change the behavior of functions in a declarative way, making your code more readable and maintainable.
  3. View and pure functions indicate that a function doesn't modify or doesn't read from the state, providing gas efficiency and security benefits.
  4. Function overloading allows multiple functions with the same name but different parameter types.
  5. Function selectors, fallback, and receive functions handle different types of contract interactions.

In the next modules, we'll explore advanced Solidity concepts, including security considerations, gas optimization, and design patterns.