Implementing Multi-sig Wallets in Smart Contracts

Tusharmahajan
Simform Engineering
5 min readSep 21, 2023

Learn how to enhance security and control in blockchain transactions with Multi-Signature validation.

A multi-signature (multi-sig) wallet is a type of cryptocurrency wallet that requires multiple signatures or approvals to authorize transactions. Unlike a traditional single-signature wallet, where a single private key is sufficient to move funds, a multi-sig wallet involves multiple private keys, and a predefined number of those keys must sign off on a transaction before it can be executed.

The primary purpose of a multi-sig wallet is to enhance security and reduce the risk of unauthorized access to funds. By requiring multiple parties to agree on a transaction, it becomes more challenging for a single individual (or hacker) to compromise the wallet and steal the funds.

Let’s get started with Multi-sig Wallets Smart Contracts

To begin, we’ll create a Hardhat project. If you’re new to Hardhat and unsure how to write and deploy smart contracts, you can refer to the following blog post:

Inside the contract folder of your Hardhat project, create a new file named MultiSigWallet.sol. Copy and paste the following contract code snippet into this file.

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

contract MultiSigWallet {
address[] public owners;
uint public numConfirm;

struct Transaction{
address to;
uint value;
bool executed;
}

mapping (uint=>mapping (address=>bool)) isConfirmed;
mapping (address=>bool) isOwner;

Transaction[] public transactions;

event TransactionSubmitted(uint transactionId,address sender,address receiver,uint amount);
event TransactionConfirmed(uint transactionId);
event TransactionExecuted(uint transactionId);

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

constructor(address[] memory _owners,uint _numConfirmationRequired){
require(_owners.length>1,"owners required must grater than 1");
require( _numConfirmationRequired>0 && _numConfirmationRequired<=_owners.length,"Num of confirmation is not sync with num of owner");
numConfirm=_numConfirmationRequired;

for(uint i=0;i<_owners.length;i++){
require(_owners[i]!=address(0),"Invalid Owner");
owners.push(_owners[i]);
isOwner[_owners[i]]=true;
}

}

function submitTrnsaction(address _to) public payable {
require(_to!=address(0),"Invalid ");
require(msg.value>0 ,"Transfer amount must be grater than 0 ");
uint transactionId=transactions.length;

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

emit TransactionSubmitted( transactionId,msg.sender,_to,msg.value);
}

function confirmTransaction(uint _transactionId) public onlyOwner{
require(_transactionId<transactions.length,"Invalid transaction");
require(!isConfirmed[_transactionId][msg.sender],"Transaction is already confirm by owner");
isConfirmed[_transactionId][msg.sender]=true;
emit TransactionConfirmed(_transactionId);

if(isTransactionConfirmed( _transactionId)){
executeTransaction(_transactionId);
}
}

function isTransactionConfirmed(uint _transactionId)public view returns (bool){
require(_transactionId<transactions.length,"Invalid transaction");
uint confirmation;
for(uint i=0;i<numConfirm;i++){
if(isConfirmed[_transactionId][owners[i]]){
confirmation++;
}
}
return confirmation>=numConfirm;
}

function executeTransaction(uint _transactionId) public payable {
require(_transactionId<transactions.length,"Invalid transaction");
require(!transactions[_transactionId].executed,"Transaction is already executed");

(bool success,)= transactions[_transactionId].to.call{value:transactions[_transactionId].value}("");

require(success,"Transaction Execution Failed ");
transactions[_transactionId].executed=true;
emit TransactionExecuted(_transactionId);
}

}
  1. Constructor:
  • The contract is deployed, and the constructor is executed with the initial set of owners and the required number of confirmations.
  • The require statements ensure that there are at least two owners and the number of required confirmations is valid.
  • The owners array is populated with the owner addresses and the isOwner mapping is updated to mark them as owners.

2. submitTransaction:

  • Any owner can call this function to submit a new transaction to send Ether to a specified recipient.
  • The function checks that the recipient address is valid and the transferred amount is greater than 0.
  • A new Transaction struct is created and added to the transactions array, representing the pending transaction.
  • An event is emitted to log the submission of the transaction.

3. confirmTransaction:

  • Only owners can call this function to confirm a specific pending transaction.
  • The function verifies that the transaction ID is valid and that the caller has not already confirmed this transaction.
  • The isConfirmed mapping is updated to mark the caller as having confirmed the transaction.
  • An event is emitted to log the confirmation of the transaction.
  • If the required number of confirmations for the transaction is reached, the executeTransaction function is called to execute the transaction.

4. isTransactionConfirmed:

  • This view function checks whether a specific transaction has received the required number of confirmations.
  • The function iterates through the owners and counts the number of confirmations for the transaction using the isConfirmed mapping.
  • It returns true if the number of confirmations is greater than or equal to the required number of confirmations; otherwise, it returns false.

5. executeTransaction:

  • Only owners can call this function to execute a confirmed transaction.
  • The function verifies that the transaction ID is valid and that the transaction has not been executed before.
  • It sends the specified amount of Ether to the intended recipient using the call function.
  • The transaction’s executed flag is set to true to prevent re-execution.
  • An event is emitted to log the execution of the transaction.

6. Modifiers:

  • The onlyOwner modifier is defined to restrict access to certain functions to only the owners of the multi-sig wallet.
  • It is used for functions like confirmTransaction to ensure that only owners can confirm transactions.

7. Events:

  • The contract emits three events (TransactionSubmitted, TransactionConfirmed, and TransactionExecuted) to log key actions within the contract.
  • Events are useful for external applications to listen to and respond to activities happening on the contract.

Overall, the contract provides a simple multi-signature wallet implementation, allowing multiple owners to submit and confirm transactions before they are executed. This enhances the security and control of the wallet’s funds.

To deploy the contract, follow these steps:

  1. Create a new file named deploy.js in the script folder.
  2. Copy and paste the code snippet provided below into the deploy.js file.
  3. Replace OWNER-1-ADDRESS and OWNER-2-ADDRESS with your owners’ address in deploy.js file.
const hre = require("hardhat");

async function main() {
const MultiSigWallet = await hre.ethers.getContractFactory("MultiSigWallet");
const multiSigWallet = await MultiSigWallet.deploy(
[
"OWNER-1-ADDRESS",
"OWNER-2-ADDRESS",
],
2
);

await multiSigWallet.deployed();

console.log(` deployed to ${multiSigWallet.address}`);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Deploy this smart contract on any network. Once done, you are ready to perform a multi-signature wallet transaction.

If you wish to review my code, you can refer to my GitHub repository.

Conclusion

Multi-sig wallets are commonly used by businesses, cryptocurrency exchanges, and projects involving joint ownership of funds, where no single individual should have complete control over the wallet’s assets.

However, the specific implementation of multi-sig wallets can vary depending on the cryptocurrency and the wallet software being used. Additionally, users should always ensure they securely store their private keys and have a backup plan in case a private key is lost or compromised.

For more updates on the latest tools and technologies, follow the Simform Engineering blog.

Follow Us: Twitter | LinkedIn

--

--