Multi-Signature Wallet (Multi-Sig Wallet) is a security mechanism that requires multiple private keys to jointly authorize transactions. It is widely used in asset management, corporate governance, and DAO organizations.
1. Core Concepts of Multi-Sig Wallet
solidity/* Multi-sig wallet principles: - Set multiple owners - Set confirmation threshold: how many signatures are needed to execute transactions - For example: 3/5 multi-sig means 3 out of 5 owners need to confirm Application scenarios: - Enterprise asset management - DAO treasuries - Project fund custody - Cold wallet secure storage */
2. Basic Multi-Sig Wallet Implementation
soliditycontract MultiSigWallet { // Events event Deposit(address indexed sender, uint256 amount); 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); event OwnerAdded(address indexed owner); event OwnerRemoved(address indexed owner); event RequirementChanged(uint256 required); // State variables address[] public owners; // Owner list mapping(address => bool) public isOwner; // Is owner uint256 public numConfirmationsRequired; // Required confirmations struct Transaction { address to; // Target address uint256 value; // Transfer amount bytes data; // Call data bool executed; // Is executed uint256 numConfirmations; // Current confirmations } Transaction[] public transactions; // Transaction index => owner => is confirmed mapping(uint256 => mapping(address => bool)) public isConfirmed; // Modifiers modifier onlyOwner() { require(isOwner[msg.sender], "Not 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(!isConfirmed[_txIndex][msg.sender], "Transaction already confirmed"); _; } // Constructor constructor(address[] memory _owners, uint256 _numConfirmationsRequired) { require(_owners.length > 0, "Owners required"); require( _numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length, "Invalid number of 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 ETH receive() external payable { emit Deposit(msg.sender, msg.value); } // Submit transaction 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); } // Confirm transaction 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); } // Execute transaction function executeTransaction(uint256 _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) { Transaction storage transaction = transactions[_txIndex]; require( transaction.numConfirmations >= numConfirmationsRequired, "Not enough confirmations" ); transaction.executed = true; (bool success, ) = transaction.to.call{value: transaction.value}( transaction.data ); require(success, "Transaction failed"); emit ExecuteTransaction(msg.sender, _txIndex); } // Revoke confirmation function revokeConfirmation(uint256 _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) { require(isConfirmed[_txIndex][msg.sender], "Transaction not confirmed"); Transaction storage transaction = transactions[_txIndex]; transaction.numConfirmations -= 1; isConfirmed[_txIndex][msg.sender] = false; emit RevokeConfirmation(msg.sender, _txIndex); } // Query functions 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 ); } }
3. Advanced Multi-Sig Wallet (Supports Dynamic Management)
soliditycontract AdvancedMultiSigWallet { // Events event Deposit(address indexed sender, uint256 amount, uint256 balance); event TransactionSubmitted( uint256 indexed txId, address indexed proposer, address indexed to, uint256 value, bytes data ); event TransactionConfirmed(uint256 indexed txId, address indexed owner); event TransactionRevoked(uint256 indexed txId, address indexed owner); event TransactionExecuted(uint256 indexed txId); event OwnerAdded(address indexed owner); event OwnerRemoved(address indexed owner); event RequirementChanged(uint256 oldRequired, uint256 newRequired); event PauseStatusChanged(bool paused); // Transaction type enum enum TransactionType { Transfer, // Transfer ContractCall, // Contract call AddOwner, // Add owner RemoveOwner, // Remove owner ChangeRequirement // Change threshold } struct Transaction { TransactionType txType; address to; uint256 value; bytes data; bool executed; uint256 confirmations; uint256 submissionTime; uint256 executionTime; } // State variables address[] public owners; mapping(address => bool) public isOwner; mapping(address => uint256) public ownerIndex; uint256 public required; // Required confirmations Transaction[] public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; mapping(uint256 => mapping(address => uint256)) public confirmationTime; bool public paused; uint256 public constant TIMELOCK_DURATION = 24 hours; // Timelock uint256 public constant MAX_OWNERS = 50; uint256 public constant MAX_TRANSACTIONS = 1000; // Modifiers modifier onlyWallet() { require(msg.sender == address(this), "Only wallet"); _; } modifier onlyOwner() { require(isOwner[msg.sender], "Not owner"); _; } modifier ownerExists(address _owner) { require(isOwner[_owner], "Owner does not exist"); _; } modifier ownerDoesNotExist(address _owner) { require(!isOwner[_owner], "Owner exists"); _; } modifier notNull(address _address) { require(_address != address(0), "Null address"); _; } modifier validRequirement(uint256 _ownerCount, uint256 _required) { require( _required <= _ownerCount && _required != 0 && _ownerCount != 0, "Invalid requirement" ); _; } modifier whenNotPaused() { require(!paused, "Wallet is paused"); _; } // Constructor constructor( address[] memory _owners, uint256 _required ) validRequirement(_owners.length, _required) { require(_owners.length <= MAX_OWNERS, "Too many owners"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(!isOwner[owner] && owner != address(0), "Invalid owner"); isOwner[owner] = true; ownerIndex[owner] = owners.length; owners.push(owner); } required = _required; } // Receive ETH receive() external payable { emit Deposit(msg.sender, msg.value, address(this).balance); } // Pause function function setPaused(bool _paused) external onlyWallet { paused = _paused; emit PauseStatusChanged(_paused); } // Submit transfer transaction function submitTransfer( address _to, uint256 _value ) external onlyOwner whenNotPaused returns (uint256 txId) { require(_to != address(0), "Invalid recipient"); require(_value > 0 && _value <= address(this).balance, "Invalid value"); txId = addTransaction(TransactionType.Transfer, _to, _value, ""); confirmTransaction(txId); } // Submit contract call function submitContractCall( address _to, uint256 _value, bytes memory _data ) external onlyOwner whenNotPaused returns (uint256 txId) { require(_to != address(0), "Invalid contract"); txId = addTransaction(TransactionType.ContractCall, _to, _value, _data); confirmTransaction(txId); } // Submit add owner function submitAddOwner( address _owner ) external onlyOwner ownerDoesNotExist(_owner) notNull(_owner) whenNotPaused returns (uint256 txId) { require(owners.length < MAX_OWNERS, "Max owners reached"); txId = addTransaction( TransactionType.AddOwner, _owner, 0, "" ); confirmTransaction(txId); } // Submit remove owner function submitRemoveOwner( address _owner ) external onlyOwner ownerExists(_owner) whenNotPaused returns (uint256 txId) { require(owners.length > required, "Cannot remove owner"); txId = addTransaction( TransactionType.RemoveOwner, _owner, 0, "" ); confirmTransaction(txId); } // Submit change requirement function submitChangeRequirement( uint256 _newRequired ) external onlyOwner whenNotPaused returns (uint256 txId) { require(_newRequired > 0 && _newRequired <= owners.length, "Invalid requirement"); txId = addTransaction( TransactionType.ChangeRequirement, address(uint160(_newRequired)), 0, "" ); confirmTransaction(txId); } // Add transaction to list function addTransaction( TransactionType _txType, address _to, uint256 _value, bytes memory _data ) internal returns (uint256 txId) { require(transactions.length < MAX_TRANSACTIONS, "Too many transactions"); txId = transactions.length; transactions.push(Transaction({ txType: _txType, to: _to, value: _value, data: _data, executed: false, confirmations: 0, submissionTime: block.timestamp, executionTime: 0 })); emit TransactionSubmitted(txId, msg.sender, _to, _value, _data); } // Confirm transaction function confirmTransaction(uint256 _txId) public onlyOwner whenNotPaused { require(_txId < transactions.length, "Invalid txId"); require(!transactions[_txId].executed, "Already executed"); require(!confirmations[_txId][msg.sender], "Already confirmed"); confirmations[_txId][msg.sender] = true; confirmationTime[_txId][msg.sender] = block.timestamp; transactions[_txId].confirmations++; emit TransactionConfirmed(_txId, msg.sender); // Auto execute if (transactions[_txId].confirmations >= required) { executeTransaction(_txId); } } // Revoke confirmation function revokeConfirmation(uint256 _txId) external onlyOwner whenNotPaused { require(_txId < transactions.length, "Invalid txId"); require(!transactions[_txId].executed, "Already executed"); require(confirmations[_txId][msg.sender], "Not confirmed"); confirmations[_txId][msg.sender] = false; transactions[_txId].confirmations--; emit TransactionRevoked(_txId, msg.sender); } // Execute transaction function executeTransaction(uint256 _txId) public onlyOwner whenNotPaused { Transaction storage txn = transactions[_txId]; require(!txn.executed, "Already executed"); require(txn.confirmations >= required, "Not enough confirmations"); require( block.timestamp >= txn.submissionTime + TIMELOCK_DURATION, "Timelock not expired" ); txn.executed = true; txn.executionTime = block.timestamp; // Execute based on transaction type if (txn.txType == TransactionType.Transfer || txn.txType == TransactionType.ContractCall) { (bool success, ) = txn.to.call{value: txn.value}(txn.data); require(success, "External call failed"); } else if (txn.txType == TransactionType.AddOwner) { addOwnerInternal(txn.to); } else if (txn.txType == TransactionType.RemoveOwner) { removeOwnerInternal(txn.to); } else if (txn.txType == TransactionType.ChangeRequirement) { changeRequirementInternal(uint256(uint160(txn.to))); } emit TransactionExecuted(_txId); } // Internal function: add owner function addOwnerInternal(address _owner) internal { isOwner[_owner] = true; ownerIndex[_owner] = owners.length; owners.push(_owner); emit OwnerAdded(_owner); } // Internal function: remove owner function removeOwnerInternal(address _owner) internal { uint256 index = ownerIndex[_owner]; address lastOwner = owners[owners.length - 1]; owners[index] = lastOwner; ownerIndex[lastOwner] = index; owners.pop(); delete isOwner[_owner]; delete ownerIndex[_owner]; emit OwnerRemoved(_owner); } // Internal function: change requirement function changeRequirementInternal(uint256 _newRequired) internal { uint256 oldRequired = required; required = _newRequired; emit RequirementChanged(oldRequired, _newRequired); } // Batch confirm function confirmMultiple(uint256[] calldata _txIds) external onlyOwner whenNotPaused { for (uint i = 0; i < _txIds.length; i++) { confirmTransaction(_txIds[i]); } } // Query functions function getOwners() external view returns (address[] memory) { return owners; } function getTransactionCount() external view returns (uint256) { return transactions.length; } function getTransaction(uint256 _txId) external view returns (Transaction memory) { return transactions[_txId]; } function isConfirmedBy(uint256 _txId, address _owner) external view returns (bool) { return confirmations[_txId][_owner]; } function getConfirmationCount(uint256 _txId) external view returns (uint256) { return transactions[_txId].confirmations; } function getConfirmations(uint256 _txId) external view returns (address[] memory) { address[] memory confirmed = new address[](owners.length); uint256 count = 0; for (uint i = 0; i < owners.length; i++) { if (confirmations[_txId][owners[i]]) { confirmed[count] = owners[i]; count++; } } // Adjust array size assembly { mstore(confirmed, count) } return confirmed; } function getPendingTransactions() external view returns (uint256[] memory) { uint256[] memory pending = new uint256[](transactions.length); uint256 count = 0; for (uint i = 0; i < transactions.length; i++) { if (!transactions[i].executed) { pending[count] = i; count++; } } assembly { mstore(pending, count) } return pending; } function getBalance() external view returns (uint256) { return address(this).balance; } }
4. Using Gnosis Safe Pattern
solidity// Simulate Gnosis Safe core functionality contract GnosisSafeStyle { // Constants bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); bytes32 private constant SAFE_TX_TYPEHASH = keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"); // State variables mapping(address => bool) public isOwner; address[] public owners; uint256 public threshold; uint256 public nonce; // Transaction hash => is executed mapping(bytes32 => bool) public executed; // Transaction hash => signer => is signed mapping(bytes32 => mapping(address => bool)) public signed; // Events event SafeSetup(address indexed initiator, address[] owners, uint256 threshold); event ApproveHash(bytes32 indexed approvedHash, address indexed owner); event SignMsg(bytes32 indexed msgHash); event ExecutionSuccess(bytes32 indexed txHash); event ExecutionFailure(bytes32 indexed txHash); // Operation types enum Operation { Call, DelegateCall } constructor(address[] memory _owners, uint256 _threshold) { require(_threshold > 0 && _threshold <= _owners.length, "Invalid threshold"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(owner != address(0) && !isOwner[owner], "Invalid owner"); isOwner[owner] = true; owners.push(owner); } threshold = _threshold; emit SafeSetup(msg.sender, _owners, _threshold); } // Receive ETH receive() external payable {} // Get transaction hash function getTransactionHash( address to, uint256 value, bytes calldata data, Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce ) public view returns (bytes32) { bytes32 safeTxHash = keccak256( abi.encode( SAFE_TX_TYPEHASH, to, value, keccak256(data), operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, _nonce ) ); return keccak256( abi.encodePacked( bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash ) ); } // Get domain separator function domainSeparator() public view returns (bytes32) { return keccak256( abi.encode( DOMAIN_SEPARATOR_TYPEHASH, block.chainid, address(this) ) ); } // Execute transaction (with signature) function execTransaction( address to, uint256 value, bytes calldata data, Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures ) public payable returns (bool success) { bytes32 txHash = getTransactionHash( to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce ); nonce++; checkSignatures(txHash, signatures); // Execute transaction if (operation == Operation.DelegateCall) { (success, ) = to.delegatecall(data); } else { (success, ) = to.call{value: value}(data); } if (success) { emit ExecutionSuccess(txHash); } else { emit ExecutionFailure(txHash); } return success; } // Check signatures function checkSignatures(bytes32 dataHash, bytes memory signatures) public view { require(signatures.length >= threshold * 65, "Not enough signatures"); address lastOwner = address(0); for (uint i = 0; i < threshold; i++) { bytes memory signature = slice(signatures, i * 65, 65); address signer = recoverSigner(dataHash, signature); require(isOwner[signer], "Invalid signer"); require(signer > lastOwner, "Signers not ordered"); lastOwner = signer; } } // Recover signer function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) internal pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); return ecrecover(_ethSignedMessageHash, v, r, s); } // Split signature function splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { require(sig.length == 65, "Invalid signature length"); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } if (v < 27) { v += 27; } } // Slice function function slice(bytes memory data, uint256 start, uint256 length) internal pure returns (bytes memory) { bytes memory result = new bytes(length); for (uint i = 0; i < length; i++) { result[i] = data[start + i]; } return result; } }
5. Multi-Sig Wallet Security Considerations
soliditycontract MultiSigSecurity { /* Security best practices: 1. Threshold setting - Recommend 2/3, 3/5 multi-sig configuration - Avoid 1/1 (single sig) or n/n (requires everyone to sign) 2. Owner management - Use hardware wallets as owners - Regular key rotation - Geographically distributed storage 3. Transaction limits - Set daily limits - Delay execution for large transactions - Whitelist addresses 4. Emergency mechanisms - Emergency pause function - Fund recovery mechanism - Upgrade capability */ } // Multi-sig wallet with security restrictions contract SecureMultiSig { address[] public owners; mapping(address => bool) public isOwner; uint256 public required; // Security restrictions uint256 public dailyLimit; uint256 public spentToday; uint256 public lastDay; mapping(address => bool) public whitelist; bool public whitelistEnabled; modifier onlyOwner() { require(isOwner[msg.sender], "Not owner"); _; } constructor( address[] memory _owners, uint256 _required, uint256 _dailyLimit ) { // ... initialization code dailyLimit = _dailyLimit; } // Check and update daily limit function checkDailyLimit(uint256 _value) internal { if (block.timestamp > lastDay + 1 days) { lastDay = block.timestamp; spentToday = 0; } require( spentToday + _value <= dailyLimit, "Daily limit exceeded" ); spentToday += _value; } // Check whitelist function checkWhitelist(address _to) internal view { if (whitelistEnabled) { require(whitelist[_to], "Address not whitelisted"); } } // Add to whitelist function addToWhitelist(address _addr) external onlyOwner { whitelist[_addr] = true; } // Remove from whitelist function removeFromWhitelist(address _addr) external onlyOwner { whitelist[_addr] = false; } // Set whitelist toggle function setWhitelistEnabled(bool _enabled) external onlyOwner { whitelistEnabled = _enabled; } }
6. Testing and Deployment Recommendations
javascript// Test multi-sig wallet using Hardhat const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("MultiSigWallet", function () { let multiSig; let owners; let required = 2; beforeEach(async function () { [owner1, owner2, owner3, nonOwner] = await ethers.getSigners(); owners = [owner1.address, owner2.address, owner3.address]; const MultiSig = await ethers.getContractFactory("MultiSigWallet"); multiSig = await MultiSig.deploy(owners, required); await multiSig.deployed(); // Send ETH to multi-sig wallet await owner1.sendTransaction({ to: multiSig.address, value: ethers.utils.parseEther("10") }); }); it("Should submit and confirm transaction", async function () { const to = nonOwner.address; const value = ethers.utils.parseEther("1"); // Submit transaction await multiSig.connect(owner1).submitTransaction(to, value, "0x"); // Confirm transaction await multiSig.connect(owner2).confirmTransaction(0); // Verify transaction is executed const tx = await multiSig.transactions(0); expect(tx.executed).to.be.true; // Verify balance const balance = await ethers.provider.getBalance(to); expect(balance).to.equal(value); }); it("Should require enough confirmations", async function () { const to = nonOwner.address; const value = ethers.utils.parseEther("1"); await multiSig.connect(owner1).submitTransaction(to, value, "0x"); // Only one confirmation, should not execute const tx = await multiSig.transactions(0); expect(tx.executed).to.be.false; }); });
7. Summary
Multi-sig wallet is an important security tool:
-
Core Mechanism:
- Multiple owners jointly manage
- Threshold confirmation mechanism
- Transaction lifecycle management
-
Implementation Points:
- Complete transaction flow (submit-confirm-execute)
- Access control
- Event logging
- Query functions
-
Security Considerations:
- Reasonable threshold setting
- Timelock mechanism
- Transaction limits
- Emergency handling
-
Production Recommendations:
- Use audited libraries (e.g., Gnosis Safe)
- Test thoroughly
- Consider using proxy pattern for upgrades
- Implement monitoring and alerts