Understanding inheritance in Solidity

Harry Thuku May 18, 2023, 11:03 p.m.

Inheritance is a powerful concept in Solidity that allows developers to build modular and reusable smart contracts. By leveraging inheritance, developers can create a hierarchy of contracts where the derived contracts inherit properties and functionalities from base contracts. This concept, borrowed from traditional object-oriented programming, plays a crucial role in organizing code, promoting code reuse, and enhancing contract functionality. In this article, we will delve into the world of inheritance in Solidity, exploring its principles, syntax, and practical applications. Whether you're new to Solidity or looking to expand your understanding, this article will provide you with the foundation needed to harness the benefits of inheritance in your smart contract development journey. So, let's dive in and unravel the intricacies of inheritance in Solidity.

Understanding inheritance in Solidity

Solidity inheritance

In Solidity, one of the key aspects of inheritance is the ability to inherit from other contracts. This feature allows developers to reuse existing contract functionality and extend it in new contracts. Solidity supports both single and multiple inheritance, enabling contracts to inherit properties and functions from multiple base contracts. To inherit from another contract, the is keyword is used in the contract declaration. Let's consider an example:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git
contract BaseContract {
    uint public baseData;
    
    function baseFunction() public pure returns (string memory) {
        return "This is a base function.";
    }
}

contract DerivedContract is BaseContract {
    uint public derivedData;
    
    function derivedFunction() public pure returns (string memory) {
        return "This is a derived function.";
    }
}
                                        
                                    

In the above example, we have two contracts: BaseContract and DerivedContract. The DerivedContract inherits from the BaseContract using the is keyword. As a result, the DerivedContract inherits the baseData state variable and the baseFunction() function from the BaseContract. Now, the DerivedContract can access and modify the baseData state variable, as well as call the baseFunction() function. Additionally, the DerivedContract introduces its own state variable called derivedData and a function called derivedFunction(). By inheriting from other contracts, developers can reuse existing code, minimize duplication, and enhance code organization. This approach promotes code modularity, making contracts easier to understand, maintain, and upgrade. In the next section, we will explore the relationship between base contracts and derived contracts, discussing how state variables and functions are inherited and how they can be accessed and modified in derived contracts.

Base Contracts and Derived Contracts

The relationship between base contracts and derived contracts forms the foundation of inheritance in Solidity. When a contract inherits from a base contract, it gains access to the state variables and functions defined in the base contract. Let's dive deeper into this relationship with code examples:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git

contract BaseContract {
    uint public baseData;
    
    function baseFunction() public pure returns (string memory) {
        return "This is a base function.";
    }
}

contract DerivedContract is BaseContract {
    uint public derivedData;
    
    function derivedFunction() public pure returns (string memory) {
        return "This is a derived function.";
    }
}
                                        
                                    

In the above example, the DerivedContract inherits from the BaseContract. As a result, the DerivedContract gains access to the baseData state variable and the baseFunction() function defined in the BaseContract. Now, the DerivedContract can access and modify the baseData state variable just like any other state variable within the contract. It can also call the baseFunction() function to execute the code defined in the BaseContract. Additionally, the DerivedContract introduces its own state variable called derivedData and a function called derivedFunction(). These are unique to the DerivedContract and are not inherited by any other contracts. It's important to note that the derived contract can override functions from the base contract. This means that if the derived contract defines a function with the same name as a function in the base contract, the derived contract's function will be executed instead of the base contract's function. Inheritance allows for the creation of contract hierarchies, where contracts can be organized based on their relationships and shared functionality. This promotes code reuse, modularity, and improves the overall design and readability of smart contracts. In the upcoming section, we will explore how functions and state variables from base contracts can be accessed and modified in derived contracts, including the use of the super keyword to invoke base contract functions.

Accessing Inherited Functions and State Variables

In Solidity, when a contract inherits from a base contract, it gains access to the functions and state variables defined in the base contract. In this section, we will explore how inherited functions and state variables can be accessed and modified within derived contracts, along with the use of the super keyword to invoke base contract functions. Let's consider the following code example:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git
contract BaseContract {
    uint public baseData;
    
    function baseFunction() public pure returns (string memory) {
        return "This is a base function.";
    }
}

contract DerivedContract is BaseContract {
    uint public derivedData;
    
    function derivedFunction() public pure returns (string memory) {
        return "This is a derived function.";
    }
    
    function updateBaseData(uint _newData) public {
        baseData = _newData;
    }
    
    function callBaseFunction() public pure returns (string memory) {
        // Invoking the base contract's function using the `super` keyword
        return super.baseFunction();
    }
}
                                        
                                    

In this example, the DerivedContract inherits from the BaseContract and gains access to the baseData state variable and the baseFunction() function. Within the DerivedContract, the baseData state variable can be accessed and modified directly, just like any other state variable. For example, the updateBaseData() function allows the derived contract to update the value of baseData. To invoke the base contract's function, the super keyword is used. In the callBaseFunction() function, the super.baseFunction() statement invokes the baseFunction() function defined in the base contract and returns the value it provides. By accessing inherited functions and state variables, derived contracts can build upon the functionality of the base contracts while introducing their own unique features. It's important to note that inherited functions and state variables can be accessed by both external parties interacting with the contract and internally within the derived contract. In the next section, we will delve into the different visibility and accessibility aspects of inherited functions and state variables, exploring the implications of using external, public, internal, and private visibility quantifiers.

In Solidity, the visibility and accessibility of inherited functions and state variables are crucial aspects of inheritance. Solidity provides four visibility quantifiers: external, public, internal, and private, which determine how these inherited elements can be accessed and utilized within derived contracts. The visibility quantifier 'external' restricts functions to be accessed externally, meaning they can only be called by other contracts or transactions. These externally accessible functions, when inherited, retain their external accessibility within derived contracts. On the other hand, functions declared as 'public' can be accessed both internally within the derived contract and externally by other contracts or transactions. Inherited functions with public visibility can be called from both internal and external contexts. The 'internal' visibility quantifier allows functions to be accessed internally within the derived contract or by derived contracts that inherit from the current contract. They cannot be accessed externally. Inherited functions with internal visibility maintain this internal accessibility. In contrast, functions declared as 'private' have the most restricted visibility. They can only be accessed internally within the contract where they are defined. Inherited functions with private visibility are not accessible within the derived contract itself. The visibility of state variables in the base contract also plays a role in their accessibility in derived contracts. If a state variable in the base contract is declared as public, internal, or private, its visibility remains the same in the derived contract. Let's consider an example to illustrate the visibility and accessibility of inherited functions and state variables:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git

contract BaseContract {
    uint public baseData;
    uint internal internalData;
    uint private privateData;
    
    function baseFunction() public pure returns (string memory) {
        return "This is a base function.";
    }
    
    function internalFunction() internal pure returns (string memory) {
        return "This is an internal function.";
    }
    
    function privateFunction() private pure returns (string memory) {
        return "This is a private function.";
    }
}

contract DerivedContract is BaseContract {
    uint public derivedData;
    
    function derivedFunction() public pure returns (string memory) {
        return "This is a derived function.";
    }
    
    function updateBaseData(uint _newData) public {
        baseData = _newData;
    }
    
    function accessInternalData() public view returns (uint) {
        return internalData;
    }
    
    function callInternalFunction() public pure returns (string memory) {
        // Cannot access privateFunction() as it is not visible in the derived contract
        return internalFunction();
    }
}
                                        
                                    

In this example, the DerivedContract inherits from the BaseContract and gains access to the baseData, internalData, and privateData state variables, as well as the baseFunction() and internalFunction() functions defined in the base contract. The DerivedContract can access and modify the inherited state variables and call the inherited functions within its own context. However, it cannot access the private function privateFunction() as it is not visible within the derived contract. Understanding the visibility and accessibility of inherited functions and state variables is essential for designing and utilizing inheritance effectively in Solidity contracts. It allows for code reuse, modularity, and improved contract design.

Inheritance Hierarchies and Multiple Inheritance

In Solidity, inheritance allows for the creation of complex contract structures through inheritance hierarchies and the ability to inherit from multiple contracts. This section explores the concepts of inheritance hierarchies and multiple inheritance and their implications in Solidity contracts. An inheritance hierarchy refers to the relationship between a base contract and its derived contracts. Derived contracts can further act as base contracts for other derived contracts, creating a hierarchical structure. This hierarchy enables the reuse of code, separation of concerns, and the ability to specialize contracts based on specific functionalities. Let's consider an example to illustrate an inheritance hierarchy:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git
contract Animal {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }

    function makeSound() public virtual pure returns (string memory);
}

contract Dog is Animal {
    constructor(string memory _name) Animal(_name) {
    }

    function makeSound() public pure override returns (string memory) {
        return "Woof!";
    }
}

contract Cat is Animal {
    constructor(string memory _name) Animal(_name) {
    }

    function makeSound() public pure override returns (string memory) {
        return "Meow!";
    }
}

contract AnimalShelter {
    Animal[] public animals;

    function addAnimal(Animal _animal) public {
        animals.push(_animal);
    }
}
                                        
                                    

In this example, we have an Animal base contract that defines a common state variable name and a makeSound() function. The Dog and Cat contracts inherit from Animal and provide their own implementation of the makeSound() function. The AnimalShelter contract demonstrates the usage of the inheritance hierarchy by storing an array of Animal contracts. It can accept any derived contract of Animal, such as Dog or Cat, and add them to the array. Multiple inheritance refers to the ability to inherit from multiple contracts. Solidity supports multiple inheritance, allowing a derived contract to inherit from multiple base contracts. This feature provides flexibility in contract design and code reuse. Let's consider an example to illustrate multiple inheritance:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git
contract BaseContract1 {
    uint public value1;
}

contract BaseContract2 {
    uint public value2;
}

contract DerivedContract is BaseContract1, BaseContract2 {
    uint public derivedValue;
    
    constructor(uint _value1, uint _value2, uint _derivedValue) {
        value1 = _value1;
        value2 = _value2;
        derivedValue = _derivedValue;
    }
}
                                        
                                    

In this example, the DerivedContract inherits from both BaseContract1 and BaseContract2. It gains access to the state variables value1 and value2 from the base contracts and introduces its own state variable derivedValue. The constructor initializes all the inherited and derived state variables. Understanding inheritance hierarchies and multiple inheritance is essential for designing complex contracts with reusable and modular code structures. It enables the construction of sophisticated contract systems and promotes code organization and maintainability.

Abstract Contracts and Interfaces

In Solidity, abstract contracts and interfaces are powerful tools for defining common structures and behaviors that derived contracts must implement. This section explores the concepts of abstract contracts and interfaces and their significance in Solidity contracts. An abstract contract serves as a blueprint or template that cannot be instantiated on its own. It contains one or more unimplemented functions, also known as abstract functions, which derived contracts must override and provide concrete implementations for. Abstract contracts are used to define common functionalities and establish a contract's overall structure while leaving the implementation details to the derived contracts. Let's consider an example to illustrate an abstract contract:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git
abstract contract Animal {
    string public name;
    
    constructor(string memory _name) {
        name = _name;
    }
    
    function makeSound() public virtual returns (string memory);
}

contract Dog is Animal {
    constructor(string memory _name) Animal(_name) {
    }
    
    function makeSound() public override returns (string memory) {
        return "Woof!";
    }
}

contract Cat is Animal {
    constructor(string memory _name) Animal(_name) {
    }
    
    function makeSound() public override returns (string memory) {
        return "Meow!";
    }
}
                                        
                                    

In this example, the Animal abstract contract defines the common state variable name and an abstract function makeSound(). The makeSound() function is marked as abstract and does not contain any implementation. The Dog and Cat contracts inherit from the Animal abstract contract and provide concrete implementations for the makeSound() function. Interfaces, on the other hand, are similar to abstract contracts but only contain function declarations without any implementation details. They define a set of function signatures that derived contracts must implement. Interfaces are used when multiple unrelated contracts need to adhere to a specific set of functions. Let's consider an example to illustrate an interface:

                                        
                                            // https://github.com/htostudios/solidity-inheritance.git
interface Token {
    function transfer(address _to, uint _amount) external;
    function balanceOf(address _owner) external view returns (uint);
}

contract MyContract {
    Token public token;
    
    constructor(address _tokenAddress) {
        token = Token(_tokenAddress);
    }
    
    function transferTokens(address _to, uint _amount) public {
        token.transfer(_to, _amount);
    }
    
    function getBalance(address _owner) public view returns (uint) {
        return token.balanceOf(_owner);
    }
}
                                        
                                    

In this example, the Token interface declares two functions: transfer() and balanceOf(). The MyContract contract utilizes the Token interface by setting it as a public state variable and interacting with the functions declared in the interface. Abstract contracts and interfaces provide a powerful way to define common structures, behaviors, and function signatures that derived contracts must adhere to. They enhance code reusability, modularity, and interoperability in Solidity contracts.

Real-World Examples and Use Cases in Solidity

In real-world scenarios, Solidity's inheritance feature plays a crucial role in the development of decentralized applications (DApps) on the Ethereum blockchain. Let's explore some common use cases and real-world examples where inheritance in Solidity is applied. One prominent application of inheritance is in token contracts, such as ERC-20 and ERC-721 tokens. These contracts often utilize inheritance to adhere to standard interfaces and provide additional functionalities. By using base token contracts that define core features and derived contracts that customize them with specific behaviors or additional functionality, developers can create tokens that are compatible with existing wallets, exchanges, and decentralized applications. Another practical use case for inheritance is in access control mechanisms within contracts. By creating an access control base contract, derived contracts can inherit the required access control functionalities, such as role-based permissions or whitelisting. This approach ensures consistency and enhances security across different components of a DApp. Furthermore, inheritance enables contract upgradability, which is a critical aspect of smart contract development. By separating storage and logic into separate contracts, derived contracts can inherit the storage state from a base contract while allowing the logic to be upgraded independently. This approach enables developers to make changes or improvements to the contract's functionality without disrupting the existing data or user interactions. Inheritance also facilitates the creation of modular and reusable code structures. Developers can design base contracts that encapsulate common functionalities and define interfaces for derived contracts to adhere to. This promotes code reuse, simplifies maintenance, and enhances the overall efficiency of the contract development process. Overall, inheritance in Solidity empowers developers to create sophisticated and scalable decentralized applications by leveraging code reuse, standardization, and modular design principles. By understanding and effectively implementing inheritance, developers can build robust and adaptable smart contracts that cater to diverse real-world use cases.

In conclusion, understanding inheritance in Solidity is essential for developing complex and scalable decentralized applications on the Ethereum blockchain. By leveraging inheritance, developers can create hierarchical relationships between contracts, inherit functions and state variables, and establish code reusability and modularity. In this article, we explored the fundamentals of inheritance in Solidity, including inheriting from other contracts, the relationship between base and derived contracts, accessing inherited functions and state variables, inheritance hierarchies, and abstract contracts and interfaces. We also discussed real-world examples and use cases where inheritance is applied. By mastering inheritance in Solidity, developers can enhance code organization, maintainability, and interoperability, ultimately leading to more efficient and robust smart contract development. As you continue your journey in Solidity development, harness the power of inheritance to unlock endless possibilities for creating innovative and decentralized solutions.

Subscribe to our newsletter

Subbscribe to our news letter for our weekly news report and to get notified when we upload new articles and tutorials.

How to create and deploy NFTs on Ethereum and OpenSea using python and solidity

How to create and deploy NFTs on Ethereum and OpenSea using python and solidity

March 27, 2023, 9:28 a.m.

This tutorial is aimed at providing a step-by-step guide on how to create, host, and publish an NFT collection using python and solidity and deploying it on the Ethereum Blockchain and OpenSea marketplace....

Understanding Solidity Functions

Understanding Solidity Functions

April 26, 2023, 11:23 a.m.

Solidity is a programming language that is specifically designed for writing smart contracts on the Ethereum blockchain. Smart contracts are self-executing contracts with the terms of the agreement between buyer and seller being directly written into lines of code. Functions play a crucial role in the functioning of smart contracts as they allow the contract to interact with the outside world. In this article, we will dive deeper into understanding the various types of functions in Solidity...

Cryptographic functions in solidity

Cryptographic functions in solidity

May 3, 2023, 9:29 p.m.

Cryptographic functions play a critical role in the security of blockchain applications. They are used to protect sensitive data, ensure the integrity of transactions, and authenticate users. In Solidity, the programming language used to write smart contracts on the Ethereum blockchain, there are several cryptographic functions that are commonly used. These functions include Keccak256, RIPEMD160, SHA256, and ECRECOVER...

Understanding the basics of Solidity Smart Contracts

Understanding the basics of Solidity Smart Contracts

May 15, 2023, 2:20 p.m.

Contracts in Solidity have become an integral part of developing decentralized applications on the Ethereum blockchain. They are self-executing code, written in Solidity, which allow developers to create and manage digital assets and execute complex business logic in a transparent and trustless manner. Solidity is a high-level programming language that is specifically designed for creating smart contracts on the Ethereum blockchain. It is similar to other programming languages in terms of syntax and structure, but it has unique features and limitations that make it ideal for creating secure and efficient smart contracts...