What’s the difference between Solidity Function Visibility (public, external, private, internal)? When should you use one over the other? How do you know when to use Solidity Modifiers (pure, view)? Read more to find out!
Solidity functions are usually of the following form, where it specifies a function visibility and a modifier attributed to a certain function. This blog goes into detailed function visibility specifiers (public, external, internal, private) and a few select modifiers (pure, view) that often cause confusion among developers.
function functionName() [public | external | internal | private] {pure | view} returns (bool) {
return true;
}
Function Visibility Keywords
In Solidity, there are 4 function visibility keywords, which are typically used in function definitions to explicitly state who should have access rights to a contract for a certain function. In many ways, these visibility keywords are similar to specifying access control on functions.
Public Functions
The public keyword is used for functions where they can be called by any external account and any internal/external contract on the blockchain. This means that public functions are used in cases where a function is expected to provide information to anyone who calls it. A good example where a public function may be useful is in cases where variables are being accessed or overall state changes.
By looking at the ERC20.sol token implementation, there are a variety of functions that are public. An example of public functions in the standard are shown below:
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(_msgSender(), recipient, amount);
return true;
}
The transfer()
function can be called by anyone with an external address (one in a wallet) and any contract to transfer token funds from the sender to a certain recipient. The public modifier on this function ensures that there are no access control restrictions on who can call this function.
External Functions
External functions are set on functions that can only be called by an external account or external contract. This means, for a contract A
, if it has a function b()
, no functions in A can access b()
. b()
can be called by externally owned addresses or contracts that are not related to A, but the external
keyword prevents all other functions in the same contract from calling it.
A prevalent example of external functions are in the IERC20.sol contracts, which are interfaces that specify the ERC20 implementation:
function transfer(address recipient, uint256 amount) external returns (bool);
Similar to interfaces in other object-oriented programming languages, interfaces define the behaviour of a certain contract and do not contain implementation details. The ERC20 Interface transfer()
function specifies that the implementation contract must have a transfer()
function, which takes the same arguments and returns the same values. The external
specification ensures that the interface cannot call its own functions (which is not possible due to the interface not containing implementation). This does allow other contracts outside of this function to call this function.
Solidity: Under the Hood for Public and External Functions (see Notes at the end)
Internal functions in Solidity are executed via JUMP, which means that the arguments for the function are expected to be located in memory. According to the Ethereum Yellowpaper, a JUMPDEST call uses 1 unit of gas.
External functions in Solidity do not use internal functions, thus do not need to keep arguments in memory. This information can be read from the CALLDATA of the transaction, as there’s no need to store this in memory for another function to interact with.
When considering the construction and access control on public functions, one could reason that a public function is one that is external AND internal. This construction makes it significantly easier for us to understand why public functions consume more gas. Since public functions can be called by the contract itself, it will always save the arguments in memory. This means that the gas consumption by a public function should be expected to be higher than an external function.
Internal Functions
Internal functions can only be accessed by the contract the function resides in, and children of the contract. In the case of class hierarchy, this function can be called by inherited contracts but not external accounts or external contracts. These functions are typically helper functions, which is called by external/public functions.
By taking a look at the native ERC20.sol token implementation on OpenZeppelin, there are two transferring functionalities (as seen in transfer
and transferFrom
). Both of these functions make use of the internal _transfer()
functionality, which serves as a helper function for this contract and all other implemented inheritances of the ERC20 token (which has the added option of being Cappable, Pausable, etc).
Through this ERC20 token code, we can take a deeper look into what the difference between internal and public functions are:
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(_msgSender(), recipient, amount);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(sender, recipient, amount);
_approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance"));
return true;
}
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(sender, recipient, amount);
_balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
_balances[recipient] = _balances[recipient].add(amount);
emit Transfer(sender, recipient, amount);
}
The transferFrom()
and the transfer()
function both have a public visibility on them, which ensures that these functions can be called by anyone. Both of these functions also call an internal _transfer()
function, which performs the transfer functionality. The _transfer()
function is a helper function to the other two functions, which provides the transfer implementations.
The internal visibility on the _transfer()
function ensures that the function can only be called within the ERC20 Token Implementation. This ensures that external accounts and external contracts are unable to invoke this function.
Private Functions
Functions with a private visibility are an even smaller subset of internal functions, specifying that the function can ONLY be called inside the contract itself. These functions are typically helper functions, with an even smaller scope of access control - limiting to the contract the code is located in. Private functions should be called by functions that have a wider access scope (such as internal, public, or external) - with the contents of the function calling a private function.
An example of a private function can be seen in ERC20Snapshot.sol as part of the OpenZeppelin library, where the _burn()
internal function invokes private functions in the contract.
function _burn(address account, uint256 value) internal virtual override {
_updateAccountSnapshot(account);
_updateTotalSupplySnapshot();
super._burn(account, value);
}
function _updateAccountSnapshot(address account) private {
_updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account));
}
function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private {
uint256 currentId = _currentSnapshotId.current();
if (_lastSnapshotId(snapshots.ids) < currentId) {
snapshots.ids.push(currentId);
snapshots.values.push(currentValue);
}
}
This example above demonstrates the expected flow of private functions, where the _burn()
function can be invoked by any of the inherited ERC20 contracts. In the function, it calls a private helper function, which updates snapshot data on the ERC20 balances at a certain point in time. In this case, there was a specific design decision chosen to keep all snapshot functionality private to the ERC20Snapshot.sol
file, since no other ERC20 implementations understand the concept of a snapshot.
Overview of Function Visibility
The table below breaks down the differences between the function visibility.
Public | External | Internal | Private | |
---|---|---|---|---|
Description | Function can be called by anyone | Function cannot be called by this contract |
Function can be called by this contract and inherited contracts |
Function can be called by this contract only |
Access Control | Wallet, this contract, External Contract |
Wallet, External Contract | this contract, Inherited Contracts |
this contract |
Function Purpose | Getting state, changing state, calling helper | Getting state, changing state, calling helper | Helper function implementation | Helper function implementation |
Note: this
contract refers to the contract where the function is written in, similar to how the Solidity language would support address(this).balance
, which checks the Ether balance of this
contract.
Solidity Function Modifiers
Aside from function visibility specifiers, there is an important difference between pure and view modifiers that are provided by Solidity.
View
A function with a view modifier specifies that the function must never “modify state”. As this definition of “modification of state” can be ambiguous, here are a few actions which are not allowed in view functions:
- Writing to a variable in a contract - this implementation is saving state.
- Emitting events - events are often emitted to notify event listeners that a contract state has changed
- Sending Ether to another address - transferring Ether among addresses changes the state of the Ether balance
- Calling a non view/pure function - all functions that a view function calls must also not change state of contracts
- Using STATICCALL - similar to (4), where the implementation/function may change the state of an object/contract
A good example of a view function is a getter function that returns state information from a certain contract. In ERC20.sol, there are a few getter functions, including retrieving token supply for a specific address:
mapping (address => uint256) private _balances;
function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}
The balanceOf()
function returns the token balance attributed to a given address, returning one of the entries in the _balance
mapping.
Pure
A function with a pure modifier specifies that the function must never modify AND access state. A pure
modifier is stricter than a view
modifier, which ensures that the internal state of a contract can never be accessed. In addition to the list of actions that view functions are not allowed to do, a pure function adds a few more:
- View or modify state - pure functions may perform calculations but are not allowed to access internal contract state
- Accessing contract Ether balance - cannot perform checks related to balance of addresses
- Unable to access block, tx, msg data - unable to access transaction information (
tx.origin
) or msg information (msg.sender
)
The specifications of a “pure” function are from functional programming, where the functions’ arguments define the scope. This means that the function must be able to execute solely with the arguments it has received, without accessing contract state or contract data.
A good example of a pure function is the SafeMath library, which provides mathematical checks to ensure that variables do not overflow/underflow after performing mathematical calculations such as adding:
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
The add()
function showcased above is a pure function, as it receives all the variables that it requires to check for overflow. Given the uint256 a
and uint256 b
, the add function returns the result of the addition, given that the value has not overflowed.
This wraps up the tutorial on the function visibility specifications that are available in native Solidity that allow developers more fine-tuned access control. The native Solidity modifiers (pure and view) provide the ability for us to define the expected behaviour of functionality, which allows smart contracts to be neater and easier to read.
Notes
Thanks Mariano Conti for pointing out the gas difference between external and public functions!