Introduction to Solidity : Visibility, Getters and Function Modifiers

In this article, we will explore visibility, getters, and function modifiers in Solidity. These concepts are essential for anyone who wants to create smart contracts that are secure and efficient.

Visibility

Visibility is a keyword that is used to define the scope of a function or a state variable. It's used to restrict access to certain functions or variables.

State Variables visibility

There are three types of visibility for state variables that are public, private, and internal.

Let's start with the public visibility, this visibility allow the state variable to be accessed from anywhere in the contract or from outside the contract.

In that case the compiler will automatically generate a getter function for the variable. This getter function will return the value of the state variable. For example, if we have a state variable name with the public visibility, the compiler will generate a function called name() which will return the value of the name variable. When we used the variable in the same contract accessing it via this.name will call the getter function while accessing it via name will return the value directly from the storage.

Here is an example of a contract with a public state variable:

pragma solidity ^0.8.0;

contract MyContract {
    string public name;

    function saysHello() public view returns (string memory) {
        return "Hello " + name;
    }

    function sayHelloUsingThis() public view returns (string memory) {
        return  "Hello " + this.name();
    }
}

After we have the internal visibility, this one allow the variable to be accessed only from inside the contract or from a contract that inherits from it. This visibility is useful when we want to create a base contract that will be inherited by other contracts. For example, we can create a base contract that will contain the logic for a token and then we can create a contract for each token that will inherit from the base contract.

If we take the example of the previous contract and change the visibility of the name variable to internal we will get the following code :

pragma solidity ^0.8.0;

contract MyContract {
    string internal name;

    function saysHello() public view returns (string memory) {
        return "Hello " + name;
    }

    function getName() internal view returns (string memory) {
        return name;
    }

    function sayHelloUsingThis() public view returns (string memory) {
        return  "Hello " + this.getName();
    }
}

In the contract right now we need to explicitly create the getter function for the name variable and use it in the sayHelloUsingThis function. This is just to illustrate how the internal visibility works. yhou shoud not do something like this in a real contract.

What we can see here is that we can create public variable that can be accessed from outside the contract or we can create internal variable with a public getter. So you may ask yourself that the result in terms of accessing the variable externally will be the same. Yes, but there is a difference in terms of gas cost when a public variable is declared, the Solidity compiler generates a getter function for it automatically. This getter function is optimized to consume a lower amount of gas than a user-defined getter function. Also when an internal variable is declared with a public getter function, a separate function needs to be created and stored on the blockchain to retrieve its value. This results in additional gas consumption for the deployment of the getter function, as well as additional gas consumption each time the function is called.

So in conlusion if you want to allow external access to a variable, it's better to use a public variable.

Finally the private visibility that is really similar to the internal visibility. The only difference is that the private variable can only be accessed from inside the contract not in derived contracts.

Function visibility

The functions have the same three types of visibility as the state variables plus the external visibility.

The external visibility is used to create a function that can only be called from outside the contract. This function can not be called from inside the contract. A function hello with the external visibility can't be called like this hello() but can be called like this.hello() but you should avoid to do it.

The public visibility works exactly the same as the public visibility for state variables. For the internal we just need to know that the function can take internal types like mapping or storeage references. For the private visibility same as for the state variables.

In terms of gast cost, just remember that each of this visibility has its own purpose. So if you try to use the visibility that is not suitable for your use case, you will end up with a contract that is not efficient.

Getters

Like we see with the public visibility, getters are functions that are automatically generated by the compiler. The getter function has the external visibility.

There is a subtily with state variable of array type, the generated getter function will not return the entire array. Instead, it will return a single element of the array of the index that is passed as a parameter to the function. If you want to return the entire array you need to create a getter function yourself. Here's an exemple of a contract that do this :

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract arrayExample {
    // public state variable
    uint[] public myArray;

    // Getter function generated by the compiler
    /*
    function myArray(uint i) public view returns (uint) {
        return myArray[i];
    }
    */

    // function that returns entire array
    function getArray() public view returns (uint[] memory) {
        return myArray;
    }
}

Function Modifiers

Function modifiers are used to modify the behavior of a function. They are used to add additional logic to a function without having to repeat the same code in multiple functions.

For exemple you can use a modifier to check if the caller of the function is the owner of the contract. If it's not the case the function will revert.

Modifiers are inheritable properties of contracts and may be overridden by derived contracts, but only if they are marked virtual.

Here is an example of a contract that use a modifier :

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract MyContract {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // definition of the modifier
    modifier onlyOwner() {
        require(msg.sender == owner, "You are not the owner");
        _;
    }

    // function that can only be called by the owner by using the using the modifier
    function changeOwner(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

The modifiers can take arguments

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract priced {

    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}
contract Register is priced {
    mapping(address => bool) registeredAddresses;
    uint price;

    constructor(uint initialPrice) { price = initialPrice; }

    // It is important to also provide the
    // `payable` keyword here, otherwise the function will
    // automatically reject all Ether sent to it.
    function register() public payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }
}

You can apply multiple modifiers to a function by specifying them in a whitespace-separated list and they are evaluated in the order presented.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract MyContract {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // definition of the modifier
    modifier onlyOwner() {
        require(msg.sender == owner, "You are not the owner");
        _;
    }

    modifier onlyAfter(uint time) {
        require(block.timestamp >= time, "Function called too early");
        _;
    }

    function changeOwner(address newOwner) public onlyOwner onlyAfter(100) {
        owner = newOwner;
    }
}

Modifiers can't implicitly access or change the arguments and return values of functions they modify. Their values can only be passed to them explicitly at the point of invocation.

You may have noticed that each modifier has a single underscore character (_) in its body. This is a placeholder for the function body on which the modifier is applied to. The function body is inserted at the point of the underscore when the modifier is applied to a function.

Here an exemple :

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(
            !locked,
            "Reentrant call."
        );
        locked = true;
        _;
        locked = false;
    }
}

In this exemple the modifier prevent the function to be called multiple before the first call is finished. To do this the we use a boolean variable locked that is set to true when the function is called and set to false when the function is finished. If the function is called again while the locked variable is set to true the function will revert. We can see that the _ which replace the function body is placed after we locked the function and before we unlock it.

Conclusion

In this article, we have discussed about visibility, getters, and function modifiers. Remember that Visibility is a keyword that defines the scope of a function or state variable and is used to restrict access to certain functions or variables and that using the right visibility for a specific use case can be very important for the efficiency of your contract. Finally, we covered function modifiers, which are functions that can modify the behavior of another function by adding additional functionality to it. All of this are essential concepts that can help you build more robust smart contracts that are secure and efficient.