A Multi-Sig Wallet (Multi-signature Wallet) in Solidity is a smart contract that requires multiple parties to sign a transaction before it can be executed. This adds an extra layer of security because a single person cannot execute critical transactions, which is useful for managing large sums of assets or for decentralized governance.
Key Features of a Multi-Sig Wallet:
- Multiple owners: The wallet is controlled by a group of owners.
- Threshold: A specific number of signatures (threshold) is required to approve a transaction.
- Transaction approval: Transactions are proposed and need to be approved by the required number of owners.
- Secure execution: Only after sufficient approvals can a transaction be executed.
Basic Solidity Implementation of a Multi-Sig Wallet
Here’s an example of how to create a simple Multi-Sig Wallet contract in Solidity.
Step 1: Define the Multi-Sig Wallet Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiSigWallet {
address[] public owners; // List of owners
mapping(address => bool) public isOwner; // Mapping to check if an address is an owner
uint256 public required; // Number of required confirmations to execute a transaction
struct Transaction {
address to;
uint256 value;
bool executed;
uint256 confirmations;
}
Transaction[] public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
event Deposit(address indexed sender, uint256 value);
event SubmitTransaction(address indexed sender, uint256 indexed txIndex, address indexed to, uint256 value);
event ConfirmTransaction(address indexed sender, uint256 indexed txIndex);
event ExecuteTransaction(address indexed sender, uint256 indexed txIndex);
modifier onlyOwner() {
require(isOwner[msg.sender], "Not an owner");
_;
}
modifier txExists(uint256 txIndex) {
require(txIndex < transactions.length, "Transaction does not exist");
_;
}
modifier notExecuted(uint256 txIndex) {
require(!transactions[txIndex].executed, "Transaction already executed");
_;
}
modifier notConfirmed(uint256 txIndex) {
require(!confirmations[txIndex][msg.sender], "Transaction already confirmed");
_;
}
constructor(address[] memory _owners, uint256 _required) {
require(_owners.length > 0, "Owners required");
require(_required > 0 && _required <= _owners.length, "Invalid required confirmations");
for (uint256 i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "Invalid owner address");
require(!isOwner[owner], "Owner is already added");
isOwner[owner] = true;
owners.push(owner);
}
required = _required;
}
// Fallback function to accept Ether
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
// Submit a transaction
function submitTransaction(address to, uint256 value) public onlyOwner {
uint256 txIndex = transactions.length;
transactions.push(Transaction({
to: to,
value: value,
executed: false,
confirmations: 0
}));
emit SubmitTransaction(msg.sender, txIndex, to, value);
}
// Confirm a transaction
function confirmTransaction(uint256 txIndex) public onlyOwner txExists(txIndex) notExecuted(txIndex) notConfirmed(txIndex) {
confirmations[txIndex][msg.sender] = true;
transactions[txIndex].confirmations += 1;
emit ConfirmTransaction(msg.sender, txIndex);
executeTransaction(txIndex);
}
// Execute a transaction if enough confirmations are received
function executeTransaction(uint256 txIndex) public onlyOwner txExists(txIndex) notExecuted(txIndex) {
require(transactions[txIndex].confirmations >= required, "Not enough confirmations");
Transaction storage txn = transactions[txIndex];
txn.executed = true;
(bool success, ) = txn.to.call{value: txn.value}("");
require(success, "Transaction execution failed");
emit ExecuteTransaction(msg.sender, txIndex);
}
// Get the number of transactions
function getTransactionCount() public view returns (uint256) {
return transactions.length;
}
// Get transaction details
function getTransaction(uint256 txIndex) public view returns (address to, uint256 value, bool executed, uint256 confirmations) {
Transaction memory txn = transactions[txIndex];
return (txn.to, txn.value, txn.executed, txn.confirmations);
}
// Get the number of owners
function getOwnersCount() public view returns (uint256) {
return owners.length;
}
// Check if a given address is an owner
function isOwner(address owner) public view returns (bool) {
return isOwner[owner];
}
}
Explanation of the Code
State Variables:
owners
: A list of addresses that are allowed to control the wallet.
isOwner
: A mapping to quickly check if an address is an owner.
required
: The number of confirmations required to execute a transaction.
transactions
: A list of transactions that have been proposed and are awaiting confirmation.
confirmations
: A mapping to track which owners have confirmed each transaction.
Modifiers:
onlyOwner
: Ensures that only owners can call certain functions.
txExists
: Ensures that the specified transaction index exists.
notExecuted
: Ensures that the transaction has not already been executed.
notConfirmed
: Ensures that the caller has not already confirmed the transaction.
Functions:
submitTransaction
: Allows an owner to propose a transaction. A transaction consists of a recipient address and the value to be sent.
confirmTransaction
: Allows an owner to confirm a proposed transaction. The transaction needs a number of confirmations (set by required
) before it can be executed.
executeTransaction
: Executes a transaction if it has enough confirmations.
receive()
: A fallback function to accept Ether sent to the contract.
getTransactionCount
: Returns the number of transactions.
getTransaction
: Returns the details of a specific transaction.
getOwnersCount
: Returns the number of owners.
isOwner(address owner)
: Checks if an address is an owner.
Usage Example
Deploying the Contract:
When deploying the contract, you pass an array of owner addresses and the number of required confirmations. For example:
address[] memory owners = [address1, address2, address3];
uint256 required = 2;
MultiSigWallet wallet = new MultiSigWallet(owners, required);
Submitting a Transaction:
An owner can propose a transaction to send Ether to another address:
wallet.submitTransaction(address(to), 1 ether);
Confirming a Transaction:
Once a transaction is proposed, other owners can confirm it:
wallet.confirmTransaction(txIndex);
Executing the Transaction:
After the required number of owners have confirmed the transaction, it will be executed automatically.
Considerations
- Security: Multi-Sig wallets add security by requiring multiple approvals. However, the owners should be trusted since they have control over the wallet.
- Gas Costs: Multi-Sig wallets can be expensive to operate in terms of gas, as each confirmation and execution may require multiple transactions.
- Threshold: The number of required confirmations can be adjusted based on the security needs. Typically, a threshold between 2 and 3 is used.
This basic implementation can be extended with more complex features such as transaction revocation, timed transactions, and other custom logic as needed.