Module 6: Inheritance Interfaces

Module 6: Inheritance and Interfaces in Solidity

6.1 Contract Inheritance

Inheritance is a fundamental concept in object-oriented programming that allows a contract to acquire properties and behaviors from another contract. In Solidity, inheritance enables code reuse, logical abstraction, and the implementation of standards.

Basic Inheritance

Solidity supports inheritance using the is keyword:

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

// Base contract
contract Owned {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "New owner cannot be zero address");
        owner = newOwner;
    }
}

// Derived contract
contract MyContract is Owned {
    uint public value;

    function setValue(uint _value) public onlyOwner {
        value = _value;
    }
}

In this example, MyContract inherits from Owned, gaining access to its state variables, functions, and modifiers.

Multiple Inheritance

Solidity supports multiple inheritance, allowing a contract to inherit from multiple parent contracts:

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

contract Owned {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }
}

contract Pausable {
    bool public paused;

    event Paused(address account);
    event Unpaused(address account);

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

    modifier whenPaused() {
        require(paused, "Contract is not paused");
        _;
    }

    function pause() public virtual {
        paused = true;
        emit Paused(msg.sender);
    }

    function unpause() public virtual {
        paused = false;
        emit Unpaused(msg.sender);
    }
}

// Multiple inheritance
contract MyToken is Owned, Pausable {
    string public name;
    mapping(address => uint) public balances;

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

    // Override functions from parent contracts
    function pause() public override onlyOwner {
        super.pause();
    }

    function unpause() public override onlyOwner {
        super.unpause();
    }

    function transfer(address to, uint amount) public whenNotPaused {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

Inheritance Linearization (C3 Linearization)

When a contract inherits from multiple contracts, Solidity uses C3 linearization to determine the order in which base contracts are considered. This is important for resolving function calls and constructor execution.

The order of inheritance is important and should be from "most base-like" to "most derived":

// Correct order
contract MyContract is BaseA, BaseB, BaseC {
    // ...
}

If BaseB inherits from BaseA, the correct order would be:

contract MyContract is BaseA, BaseB {
    // ...
}

Constructor Arguments

When inheriting from contracts with constructors that require arguments, you must provide those arguments:

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

contract Base {
    uint public value;

    constructor(uint _value) {
        value = _value;
    }
}

contract Derived is Base(10) {
    // Passes 10 to the Base constructor
}

contract DerivedWithArg is Base {
    constructor(uint _value) Base(_value) {
        // Passes the argument to the Base constructor
    }
}

Function Overriding

Derived contracts can override functions from base contracts. The virtual keyword indicates that a function can be overridden, and the override keyword indicates that a function overrides a parent function:

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

contract Base {
    function foo() public virtual returns (string memory) {
        return "Base";
    }
}

contract Derived is Base {
    function foo() public override returns (string memory) {
        return "Derived";
    }
}

When overriding a function from multiple base contracts, you must specify all the contracts being overridden:

contract A {
    function foo() public virtual returns (string memory) {
        return "A";
    }
}

contract B {
    function foo() public virtual returns (string memory) {
        return "B";
    }
}

contract C is A, B {
    // Must override both A and B
    function foo() public override(A, B) returns (string memory) {
        return "C";
    }
}

Calling Parent Functions

You can call functions from parent contracts using the super keyword:

contract Derived is Base {
    function foo() public override returns (string memory) {
        // Call the parent implementation
        string memory baseResult = super.foo();
        return string(abi.encodePacked("Derived: ", baseResult));
    }
}

When using super in a contract with multiple inheritance, it calls the function in all parent contracts according to the C3 linearization order.

6.2 Abstract Contracts

Abstract contracts are contracts that contain at least one function without an implementation. They cannot be deployed directly but can be used as base contracts.

Declaring Abstract Contracts

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

abstract contract Animal {
    function makeSound() public virtual returns (string memory);

    function sleep() public pure returns (string memory) {
        return "Zzz";
    }
}

In this example, Animal is an abstract contract because makeSound() doesn't have an implementation.

Implementing Abstract Contracts

To use an abstract contract, you must inherit from it and implement all its abstract functions:

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

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

Use Cases for Abstract Contracts

Abstract contracts are useful for:

  1. Defining a common interface: Ensuring that derived contracts implement specific functions.
  2. Providing partial implementations: Implementing common functionality while leaving specific details to derived contracts.
  3. Creating contract templates: Defining the structure of a contract without implementing all details.

6.3 Interfaces

Interfaces are similar to abstract contracts but have more restrictions. They define a contract's external interface without implementation details.

Declaring Interfaces

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

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

Interface Restrictions

Interfaces have the following restrictions:

  1. Cannot have any function implementations
  2. Cannot have constructor
  3. Cannot have state variables
  4. Cannot have structs or enums (but can use structs and enums defined outside the interface)
  5. All functions must be external

Implementing Interfaces

To implement an interface, a contract must override all the functions defined in the interface:

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

interface ICounter {
    function getCount() external view returns (uint);
    function increment() external;
}

contract Counter is ICounter {
    uint private count;

    function getCount() external view override returns (uint) {
        return count;
    }

    function increment() external override {
        count += 1;
    }
}

Interface vs. Abstract Contract

Feature Interface Abstract Contract
Function Implementation Not allowed Allowed
State Variables Not allowed Allowed
Constructor Not allowed Allowed
Inheritance Can inherit from other interfaces Can inherit from other contracts
Visibility All functions must be external Any visibility allowed
Use Case Defining external contract interfaces Providing partial implementations

Using Interfaces for Contract Interaction

Interfaces are commonly used to interact with other contracts:

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

interface IToken {
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

contract TokenSender {
    function sendToken(address tokenAddress, address recipient, uint256 amount) public {
        IToken token = IToken(tokenAddress);
        require(token.transfer(recipient, amount), "Transfer failed");
    }

    function getBalance(address tokenAddress, address account) public view returns (uint256) {
        IToken token = IToken(tokenAddress);
        return token.balanceOf(account);
    }
}

6.4 Standard Interfaces (ERC Standards)

Ethereum Request for Comments (ERC) standards define common interfaces for tokens and other contracts. Implementing these standards ensures compatibility with wallets, exchanges, and other applications.

ERC-20 Token Standard

The ERC-20 standard defines an interface for fungible tokens:

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

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

ERC-721 Non-Fungible Token Standard

The ERC-721 standard defines an interface for non-fungible tokens (NFTs):

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

interface IERC721 {
    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function setApprovalForAll(address operator, bool _approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}

interface IERC721Metadata {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

Other Common Standards

6.5 Inheritance Patterns and Best Practices

The "Is-A" Relationship

Use inheritance when there's an "is-a" relationship between contracts:

// A token is a type of ERC20
contract MyToken is IERC20 {
    // Implementation
}

// A staking contract is a type of Ownable contract
contract StakingContract is Ownable {
    // Implementation
}

Composition vs. Inheritance

Sometimes composition (using another contract as a state variable) is better than inheritance:

// Inheritance
contract MyTokenWithInheritance is ERC20 {
    // Implementation
}

// Composition
contract MyTokenWithComposition {
    ERC20 private token;

    constructor(address tokenAddress) {
        token = ERC20(tokenAddress);
    }

    // Implementation that uses token
}

Use inheritance when: - You need to override behavior - The derived contract is a specialized version of the base contract

Use composition when: - You only need to use the functionality of another contract - You want to keep contracts smaller and more focused

Diamond Pattern for Complex Inheritance

For complex inheritance hierarchies, consider using the Diamond pattern:

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

contract DiamondStorage {
    bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.storage");

    struct DiamondData {
        mapping(bytes4 => address) facets;
    }

    function diamondStorage() internal pure returns (DiamondData storage ds) {
        bytes32 position = DIAMOND_STORAGE_POSITION;
        assembly {
            ds.slot := position
        }
    }
}

contract Diamond is DiamondStorage {
    event FacetAdded(address facet, bytes4[] functionSelectors);

    constructor() {
        // Initialize
    }

    function addFacet(address _facet, bytes4[] memory _functionSelectors) public {
        DiamondData storage ds = diamondStorage();
        for (uint i = 0; i < _functionSelectors.length; i++) {
            ds.facets[_functionSelectors[i]] = _facet;
        }
        emit FacetAdded(_facet, _functionSelectors);
    }

    fallback() external payable {
        DiamondData storage ds = diamondStorage();
        address facet = ds.facets[msg.sig];
        require(facet != address(0), "Function does not exist");

        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Inheritance Best Practices

  1. Keep inheritance hierarchies shallow: Deep inheritance hierarchies can be difficult to understand and maintain.
  2. Use interfaces for contract interactions: Define clear interfaces for contract interactions.
  3. Be careful with multiple inheritance: Understand the linearization order and potential conflicts.
  4. Use modifiers for common checks: Extract common checks into modifiers for reuse.
  5. Document inheritance relationships: Clearly document the purpose and relationships of contracts.
  6. Test thoroughly: Inheritance can introduce subtle bugs, so test all inherited functionality.

6.6 Practical Example: Token with Extensions

Let's create a token contract that uses inheritance to add extensions:

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

// Base token interface
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

// Base token implementation
abstract contract ERC20 is IERC20 {
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    uint256 private _totalSupply;
    string private _name;
    string private _symbol;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function name() public view virtual returns (string memory) {
        return _name;
    }

    function symbol() public view virtual returns (string memory) {
        return _symbol;
    }

    function decimals() public view virtual returns (uint8) {
        return 18;
    }

    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        _transfer(msg.sender, recipient, amount);
        return true;
    }

    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
        _transfer(sender, recipient, amount);

        uint256 currentAllowance = _allowances[sender][msg.sender];
        require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
        unchecked {
            _approve(sender, msg.sender, currentAllowance - amount);
        }

        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

        uint256 senderBalance = _balances[sender];
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[sender] = senderBalance - amount;
        }
        _balances[recipient] += amount;

        emit Transfer(sender, recipient, amount);
    }

    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _totalSupply += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);
    }

    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
        }
        _totalSupply -= amount;

        emit Transfer(account, address(0), amount);
    }

    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }
}

// Ownable extension
abstract contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        _transferOwnership(msg.sender);
    }

    function owner() public view virtual returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(owner() == msg.sender, "Ownable: caller is not the owner");
        _;
    }

    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// Pausable extension
abstract contract Pausable {
    bool private _paused;

    event Paused(address account);
    event Unpaused(address account);

    constructor() {
        _paused = false;
    }

    function paused() public view virtual returns (bool) {
        return _paused;
    }

    modifier whenNotPaused() {
        require(!paused(), "Pausable: paused");
        _;
    }

    modifier whenPaused() {
        require(paused(), "Pausable: not paused");
        _;
    }

    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(msg.sender);
    }

    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(msg.sender);
    }
}

// Burnable extension
abstract contract ERC20Burnable is ERC20 {
    function burn(uint256 amount) public virtual {
        _burn(msg.sender, amount);
    }

    function burnFrom(address account, uint256 amount) public virtual {
        uint256 currentAllowance = allowance(account, msg.sender);
        require(currentAllowance >= amount, "ERC20: burn amount exceeds allowance");
        unchecked {
            _approve(account, msg.sender, currentAllowance - amount);
        }
        _burn(account, amount);
    }
}

// Mintable extension
abstract contract ERC20Mintable is ERC20, Ownable {
    function mint(address to, uint256 amount) public virtual onlyOwner {
        _mint(to, amount);
    }
}

// Final token with all extensions
contract MyExtendableToken is ERC20, ERC20Burnable, ERC20Mintable, Pausable {
    constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual whenNotPaused {
        super._beforeTokenTransfer(from, to, amount);
    }

    function _transfer(address sender, address recipient, uint256 amount) internal override whenNotPaused {
        super._transfer(sender, recipient, amount);
    }
}

This example demonstrates how to use inheritance to create a token with multiple extensions:

  1. ERC20: Base token implementation
  2. Ownable: Adds ownership functionality
  3. Pausable: Adds the ability to pause transfers
  4. ERC20Burnable: Adds the ability to burn tokens
  5. ERC20Mintable: Adds the ability to mint new tokens

The final MyExtendableToken contract inherits from all these contracts to create a feature-rich token.

6.7 Exercise: Creating a Modular NFT Contract

Task: Create a modular NFT contract using inheritance and interfaces.

Solution:

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

// ERC-165 Interface
interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// ERC-721 Interface
interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function setApprovalForAll(address operator, bool _approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
}

// ERC-721 Metadata Interface
interface IERC721Metadata is IERC721 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

// ERC-721 Receiver Interface
interface IERC721Receiver {
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4);
}

// Base ERC-721 Implementation
abstract contract ERC721 is IERC721, IERC721Metadata {
    string private _name;
    string private _symbol;

    // Token ID to owner address mapping
    mapping(uint256 => address) private _owners;

    // Owner address to token count mapping
    mapping(address => uint256) private _balances;

    // Token ID to approved address mapping
    mapping(uint256 => address) private _tokenApprovals;

    // Owner to operator approvals mapping
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }

    function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return _balances[owner];
    }

    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "ERC721: owner query for nonexistent token");
        return owner;
    }

    function name() public view virtual override returns (string memory) {
        return _name;
    }

    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        return "";
    }

    function approve(address to, uint256 tokenId) public virtual override {
        address owner = ownerOf(tokenId);
        require(to != owner, "ERC721: approval to current owner");

        require(
            msg.sender == owner || isApprovedForAll(owner, msg.sender),
            "ERC721: approve caller is not owner nor approved for all"
        );

        _approve(to, tokenId);
    }

    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        require(_exists(tokenId), "ERC721: approved query for nonexistent token");
        return _tokenApprovals[tokenId];
    }

    function setApprovalForAll(address operator, bool approved) public virtual override {
        require(operator != msg.sender, "ERC721: approve to caller");
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    function transferFrom(address from, address to, uint256 tokenId) public virtual override {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");
        _transfer(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
        safeTransferFrom(from, to, tokenId, "");
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");
        _safeTransfer(from, to, tokenId, _data);
    }

    function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual {
        _transfer(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _owners[tokenId] != address(0);
    }

    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
        address owner = ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }

    function _safeMint(address to, uint256 tokenId) internal virtual {
        _safeMint(to, tokenId, "");
    }

    function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual {
        _mint(to, tokenId);
        require(
            _checkOnERC721Received(address(0), to, tokenId, _data),
            "ERC721: transfer to non ERC721Receiver implementer"
        );
    }

    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    function _burn(uint256 tokenId) internal virtual {
        address owner = ownerOf(tokenId);

        // Clear approvals
        _approve(address(0), tokenId);

        _balances[owner] -= 1;
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);
    }

    function _transfer(address from, address to, uint256 tokenId) internal virtual {
        require(ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
        require(to != address(0), "ERC721: transfer to the zero address");

        // Clear approvals
        _approve(address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    function _approve(address to, uint256 tokenId) internal virtual {
        _tokenApprovals[tokenId] = to;
        emit Approval(ownerOf(tokenId), to, tokenId);
    }

    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) {
        if (to.code.length > 0) {
            try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data) returns (bytes4 retval) {
                return retval == IERC721Receiver.onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert("ERC721: transfer to non ERC721Receiver implementer");
                } else {
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        } else {
            return true;
        }
    }
}

// Ownable extension
abstract contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        _transferOwnership(msg.sender);
    }

    function owner() public view virtual returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(owner() == msg.sender, "Ownable: caller is not the owner");
        _;
    }

    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// URI Storage extension
abstract contract ERC721URIStorage is ERC721 {
    mapping(uint256 => string) private _tokenURIs;

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token");

        string memory _tokenURI = _tokenURIs[tokenId];

        if (bytes(_tokenURI).length > 0) {
            return _tokenURI;
        }

        return super.tokenURI(tokenId);
    }

    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
        require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token");
        _tokenURIs[tokenId] = _tokenURI;
    }

    function _burn(uint256 tokenId) internal virtual override {
        super._burn(tokenId);

        if (bytes(_tokenURIs[tokenId]).length != 0) {
            delete _tokenURIs[tokenId];
        }
    }
}

// Enumerable extension
abstract contract ERC721Enumerable is ERC721 {
    // Array of all token IDs
    uint256[] private _allTokens;

    // Mapping from token ID to position in _allTokens array
    mapping(uint256 => uint256) private _allTokensIndex;

    // Mapping from owner to list of owned token IDs
    mapping(address => mapping(uint256 => uint256)) private _ownedTokens;

    // Mapping from token ID to index of the owner tokens list
    mapping(uint256 => uint256) private _ownedTokensIndex;

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

    function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256) {
        require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
        return _ownedTokens[owner][index];
    }

    function totalSupply() public view virtual returns (uint256) {
        return _allTokens.length;
    }

    function tokenByIndex(uint256 index) public view virtual returns (uint256) {
        require(index < totalSupply(), "ERC721Enumerable: global index out of bounds");
        return _allTokens[index];
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual {
        if (from == address(0)) {
            _addTokenToAllTokensEnumeration(tokenId);
        } else if (from != to) {
            _removeTokenFromOwnerEnumeration(from, tokenId);
        }
        if (to == address(0)) {
            _removeTokenFromAllTokensEnumeration(tokenId);
        } else if (to != from) {
            _addTokenToOwnerEnumeration(to, tokenId);
        }
    }

    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
        uint256 lastTokenIndex = balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId;
            _ownedTokensIndex[lastTokenId] = tokenIndex;
        }

        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId;
        _allTokensIndex[lastTokenId] = tokenIndex;

        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}

// Final NFT contract with all extensions
contract ModularNFT is ERC721, ERC721URIStorage, ERC721Enumerable, Ownable {
    uint256 private _nextTokenId;
    string private _baseTokenURI;

    constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) {
        _baseTokenURI = baseTokenURI;
    }

    function _baseURI() internal view virtual returns (string memory) {
        return _baseTokenURI;
    }

    function setBaseURI(string memory baseTokenURI) public onlyOwner {
        _baseTokenURI = baseTokenURI;
    }

    function mint(address to) public onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        return tokenId;
    }

    function mintWithURI(address to, string memory uri) public onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        return tokenId;
    }

    function burn(uint256 tokenId) public {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: caller is not owner nor approved");
        _burn(tokenId);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }
}

This modular NFT contract demonstrates how to use inheritance to create a feature-rich NFT with multiple extensions:

  1. ERC721: Base NFT implementation
  2. ERC721URIStorage: Adds storage for token URIs
  3. ERC721Enumerable: Adds enumeration capabilities
  4. Ownable: Adds ownership functionality

The final ModularNFT contract inherits from all these contracts to create a complete NFT implementation with various features.

Conclusion

Inheritance and interfaces are powerful features in Solidity that enable code reuse, modularity, and standardization. By understanding these concepts, you can create more maintainable and extensible smart contracts.

Key takeaways from this module:

  1. Inheritance allows contracts to inherit properties and behaviors from other contracts.
  2. Multiple inheritance is supported, with C3 linearization determining the order of resolution.
  3. Abstract contracts provide partial implementations, while interfaces define external contract interfaces.
  4. Standard interfaces like ERC-20 and ERC-721 ensure compatibility with existing systems.
  5. Proper use of inheritance patterns can lead to more modular and maintainable code.

In the next module, we'll explore function visibility and modifiers in Solidity, which are essential for controlling access to contract functions and implementing reusable code patterns.