What is a Multi-Signature Wallet?

A Multi-Signature Wallet (MultiSig Wallet) is a type of cryptocurrency wallet that requires multiple signatures (approvals) from a predefined set of owners to execute a transaction. Unlike a Single-Signature Wallet, where one owner has full control over the funds (can send or withdraw at will), a MultiSig Wallet distributes control among multiple owners to enhance security and trust.

Key Features of a MultiSig Wallet:

  1. Multiple Owners: The wallet has multiple owners, each with their own private key.
  2. Minimum Confirmations: A transaction requires approval (confirmation) from a minimum number of owners before it can be executed. For example, if there are 3 owners, you might require at least 2 to approve a transaction.
  3. Transaction Submission and Approval: Any owner can submit a transaction, but it won't execute until the required number of owners confirm it.
  4. Enhanced Security: This setup ensures that no single owner can misuse the funds, making it ideal for organizations, shared accounts, or high-security scenarios.

How It Differs from a Single-Signature Wallet:


How a MultiSig Wallet Works (Conceptually)

Imagine a contract with 3 owners (Owner 1, Owner 2, Owner 3) and a rule that at least 2 owners must approve any transaction. Here's how it works:

  1. Transaction Submission:
  2. Confirmation Process:
  3. Execution:
  4. Security Checks:

This process ensures that no single owner can act alone, adding a layer of security and consensus.

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

contract MultiSig {
    // Required number of confirmations to execute a transaction
    uint256 public numOfConfirmations;

    // Structure to store transaction details
    struct Transaction {
        uint256 value;           // Amount of ETH to send
        uint256 confirmations;   // Number of confirmations received (can be removed for optimization)
        address to;              // Recipient address
        bool executed;           // Execution status
    }

    // List of wallet owners
    address[] internal owners;
    
    // List of all submitted transactions
    Transaction[] internal transactions;

    // Mapping to check if an address is an owner
    mapping(address => bool) internal isOwner;

    // Mapping to check if a specific owner confirmed a specific transaction
    mapping(uint256 => mapping(address => bool)) internal isConfirmed;

    // Constructor to initialize owners and number of required confirmations
    constructor(address[] memory _owners, uint256 _numOfConfirmations) {
        require(_owners.length > 0, "Owners required");
        require(_numOfConfirmations > 0, "Confirmations required");

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];

            require(owner != address(0), "Invalid owner");
            require(!isOwner[owner], "Duplicate owner");

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

        require(_numOfConfirmations <= owners.length, "Too many confirmations");
        numOfConfirmations = _numOfConfirmations;
    }

    // Events for tracking contract activity
    event SubmitTransaction(uint256 indexed txIndex, address indexed owner, address indexed to, uint256 value);
    event ConfirmTransaction(uint256 indexed txIndex, address indexed owner);
    event ExecuteTransaction(uint256 indexed txIndex, bool success);
    event RevokeTransaction(uint256 indexed txIndex, uint256 newConfirmations, address indexed owner);

    // Modifier to restrict function access to wallet owners only
    modifier onlyOwner() {
        require(isOwner[msg.sender], "Not an owner");
        _;
    }

    // Modifier to ensure the transaction exists
    modifier txExists(uint256 _txIndex) {
        require(_txIndex < transactions.length, "Transaction does not exist");
        _;
    }

    // Submit a new transaction with ETH to be sent
    function submit(address _to) public payable onlyOwner {
        require(_to != address(0), "Invalid recipient");
        require(msg.value > 1, "Insufficient ETH");

        uint256 txIndex = transactions.length;

        transactions.push(
            Transaction({
                value: msg.value,
                confirmations: 0,
                to: _to,
                executed: false
            })
        );

        emit SubmitTransaction(txIndex, msg.sender, _to, msg.value);
    }

    // Confirm a pending transaction
    function confirm(uint256 txIndex) public onlyOwner txExists(txIndex) {
        require(!isConfirmed[txIndex][msg.sender], "Already confirmed");

        Transaction storage transaction = transactions[txIndex];

        transaction.confirmations += 1;
        isConfirmed[txIndex][msg.sender] = true;

        emit ConfirmTransaction(txIndex, msg.sender);

        // Execute if required number of confirmations met
        if (IsConfirmation(txIndex)) {
            execute(txIndex);
        }
    }

    // Revoke a previously submitted confirmation
    function revokeConfirmation(uint256 txIndex) public onlyOwner txExists(txIndex) {
        require(isConfirmed[txIndex][msg.sender], "Not yet confirmed");

        Transaction storage transaction = transactions[txIndex];

        transaction.confirmations -= 1;
        isConfirmed[txIndex][msg.sender] = false;

        emit RevokeTransaction(txIndex, transaction.confirmations, msg.sender);
    }

    // Internal function to check if transaction is fully confirmed
    function IsConfirmation(uint256 txIndex)
        internal
        view
        txExists(txIndex)
        returns (bool)
    {
        uint256 count = 0;

        for (uint256 i = 0; i < owners.length; i++) {
            if (isConfirmed[txIndex][owners[i]]) {
                count += 1;
            }
        }

        return count >= numOfConfirmations;
    }

    // Internal function to execute a transaction
    function execute(uint256 txIndex) internal txExists(txIndex) {
        Transaction storage transaction = transactions[txIndex];

        require(!transaction.executed, "Already executed");

        (bool success, ) = transaction.to.call{value: transaction.value}("");
        if (success) {
            transaction.executed = true;
            emit ExecuteTransaction(txIndex, true);
        } else {
            revert("Transaction failed");
        }
    }

    // Public view function to get confirmation count of a transaction
    function getConfirmations(uint256 txIndex) public view returns (uint256 count) {
        for (uint256 i = 0; i < owners.length; i++) {
            if (isConfirmed[txIndex][owners[i]]) {
                count += 1;
            }
        }
    }
}