Solidity has many useful features that enable us, smart contract developers, to make safe, secure, and functional smart contracts. We have discussed other interesting Solidity features, and this time, we’ll focus on one very important feature of Solidity called function visibility. It determines who can call a function and who can access its variables.
In this article, we will explore the different types of function visibility in Solidity and how the types are used. It’s part of our long-standing tradition to make this (and other) articles a faithful companion, or a supplement to the official Solidity docs for this article’s topic.
Overview
There are four types of function visibility in Solidity:
- public,
- external,
- internal, and
- private.
Public functions are the most permissive, as they can be called by anyone, including other contracts and external users. We declare them using the public
keyword, which is the default visibility type if we leave out the visibility specifier keyword. We can imagine public functions as being internal and external at the same time.
External functions are similar to public functions, but they are intended to be called by other contracts rather than external users. They are declared using the external
keyword and are useful for defining a contract’s interface, which is the set of functions that other contracts can use to interact with it.
Internal functions are similar to public functions, but they can only be called from within the contract in which they are defined and from the derived contracts. We declare them using the internal
keyword and they are useful for separating out functionality only needed within the contract.
πͺ Warning: The default function visibility type is public and must not be confused with the default state variable visibility type, which is internal.
Private functions are the most restrictive, as they can only be called from within the contract in which they are defined. They are declared using the private
keyword and are useful for hiding implementation details that are not relevant to external users. In contrast to internal functions, private functions cannot be called by the derived contracts, but only within the base contract.
π©βπ» One-Paragraph Summary: We should carefully consider function visibility when designing a contract, as it can significantly affect security and usability. Public functions can be called by anyone, so they should be used sparingly and only for functions that need to be exposed to both external and internal calls. External functions should be used to define the contract’s interface and make it easier for other contracts to interact with it. Internal and private functions can be used to hide implementation details and reduce the attack surface of the contract.
Public Functions
Public functions are part of the contract interface, which enables us to call these functions either internally, i.e. f()
or via message calls, i.e., this.f()
.
We’d commonly declare a function as public to make it callable by other contracts or externally by users, such as us, when calling a function in e.g., Remix.
π©βπ» Recommended: Solidity Remix IDE Quickstart
A very simple example of a public function is given below:
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; contract SimpleFunction { function addNumbers(uint a, uint b) external pure returns (uint) { return a + b; } function callAddNumbers(uint a, uint b) public pure returns (uint) { // This would work. return addNumbers(a, b); // This would work, but the mutability keyword must be // at least 'view' because it calls the function addNumbers(...) // from its environment. return this.addNumbers(a, b); } }
Public functions provide us with the most freedom in terms of the simplest proof-of-concept approach for quickly testing new ideas, without concerning ourselves with potential design issues or security.
In such cases, however, we should remain aware that an “all-public” approach should be later replaced with stricter implementation, guided by best practices in terms of design and security.
External Functions
External functions are also part of the contract interface, which enables us to call them from other contracts and via transactions. However, there’s a restriction: we cannot call an external function f
internally. That’s why an f()
call does not work due to internal reference, however, this.f()
does work because the keyword this
refers to the contract externally.
π©βπ» Recommended: Solidity Function Calls β Internal and External
Let’s take a look at the following example:
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; contract SimpleFunction { function addNumbers(uint a, uint b) external pure returns (uint) { return a + b; } function callAddNumbers(uint a, uint b) public // Mutability keyword must be at least 'view', because it calls // the function addNumbers(...) from its environment. // It's not the case with internal function call. view returns (uint) { // This wouldn't work. An error would show up saying: // "DeclarationError: Undeclared identifier. "addNumbers" is not // (or not yet) visible at this point." return addNumbers(a, b); // This will work. return this.addNumbers(a, b); } }
In the example above, we notice an error caused by trying to call the function callAddNumbers(...)
declared as external. On the other hand, if we tried calling an external function via a message call by adding the keyword this, i.e., this.addNumbers(a, b)
, it would work just fine.
π‘ Note: A caller function calling another function via a message call should be declared at least as view (read-only), because it reads from its environment and pure is too restrictive.
The public visibility type can be considered external type and internal type simultaneously.
Internal Functions
We can access internal functions only from within the base (current) contract or contracts deriving (inheriting) from the base contract. This means internal functions are unavailable for external access because they are not exposed through the contractβs ABI. Because of that, internal functions can take parameters of internal types, like mappings or storage references.
Let’s revisit the modified example:
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; contract SimpleFunction { function addNumbers(uint a, uint b) internal pure returns (uint) { return a + b; } function callAddNumbers(uint a, uint b) public // Mutability keyword may be 'pure', since it calls // the function addNumbers(...) internally. pure returns (uint) { // This will work. return addNumbers(a, b); // This wouldn't work. An error would show up saying: // TypeError: Member "addNumbers" not found or not visible after // argument-dependent lookup in contract SimpleFunction. return this.addNumbers(a, b); } }
The situation with internal functions is quite the opposite with regard to external functions: calling an internal function via a message call is not possible, i.e., this.addNumbers(a, b)
won’t work. However, an internal call to an internal function will work just fine: addNumbers(a, b)
.
Private Functions
We’d declare a function as private function is used when the function is only intended to be called by functions within the same contract and is not accessible to external contracts or users.
The example below will show us how to make a function invisible to the outside world, but still an integral part of a contract.
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; contract SimpleFunction { function addNumbers(uint a, uint b) private pure returns (uint) { return a + b; } function callAddNumbers(uint a, uint b) public // Mutability keyword may be 'pure', since it calls // the function addNumbers(...) internally. pure returns (uint) { // This will work. return addNumbers(a, b); } }
Conclusion
In this article, we learned about function visibility types in Solidity. Function visibility is an important concept in Solidity that determines who can call a function and access its variables. By carefully considering function visibility, we, as smart contract developers, can create secure, maintainable, and easy-to-use contracts.
π©βπ» Recommended: Blockchain Developers – Income and Opportunity
First, we introduced public functions and saw how they work and behave.
Second, we explained what external functions are and what their role is in the notion of public functions.
Third, we learned about internal functions and showed how they can be perceived as a second half of public functions.
Fourth, we rounded the story on function visibility with the introduction of private functions as the most restrictive form of function visibility type.
What’s Next?
This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):