All articles
Web3 FoundationsSeptember 16, 20269 min read

Solidity Inheritance: is, virtual, override, abstract, super

What the word is does to a contract, how to trace an inherited function back to the file that defines it, and why virtual, override, abstract and super exist.

By Carlos (Bloqarl)

TL;DR

  • is makes everything another contract declares part of this one. After that word, the file you are reading is no longer the whole contract.
  • Reading inherited code is a procedure, not a talent: unknown name, then the contract line, then the matching import line, then that file.
  • abstract means this contract cannot be deployed on its own. A library of building blocks is full of them, and that is the point.
  • virtual marks a function a child is allowed to replace. override marks the replacement. Neither happens by accident.
  • super calls the version further up the chain, which is why an overriding function can add an answer instead of discarding the one below it.

What does inheritance mean in Solidity?

Inheritance means one contract takes on everything another contract declares, as though it had been written in the same file. The keyword is is, it appears on the contract line before the opening brace, and after it the deployed machine is the sum of every contract in the chain, not the file in front of you.

Here is OpenZeppelin's ERC20Burnable, thirty eight lines in total, of which this is the contract:

abstract contract ERC20Burnable is ERC20 {
    /**
     * @dev Destroys a `value` amount of tokens from the caller.
     *
     * See {ERC20-_burn}.
     */
    function burn(uint256 value) public virtual {
        _burn(_msgSender(), value);
    }

One function, one line of body. And the second one is barely longer:

    function burnFrom(address account, uint256 value) public virtual {
        _spendAllowance(account, _msgSender(), value);
        _burn(account, value);
    }
}

Two functions, three lines of body between them, and you cannot say what either one does without opening a different file. _burn, _msgSender and _spendAllowance are not declared anywhere in this file. That is not a complaint about the code. That is the lesson: this file is a small amount of new behaviour bolted onto a much larger amount that lives elsewhere, and is ERC20 is the bolt.

How do you find where an inherited function is defined?

Three steps, in order. Take the name you do not recognise, read the contract line to see what this contract inherits, then read the import lines to find which file that parent lives in, then open that file and search for the name. Repeat if it is not there.

Run it on _burn. The contract line says is ERC20, so the parent is called ERC20. The import tells you where it is:

import {ERC20} from "../ERC20.sol";

One directory up, a file called ERC20.sol. Open it, search for _burn, and it is there:

    function _burn(address account, uint256 value) internal {
        if (account == address(0)) {
            revert ERC20InvalidSender(address(0));
        }
        _update(account, address(0), value);
    }

One hop. Now run the same procedure on _msgSender, and it is not in ERC20.sol at all. So you repeat: ERC20's own contract line names its parents, one of which is Context, and Context.sol has it:

abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

Two hops. Same three steps, run twice. The only difference between a one-hop name and a two-hop name is how many times you repeat the loop. This is the part of how to read a smart contract that people skip, and skipping it is why real code feels unreadable when a 24-line example did not.

What does abstract mean in Solidity?

abstract on a contract means it cannot be deployed by itself. It is a building block, meant to be inherited from, and the compiler refuses to let anyone deploy it directly.

Look at the trail we just walked. ERC20Burnable is abstract. ERC20 is abstract. Context is abstract. Three files in a row, none of them a thing you can put on chain.

A contract is abstract for one of two reasons. Either it is declared abstract explicitly, or it inherits a function it never implements, which forces the same status. ERC20Burnable is the honest case: burning tokens is a feature, not a token, so shipping it as a deployable contract would make no sense.

For a reader, abstract is a useful signal on the very first line. It tells you this file is one layer of something, and that somewhere in the project a non-abstract contract stacks these layers and is the thing actually deployed. Chasing it down is often the fastest way to understand a codebase, because it is the only file whose parent list is complete.

What do virtual and override mean in Solidity?

virtual on a function means a child contract is allowed to replace it. override on a function means this one is the replacement. Both words are required, on both sides, and without them the compiler rejects the attempt.

ERC165 documents the pattern in its own header comment, which is a rare case of a file explaining exactly how it expects to be extended:

 * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);

And here is the function that example is overriding, in the real contract below the comment:

abstract contract ERC165 is IERC165 {
    /// @inheritdoc IERC165
    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
        return interfaceId == type(IERC165).interfaceId;
    }
}

The parent's version is virtual, so replacing it is permitted. The child's version carries both virtual and override: override because it is replacing the parent, virtual because it is in turn leaving the door open for its own children. That pairing is worth recognising on sight, because it tells you the function you are reading sits in the middle of a chain rather than at the end of one.

What does super do in Solidity?

super calls the version of the function that sits further up the inheritance chain, the one you just overrode. It is how a replacement can add to the parent's answer instead of throwing it away.

Look again at the single line in the ERC165 example:

 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);

Read it as a question and an escalation. First, is the caller asking about my own interface? If yes, return true and stop. If not, hand the same question to the contract above me and return whatever it says.

That || is doing something specific. Without super, an override is a replacement: the parent's logic is simply gone, and a contract that overrode supportsInterface would stop reporting support for the interfaces its parents implement. With super, each layer answers for itself and defers the rest, so the answers accumulate down the chain instead of the bottom layer winning outright.

The practical reason to care as a reader: when you see super, the function in front of you is not the whole answer. It is one contribution to an answer assembled across several files, and you have to walk up to know the rest.

How do you read a contract line with four parents?

Inheritance lists get long. Here is ERC20 itself, which is what ERC20Burnable sat on top of:

abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {

Four names on one line, and they are not four of the same thing. The useful split is between parents that carry behaviour and parents that only carry declarations.

Context is behaviour. It is a real contract with real function bodies, and it is where _msgSender came from. The other three are interfaces, which the leading I conventionally signals and opening the file confirms. An interface declares function signatures and nothing else: no bodies, no state. Inheriting one adds no code at all. It is a promise that this contract implements those functions, plus, in the case of IERC20Errors, a set of named errors it can revert with.

So read the list, then triage it. Interfaces tell you what shape the contract claims to be. Non-interface parents tell you where its actual behaviour is hiding, and those are the files worth opening. A four-parent line is usually one file to read and three to note.

Delete the line and ask what breaks

  • Delete is ERC20 from ERC20Burnable and the file stops compiling immediately, because _burn, _msgSender and _spendAllowance all vanish at once. Three quarters of a three-line function was coming from that one word.
  • Delete abstract from ERC20Burnable and it still will not deploy, because it inherits functions it never implements. The keyword was describing a fact, not creating one.
  • Delete virtual from the parent's supportsInterface and no child can ever override it. The extension pattern the file documents in its own comment becomes impossible.
  • Delete super.supportsInterface(interfaceId) from an override, leaving only the child's own check, and the contract stops reporting every interface its parents support. Nothing errors. It just quietly answers false to questions it used to answer true.

Related questions

What is the difference between import and is in Solidity? import makes another file's definitions visible in this one. is makes this contract actually take on another contract's state and functions. You almost always need the import in order to write the is, but an import on its own inherits nothing.

Can a Solidity contract inherit from more than one contract? Yes. List the parents comma-separated after is. Order matters, because it determines which version of a shared function wins and what super resolves to, so parents are conventionally written from the most general to the most derived.

Do you need both virtual and override? Yes, on opposite sides. The parent's function must be virtual for overriding to be allowed, and the child's function must say override. A child that is itself meant to be extendable carries both words at once.

Is an abstract contract deployable? No. The compiler refuses to deploy it. A contract is abstract if it is declared so with the keyword, or if it inherits any function it does not implement. Somewhere above it in the project there is a concrete contract that stacks the layers and is the deployable one.

What happens if you override without calling super? The parent's version of that function no longer runs at all. That is sometimes exactly what you want, and sometimes a silent loss of behaviour, which is why super shows up so often in functions that report or accumulate rather than decide.

Can a child contract access its parent's private functions? No. private is not inherited. internal is the visibility that means "this contract and everything that inherits from it", which is why shared helper code in library-style contracts is almost always internal. See Solidity visibility.

Where to go next

Inheritance is the reason a forty line file can describe a four hundred line contract, and the reason a reader who only ever opens one file at a time will be wrong about what they are reading. The fix is not cleverness. It is the three step loop: unknown name, contract line, import line, next file. Run it until the name turns up.

When you open that next file you are back at the top of a .sol document, and the landmarks are the same every time: what is actually in a .sol file covers the paperwork above the contract, and state variables in Solidity covers the lines directly under the opening brace, which is where reading any inherited parent should start.

Tagged

SoliditySmart ContractsLearn to Code