A Guide to Solidity Visibility

4 min read

Understanding how to manage the visibility of functions, and variables in Solidity.

The objective of this blog post is to provide a detailed understanding of visibility specifiers in solidity, their hidden dangers, and a set of best practices to prevent them. This is an addition to the series from our earlier blog, where we shared the details of Integer Overflow and Underflow, Front Running Attack, Understanding Block Timestamp Manipulation, Issues with Authorization Using tx.origin, and Precision Loss in Solidity Operations.

Introduction#

There are four levels of visibility for functions and state variables in Solidity: public, private, internal, and external. Each visibility level governs how a function or state variable can be accessed. The visibility level for functions is set to public by default, which means that anyone can access them. Variable has a private visibility level by default, which means it can only be accessed within the same contract.

These specifiers are classified as follows:

  • Public: The public functions and state variables can be accessed from anywhere, both within and outside the contract. This means that anyone can call the function or access the state variable.
  • Private: The private functions and state variables can only be accessed from within the contract. This means that other contracts and external accounts cannot access them.
  • Internal: The internal functions and state variables can only be accessed from within the contract and any contracts that inherit from it. This means that other external contracts cannot access them, but contracts that inherit from the current contract can.
  • External: The external functions can only be called by other contracts and external accounts. They cannot be accessed from within the contract itself, unless you use `this` keyword that performs an external call to the same contract.

Vulnerability#

It is critical to select the appropriate level of visibility to ensure the security and integrity of a smart contract. The vulnerability occurs when smart contract developers unintentionally extend the visibility level of a function or data beyond what is required. For example, if a function is set to public when it should be private, anyone, including malicious actors, can access it. This can result in unauthorized data access, manipulation, or deletion, as well as unintended function execution. Making a function or state variable too restrictive, on the other hand, can limit the contract's functionality and usefulness.

Attack Scenario#

Consider the below smart contract Visibility.sol.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.17;

contract Visibility{
  address public owner;

  constructor() {
    owner = msg.sender;
  }

  function changeOwner(address newOwner) public {
    require(newOwner != address(0));
    owner = newOwner;
  }
}

Line 5:

In line 5 of the contract, we have defined a public state variable `owner` of type address. This means that the variable can be accessed by any other contract or external account.

Line 7-9:

In line 7 of the contract, we have declared a constructor that sets the `owner` variable to the address of the account that deployed the contract, which is accessed using msg.sender.

Line 11-14:

In line 11 of the contract, we have declared a public function `changeOwner` that takes an address parameter `newOwner`. It first checks that `newOwner` is not the zero address. If `newOwner` is not the zero address, it sets the owner variable to newOwner.

Attack Explained#

The above contract can be easily exploited by anyone who has access to call it. This is because the `owner` variable is declared as public, which means it can be accessed and modified by anyone, even from outside the contract.

Suppose an attacker calls the `changeOwner` function with an address of their choice, they will be able to change the `owner` of the contract to their address. Following the course of actions, the malicious actor will be able to perform actions on behalf of the owner and potentially steal funds or perform other unintended behavior.

Additionally, the require statement in the `changeOwner` function only checks that the new owner is not the zero address, but it does not check if the caller is authorized to change the owner. This means that any address can call the function and change the owner, even if they are not authorized to do so.

Prevention#

Following are the preventive measures that can be undertaken to ensure that the state variables and functions are set with appropriate visibility specifiers. 

  • Developers should carefully consider the appropriate visibility level for each variable and function in their contract. Functions that should only be accessed within the same contract should be set to internal or private, while functions that can be called by external contracts or users should be set to external or public.

  • It is critical to use access control mechanisms using modifiers, or by implementing role-based access control, in order to restrict access to sensitive data or functions. 

  • Smart contract developers should also use event-driven architecture to notify external contracts or users of state changes instead of exposing data or functions.

Conclusion#

Solidity provides many features for developing smart contracts, but it also has significant attack vectors that can be exploited by attackers. Visibility-related issues are among the most common vulnerabilities in Solidity, but they can be prevented by following best practices as shared in this blog. Developers are likely to reduce the likelihood of visibility-related vulnerabilities and make their smart contracts more secure by implementing these standard prevention measures.

By

Tags