Developing Smart Contracts IV On Base: A Multi-sig Wallet.

prerequisites:-

  • What is an Multisig-Wallet?

  • Usecases of an Multisig-Wallet.

  • Building a Multisig Wallet contract.

  • Deployment on Base.

What is a Multisig Wallet?

It is a type of cryptocurrency wallet that requires multiple private keys (signatures) to authorize a transaction, rather than relying on a single key. The setup enhances security by ensuring that no single person or entity has unilateral control over the funds. Multisig wallets are particularly useful in scenarios where multiple stakeholders or parties are involved, and consensus is required for key decisions, such as transferring funds.

The structure of a multisig wallet can vary depending on how many signatures are needed to authorize transactions. For better understaing, i am going to sight few examples below:

  • 2-of-3 multisig: Two out of three assigned signatories must sign off on a transaction for it to be executed.

  • 3-of-5 multisig: Three out of five signatories must approve the transaction for it to proceed.

Use Cases of Multi-sig Wallets

1. Enhanced Security for Personal Funds

These specific wallets add an extra layer of security for individuals who want to safeguard their cryptocurrency. For example, a person can create a 2-of-3 multisig wallet and distribute the keys across different devices. In the event that one device is compromised or lost, the attacker would still need access to at least one more device to authorize a transaction. This prevents unauthorized access and reduces the risk of loss from a single point of failure.

2. Corporate Treasury Management

They are commonly used by companies to manage corporate funds. A company can set up a wallet where multiple executives or board members are required to authorize any significant transaction. This ensures that no single individual can unilaterally move or misuse company funds, thereby increasing accountability and minimizing the risk of fraud or theft. For example, a company could implement a 3-of-5 multisig wallet requiring approvals from three out of five executives for any transaction above a certain threshold.

3. Decentralized Autonomous Organizations (DAOs)

DAOs, which are organizations governed by smart contracts and token holders, often use multisig wallets to manage their funds. In a DAO, key decisions regarding treasury management typically require the consent of multiple stakeholders or representatives. A multisig wallet ensures that no single party can control the funds, promoting decentralized governance and preventing malicious or reckless decisions.

For instance, a DAO could set up a 4-of-7 multisig wallet where four votes are required from seven elected representatives to approve funding proposals or large transactions. This protects the organization's assets and ensures collective decision-making.

4. Joint Business Ventures

In partnerships or joint ventures where two or more entities share control of a pool of funds, a multisig wallet helps manage those funds collaboratively. In this case, the parties involved can agree on a certain number of signatures needed to release funds, ensuring that both (or all) parties have oversight and approval over financial decisions. This structure fosters trust and accountability in joint ventures.

For example, in a 2-of-2 multisig wallet, both partners would need to agree and sign off before any funds can be moved. This ensures that no funds can be spent without mutual consent.

5. Estate Planning and Inheritance

Multisig wallets can be used in estate planning, ensuring that cryptocurrency assets are passed on to the intended beneficiaries. An individual can create a multisig wallet with trusted family members or legal representatives as co-signers. This setup guarantees that upon death, the heirs or beneficiaries can access the assets according to the decedent’s wishes, while still requiring multiple approvals to ensure the transfer is legitimate and follows the estate plan.

For instance, a 2-of-3 multisig wallet can be set up with the individual, a legal representative, and a family member. Upon death, the legal representative and family member can approve the transfer of assets to the rightful heirs.

BUILDING A MULTI-SIG WALLET.

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

contract MultiSigWallet {
    event Deposit(address indexed sender, uint256 amount, uint256 balance);
    event SubmitTransaction(
        address indexed owner,
        uint256 indexed txIndex,
        address indexed to,
        uint256 value,
        bytes data
    );
    event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);
    event RevokeConfirmation(address indexed owner, uint256 indexed txIndex);
    event ExecuteTransaction(address indexed owner, uint256 indexed txIndex);

    address[] public owners;
    mapping(address => bool) public isOwner;
    uint256 public numConfirmationsRequired;

    struct Transaction {
        address to;
        uint256 value;
        bytes data;
        bool executed;
        uint256 numConfirmations;
    }

    // mapping from tx index => owner => bool
    mapping(uint256 => mapping(address => bool)) public isConfirmed;

    Transaction[] public transactions;

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

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

    modifier notExecuted(uint256 _txIndex) {
        require(!transactions[_txIndex].executed, "tx already executed");
        _;
    }

    modifier notConfirmed(uint256 _txIndex) {
        require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
        _;
    }

    constructor(address[] memory _owners, uint256 _numConfirmationsRequired) {
        require(_owners.length > 0, "owners required");
        require(
            _numConfirmationsRequired > 0
                && _numConfirmationsRequired <= _owners.length,
            "invalid number of required confirmations"
        );

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

            require(owner != address(0), "invalid owner");
            require(!isOwner[owner], "owner not unique");

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

        numConfirmationsRequired = _numConfirmationsRequired;
    }

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

    function submitTransaction(address _to, uint256 _value, bytes memory _data)
        public
        onlyOwner
    {
        uint256 txIndex = transactions.length;

        transactions.push(
            Transaction({
                to: _to,
                value: _value,
                data: _data,
                executed: false,
                numConfirmations: 0
            })
        );

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

    function confirmTransaction(uint256 _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notExecuted(_txIndex)
        notConfirmed(_txIndex)
    {
        Transaction storage transaction = transactions[_txIndex];
        transaction.numConfirmations += 1;
        isConfirmed[_txIndex][msg.sender] = true;

        emit ConfirmTransaction(msg.sender, _txIndex);
    }

    function executeTransaction(uint256 _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notExecuted(_txIndex)
    {
        Transaction storage transaction = transactions[_txIndex];

        require(
            transaction.numConfirmations >= numConfirmationsRequired,
            "cannot execute tx"
        );

        transaction.executed = true;

        (bool success,) =
            transaction.to.call{value: transaction.value}(transaction.data);
        require(success, "tx failed");

        emit ExecuteTransaction(msg.sender, _txIndex);
    }

    function revokeConfirmation(uint256 _txIndex)
        public
        onlyOwner
        txExists(_txIndex)
        notExecuted(_txIndex)
    {
        Transaction storage transaction = transactions[_txIndex];

        require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");

        transaction.numConfirmations -= 1;
        isConfirmed[_txIndex][msg.sender] = false;

        emit RevokeConfirmation(msg.sender, _txIndex);
    }

    function getOwners() public view returns (address[] memory) {
        return owners;
    }

    function getTransactionCount() public view returns (uint256) {
        return transactions.length;
    }

    function getTransaction(uint256 _txIndex)
        public
        view
        returns (
            address to,
            uint256 value,
            bytes memory data,
            bool executed,
            uint256 numConfirmations
        )
    {
        Transaction storage transaction = transactions[_txIndex];

        return (
            transaction.to,
            transaction.value,
            transaction.data,
            transaction.executed,
            transaction.numConfirmations
        );
    }
}

Key Components

  1. Owners & Confirmation Requirement

    • The contract supports multiple owners, and each owner can submit or confirm transactions. Owners are stored in an array called owners, and a mapping isOwner keeps track of whether an address is an owner.

    • There is a required number of confirmations (numConfirmationsRequired) that must be met before any transaction can be executed. This number is set during contract deployment.

  2. Transactions

    • The contract manages a list of transactions in the form of the Transaction struct, which stores details such as the recipient (to), the amount (value), the data for a contract call (data), whether the transaction has been executed (executed), and the number of confirmations it has received (numConfirmations).

    • Each transaction needs to be confirmed by a specified number of owners before it can be executed.

Key Functions

  1. Constructor

    • The constructor initializes the contract by accepting a list of _owners and a required number of confirmations _numConfirmationsRequired.

    • It checks that:

      • The list of owners is not empty.

      • The required number of confirmations is greater than 0 and does not exceed the number of owners.

      • Each owner address is valid and unique.

  2. Deposit Function

    • The contract can receive funds through the receive function, and any ETH sent to the contract is logged with the Deposit event.
  3. submitTransaction

    • This function allows an owner to submit a transaction by specifying the recipient (_to), value (_value), and additional data (_data).

    • The transaction is added to the transactions array, and the event SubmitTransaction is emitted to log the submission.

  4. confirmTransaction

    • Owners can confirm a submitted transaction via this function. Only owners can confirm transactions, and the transaction must exist, not be already executed, and not be confirmed by the same owner twice.

    • When confirmed, the numConfirmations for that transaction is incremented, and the ConfirmTransaction event is emitted.

  5. executeTransaction

    • This function executes a confirmed transaction if it has received the required number of confirmations (numConfirmationsRequired).

    • It checks that the transaction exists, hasn't been executed, and has the necessary confirmations.

    • If the transaction succeeds, the executed flag is set to true, and the event ExecuteTransaction is emitted.

  6. revokeConfirmation

    • Owners can revoke their confirmation for a transaction that has not yet been executed. If the transaction exists and the owner had previously confirmed it, the numConfirmations is decremented.

    • The RevokeConfirmation event is emitted.

Utility Functions

  • getOwners: Returns the list of owners.

  • getTransactionCount: Returns the total number of submitted transactions.

  • getTransaction: Allows retrieving detailed information about a specific transaction, including its recipient, value, data, execution status, and number of confirmations.

Security & Safeguards

  • Modifiers:

    • onlyOwner: Ensures only owners can perform certain actions (e.g., submitting, confirming, or revoking transactions).

    • txExists: Verifies the existence of a transaction.

    • notExecuted: Ensures a transaction is not executed twice.

    • notConfirmed: Prevents owners from confirming the same transaction multiple times.

  • Event Logging: Events such as Deposit, SubmitTransaction, ConfirmTransaction, RevokeConfirmation, and ExecuteTransaction help track actions in the contract, providing transparency.

DEPLOYMENT ON BASE.

Navigate to your metamask and add Base Sepolia Testnet to the list of networks. Click this to get some gas fees for deployment on base testnet.

After this, you should have been rewarded with 0.1Base Sepolia ETH!

Navigate back to your wallet.

Next is to connect your wallet to remix for smooth deployment.

Check for the address.

And verify on Base Sepolia Etherscan. Paste Address.

Conclusion

Congratulations on successfully creating your very own multisig smart contract on the Base network