What Is a Multi-Signature Wallet?
A multi-signature (multisig) wallet enhances security by requiring multiple authorized signatures to execute transactions. The core principle: funds can only be moved when a predefined threshold of signers (e.g., 2 out of 3) approves an action. This reduces single-point failure risks common in traditional wallets.
Example:
- A traditional wallet’s funds are compromised if its private key leaks.
- A multisig wallet with 3 signers (threshold: 2) remains secure even if one signer’s key is exposed—attackers cannot act unilaterally.
Key Features of Multisig Wallets
- Shared Authority: Managed collectively by multiple users, each with partial control.
- Threshold Configuration: Rules like 2/3 or 3/5 ensure no single user can unilaterally execute sensitive operations.
- Enhanced Security: Leaked keys alone cannot initiate transactions.
- Prevent Misuse: Requires consensus, reducing individual abuse risks.
Gnosis Safe: A Leading Multisig Solution
Gnosis Safe is a popular Ethereum-based multisig wallet managing over $1B in assets. Its decentralized, audited smart contracts offer robust security for collective fund management.
How Multisig Wallets Work
Core Components
Smart Contract Storage:
- An array of owner addresses (
address[] owners). - A threshold value (
uint8 threshold).
uint8 public constant threshold = 2; address[MAX_SIGNEE] public owners;- An array of owner addresses (
Execution Flow:
- Off-Chain Signing: Collect signatures exceeding the threshold.
- On-Chain Verification: Validate signatures against stored owners; execute the transaction if valid.
Step-by-Step Implementation
Off-Chain Signing
Transaction Data: Define recipient (
to), value (value), and calldata (data).address to = address(0x123); uint256 value = 1 ether; bytes data = abi.encodeWithSignature("setValue(uint256)", 123);Message Hash: Hash the data twice (Ethereum standards):
- First hash:
keccak256(to, value, keccak256(data)). - Final hash: Prepend
\x19Ethereum Signed Message:\n32.
function buildMessageHash(address to, uint256 value, bytes memory data) internal view returns (bytes32) { bytes32 dataHash = keccak256(abi.encode(to, value, keccak256(data))); return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)); }- First hash:
Sign & Aggregate: Individual signers generate signatures (
r,s,v); concatenate into one payload.function generateSignatures(bytes32 messageHash) public returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey1, messageHash); bytes memory sig1 = abi.encodePacked(r, s, v); // Repeat for other signers return abi.encodePacked(sig1, sig2); // 130 bytes for 2 sigs }- Submit to Contract: Send original data + aggregated signatures to the multisig contract.
On-Chain Verification
- Recover Hash: Reconstruct the message hash identically to off-chain.
Validate Signatures:
- Split aggregated signatures into individual
(r,s,v)components. - Recover signer addresses via
ECDSA.recover(). - Check recovered addresses exist in
ownersarray.
function checkSignatures(bytes32 messageHash, bytes memory signatures) internal view { require(signatures.length >= threshold * 65, "Invalid length"); for (uint i = 0; i < threshold; i++) { address recovered = ECDSA.recover(messageHash, signatures.slice(i*65, 65)); require(isOwner(recovered), "Invalid signer"); } }- Split aggregated signatures into individual
Execute Transaction: Use
call()to forward funds/data if valid.(bool success,) = to.call{value: value}(data); require(success, "Transaction failed");
Minimal Gnosis Safe Clone: Full Code
👉 Explore the full repository for tested implementations.
On-Chain Contract (MultiSignature.sol)
contract MultiSignature {
uint8 public constant MAX_SIGNEE = 3;
uint8 public constant threshold = 2;
address[MAX_SIGNEE] public owners;
constructor(address[MAX_SIGNEE] memory _owners) {
for (uint i = 0; i < MAX_SIGNEE; i++) {
require(_owners[i] != address(0), "Invalid owner");
owners[i] = _owners[i];
}
}
function executeTransaction(address to, uint256 value, bytes memory data, bytes memory signatures) external {
bytes32 messageHash = recoverMessageHash(to, value, data);
checkSignatures(messageHash, signatures);
(bool success,) = to.call{value: value}(data);
require(success, "Execution failed");
}
function checkSignatures(bytes32 messageHash, bytes memory signatures) internal view {
require(signatures.length >= threshold * 65, "Bad signature length");
uint pos = 0;
for (uint i = 0; i < threshold; i++) {
address recovered = ECDSA.recover(messageHash, signatures.slice(pos, 65));
pos += 65;
require(isOwner(recovered), "Invalid signer");
}
}
function isOwner(address addr) internal view returns (bool) {
for (uint i = 0; i < MAX_SIGNEE; i++) {
if (owners[i] == addr) return true;
}
return false;
}
function recoverMessageHash(address to, uint256 value, bytes memory data) internal pure returns (bytes32) {
bytes32 dataHash = keccak256(abi.encode(to, value, keccak256(data)));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
} Testing Script
forge test --match-path test/multiSignatureTest.t.sol -vvvv FAQ
Q: Why use multisig wallets over traditional ones?
A: They mitigate single-point failures (e.g., leaked keys) and require collective approval for transactions.
Q: What’s the role of the threshold?
A: It sets the minimum number of signatures needed (e.g., 2/3 means two signers must approve).
Q: Can I dynamically add/remove signers?
A: This minimal version doesn’t support it, but full implementations (like Gnosis Safe) do.
Q: Is multisig slower due to multiple signatures?
A: Slightly—it requires coordination but adds critical security for high-value transactions.
👉 Learn more about advanced multisig features in our deep-dive guides.