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:
- Public: Creates an automatic getter function
- Internal: Accessible within the contract and derived contracts (default)
- 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
- Default to private/internal: Start with the most restrictive visibility and only increase it when necessary.
- Use external for public functions: If a function is only called externally, mark it as
externalfor gas optimization. - Be careful with public state variables: Public state variables automatically create getter functions, which might expose more information than intended.
- 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:
- Use modifiers for:
- Cross-cutting concerns (access control, validation)
- Code that needs to wrap around function execution
-
Declarative checks that improve readability
-
Use functions for:
- Complex logic
- Code that needs to be reused in different contexts
- Logic that doesn't need to wrap around function execution
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
- Gas efficiency: View and pure functions don't cost gas when called externally (from outside the blockchain).
- Security: They clearly indicate their behavior, making the code more predictable.
- 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
- You cannot overload functions based on return type alone.
- You cannot overload functions with different visibility.
- 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:
- If the call data matches a function signature, that function is called.
- If no function matches and Ether is sent with empty call data, the
receive()function is called. - If no function matches and there's no
receive()function or call data is not empty, thefallback()function is called. - 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:
- Different function visibility (public, internal)
- Various modifiers for access control and state validation
- View functions for reading state
- Role-based access control
- Ownership management
- Pause/unpause functionality
- 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:
- Function visibility (public, private)
- Modifiers for access control and validation
- View functions for reading state
- Complex transaction management with confirmations
- Fallback function for receiving Ether
- 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:
- Function visibility (public, external, internal, private) determines who can call a function and from where.
- Modifiers allow you to change the behavior of functions in a declarative way, making your code more readable and maintainable.
- View and pure functions indicate that a function doesn't modify or doesn't read from the state, providing gas efficiency and security benefits.
- Function overloading allows multiple functions with the same name but different parameter types.
- 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.