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.