Solidity: Advanced - Kerala-Blockchain-Academy/ethereum-developer-program GitHub Wiki

Inheritance

Solidity supports multiple inheritance. The contract that inherits another contract is known as derived or child contract. The contract that gets inherited is known as the base or parent contract.

When a contract that inherits another contract is deployed, only one contract is created; all the other base contracts are compiled into this single contract.Thus function calls between parent-child contracts are internal.

We can only use a state variable name in the derived contract if it does not shadow any declaration in the base contract; otherwise, it will cause an error.

The derived contract will have access to all public and internal state variables and all public, internal and external functions in the base contract to be used as its own.

is keyword is used to inherit another contract. The syntax is as follows,

<derived-contract-name> is <base-contract-name>

In the example below, we have a contract called BaseOne, which contains a struct to store the details of a book named Book, and created a variable of the type Book named book. Also, we have a function called setBook to store values to the book variable.

Now we can define another contract to inherit BaseOne contract properties, let's call it DerivedOne.

contract DerivedOne is BaseOne { }

Inside that, we can define a function to get the values of the struct and access it just like a variable defined in a DerivedOne contract.

Now try to run it in Remix IDE.

In the case of inheritance, we don't need to deploy the base contract separately; rather, we need to deploy only the derived contract, which will contain the features of the base contract except the ones restricted with appropriate access modifiers.

Try to deploy the DerivedOne and check the interaction section in Remix IDE. We can see the public function of the BaseOne contract and the functions available in DerivedOne.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract BaseOne {
   struct Book {
        uint id;
        string title;
        string author;
   }

   Book public book;

   function setBook() public {
        book = Book(1001, 'Learn Blockchain Part 1', 'KBA');
   }
}

contract DerivedOne is BaseOne {
    function getBookDetails() public view returns (uint, string memory, string memory) {
        return (book.id, book.title, book.author);
   }
}

We don't need to write all the code in a single file; instead, we can separately write the contract in different files and use the keyword import to make those available in the needed file. The above example can be rewritten as two contracts shown below.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract BaseOne {
   struct Book {
        uint id;
        string title;
        string author;
   }

   Book public book;

   function setBook() public {
        book = Book(1001, 'Learn Blockchain Part 1', 'KBA');
   }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./BaseOne.sol";

contract DerivedOne is BaseOne {
    function getBookDetails() public view returns (uint, string memory, string memory) {
        return (book.id, book.title, book.author);
   }
}

Using the import keyword we can refer codes written in other Solidity files in our file by specifying the file name along with its path.

The languages that allow multiple inheritance have to deal with some problems and one of them is called the Diamond Problem.

Solidity uses C3 Linearization, which is an algorithmic method primarily used to get the order in which methods should be inherited if multiple inheritance are implemented.

Will see an example where this scenario occurs.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract X {}
contract A is X {}

contract C is A, X {}

What happens here is that Contract C requests Contract X to override Contract A (by specifying the inheritance order A, X), but the contradiction here is that Contract A itself requests to override Contract X. This leads to an unresolved circumstance.

The execution order of the constructor in the inheritance hierarchy will follow the linearized order to prevent such scenarios.

Function Overload

The instance in which a contract has multiple functions with the same name but different parameters is called function overloading. In the Test contract, we have functions of the name sum. First sum function adds two numbers and returns the result, second one adds three number, and finally third one adds three numbers.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
    function sum(uint a, uint b) public pure returns(uint) {
        return a + b;
    }

    function sum(uint a, uint b, uint c) public pure returns(uint) {
        return a + b + c;
    }

    function sum(uint a, uint b, uint c, uint d) public pure returns(uint) {
        return a + b + c + d;
    }
}

In the below example, even though both are different types (one being contract while other being address), to an external interface, both functions will receive an address argument.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
    function fun(A) public pure returns (A) { }

    function fun(address) public pure returns (address) { }
}

contract A { }

Function Override

The base contract function can be overridden by the derived contract to change the behaviours of the function. For a function to be overridable, it has to be marked with the keyword virtual, and the overriding function has to use the override keyword in the function header to complete the process.

Apart from changing the process defined inside the function, the overriding function can change the function's visibility from external to internal. The function's mutability can be changed from non-payable to view and view to pure. The exception here is that payable functions can't be changed to any other mutability.

The contract Test has a virtual function to add two numbers. It is overridden by the child contract A to find product of two numbers.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
    function calculate(uint a, uint b) public view virtual returns(uint){
        return a + b;
    }
}

contract A is Test {
    function calculate(uint a, uint b) public pure override returns(uint){
        return a * b;
    }
}

Abstract Contract

A contract can be called an abstract contract when at least one of the functions in the contract lacks a definition. In this case, it must be marked as an abstract contract using the keyword abstract.

Here, the abstract contract Test has an undefined function sum.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract Test {
    function calculate(uint a, uint b) public pure returns(uint){
        return a * b;
    }

    function sum(uint a, uint b) public pure virtual returns(uint);
}

Note: A contract can be marked abstract even if all the functions are defined.

The abstract contract can't be deployed directly; it must be inherited by another contract to be deployed. Let us write another contract, A, as a child of the abstract contract, Test and define the function sum.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract Test {
    function calculate(uint a, uint b) public pure returns(uint) {
        return a * b;
    }

    function sum(uint a, uint b) public pure virtual returns(uint);
}

contract A is Test {
    // Try removing the function definition and see what happens.
    function sum(uint a, uint b) public pure override returns(uint) {
        return a + b;
    }
}

Try to deploy the contract A, and interact with it.

Note: If the inherited contract does not define the undefined function in the abstract contract, the child contract should also be marked abstract.

Constructor

The constructor is a function executed at the time of contract creation. The constructor can be used to run the initialization code for your contract. The keyword constructor is used to declare it.

If they are not initialized with the values inline, state variables are initialized with their default values before the constructor is executed.

After the constructor is executed, the final code is deployed to the contract. The deployment will cost additional gas linear to the length of the code. The deployed code will include all public functions and code reachable through public functions. Constructor code and any function that is only called from the constructor will not be included in the final code.

If there is no constructor then the default one is used

constructor() {}

There will be only one constructor per contract, and the constructor's visibility will be public unless it is an abstract contract.

A specific way of using constructor is to say the address of the one who deployed the contract, which can be accessed through the global variable msg.sender. It always returns the address of the caller entity.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract A {
    address public owner;
    constructor () public {
        owner = msg.sender;
    }
}

If the constructor has arguments, they need to be passed and inherited at the time of contract creation. A derived contract should be marked as abstract if it does not pass arguments.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract A {
    uint number;
    constructor (uint _number) public {
        number = _number;
    }
}

contract B is A(4) { }

contract C {
    A obj = new A(4);
}

abstract contract D is A {}

Contract A has a constructor that initializes its state variable number. When contract B inherits A, it should also pass the input for the constructor. Contract C is defining an instance of contract A, using the new keyword and passing the constructor parameter.

If you want to initialize the base contract constructor inside the derived contract, it can be done as follows.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Base contracts just to make this compile
contract B {
    uint x;

    constructor(uint _x) {
        x = _x;
    }
}
contract C {
    uint a;
    uint b;

    constructor(uint _a, uint _b) {
        a = _a;
        b = _b;
    }
}
contract D {
    uint y;

    constructor(uint _y) {
        y = _y;
    }
}

contract A is B, C, D {
    uint z;

    constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
        B(param1)
        C(param2, param3)
        D(param4)
    {
        z = param5;
    }
}

Here contract A is inheriting B, C, and D smart contracts. All the individual constructors are invoked within the constructor of A.

We can also incorporate constructor with the function modifier. In the following example, we set the address of the entity who deploys the contract as owner. Using modifier, we can write a function only accessible to this particular address.

Let's try it out.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier isOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    function changeOwner(address _owner)isOwner public {
        owner = _owner;
    }
}

Here constructor sets the owner variable. The modifier check whether caller is the owner. Any function which is a privilege of owner can make use of this modifier to implement the access restriction.

Interface

The interface is similar to an abstract contract in behaviour except that no function in them is defined, along with the following restrictions:

  • Can't inherit other contracts but can inherit other interfaces
  • All function declarations must have the external visibility
  • Can't declare a constructor and state variable
  • All functions in the interface are implicitly virtual; this means they can be overridden and do not need to be marked as virtual.

The below example depicts how an interface is declared and used.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface Test {
    function calculate(uint a, uint b) external pure returns(uint);
    function sum(uint a, uint b) external pure returns(uint);
}

contract A is Test {

    function calculate(uint a, uint b) public pure override returns(uint) {
        return a * b;
    }
    function sum(uint a, uint b) public pure override returns(uint) {
        return a + b;
    }
}

The interface Test has only declared two functions, calculate and sum. The contract A, which inherits the interface, overrides and defines these functions.

Library

A library is similar to a contract but is only deployed once to a specific address and reused many times. Libraries enable a modular way of development.

Advantages of libraries

  • Usability: A library can be used by many contracts. If multiple contracts have to use common codes, the common logic can be coded and deployed as a library.
  • Economical: Deploying common code as a library saves gas costs by reducing the size of the contract.
  • Member Functions: Libraries can be added as member functions to Solidity types.

Limitations

Libraries in Solidity are considered stateless, so the following restrictions are applicable.

  • Cannot have state variables
  • Cannot hold or receive Ether
  • Cannot inherit nor be inherited
  • Cannot be destroyed

Ideally, libraries are used to perform simple operations based on input and return the result, not to replace a contract. The interesting fact is that the same piece of code is reused throughout multiple contracts, which makes it more secure. Due to this fact that it will be scrutinized by all those developers.

Let us define a library to search and find the power of a number.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library Lib {

    function powerOf(uint a, uint b) public pure returns (uint) {
        return (a ** b);
    }
}

contract LibraryTest {

    using Lib for uint;

    function getPower(uint num1, uint num2) public pure returns (uint) {
        return num1.powerOf(num2);
    }
}

The LibraryTest contract uses the Lib in it. It should first attach the library to the datatypes. Inside the function getPower, it invokes the library function powerOf. The first parameter is used to invoke the function and the second parameter is passed as an input.

Receive and Fallback Functions

Receive

The receive function enables the contract to receive Ether, just like transferring Ether between two externally owned accounts. The receive function should be of external visibility with payable mutability. It neither has any arguments nor returns anything.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
   receive() external payable { }
}

Fallback

The fallback function is triggered when the call to the contract does not match any existing function signature. In the case of a contract with no receive function and a call containing Ether transactions, the fallback function has to be marked payable to receive Ether. The fallback function should be of external visibility. The function neither has any argument nor returns anything.

FallbackCopy
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
   fallback() external payable { }
}

Events

Events are a top-level EVM logging facility to which applications can subscribe and listen. They can be inherited by other contracts or interfaces. When an event is triggered, arguments can be stored in the transaction log, a special data structure in the blockchain. Events can be declared using the keyword event and triggered using the keyword emit.

Syntax, for declaring an event is:

event <name>(<type> <variable_name>);

There is no definition for the event; it's only declared. We start by using the keyword event followed by the event's name, and inside the parenthesis, we specify the parameters name and type to be passed when an event is triggered(executed).

Example:

event BookAdded(uint bookId);

Now, to trigger an event, we use the keyword emit followed by the event's name and then pass in the parameter values.

Example:

emit BookAdded(1001);

The events are usually used by the developer to indicate whether a state change occurred or not. They are called inside functions.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract BookContract {
    struct Book {
        uint id;
        string title;
        string author;
    }

    mapping(uint => Book) books;
    uint bookCont = 0;

    event BookAdded(uint bookId);

    function addBook() public {
        books[bookCont] = Book(1001, 'Learn Blockchain Part 1', 'KBA');
        bookCont += 1;

        emit BookAdded(1001);
    }

    function getBookDetails() public view returns (uint, string memory, string memory) {
        return (books[1].id, books[1].title, books[1].author);
    }
}

Error Handling

Solidity uses state-reverting exceptions to handle errors, which reverts all the changes made to the state by the current call and its sub-calls and returns an error to the caller.

assert and require

assert and require can be used to check for conditions and throw an exception in case the conditions are not met. The assert function should be used to test for internal errors. require function should be used to check conditions that can only be checked at runtime. Like verifying user inputs, the require function can also rely on a message on failure to the caller.

revert

The revert function can be used to flag an error and revert the current call. It is possible to provide a string message containing details about the error that will be passed back to the caller. The exceptions can also be triggered manually by relaying an error message to the caller.

Let us see an example contract which demonstrates error-handling options.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function checkIsOwner() public view returns(bool) {
        require(msg.sender == owner, "Not owner");
        return true;
    }

    function isBalanceGreaterThanZero() public view returns(bool) {
        assert(address(this).balance > 0);
        return true;
    }

    function checkIsOwnerAnother() public view returns(bool) {
        if(msg.sender == owner) {
            return true;
        }
        else {
            revert("Not owner");
        }
    }
}

Here the checkIsOwner function use a require function to check if the caller is the owner,if not it reverts the transaction with the message "Not owner". The isBalanceGreaterThanZero function has an assert statement to make sure the balance of the contract is greater than zero. The checkIsOwnerAnother function checks whether the caller is the owner, and reverts if caller is not owner stating the reason as "Not owner".

Delete & Self-Destruct

delete

delete is used to reset a Solidity type to its default value. For example, if we reset an int type, the value will be set to 0.

selfdestruct

If we want to remove a contract in action from blockchain, the only way to do so is to call selfdestruct on the contract which will send the Ether balance in the contract to the specified address and clear the storage and code associated with the contract address.

In the below example, the Test smart contract maintains a list of books. The deleteBookDetails function is used to delete a specific book. It will reset the mapping value to default. The deleteContract function is used to send the smart contract Ether balance to the caller and destroy the smart contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {
   struct Book {
        uint id;
        string title;
        string author;
   }

   mapping(uint => Book) books;

   function addBook() payable public {
        books[1001] = Book(1001, 'Learn Blockchain Part 1', 'KBA');
   }

   function getBookDetails() public view returns (uint, string memory, string memory) {
        return (books[1001].id, books[1001].title, books[1001].author);
   }

   function deleteBookDetails() public {
       delete books[1001];
   }

   function deleteContract() public {
       address payable addr = payable(msg.sender);
        selfdestruct(addr);
   }
}

Contract Type

The ones we have seen so far are simple types where we have a variable, and the variable can hold a particular type of value. For example, a bool can hold true or false. On the other hand, a contract type will have all the members of the contract that it represents, so this type can be called a user-defined type.

Each contract defines its own type. Explicit conversion of contract to address type and vice versa is possible. However, explicit conversion from address payable to contract and vice versa is only possible if the contract type has a payable function or a fallback function.

Contract types do not support any operators.

We can declare a variable of the type of a deployed contract and then call the function of that contract using the variable, just like class-object relation in object-oriented languages. We only need to provide the deployed contract address as a parameter to do this.

Let's see how we can declare one contract data type, the basic syntax is:

<contract_name> <variable_name> = <contract_name> (<deployed_contract_address>)

First, write a contract named ContractA, with a state variable message of the type string with public accessibility and has a value "Default value".

string public message = "Default Value";

Next, let's write a function to change the value of the message variable.

function setMessage(string memory _message) public {
    message = _message;
}

Deploy ContractA and copy the contract address. Then, we need to create another contract named ContractB, which creates a contract type variable.

ContractA obj = ContractA(0x5Dfbf2e1b1473b8076298a845AA42D179c787B54);

The contract type variable will have access to all the members of the created contract. We may try to get ContractA's message variable's data using this contract type variable named obj.

obj.message();

But we cannot just simply call this; we have to call this inside another function.

function getMessageFromContractA() public returns(string memory) {
    return obj.message();
}

Now go ahead and try executing the contract to get a feel for the contract type. However, don't forget the order: deploy ContractA, copy the address, and edit the address specified at the time of the obj declaration. Then, deploy ContractB and interact with it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ContractA {
    string public message = "Default Value";

    function setMessage(string memory _message) public {
        message = _message;
    }
}

contract ContractB {
    ContractA obj = ContractA(0x5Dfbf2e1b1473b8076298a845AA42D179c787B54);

    function getMessageFromContractA() public returns(string memory) {
        return obj.message();
    }

}

In the above method, we created a contract type variable of an existing/deployed contract. But it is also possible to create a new instance of ContractA using ContractB and link obj to the newly deployed ContractA using the new keyword. Then, our contract type declaration will change, as shown below.

ContractA obj = new ContractA();

Everything else will be the same as the above example, now go ahead and run it in Remix IDE.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ContractA {
    string public message = "Default Value";

    function setMessage(string memory _message) public {
        message = _message;
    }
}

contract ContractB {
    ContractA obj = new ContractA();

    function getMessageFromContractA() public returns(string memory) {
        return obj.message();
    }

}


⚠️ **GitHub.com Fallback** ⚠️