Implementing Multi-sig Wallets in Smart Contracts
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);
}
}
- 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 theisOwner
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 thetransactions
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 returnsfalse
.
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 totrue
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
, andTransactionExecuted
) 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:
- Create a new file named deploy.js in the script folder.
- Copy and paste the code snippet provided below into the deploy.js file.
- 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.