Visibility in Solidity

Solidity functions and state variables can be defined with visibility.

According to the docs:

  • public: visible externally and internally (creates a getter function for storage/state variables)
  • private: only visible in the current contract
  • external: only visible externally (only for functions) - i.e. can only be message-called (via this.func)
  • internal: only visible internally

Visibility Table

If those visibility patterns are summarised in the table, it looks like as follows:

callable? private internal external public
inside the same contract ✅ ✅ 🚫 ✅
from inherited contracts 🚫 ✅ 🚫 ✅
outside the contract 🚫 🚫 ✅ ✅

Example

Let's check this by example. In this example, Truffle Suite is used to test locally.

If you'd like to know how to test your smarts contract locally, please check out this blog.

Here is a contract called Visibility with four different types of visibility.

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;

contract Visibility {
    function privateFn() private pure returns (string memory) {
        return "private";
    }

    function internalFn() internal pure returns (string memory) {
        return "internal";
    }

    function externalFn() external pure returns (string memory) {
        return "external";
    }

    function publicFn() public pure returns (string memory) {
        return "public";
    }
}

Once you deploy this to the contract, both public and external functions are callable.

truffle(development)> let vi = await Visibility.deployed()
undefined
truffle(development)> vi.publicFn()
'public'
truffle(development)> vi.externalFn()

However, both private and internal functions cannot be called with TypeError error messages.

truffle(development)> vi.internalFn()
evalmachine.<anonymous>:0
vi.internalFn()
   ^

Uncaught TypeError: vi.internalFn is not a function
    (...8<...)
truffle(development)> vi.privateFn()
evalmachine.<anonymous>:0
vi.privateFn()
   ^

Uncaught TypeError: vi.privateFn is not a function
    (...8<...)

Next, let's define another contract that inherits from Visibility contract.

contract VisibilityChild is Visibility {
    function canCallInternalFn() public pure returns (string memory) {
        return internalFn();
    }
}

Now, internalFn() can be called.

truffle(development)> let child = await VisibilityChild.deployed()
undefined
truffle(development)> child.canCallInternalFn()
'internal'

How about external and private visibility? Actually, compilers are smart enough to warn you that they cannot be defined in the VisibilityChild contract.

Here is a invalid contract.

contract VisibilityChild is Visibility {
    function canCallInternalFn() public pure returns (string memory) {
        return internalFn();
    }

    //XXX: compilation fails
    function tryPrivateFn() public pure returns (string memory) {
        return privateFn();
    }

    //XXX: compilation fails
    function tryExternalFn() public pure returns (string memory) {
        return externalFn();
    }
}

If you try compiling this contract, it'll throw CompileError: DeclarationError: Undeclared identifier. error messages.

$ truffle compile
Compiling your contracts...
===========================
> Compiling ./contracts/visibility.sol

CompileError: DeclarationError: Undeclared identifier.
  --> project:/contracts/visibility.sol:28:16:
   |
28 |         return privateFn();
   |                ^^^^^^^^^

,DeclarationError: Undeclared identifier. Did you mean "internalFn"?
  --> project:/contracts/visibility.sol:32:16:
   |
32 |         return externalFn();
   |                ^^^^^^^^^^

Compilation failed. See above.

Summary

Visibility in Solidity gives developers flexibility to design well-modulised smart contracts and avoid unintentional accesss to private variables.

2022-10-25