Deciphering Visibility Specifiers in Solidity: From Basics to Optimizations to Security

Deciphering Visibility Specifiers in Solidity: From Basics to Optimizations to Security

One of the imperative parts while learning smart contract development with Solidity language is to get a good grasp on the visibility specifiers.

Visibility specifiers in solidity provide developers with additional power to control the accessibility(or visibility) of functions or state variables in your smart contract.

They are primarily of 4 different types -> external, public, internal, and private.

Alright! Now we are now going to revise the basics very quickly just to get a good understanding of the sections that are coming up next in this article.

However, if you are already aware of the basics, feel free to move to the Optimization section.

The Fundamentals

  1. Public:
  • A function with public visibility is the most accessible of them all. It is accessible from everywhere,i.e., within the contract, outside the contract, or even through inheriting contracts.
  • Interestingly when a public specifier is assigned to state variables, a getter function for that specific state variable is automatically generated.

2. External

  • Unlike Public visibility, an external specifier cannot be called internally from within the contract but only from outside the contract or by a third party.
  • Moreover, external visibility can only be assigned to functions and not state variables.
A really interesting fact about gas consumption differences of public & external functions is explained in the optimization section of this article.

3. Internal

  • As the name suggests, functions with internal visibility can only be accessed from within the contract defining the function or from the one that inherits it.
  • The same goes for state variables with internal visibility specifiers.

4. Private

  • Private visibility is the most restricted one. Once assigned to a function, it only allows the accessibility of that function from within the contract that it is defined.
  • Unlike Internal, functions with private visibility can not be accessed even via inheriting contracts.
  • Similar rules for private visibility also apply when used with state variables. Private state variables cannot be accessed or modified by any other contracts except the one that includes it.
❓Quick question ❓

If private visibility restricts access to state variables by any third-party contract or user, does it mean we can store secret information within the private state variable of a smart contract?

🤔 Think about it, we will get back to it in the Security Section.

Optimization: External functions cost less gas than Public functions

While functions with external and public visibilities behave almost similarly, there are some important gas optimization differences between the two that must be kept in mind.

For instance, take a quick look at the image below. 👇

A clear difference in Gas Consumption between External & Public visibility function

Do you notice how public function costs comparatively more gas than the external one?

Well, Why?

In the case of public functions, arguments of the functions are copied to 𝐌𝐄𝐌𝐎𝐑𝐘. While on the other hand, functions with external visibility can directly read arguments from 𝐂𝐀𝐋𝐋𝐃𝐀𝐓𝐀.

Now, since CALLDATA is cheaper than MEMORY, external functions result in a lower execution cost than public functions.

Well, this might lead to one more question:

Why do public functions copy arguments to memory while external functions don’t?

As previously discussed, public functions can be called from outside as well as within the contract, i.e., internal calls.

Internal calls are executed via opcode JUMP as array arguments are passed internally by pointers to memory.

Hence, when the compiler generates opcodes for an internal function, the function expects its arguments to be located in memory itself.

However, this is not at all the case in external functions. They don’t really care about internal calls at all and thus end up saving some gas.

Security Pointers around Visibility Specifiers

  1. PRIVATE isn’t really PRIVATE ⚠️
Remember the quick question, in The Fundamentals section?

Time to answer that. ⏰

It’s a usual misconception among smart contract developers, especially beginners, to believe that a private visibility specifier actually makes the state variables unreadable or completely inaccessible.

Well, it doesn’t. 💡

A state variable with a private keyword can still be read on-chain.

Moreover, even if your contract isn’t verified, any third-party actor can take a look at its transaction to figure out the values stored in the contract state.

How exactly, you ask? 🤔

Check this quick amazing video that clearly explains the underlying details of how it's done.

Therefore, it becomes a security issue if any unencrypted imperative data is stored on-chain even with private visibility.

Sensitive or crucial data should either be stored completely off-chain or with adequate encryption on-chain.

2. Default visibilities can become really dangerous 🔴

Still, using a solidity version lesser than 0.5?

Then you might want to be careful of default visibilities.

In solidity versions lesser than 0.5.0, functions without any explicit assignment of visibility keywords are assumed to be public by default.

This could lead to a potential exploit scenario if the function with crucial state modifications wasn’t assigned any visibility keyword at all by mistake.

Since the default visibility public will be assigned to this important function, any malicious actor can make an unintended state modification by triggering the function.

This security issue clearly depicts the significance of visibility types in solidity and why they should be very carefully selected for specific functions.

3. For functions accessible to the world, Access Controls are unquestionably significant ⚠️

Considering the previous pointer in mind, it’s important to note that any function with external/public visibility is basically visible, readable, and accessible to any user.

A quick example of how inadequate access controls or absence of input validations in external or public functions can lead to a 2 million dollar hack. 🥷

TempleDao hack due to inadequate access controls

Therefore, while developing or auditing such functions, 2 important points must be kept in mind.

WHO can call these functions?

  • Since external/public visibility keywords make functions completely accessible by third-party actors, it becomes extremely crucial to assign adequate access control modifiers to such functions.
  • Access control modifier helps us add a layer of security over such functions since the visibility specifiers alone won’t stop them from being accessed by a wallet or contract.
Read more about the different types of access control mechanisms here.

HOW to call these functions?

  • While access control can definitely help provide additional security, it might not always be intended.
  • Some public or external functions are meant to be called by every user on-chain.
  • Therefore, at this point, it’s no more about who can call these functions, but more about how should one call these functions (i.e., passing the right parameters while calling the function).
  • In simpler terms, for open functions, it’s extremely important to ensure that proper input validations are included.
  • This is to ensure that although the function is accessible to everyone, the arguments being passed to the function are within the valid range and as per the contract’s expectation.

Additional Notes on Visibility Specifiers

  1. Visibility of a function within an Interface should always be External
  2. Visibility of a “Free function” shall always be Internal. Learn more about Free functions here
  3. receive() and fallback() functions in solidity must always be assigned External visibility.
  4. Function layout order: Including an adequate layout of functions in the contract enhances its readability. It’s considered a better practice in solidity to arrange the different functions (based on their type and visibility) in the following order 👇
  • constructor
  • receive or fallback function (if need be)
  • external functions
  • public functions
  • internal functions
  • private functions
  • view or pure functions ( if any )