solidity logo

Question: What Is Inheritance in Solidity and How Is It Applied in Smart Contracts? Provide an Example of Extending a MultiSigWallet Contract.

In Solidity, inheritance allows one contract to reuse the code of another contract. This promotes modularity, reusability, and readability in smart contract development. Solidity supports multiple inheritance, meaning a contract can inherit from more than one parent contract. Child contracts can override parent functions or call parent functions to extend their logic.

Inheritance is commonly used in role separation, access control, and wallet implementations such as MultiSigWallets.

Key Concepts of Inheritance in Solidity

  1. Single Inheritance
    A contract inherits from a single parent using the is keyword. contract B is A { ... }
  2. Multiple Inheritance
    Solidity supports multiple inheritance, e.g.: contract C is A, B { ... }
  3. Virtual and Override
    Functions in a parent contract can be marked as virtual, and child contracts use override to replace or extend them.
  4. Constructors in Inheritance
    When a contract inherits from others, the constructors of all parent contracts must be properly initialized, often via parameters passed in the child contract’s constructor.

Application of Inheritance in Smart Contracts

Inheritance is widely used in Ethereum smart contract development. For example:

  • Access Control: A base contract can define ownership and permissions, while derived contracts implement application-specific logic.
  • MultiSig Wallets: A base wallet can implement signing and execution logic, while derived contracts can extend functionality, such as adding time locks or transaction limits.

Example: Extending a MultiSigWallet with Time Lock

Below is an example of a base MultiSigWallet contract and a TimeLockedMultiSigWallet that inherits and extends it.

Part 1: MultiSigWallet Implementation

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

/// @title MultiSigWallet
/// @notice A simple multisignature wallet that requires multiple owners to confirm before executing a transaction
contract MultiSigWallet {
    // ================================
    // State variables
    // ================================

    address[] public owners; // List of wallet owners
    mapping(address => bool) public isOwner; // Quick lookup to check if an address is an owner
    uint public required; // Number of confirmations required before executing a transaction

    struct Transaction {
        address destination; // Address to send ETH or call contract
        uint value; // ETH value to send
        bytes data; // Encoded function call or empty for ETH transfer
        bool executed; // Whether the transaction has already been executed
    }

    Transaction[] public transactions; // Array storing all submitted transactions
    mapping(uint => mapping(address => bool)) public confirmations; 
    // confirmations[txIndex][owner] = true if owner confirmed the transaction

    // ================================
    // Events
    // ================================

    event Deposit(address indexed sender, uint amount);
    event Submit(uint indexed txIndex, address indexed destination, uint value, bytes data);
    event Confirm(address indexed owner, uint indexed txIndex);
    event Execute(uint indexed txIndex);

    // ================================
    // Modifiers
    // ================================

    modifier onlyOwner() {
        require(isOwner[msg.sender], "Not wallet owner");
        _;
    }

    modifier txExists(uint _txIndex) {
        require(_txIndex < transactions.length, "Transaction does not exist");
        _;
    }

    modifier notConfirmed(uint _txIndex) {
        require(!confirmations[_txIndex][msg.sender], "Already confirmed");
        _;
    }

    modifier notExecuted(uint _txIndex) {
        require(!transactions[_txIndex].executed, "Already executed");
        _;
    }

    // ================================
    // Constructor
    // ================================

    constructor(address[] memory _owners, uint _required) {
        require(_owners.length > 0, "Owners required");
        require(
            _required > 0 && _required <= _owners.length,
            "Invalid required number of owners"
        );

        for (uint i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "Invalid owner address");
            require(!isOwner[owner], "Owner not unique");

            isOwner[owner] = true;
            owners.push(owner);
        }

        required = _required;
    }

    // ================================
    // Fallback to receive ETH
    // ================================

    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    // ================================
    // Core wallet functions
    // ================================

    /// @notice Submit a transaction proposal
    function submitTransaction(address _destination, uint _value, bytes memory _data)
        public
        onlyOwner
    {
        uint txIndex = transactions.length;

        transactions.push(
            Transaction({
                destination: _destination,
                value: _value,
                data: _data,
                executed: false
            })
        );

        emit Submit(txIndex, _destination, _value, _data);
    }

    /// @notice Confirm a transaction
    function confirmTransaction(uint _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notConfirmed(_txIndex)
    {
        confirmations[_txIndex][msg.sender] = true;
        emit Confirm(msg.sender, _txIndex);
    }

    /// @notice Check if a transaction has enough confirmations
    function isConfirmed(uint _txIndex) public view returns (bool) {
        uint count = 0;
        for (uint i = 0; i < owners.length; i++) {
            if (confirmations[_txIndex][owners[i]]) {
                count += 1;
            }
            if (count >= required) {
                return true;
            }
        }
        return false;
    }

    /// @notice Execute a confirmed transaction
    function executeTransaction(uint _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notExecuted(_txIndex)
    {
        require(isConfirmed(_txIndex), "Not enough confirmations");

        Transaction storage txn = transactions[_txIndex];

        txn.executed = true;
        (bool success, ) = txn.destination.call{value: txn.value}(txn.data);
        require(success, "Transaction failed");

        emit Execute(_txIndex);
    }
}

With this contract, multiple owners must confirm a transaction before it can be executed.

Part 2: Adding Timelock with OpenZeppelin

Instead of writing our own timelock, we can use OpenZeppelin’s battle-tested implementation:

TimelockController

This contract enforces a minimum delay between when a transaction is scheduled and when it can be executed.

Here’s how you can integrate it with the MultiSigWallet:

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

import "@openzeppelin/contracts/governance/TimelockController.sol";

/// @title MultiSigWalletWithTimelock
/// @notice Extends MultiSigWallet with OpenZeppelin TimelockController for delayed execution
contract MultiSigWalletWithTimelock is MultiSigWallet {
TimelockController public timelock;

constructor(
address[] memory _owners,
uint _required,
uint minDelay, // Minimum delay for timelock
address[] memory proposers, // Who can propose timelocked operations
address[] memory executors // Who can execute timelocked operations
) MultiSigWallet(_owners, _required) {
timelock = new TimelockController(minDelay, proposers, executors, msg.sender);
}

/// @notice Override executeTransaction to route through timelock
function executeTransaction(uint _txIndex)
public
override
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
{
require(isConfirmed(_txIndex), "Not enough confirmations");

Transaction storage txn = transactions[_txIndex];
txn.executed = true;

// Encode the call for timelock
bytes32 id = timelock.hashOperation(
txn.destination,
txn.value,
txn.data,
bytes32(0),
bytes32(0)
);

// Schedule the transaction via timelock
timelock.schedule(
txn.destination,
txn.value,
txn.data,
bytes32(0),
bytes32(0),
timelock.getMinDelay()
);

// Execute through timelock after delay
timelock.execute(
txn.destination,
txn.value,
txn.data,
bytes32(0),
bytes32(0)
);

emit Execute(_txIndex);
}
}

How It Works

  1. Owners confirm a transaction in the MultiSigWallet.
  2. Once enough confirmations are collected, instead of executing immediately, the transaction is scheduled in the TimelockController.
  3. The timelock enforces a delay period.
  4. After the delay passes, the transaction can be executed safely.

This ensures no sudden malicious execution and gives the community time to react.

With this setup, you now have a multi-signature governance model with enforced execution delay, which is the foundation of modern DAO treasury management.

Subscribe for New Articles!

Leave a Comment

Your email address will not be published. Required fields are marked *