DEV Community

Cover image for The Complete Guide to Solidity Inheritance
Azeez Abidoye
Azeez Abidoye

Posted on

The Complete Guide to Solidity Inheritance

If you're new to blockchain development, understanding inheritance in Solidity will make your smart contracts less complicated, more efficient, and easier to maintain. Let's break down this fundamental concept in a way that makes sense, even if you're completely new to it.

What is solidity?

Solidity is the primary programming language used to create smart contracts for the Ethereum blockchain and other compatible networks. Consider it the language that enables blockchain applications, such as decentralized finance (DeFi) protocols and NFT marketplaces. Smart contracts are self-executing programs that automatically enforce agreements without the need for intermediaries, and developers use Solidity to create them.

What Does Inheritance Mean?

Inheritance is a programming technique that enables you to generate a new contract from an existing one. Assume you're developing a variety of vehicles, including automobiles, trucks, and motorcycles. Instead of implementing the code for wheels, motors, and steering from scratch for each vehicle, you could create a base "Vehicle" layout and have each unique type inherit those shared elements.

In Solidity, inheritance works the same way. You can create a parent contract with common functionality, then create child contracts that inherit everything from the parent while adding their own unique features. This approach is powerful because:

  • Code Reusability: Write once, use many times
  • Easier Maintenance: Fix a bug in the parent contract, and all children will benefit
  • Accurate Organization: Keep related functionality grouped logically
  • Reduced Errors: Less code duplication lowers the possibilities of errors

How Inheritance Works in Solidity

In Solidity, when one contract inherits from another, it acquires access to all of the parent contract's functions, variables, and modifiers. The child contract can use the inherited features as if they were written in its own code.

Let's look at a simple example:

// Parent contract
contract Animal {
    string public name;

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

    function speak() public pure virtual returns (string memory) {
        return "Some generic animal sound";
    }
}

// Child contract inheriting from Animal
contract Dog is Animal {
    constructor(string memory _name) Animal(_name) {}

    function speak() public pure override returns (string memory) {
        return "Woof! Woof!";
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, Dog inherits from Animal using the is keyword. The Dog contract automatically has access to the name variable from Animal, but it customizes the speak() function to return a dog-specific sound.

Understanding Function Overriding.

Function overriding occurs when a child contract defines its own implementation of a function that already exists in the parent contract. This is incredibly helpful when you want to maintain the same function structure while changing the behaviour for specific instances.

Here's where the virtual and override keywords come into play:

virtual: Added to a function in the parent contract to indicate it can be overridden by child contracts
override: Added to a function in the child contract to indicate it's replacing the implementation of the parent contract

Think of virtual as the parent saying, "You can change this if you need to," and override as the child saying, "Thanks, I'm changing it."

contract BankAccount {
    uint256 public balance;

    constructor(uint256 initialBalance) {
        balance = initialBalance;
    }

    // Virtual function - can be overridden
    function withdraw(uint256 amount) public virtual {
        require(amount <= balance, "Insufficient funds");
        balance -= amount;
    }
}

contract PremiumBankAccount is BankAccount {
    uint256 public bonusPoints;

    constructor(uint256 initialBalance) BankAccount(initialBalance) {
        bonusPoints = 0;
    }

    // Override the withdraw function to add bonus points
    function withdraw(uint256 amount) public override {
        require(amount <= balance, "Insufficient funds");
        balance -= amount;
        bonusPoints += amount / 100; // Earn 1 point per 100 withdrawn
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, PremiumBankAccount inherits the basic functionality of BankAccount but overrides the withdraw function to add a bonus points system. The child contract maintains the same basic logic but extends it with additional features.

Practical Real-World Use Cases

1. Token Standards
One of the most common applications of inheritance in Solidity is the implementation of token standards. The ERC-20 standard defines a basic token interface, and developers create their own tokens by inheriting from the base ERC-20 implementation.

// Simplified base ERC-20 contract
contract ERC20 {
    mapping(address => uint256) public balanceOf;

    function transfer(address to, uint256 amount) public virtual returns (bool) {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

// Custom token with added features
contract RewardToken is ERC20 {
    mapping(address => uint256) public stakingRewards;

    // Add custom functionality specific to this token
    function claimRewards() public {
        uint256 reward = stakingRewards[msg.sender];
        stakingRewards[msg.sender] = 0;
        balanceOf[msg.sender] += reward;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Access Control Systems
Inheritance is perfect for building access control systems where different contracts need different permission levels:

contract Ownable {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

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

contract Marketplace is Ownable {
    uint256 public platformFee;

    // Only owner can set the fee
    function setPlatformFee(uint256 newFee) public onlyOwner {
        platformFee = newFee;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Pausable Contracts
Many smart contracts need emergency stop functionality. Inheritance makes this pattern reusable:

contract Pausable {
    bool public paused;

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    function pause() internal virtual {
        paused = true;
    }
}

contract TokenSale is Pausable {
    function buyTokens() public whenNotPaused {
        // Token purchase logic here
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Mistakes and How to Avoid them

1. Forgetting the virtual and override keywords
A common beginner mistake is attempting to override a function without first declaring the parent function as virtual, or failing to use override in the child contract. Solidity will return a compilation error if you miss these terms, which is really helpful because it drives you to be precise about your goals.

Wrong:

contract Parent {
    function getValue() public pure returns (uint) {
        return 10;
    }
}

contract Child is Parent {
    function getValue() public pure returns (uint) {
        return 20;
    }
}
// This will not compile!
Enter fullscreen mode Exit fullscreen mode

Correct:

contract Parent {
    function getValue() public pure virtual returns (uint) {
        return 10;
    }
}

contract Child is Parent {
    function getValue() public pure override returns (uint) {
        return 20;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Not Calling Parent Constructors Properly
When your child contract's constructor needs to initialize the parent contract, you must explicitly call the parent's constructor:

contract Token {
    string public name;

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

// Correct way to call parent constructor
contract CustomToken is Token {
    constructor(string memory _name) Token(_name) {
        // Additional initialization
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Visibility Issues
Remember that private functions and variables in the parent contract are not accessible to child contracts. Use internal if you want inherited contracts to access these members:

contract Parent {
    uint256 private secretValue;    // NOT accessible to children
    uint256 internal sharedValue;   // Accessible to children
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for Using Inheritance

  • Keep It Simple: Avoid complex inheritance hierarchies. If you find yourself inheriting from multiple layers of contracts, think about how your design may be simplified.
  • Use meaningful names: To demonstrate the relationship between your contracts, give them explicit names. Base contracts frequently use names like "Base," "Abstract," or descriptive terms like "Ownable" or "Pausable."
  • Document your code: Include comments that explain what each contract does and why you choose to use inheritance. In the future, you (and other developers) will enjoy it.
  • Test thoroughly: When overriding functions, test both the parent and child implementations to ensure that they work properly together.
  • Consider security: If not handled properly, inheritance can raise security risks. Always audit inherited code and understand what you're getting, especially when using third-party contracts.

Conclusion 

In Solidity, inheritance is an efficient tool that allows you to create smart contracts that are easier to maintain. Understanding how to use parent and child contracts, function overriding, and the virtual and override keywords will allow you to better structure your code to avoid repetition.
Begin with simple examples like those in this article, practice overriding functions, and then progress to more sophisticated inheritance patterns. Remember that the goal is not to develop the most complex inheritance tree possible, but to write code that is simple to comprehend, maintain, and secure.
As you progress through your Solidity journey, inheritance will become a natural part of your development workflow, allowing you to construct stronger smart contracts with less code and fewer errors.

Top comments (0)