How to Create a MultiSig Wallet in Solidity

How to Create a MultiSig Wallet in Solidity
11 min read

Creating a Multi-Sig Wallet in Solidity

A multi-signature (multi-sig) wallet is like a safe that needs multiple keys to open. It is a smart contract that stores cryptocurrency and needs permission from many parties in order to conduct transactions. We'll explore building a multi-sig wallet with Ethereum today, utilising the well-liked Hardhat development environment. Creating a multi-sig wallet requires significant expertise in smart contract development.

Prerequisites

  1. A basic understanding of Solidity and Ethereum.
  2. Node.js is installed on your machine.
  3. Hardhat is installed globally using the command: npm install -g hardhat.

Setting Up Hardhat

  1. To initiate a new task, open a terminal window, type npx hardhat, and then follow the prompts to create a new project.
  2. Installing Dependencies: Inside your project directory, install the necessary npm packages with:
    bash
npm install @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers

Writing the Multi-Sig Wallet Smart Contract

  1. Create a new file called MultiSigWallet.sol in the contracts folder.
  2. Paste the below code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MultiSigWallet {
event Deposit(address indexed sender, uint amount, uint balance);
event SubmitTransaction(
address indexed owner,
uint indexed txIndex,
address indexed to,
uint value,
bytes data
);
event ConfirmTransaction(address indexed owner, uint indexed txIndex);
event RevokeConfirmation(address indexed owner, uint indexed txIndex);
event ExecuteTransaction(address indexed owner, uint indexed txIndex);
address[] public owners;
mapping(address => bool) public isOwner;
uint public numConfirmationsRequired;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint numConfirmations;
}
// mapping from tx index => owner => bool
mapping(uint => mapping(address => bool)) public isConfirmed;
Transaction[] public transactions; modifier onlyOwner() {
require(isOwner[msg.sender], "not owner");
_;
}
modifier txExists(uint _txIndex) {
require(_txIndex < transactions.length, "tx does not exist");
_;
}
modifier notExecuted(uint _txIndex) {
require(!transactions[_txIndex].executed, "tx already executed");
_;
}
modifier notConfirmed(uint _txIndex) {
require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
_;
}
constructor(address[] memory _owners, uint _numConfirmationsRequired) {
require(_owners.length > 0, "owners required");
require(
_numConfirmationsRequired > 0 &&
_numConfirmationsRequired <= _owners.length,
"invalid number of required confirmations"
);
for (uint 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,
uint _value,
bytes memory _data
) public onlyOwner {
uint 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(
uint _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(
uint _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(
uint _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 (uint) {
return transactions.length;
}
function getTransaction(
uint _txIndex
)
public
view
returns (
address to,
uint value,
bytes memory data,
bool executed,
uint numConfirmations
)
{
Transaction storage transaction = transactions[_txIndex];
return (
transaction.to,
transaction.value,
transaction.data,
transaction.executed,
transaction.numConfirmations
);
}
}

Compiling Your Smart Contract:

  • In your terminal, run npx hardhat compile.

Contract Details

Contract Setup

  • Defined using pragma solidity ^0.8.20; to specify the Solidity compiler version.
  • contract SecureMultiWallet { … } initiates the contract definition.

Event Definitions

  • Events such as FundsDeposited, TransactionSubmitted, TransactionConfirmed, ConfirmationRevoked, and TransactionExecuted are defined to emit logs for significant actions within the contract.

State Variables

  • authorizedUsers is an array to track wallet signatories.
  • isAuthorized is a mapping to quickly verify if an address is authorized.
  • requiredApprovals specifies the number of approvals needed to execute a transaction.
  • pendingTransactions is an array to store all proposed transactions.
  • hasConfirmed is a nested mapping to keep track of approvals per transaction.

Struct Definition

  • PendingTransaction struct is defined to hold information about each proposed transaction.

Modifiers

  • onlyAuthorized ensures the function is called by an authorized user.
  • transactionExists checks if the transaction ID exists.
  • notYetExecuted checks if the transaction has not been executed yet.
  • notYetConfirmed checks if the transaction has not already been approved by the caller.

Constructor

  • The constructor initializes the contract with a list of authorized users and the required number of approvals.

Fallback Function

  • The receive function allows the contract to accept ether and emits a FundsDeposited event.

Transaction Management Functions

  • addTransaction: Allows an authorized user to propose a new transaction.
  • approveTransaction: Allows an authorized user to approve a proposed transaction.
  • runTransaction: Allows an authorized user to execute a transaction once the required number of approvals have been met.
  • retractApproval: Allows an authorized user to retract their approval from a proposed transaction.

View Functions

  • listUsers: Allows anyone to query the list of authorized users.
  • countTransactions: Allows anyone to query the total number of proposed transactions.
  • fetchTransaction: Allows anyone to query details of a specific transaction by its ID.

Githubhttps://github.com/AshishG2/MultiSigSolidity

If you are looking for smart contract development or crypto wallet development, connect with our skilled smart contract developers to get started.

In case you have found a mistake in the text, please send a message to the author by selecting the mistake and pressing Ctrl-Enter.
Oodles Blockchain 68
Full-Stack Blockchain Development Services and Solutions for Startups and Enterprises for Business Excellence Transform your business processes into highly sec...
Comments (0)

    No comments yet

You must be logged in to comment.

Sign In / Sign Up