Skip to content

Solidity Tutorial - Basics (Part 3)

In the previous tutorial, we looked at the different types of variables and how to assign them. In this section we will look into an essential component in smart contracts (and generally most programming languages) - Functions.

Functions

Functions are reusable bits of code that can be called multiple times. In Solidity, they can be declared using the function keyword and the following syntax:

function <function-name>(<list of parameters and type>) <visibility> <mutability> <modifier> returns (<type of return value>) {
<embed logic here>
}

Function Visibility

When creating functions, you need to specify who can directly call functions inside of that smart contract by including its function visibility.

There are 4 types of visibility:

  • public
  • private
  • internal
  • external

Public

As the name suggests, public functions with the public keyword will be accessible to all users and contracts. State variables can also be assigned the public keyword, and the compiler will automatically create a getter function.

contract Parent {

// We can make a state variable public and can access it using the getter function created by the compiler
string public publicString = "This is a public function!";

// We create a public function that returns a string
function publicFunction() public view returns (string memory) {
    return publicString;
}

// functions inside of our contract can call this public function
function functionOne() public view returns (string memory) {
    return publicFunction();
}

}

// using the "is" keyword we can use this to inherit functions
contract Child is Parent {

// functions inherited from another contract can also call this function
function functionTwo() public view returns (string memory) {
    return publicFunction();
}

}

We can also call publicFunction() from within another smart contract if contract Parent is deployed on the blockchain:

// We first need to declare an interface to interact with another contract
interface ParentInterface {

    // functions must be declared as external in the interface even if they are public in the contract they are being called from
    function publicFunction() external view returns (string memory);
}

contract AnotherContract {

    // address 0xf8e81D47203A594245E36C48e151709F0C19fBe8 is where our Parent contract is stored (for example sake)
    address parentAddress = 0xf8e81D47203A594245E36C48e151709F0C19fBe8;

    function functionTwo() external view returns (string memory) {
        return ParentInterface(parentAddress).publicFunction();
    }
}

// functionTwo() => string : "This is a public function!"

Private

Functions and variables labelled as private can only be accessed by the contract that has created the function. They cannot be passed on through inherited contracts or other contracts deployed on the blockchain.

contract Parent {

// This does not have to be declared private for use in a private function
string private privateString = "This is a private function!";

function privateFunction() private view returns (string memory) {
    return privateString;
}

// This function will be able to call privateFunction()
function functionOne() public view returns (string memory) {
    return privateFunction();
}
}

// Inherited contracts will not be able to inherit private functions
contract Child is Parent {

// This function will not be able to call privateFunction() and produce an error
function functionTwo() public view returns (string memory) {
    return privateFunction(); // <= error produced here
}
}

Internal

internal functions and variables are similar to private except they can be inherited by other contracts. However they cannot be called from other contracts using an interface:

contract Parent {

// According to Solidity docs, this is the default visibility level for state variables therefore internal keyword is not neccesary
string internal internalString = "This is a internal function!";

function internalFunction() internal view returns (string memory) {
    return internalString;
}

// This will work
function functionOne() public view returns (string memory) {
    return internalFunction();
}
}

// Inherited contracts will be able to call internal functions
contract Child is Parent {

// This will also work
function functionTwo() public view returns (string memory) {
    return internalFunction(); 
}
}

External

external functions can only be called on the outside by an external user or contract. Variables cannot have the external visibility.

contract Parent {

// Variables cannot be declared external
string externalString = "This is an external function!";

function externalFunction() external view returns (string memory) {
    return externalString;
}

// This will not work
function functionOne() public view returns (string memory) {
    return externalFunction();
}
}

// Inherited contracts will also NOT be able to call external functions
contract Child is Parent {

// This will NOT work
function functionTwo() public view returns (string memory) {
    return externalFunction(); 
}
}

State Mutability

In some of our examples we have repeatedly seen the keywords view and pure used but did not discuss what these terms mean. When defining a function, we can declare whether the function is going to alter the state of the blockchain. If the function simply reads the state, we can declare the function with the view keyword. If the function does not read or write to the state, then we can declare this function as pure .

Functions that are declared view or pure will not cost any gas if they are called externally as any computation can be done through the local node. However, if these functions are called internally by other functions, then this will incur a gas fee.

Return Statement

For a function to return something, you must specify the the returns keyword in the function declaration and include the type that is going to be returned inside brackets. You would then embed the return keyword before the value you would want to return.

For instance, if you wanted to create a function to return a number:

uint number = 10;

// we first use the returns key word and specify the type (in this case its uint)
function retrieveNumber() public view returns (uint) {
    // add the return keyword before the value you want to return
    return number;
}

If you want to return something that is of reference type (e.g. array, string), then you must also specify the data location (most of the times this will be memory ):

string myString = "Hello World!";

// The memory keyword must be used here as string is a reference type
function retrieveString() public view returns (string memory) {
    return myString;
}

If we want to return multiple values, we need to also include their type into the function declaration:

uint number = 10;
string numAsString = "Ten";

function retrieveNumber() public view returns (uint, string memory) {
    // We add brackets which will return a tuple of our values
    return (number, numAsString);
}

// We can retrieve our values from our function like this
function exampleOne() public view {
    (uint a, string memory b) = retrieveNumber();
}

// or like this
function exampleTwo() public view {
    uint a;
    string memory b;

    (a,b) = retrieve();
}

Modifiers

Modifiers are special types of functions that can be “added” to modify the behaviour of an existing function. One common use case for modifiers is to ensure certain conditions are met before the function being called can be invoked e.g. checking to see if user is admin or has required permissions. Whilst modifiers may not be necessary and any logic inside the modifier can be simply embedded into a function, its use makes the code more readable for other developers.

uint256 number;
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

// We create a modifier that will only allow the specified address to call a function
modifier onlyOwner() {
    require(msg.sender == owner, "Permission Denied - Not the Owner!");
    // The "_;" is a special syntax that determines when you want the modifier to execute
    _;
}

// We append the onlyOwner modifier to our function which will be executed first before storeNumber
function storeNumber(uint num) public onlyOwner {
    number = num;
}

// If we attempt to call the function using a different address than variable "owner", it will produce an error and revert the state

The location of _; is important as it instructs when the code inside the modifier should be run. Anything above the _; is ran before the function (which the modifier has been appended to) is itself executed. Any code that is below _; will be executed after the after the function has finished executing.

We can use the previous example to demonstrate this concept:

/*
We will be calling storeNumber using address 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

Depending on where the _; is placed, will determine whether we can call storeNumber
*/

uint256 number;
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

modifier onlyOwner() {
    // This line is going to be executed first before storeNumber
    // Since the check comes first, we will not be able to call storeNumber
    require(msg.sender == owner, "Permission Denied - Not the Owner!");
    _;
}

function storeNumber(uint num) public onlyOwner {
    number = num;
    // Lets change the address of the owner
    owner = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
}

// In this case we will not be able to call storeNumber as the require statment is executed first before we can change the owner variable
/*
By placing the _; above the require statement we can change the address before running the require function
*/

uint256 number;
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

modifier onlyOwner() {
    // we can now change the address of the owner before the require statement
    _;
    require(msg.sender == owner, "Permission Denied - Not the Owner!");
}

function storeNumber(uint num) public onlyOwner {
    number = num;
    owner = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
}

// In this example we will now be able call storeNumber using address 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

Since the modifier is a function, we can also include function parameters just like normal functions. We will add a modifier to our original code with the condition that number > 10.

uint256 number;
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

modifier onlyOwner() {
    require(msg.sender == owner, "Permission Denied - Not the Owner!");
    // The "_;" is a special syntax that determines when you want the modifier to execute
    _;
}

modifier greaterThanTen(uint _number) {
    require(_number > 10, "Number must be greater than 10!");
    _;
}

function storeNumber(uint num) public onlyOwner greaterThanTen(num) {
    number = num;
}

// calling storeNumber(5) will produce an error and revert