P16: Multi-Signature Wallet
P16: Multi-Signature Wallet
Project Overview
| Attribute | Value |
|---|---|
| Main Language | Solidity |
| Alternative Languages | Vyper |
| Difficulty | Advanced |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | The โService & Supportโ Model |
| Knowledge Area | Security / Wallet Design |
| Software or Tool | Gnosis Safe Clone |
| Main Book | โMastering Ethereumโ by Andreas M. Antonopoulos & Gavin Wood |
Learning Objectives
By completing this project, you will:
- Master ECDSA signature verification understanding how
ecrecoverextracts signer addresses from signatures and why this is fundamental to blockchain authentication - Implement EIP-712 typed data signing creating human-readable, phishing-resistant signature requests that work across all major wallets
- Design robust nonce management preventing replay attacks across chains and within transactions
- Build owner management systems enabling secure addition, removal, and rotation of signers without compromising funds
- Understand threshold cryptography concepts learning why M-of-N schemes eliminate single points of failure
- Master gas estimation for delegate calls enabling accurate execution cost prediction for arbitrary transactions
- Implement batch transaction systems allowing atomic execution of multiple operations in a single transaction
The Core Question Youโre Answering
How can multiple parties collectively control funds without any single party having unilateral access?
This is the fundamental problem of collective custody. In traditional finance, this is solved through legal contracts and trusted intermediaries. In Web3, we solve it through mathematics and smart contracts.
Consider a DAO treasury with $10 million. If one person controls the private key:
- They could steal everything
- They could lose the key
- They could be coerced into signing
- They could die, locking funds forever
Multi-signature wallets solve this by requiring M of N parties to agree before any action:
- No single point of failure
- Resistant to key compromise
- Enables organizational decision-making
- Provides audit trails on-chain
Deep Theoretical Foundation
The Problem with Single-Key Wallets
A standard Ethereum externally owned account (EOA) is controlled by a single private key. This creates several risks:
Single Point of Failure
========================
[Private Key] ----controls----> [Wallet with $10M]
|
v
If compromised: Total loss
If lost: Funds locked forever
If coerced: No defense
Multi-signature wallets transform this into a distributed trust model:
Distributed Trust Model
========================
[Key 1 (Alice)] ----+
|
[Key 2 (Bob)] ----+---> [2-of-3 Multisig] ----> [Treasury]
|
[Key 3 (Charlie)]----+
To move funds: Any 2 of 3 must sign
How Multi-Sig Works: The Transaction Flow
+------------------+ +------------------+ +------------------+
| 1. PROPOSAL | --> | 2. COLLECTION | --> | 3. EXECUTION |
+------------------+ +------------------+ +------------------+
| | | | | |
| Alice proposes | | Bob reviews and | | Anyone can call |
| sending 1 ETH to | | signs off-chain | | execute() once |
| recipient | | or on-chain | | threshold met |
| | | | | |
| Creates tx with: | | Signature added | | Contract verifies|
| - target | | to pending tx | | M signatures, |
| - value | | | | executes via |
| - data | | Charlie also | | delegatecall |
| - nonce | | signs | | |
+------------------+ +------------------+ +------------------+
| | |
v v v
tx stored in confirmations funds move,
pending queue counter++ state changes
ECDSA and ecrecover: The Cryptographic Foundation
At the heart of multi-sig is the ability to verify who signed a message without them submitting an on-chain transaction. This is done through ECDSA signatures and the ecrecover precompile.
The ECDSA Signature Components:
r: The x-coordinate of a random point on the elliptic curves: A value computed from the message hash, private key, and random pointv: Recovery identifier (27 or 28 in Ethereum, indicating which of two possible public keys)
// Recovering the signer from a signature
function recoverSigner(bytes32 messageHash, bytes memory signature)
internal pure returns (address)
{
require(signature.length == 65, "Invalid signature length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
// EIP-2 still allows v = 0 or 1, but ecrecover expects 27 or 28
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "Invalid v value");
return ecrecover(messageHash, v, r, s);
}
Why ecrecover is Special:
- Itโs a precompiled contract at address 0x01
- Costs only 3000 gas (extremely efficient)
- Returns address(0) on failure rather than reverting
- Vulnerable to signature malleability without proper checks
EIP-712: Typed Structured Data Signing
Raw message signing is dangerous. Users see only a hash, making phishing trivial. EIP-712 solves this by:
- Defining a structured data format
- Including domain separation (contract address, chain ID)
- Making signatures human-readable in wallets
EIP-712 Signature Structure
============================
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DOMAIN SEPARATOR โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ name: "MultiSigWallet" โ
โ version: "1" โ
โ chainId: 1 โ
โ verifyingContract: 0x1234... โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TYPED DATA โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Transaction( โ
โ address to, โ
โ uint256 value, โ
โ bytes data, โ
โ uint256 nonce, โ
โ uint256 gasPrice โ
โ ) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
=
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FINAL HASH โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ keccak256("\x19\x01" ++ domainSeparator ++ structHash) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Nonce Management: Preventing Replay Attacks
A nonce (number used once) ensures each transaction can only be executed once. Without proper nonce handling:
Replay Attack Scenario
======================
1. Alice signs tx to send 1 ETH to Bob (nonce = 0)
2. Transaction executes successfully
3. Attacker replays the same signed message
4. Without nonce check, same 1 ETH sent again!
5. Repeat until wallet drained
Nonce Strategies:
| Strategy | Pros | Cons |
|---|---|---|
| Sequential (0, 1, 2โฆ) | Simple, deterministic | Transactions must execute in order |
| Hash-based | Order-independent | Larger storage footprint |
| Bitmap | Gas-efficient for many nonces | Complex implementation |
| Deadline-based | Time-limited signatures | Requires timestamp trust |
Gnosis Safe uses sequential nonces with the ability to batch transactions, getting the best of both worlds.
The Gnosis Safe Architecture
Gnosis Safe (now Safe) is the gold standard for multi-signature wallets, securing over $100 billion in assets. Understanding its architecture is essential:
Gnosis Safe Module Architecture
================================
โโโโโโโโโโโโโโโโโโโโ
โ Safe Proxy โ
โ (user's wallet) โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ โ โ
v v v
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
โ Singleton โ โ Modules โ โ Guards โ
โ (logic) โ โ (extensions)โ โ (checks) โ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
โ โ โ
โ โโโโโโโโโโโดโโโโโโโโโโ โ
โ โ โ โ
v v v v
โโโโโโโโโโโโโ โโโโโโโโโโโโโ
โ Fallback โ โ Module โ
โ Manager โ โ Manager โ
โโโโโโโโโโโโโ โโโโโโโโโโโโโ
Key Components:
- Proxy Pattern: Each Safe is a minimal proxy delegating to a singleton
- Modules: Optional extensions (recurring payments, social recovery)
- Guards: Transaction validation hooks (spending limits, allowlists)
- Fallback Handler: Handles unknown function calls (ERC-721 receiver)
Concepts You Must Understand First
1. ECDSA Signatures and ecrecover
Before building a multi-sig, you must deeply understand how Ethereum validates signatures.
Key reading:
- โMastering Ethereumโ Chapter 6: Transactions (signature creation and validation)
- EIP-191: Signed Data Standard
- EIP-712: Typed structured data hashing and signing
What to understand:
- How private keys generate signatures
- Why
ecrecoverreturns address(0) on failure - Signature malleability and the low-S requirement
- The difference between
personal_signandsignTypedData
2. EIP-712 Typed Data Signing
EIP-712 is the foundation for secure off-chain signing. Without it, users blindly sign hashes.
Key reading:
- EIP-712 specification: https://eips.ethereum.org/EIPS/eip-712
- MetaMask documentation on signTypedData_v4
What to understand:
- How domain separators prevent cross-contract replay attacks
- How type hashes are computed
- How nested structs are encoded
- The
\x19\x01prefix and its purpose
3. Nonce Management and Replay Protection
Nonces are simple in concept but subtle in implementation.
Key reading:
- โSerious Cryptographyโ Chapter 14: Authenticated Encryption (nonce requirements)
- Gnosis Safe nonce implementation
What to understand:
- Why nonces must be unique per signature
- How chain ID prevents cross-chain replay
- The trade-offs between sequential and hash-based nonces
- Deadline-based expiration as additional protection
4. Access Control Patterns
Multi-sigs are fundamentally access control systems.
Key reading:
- OpenZeppelin AccessControl documentation
- โMastering Ethereumโ Chapter 9: Smart Contract Security
What to understand:
- The difference between
msg.senderand recovered signers - How to prevent owner enumeration issues
- Safe patterns for adding/removing owners
- Time-locked changes for critical operations
5. Gas Estimation for Delegate Calls
Multi-sigs execute arbitrary transactions. Estimating gas accurately is critical.
Key reading:
- Gnosis Safe gas estimation logic
- EIP-150: Gas cost changes for IO-heavy operations
What to understand:
- Why
gasleft()matters for safe execution - The 63/64 rule for call gas
- How to handle gas refunds
- Why gas estimation is harder than you think
6. Proxy Patterns and Delegate Call
Understanding how Safe uses proxies is essential for efficiency.
Key reading:
- EIP-1167: Minimal Proxy Contract
- OpenZeppelin Proxy documentation
What to understand:
- How delegate call preserves context
- Storage layout considerations with proxies
- Initialization patterns for proxy contracts
- Why proxies save gas on deployment
Questions to Guide Your Design
Storage Design
- How will you store owner addresses?
- Array for enumeration? Mapping for O(1) lookup?
- How do you prevent duplicate owners?
- Whatโs the maximum number of owners?
- How will you track pending transactions?
- Mapping from txId to Transaction struct?
- How do you handle large data payloads?
- Do you store signatures on-chain or collect them off-chain?
- How will you track confirmations?
- Mapping from txId to confirmer addresses?
- Counter per transaction?
- How do you prevent double-confirmation?
Signature Verification
- Will you use on-chain or off-chain signature collection?
- On-chain: Each signer submits a transaction (expensive, simple)
- Off-chain: Collect signatures, submit once (cheap, complex)
- Hybrid: Allow both options
- How will you implement EIP-712?
- What types go in your domain separator?
- What fields are in your transaction type?
- How do you handle upgrades (version field)?
- How will you prevent signature malleability?
- Require low-S values?
- Use compact signature format?
- Validate v values strictly?
Transaction Lifecycle
- What states can a transaction be in?
- Pending, Confirmed, Executed, Cancelled?
- Can executed transactions be re-executed? (Should be NO)
- Can cancelled transactions be un-cancelled?
- Who can propose transactions?
- Only owners?
- Anyone, with owner approval required?
- Different tiers for different transaction types?
- How do you handle failed executions?
- Revert entire transaction?
- Mark as failed, allow retry?
- What about partial failures in batch transactions?
Owner Management
- How do you add/remove owners securely?
- Same threshold as regular transactions?
- Higher threshold for governance changes?
- Time-lock for owner changes?
- What happens when an owner is removed?
- Are their pending confirmations revoked?
- Can threshold become impossible (threshold > owner count)?
- How do you handle the last owner removal?
- How do you change the threshold?
- Must new threshold be <= owner count?
- What about in-flight transactions?
- Time-lock considerations?
Security Considerations
- How do you prevent replay attacks?
- Nonce per transaction?
- Chain ID in signature?
- Contract address in domain separator?
- How do you handle reentrancy?
- What if the called contract calls back into the multisig?
- CEI pattern for transaction execution?
- Reentrancy guard on sensitive functions?
- What about front-running?
- Can an attacker see pending signatures and race to submit?
- Does transaction ordering matter?
- How do you handle MEV?
Thinking Exercise
Before writing code, work through these scenarios on paper:
Exercise 1: Trace a Complete Transaction
Setup: A 2-of-3 multisig with owners Alice, Bob, and Charlie.
Scenario: Transfer 1 ETH to recipient 0xRecipient.
Trace through:
- Alice proposes the transaction. What data is stored? What events are emitted?
- Bob reviews and signs off-chain. What does he sign exactly (EIP-712 format)?
- Alice collects Bobโs signature and submits for execution. What verification happens?
- The execution succeeds. What state changes? What events?
- Charlie tries to confirm after execution. What happens?
Exercise 2: Invalid Signature Scenarios
For each scenario, determine what should happen and why:
- Signature from non-owner address
- Valid signature but wrong nonce
- Valid signature but for different chain
- Duplicate signature from same owner
- Signature with high-S value (malleability)
- Empty signature bytes
- Signature from a removed owner (removed after signing, before execution)
Exercise 3: Owner Rotation
Setup: 2-of-3 multisig with Alice, Bob, Charlie.
Scenario: Replace Bob with Dave.
Trace through:
- Whatโs the safest order of operations?
- Can this be done in one transaction or multiple?
- What happens to Bobโs pending confirmations?
- What if Bob has signed a transaction that hasnโt executed yet?
- Could this be exploited? (Hint: What if threshold is changed simultaneously?)
Real-World Outcome
Comprehensive Command-Line Demonstration
# ================================================================
# PART 1: DEPLOYMENT
# ================================================================
# Deploy a 2-of-3 multisig with initial owners
$ forge create src/MultiSigWallet.sol:MultiSigWallet \
--constructor-args \
"[0xA1c3...Alice, 0xB0b5...Bob, 0xC4a3...Charlie]" \
2 \
--rpc-url $RPC_URL \
--private-key $DEPLOYER_KEY
Deployer: 0xDe91...
Deployed to: 0x7890...MultiSig
Transaction hash: 0xabc123...
# Verify the initial setup
$ cast call $MULTISIG "getOwners()" --rpc-url $RPC_URL
[0xA1c3...Alice, 0xB0b5...Bob, 0xC4a3...Charlie]
$ cast call $MULTISIG "threshold()" --rpc-url $RPC_URL
2
$ cast call $MULTISIG "nonce()" --rpc-url $RPC_URL
0
# Fund the multisig
$ cast send $MULTISIG --value 10ether --rpc-url $RPC_URL --private-key $FUNDER_KEY
Transaction hash: 0xdef456...
$ cast balance $MULTISIG --rpc-url $RPC_URL
10000000000000000000 (10 ETH)
# ================================================================
# PART 2: SIMPLE ETH TRANSFER
# ================================================================
# Alice proposes sending 1 ETH to recipient
$ cast send $MULTISIG "submitTransaction(address,uint256,bytes)" \
0xRecipient 1ether 0x \
--rpc-url $RPC_URL \
--private-key $ALICE_KEY
Transaction hash: 0x111...
Logs:
SubmitTransaction(txId=0, owner=0xA1c3...Alice, to=0xRecipient, value=1000000000000000000, data=0x)
ConfirmTransaction(txId=0, owner=0xA1c3...Alice)
# Check transaction status
$ cast call $MULTISIG "transactions(uint256)" 0 --rpc-url $RPC_URL
(0xRecipient, 1000000000000000000, 0x, false, 1)
# (to, value, data, executed, confirmationCount)
# Bob confirms the transaction
$ cast send $MULTISIG "confirmTransaction(uint256)" 0 \
--rpc-url $RPC_URL \
--private-key $BOB_KEY
Transaction hash: 0x222...
Logs:
ConfirmTransaction(txId=0, owner=0xB0b5...Bob)
# Note: 2/2 confirmations reached!
# Check confirmations
$ cast call $MULTISIG "getConfirmations(uint256)" 0 --rpc-url $RPC_URL
[0xA1c3...Alice, 0xB0b5...Bob]
# Execute the transaction (anyone can call after threshold reached)
$ cast send $MULTISIG "executeTransaction(uint256)" 0 \
--rpc-url $RPC_URL \
--private-key $ANYONE_KEY
Transaction hash: 0x333...
Logs:
ExecuteTransaction(txId=0)
# Verify the transfer happened
$ cast balance 0xRecipient --rpc-url $RPC_URL
1000000000000000000 (1 ETH)
$ cast balance $MULTISIG --rpc-url $RPC_URL
9000000000000000000 (9 ETH)
# ================================================================
# PART 3: EIP-712 OFF-CHAIN SIGNATURE FLOW
# ================================================================
# Generate the transaction hash for off-chain signing
$ cast call $MULTISIG "getTransactionHash(address,uint256,bytes,uint256)" \
0xAnotherRecipient 2ether 0x 1 \
--rpc-url $RPC_URL
0x7890abcd...transactionHash
# Alice signs off-chain using EIP-712
$ cast wallet sign --private-key $ALICE_KEY \
--typed-data '{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Transaction": [
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "data", "type": "bytes"},
{"name": "nonce", "type": "uint256"}
]
},
"primaryType": "Transaction",
"domain": {
"name": "MultiSigWallet",
"version": "1",
"chainId": 1,
"verifyingContract": "0x7890...MultiSig"
},
"message": {
"to": "0xAnotherRecipient",
"value": "2000000000000000000",
"data": "0x",
"nonce": "1"
}
}'
Signature (Alice): 0xAliceSig...
# Bob signs the same transaction hash off-chain
$ cast wallet sign --private-key $BOB_KEY [same typed data]
Signature (Bob): 0xBobSig...
# Submit with collected signatures (gasless for signers!)
$ cast send $MULTISIG \
"execTransaction(address,uint256,bytes,bytes)" \
0xAnotherRecipient 2ether 0x \
$(echo $ALICE_SIG$BOB_SIG | sed 's/0x//g') \
--rpc-url $RPC_URL \
--private-key $RELAYER_KEY # Any account can relay
Transaction hash: 0x444...
Logs:
ExecuteTransaction(txId=1)
# ================================================================
# PART 4: OWNER MANAGEMENT
# ================================================================
# Propose adding a new owner (Dave)
$ cast send $MULTISIG "submitTransaction(address,uint256,bytes)" \
$MULTISIG 0 \
$(cast calldata "addOwner(address,uint256)" 0xD4ve...Dave 2) \
--rpc-url $RPC_URL \
--private-key $ALICE_KEY
Transaction hash: 0x555...
Logs:
SubmitTransaction(txId=2, ...)
# Bob confirms
$ cast send $MULTISIG "confirmTransaction(uint256)" 2 \
--rpc-url $RPC_URL \
--private-key $BOB_KEY
# Execute the owner addition
$ cast send $MULTISIG "executeTransaction(uint256)" 2 \
--rpc-url $RPC_URL \
--private-key $ALICE_KEY
Transaction hash: 0x666...
Logs:
ExecuteTransaction(txId=2)
OwnerAdded(owner=0xD4ve...Dave)
# Verify new owner list
$ cast call $MULTISIG "getOwners()" --rpc-url $RPC_URL
[0xA1c3...Alice, 0xB0b5...Bob, 0xC4a3...Charlie, 0xD4ve...Dave]
# Now it's a 2-of-4 multisig
# Propose increasing threshold to 3-of-4
$ cast send $MULTISIG "submitTransaction(address,uint256,bytes)" \
$MULTISIG 0 \
$(cast calldata "changeThreshold(uint256)" 3) \
--rpc-url $RPC_URL \
--private-key $ALICE_KEY
# ... (confirmations and execution)
$ cast call $MULTISIG "threshold()" --rpc-url $RPC_URL
3
# ================================================================
# PART 5: BATCH TRANSACTIONS
# ================================================================
# Execute multiple operations atomically
$ cast send $MULTISIG "submitBatchTransaction(address[],uint256[],bytes[])" \
"[0xRecip1, 0xRecip2, 0xRecip3]" \
"[0.5ether, 0.3ether, 0.2ether]" \
"[0x, 0x, 0x]" \
--rpc-url $RPC_URL \
--private-key $ALICE_KEY
Transaction hash: 0x777...
Logs:
SubmitTransaction(txId=4, ...)
# After confirmations and execution, all three transfers happen atomically
# ================================================================
# PART 6: EMERGENCY SCENARIOS
# ================================================================
# Revoke confirmation before execution
$ cast send $MULTISIG "revokeConfirmation(uint256)" 5 \
--rpc-url $RPC_URL \
--private-key $BOB_KEY
Transaction hash: 0x888...
Logs:
RevokeConfirmation(txId=5, owner=0xB0b5...Bob)
# Remove a compromised owner
# (Alice and Charlie confirm, assuming Bob is compromised)
$ cast send $MULTISIG "submitTransaction(address,uint256,bytes)" \
$MULTISIG 0 \
$(cast calldata "removeOwner(address,uint256)" 0xB0b5...Bob 2) \
--rpc-url $RPC_URL \
--private-key $ALICE_KEY
# After confirmations (need 3 since it's now 3-of-4)...
$ cast call $MULTISIG "getOwners()" --rpc-url $RPC_URL
[0xA1c3...Alice, 0xC4a3...Charlie, 0xD4ve...Dave]
$ cast call $MULTISIG "threshold()" --rpc-url $RPC_URL
2 # Automatically reduced since owners = 3, threshold was 3
Web Interface Description
A production multi-sig wallet would include a web interface with:
Dashboard View:
- Current balance (ETH and ERC-20 tokens)
- List of owners with their addresses
- Current threshold display (e.g., โ2 of 3 signatures requiredโ)
- Pending transactions queue with confirmation status
Transaction Proposal:
- Form for proposing new transactions
- Token selector for ERC-20 transfers
- Advanced mode for arbitrary contract calls
- Gas estimation before submission
Signature Collection:
- QR code for mobile wallet signing
- WalletConnect integration
- Hardware wallet support (Ledger, Trezor)
- Email/notification to other owners
Transaction History:
- Complete audit trail of all executed transactions
- Who confirmed what and when
- Failed execution attempts with error messages
- Export to CSV for accounting
The Interview Questions Theyโll Ask
Multi-Sig Security Model
- Why is M-of-N better than a single private key?
- Expected answer: Eliminates single point of failure, requires collusion to compromise, provides redundancy against key loss, enables organizational decision-making, creates an audit trail.
- Whatโs the difference between on-chain and off-chain signature collection? When would you use each?
- On-chain: Each signer submits a transaction. Simple but expensive (N transactions for N signatures). Use for low-frequency, high-value operations.
- Off-chain: Signatures collected via EIP-712, submitted together. One transaction for all signatures. Use for frequent operations, better UX, lower gas.
- How does a multi-sig prevent a single owner from draining the wallet?
- The smart contract enforces signature verification. Even if one owner calls
executeTransaction, the contract checks that M valid signatures exist. No bypass is possible without M private keys.
- The smart contract enforces signature verification. Even if one owner calls
- What happens if the threshold is set higher than the number of owners?
- The wallet becomes permanently locked. Proper implementations check
require(threshold <= owners.length)before any change.
- The wallet becomes permanently locked. Proper implementations check
Signature Verification Mechanics
- Explain how
ecrecoverworks. What does it return on failure?ecrecoveris a precompile that takes a message hash and signature components (v, r, s), uses ECDSA math to recover the public key, then derives the address. On failure (invalid signature), it returnsaddress(0), NOT revert. This is why you must checkrequire(recovered != address(0)).
- What is signature malleability and how do you prevent it?
- For any valid (r, s), thereโs another valid signature (r, n-s) where n is the curve order. An attacker could create a second valid signature for the same message. Prevention: require s <= n/2 (low-S), or use compact signatures, or simply check that the signer hasnโt already confirmed.
- Why do we use EIP-712 instead of just hashing the transaction data?
- EIP-712 provides human-readable signature requests, preventing phishing. Users see โSend 1 ETH to 0x1234โฆโ not a hex hash. It also includes domain separation (chain ID, contract address) preventing replay across chains and contracts.
- Whatโs the purpose of the domain separator in EIP-712?
- The domain separator includes name, version, chainId, and verifyingContract. This ensures a signature for โMultiSig v1 on Mainnet at 0xABCโ canโt be replayed on โMultiSig v1 on Polygon at 0xABCโ or even โMultiSig v2 on Mainnet at 0xABCโ.
Replay Attack Prevention
- What is a replay attack and how do nonces prevent it?
- Replay attack: Re-submitting a valid signed transaction to execute it multiple times. Nonces prevent this by making each transaction unique. After execution, the nonce is incremented, so the same signature becomes invalid.
- Can a signature for one multi-sig wallet be used on another? Why or why not?
- No, because EIP-712โs domain separator includes
verifyingContract. The hash being signed includes the specific contract address, so a signature for Wallet A is invalid for Wallet B even with identical transaction data.
- No, because EIP-712โs domain separator includes
- How does chain ID prevent cross-chain replay attacks?
- Chain ID is part of the EIP-712 domain separator. A signature for Ethereum mainnet (chainId=1) wonโt validate on Arbitrum (chainId=42161) because the domain separatorโand thus the message hashโdiffers.
- What happens if someone tries to re-submit a transaction with the same nonce after itโs been executed?
- The contract either marks nonces as used (mapping) or requires sequential nonces. Either way, the โusedโ check fails and the transaction reverts.
Key Rotation Strategies
- How would you implement social recovery in a multi-sig?
- Add a recovery mechanism where a supermajority (e.g., 3-of-5) of guardians can replace all owners after a time lock. Guardians could be friends, family, or hardware wallets. The time lock gives legitimate owners time to cancel malicious recovery attempts.
- Whatโs the safe way to remove a compromised owner?
- First, ensure the new configuration will still meet threshold. Submit a removeOwner transaction signed by the remaining honest owners. Execute it, removing the compromised ownerโs ability to participate. Consider also rotating to a new multi-sig if the attacker might have seen pending transactions.
- If the threshold is 3-of-5 and you remove one owner, what should happen to the threshold?
- Option A: Keep threshold at 3 (now 3-of-4). This is stricterโsame number of signatures needed with fewer owners.
- Option B: Automatically reduce threshold proportionally (to 3 or 2). Gnosis Safe keeps the threshold but validates itโs still <= owner count.
- Best practice: Let the owner removal transaction also specify the new threshold.
Gas Optimization
- Why is gas estimation for multi-sig transactions challenging?
- The multi-sig executes arbitrary transactions via
delegatecallorcall. The actual gas depends on what the called contract does, which may depend on current state. The multi-sig overhead (signature verification) is known, but the inner call cost varies.
- The multi-sig executes arbitrary transactions via
- How does the 63/64 rule affect multi-sig execution?
- EIP-150 limits forwarded gas to 63/64 of remaining gas. If your multi-sig has 100k gas, the inner call gets at most ~98.4k. For nested calls, this compounds. You need to account for this when estimating or risk out-of-gas in the inner call.
- What gas optimizations did Gnosis Safe implement?
- Minimal proxy pattern (cheap deployment), packed storage slots, off-chain signature collection, batch transactions, gas tokens (deprecated), and careful ordering of storage operations.
Gnosis Safe Architecture
- Why does Gnosis Safe use the proxy pattern?
- To minimize deployment cost. The proxy is ~100 bytes and delegates all calls to a singleton logic contract. Deploying a new Safe costs ~45k gas instead of ~500k+ for the full implementation.
- What are Safe Modules and when would you use them?
- Modules are extensions that can execute transactions without the normal threshold. Examples: allowance modules (daily spending limits), recovery modules (social recovery), and automation modules (recurring payments). Theyโre powerful but riskyโa malicious module can drain the wallet.
- What is a Guard in Gnosis Safe and how does it differ from a Module?
- Guards are hooks that validate transactions before and after execution. They can block transactions (e.g., โno transfers > 100 ETHโ or โno calls to unapproved contractsโ). Unlike Modules, Guards canโt initiate transactions, only validate them.
Hints in Layers
Layer 1: Basic Storage Structure
Start with the minimum viable data structures:
contract MultiSigWallet {
// Core state
address[] public owners;
mapping(address => bool) public isOwner;
uint256 public threshold;
uint256 public nonce;
// Transaction storage
struct Transaction {
address to;
uint256 value;
bytes data;
bool executed;
}
Transaction[] public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
// Events
event SubmitTransaction(uint256 indexed txId, address indexed owner);
event ConfirmTransaction(uint256 indexed txId, address indexed owner);
event ExecuteTransaction(uint256 indexed txId);
event RevokeConfirmation(uint256 indexed txId, address indexed owner);
}
Layer 2: Basic Signature Verification
Implement simple on-chain confirmation first:
function confirmTransaction(uint256 _txId) public {
require(isOwner[msg.sender], "Not owner");
require(_txId < transactions.length, "Invalid txId");
require(!transactions[_txId].executed, "Already executed");
require(!confirmations[_txId][msg.sender], "Already confirmed");
confirmations[_txId][msg.sender] = true;
emit ConfirmTransaction(_txId, msg.sender);
}
function getConfirmationCount(uint256 _txId) public view returns (uint256 count) {
for (uint256 i = 0; i < owners.length; i++) {
if (confirmations[_txId][owners[i]]) {
count++;
}
}
}
Layer 3: EIP-712 Implementation
Add typed data signing for off-chain signatures:
bytes32 public constant DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 public constant TRANSACTION_TYPEHASH = keccak256(
"Transaction(address to,uint256 value,bytes data,uint256 nonce)"
);
bytes32 public immutable DOMAIN_SEPARATOR;
constructor(...) {
DOMAIN_SEPARATOR = keccak256(abi.encode(
DOMAIN_TYPEHASH,
keccak256("MultiSigWallet"),
keccak256("1"),
block.chainid,
address(this)
));
}
function getTransactionHash(
address _to,
uint256 _value,
bytes memory _data,
uint256 _nonce
) public view returns (bytes32) {
bytes32 structHash = keccak256(abi.encode(
TRANSACTION_TYPEHASH,
_to,
_value,
keccak256(_data),
_nonce
));
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
}
Layer 4: Owner Management
Add secure owner modification:
modifier onlyWallet() {
require(msg.sender == address(this), "Only wallet");
_;
}
function addOwner(address _owner, uint256 _newThreshold) public onlyWallet {
require(_owner != address(0), "Invalid owner");
require(!isOwner[_owner], "Already owner");
require(_newThreshold >= 1 && _newThreshold <= owners.length + 1, "Invalid threshold");
isOwner[_owner] = true;
owners.push(_owner);
threshold = _newThreshold;
emit OwnerAdded(_owner);
emit ThresholdChanged(_newThreshold);
}
function removeOwner(address _owner, uint256 _newThreshold) public onlyWallet {
require(isOwner[_owner], "Not owner");
require(owners.length - 1 >= 1, "Need at least 1 owner");
require(_newThreshold >= 1 && _newThreshold <= owners.length - 1, "Invalid threshold");
isOwner[_owner] = false;
// Remove from array (swap and pop)
for (uint256 i = 0; i < owners.length; i++) {
if (owners[i] == _owner) {
owners[i] = owners[owners.length - 1];
owners.pop();
break;
}
}
threshold = _newThreshold;
emit OwnerRemoved(_owner);
emit ThresholdChanged(_newThreshold);
}
Layer 5: Off-Chain Execution with Signatures
Execute with collected signatures:
function execTransaction(
address _to,
uint256 _value,
bytes calldata _data,
bytes calldata _signatures
) public returns (bool success) {
bytes32 txHash = getTransactionHash(_to, _value, _data, nonce);
// Check signatures
_checkSignatures(txHash, _signatures);
// Increment nonce BEFORE execution (CEI pattern)
nonce++;
// Execute transaction
(success,) = _to.call{value: _value}(_data);
require(success, "Transaction failed");
emit ExecuteTransaction(nonce - 1);
}
function _checkSignatures(bytes32 _hash, bytes calldata _signatures) internal view {
require(_signatures.length >= threshold * 65, "Not enough signatures");
address lastOwner = address(0);
for (uint256 i = 0; i < threshold; i++) {
bytes memory signature = _signatures[i * 65:(i + 1) * 65];
address recovered = _recoverSigner(_hash, signature);
require(isOwner[recovered], "Invalid signer");
require(recovered > lastOwner, "Signatures not sorted"); // Prevents duplicates
lastOwner = recovered;
}
}
Layer 6: Batch Transactions
Support atomic multi-operation execution:
struct Operation {
address to;
uint256 value;
bytes data;
}
function execBatchTransaction(
Operation[] calldata _operations,
bytes calldata _signatures
) public returns (bool success) {
bytes32 txHash = getBatchTransactionHash(_operations, nonce);
_checkSignatures(txHash, _signatures);
nonce++;
for (uint256 i = 0; i < _operations.length; i++) {
(success,) = _operations[i].to.call{value: _operations[i].value}(
_operations[i].data
);
require(success, "Operation failed");
}
emit ExecuteBatchTransaction(nonce - 1, _operations.length);
}
function getBatchTransactionHash(
Operation[] calldata _operations,
uint256 _nonce
) public view returns (bytes32) {
bytes32 operationsHash = keccak256(abi.encode(_operations));
return keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(BATCH_TYPEHASH, operationsHash, _nonce))
));
}
Layer 7: Gas Estimation
Accurate gas estimation for execution:
function estimateGas(
address _to,
uint256 _value,
bytes calldata _data
) external returns (uint256) {
// Temporarily set threshold to 0 to bypass signature checks
uint256 originalThreshold = threshold;
threshold = 0;
uint256 startGas = gasleft();
// Simulate execution
(bool success,) = _to.call{value: _value}(_data);
uint256 endGas = gasleft();
// Restore threshold
threshold = originalThreshold;
if (!success) {
return 0; // Transaction would fail
}
return startGas - endGas + 21000 + _signatures_gas_overhead();
}
Layer 8: Full Gnosis-Style Implementation
For production, add:
- Modules for extensions
- Guards for transaction validation
- Fallback handler for token receiving
- EIP-1271 for contract signature validation
- Gas refund mechanisms
// EIP-1271: Contract Signature Validation
function isValidSignature(bytes32 _hash, bytes calldata _signature)
external view returns (bytes4)
{
if (_checkSignaturesView(_hash, _signature)) {
return 0x1626ba7e; // EIP-1271 magic value
}
return 0xffffffff;
}
// Module execution (risky but powerful)
function execTransactionFromModule(
address _to,
uint256 _value,
bytes calldata _data,
Operation _operation
) external returns (bool success) {
require(enabledModules[msg.sender], "Module not enabled");
if (_operation == Operation.Call) {
(success,) = _to.call{value: _value}(_data);
} else {
(success,) = _to.delegatecall(_data);
}
}
Books That Will Help
| Book | Author(s) | Relevant Chapters | What Youโll Learn |
|---|---|---|---|
| Mastering Ethereum | Andreas M. Antonopoulos & Gavin Wood | Ch. 6: Transactions, Ch. 7: Smart Contracts, Ch. 9: Security | Transaction structure, signature verification, smart contract security patterns |
| Serious Cryptography | Jean-Philippe Aumasson | Ch. 13: Elliptic Curves, Ch. 14: TLS | ECDSA deep dive, why nonces must never be reused, signature schemes |
| Practical Cryptography | Niels Ferguson & Bruce Schneier | Ch. 5: Key Agreement, Ch. 13: Using Cryptography | Threshold schemes, multi-party computation basics |
| Building Secure Smart Contracts | Trail of Bits | All chapters | Security patterns, common vulnerabilities, access control |
| Ethereum Smart Contract Development | Mayukh Mukhopadhyay | Ch. 4: Security Patterns | Practical Solidity security patterns |
Additional Resources
Official Documentation
- Gnosis Safe Contracts: https://github.com/safe-global/safe-contracts
- EIP-712 Specification: https://eips.ethereum.org/EIPS/eip-712
- EIP-191 (Signed Data Standard): https://eips.ethereum.org/EIPS/eip-191
- EIP-1271 (Contract Signature Validation): https://eips.ethereum.org/EIPS/eip-1271
Security Audits (Learn from the experts)
- Safe (Gnosis) Audit Reports: https://github.com/safe-global/safe-contracts/tree/main/docs
- OpenZeppelin Audits: https://blog.openzeppelin.com/security-audits/
- Trail of Bits Publications: https://blog.trailofbits.com/
Tutorials and Guides
- Gnosis Safe Developer Docs: https://docs.safe.global/
- OpenZeppelin Multisig Guide: https://docs.openzeppelin.com/contracts/governance
- Solidity by Example - Multi-Sig Wallet: https://solidity-by-example.org/app/multi-sig-wallet/
Research Papers
- Threshold ECDSA: https://eprint.iacr.org/2019/114.pdf
- Multi-Party Computation in Blockchain: Various papers on MPC-based key management
ASCII Diagrams
Multi-Sig Transaction Flow
MULTI-SIGNATURE TRANSACTION FLOW
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. PROPOSAL PHASE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Alice (Owner 1)
โ
โ submitTransaction(to, value, data)
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MULTISIG CONTRACT โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ transactions[0] = { โ โ
โ โ to: 0xRecipient, โ โ
โ โ value: 1 ETH, โ โ
โ โ data: 0x, โ โ
โ โ executed: false โ โ
โ โ } โ โ
โ โ confirmations[0][Alice] = true โโโโโโโโโ> [1/2] โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. CONFIRMATION PHASE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Bob (Owner 2)
โ
โ confirmTransaction(0)
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MULTISIG CONTRACT โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ confirmations[0][Alice] = true โ โ
โ โ confirmations[0][Bob] = true โโโโโโโโโโ> [2/2] โ โ โ
โ โ โ โ
โ โ THRESHOLD REACHED! โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. EXECUTION PHASE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Anyone (could be owner or relayer)
โ
โ executeTransaction(0)
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MULTISIG CONTRACT โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 1. Verify confirmations >= threshold โ โ
โ โ 2. Mark transaction as executed โ โ
โ โ 3. Execute: to.call{value: 1 ETH}(data) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ 1 ETH
v
โโโโโโโโโโโโโโโโ
โ 0xRecipient โ
โโโโโโโโโโโโโโโโ
Signature Verification Process
EIP-712 SIGNATURE VERIFICATION
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 1: COMPUTE DOMAIN SEPARATOR
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ domainSeparator = keccak256( โ
โ DOMAIN_TYPEHASH, โ
โ keccak256("MultiSigWallet"), // name โ
โ keccak256("1"), // version โ
โ 1, // chainId (mainnet) โ
โ 0x1234...MultiSig // verifyingContract โ
โ ) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
v
[0xABCD...domainSeparator]
STEP 2: COMPUTE STRUCT HASH
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ structHash = keccak256( โ
โ TRANSACTION_TYPEHASH, โ
โ to, // recipient โ
โ value, // ETH amount โ
โ keccak256(data), // calldata hash โ
โ nonce // unique identifier โ
โ ) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
v
[0x5678...structHash]
STEP 3: COMPUTE FINAL HASH
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ "\x19\x01" โ domainSeparator โ structHash
โ (prefix) โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ keccak256()
v
[0x9999...messageHash]
STEP 4: RECOVER SIGNER
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ messageHash โโโโโโโโโ โ
โ โ โ
โ signature โโโโโโโโโโโ โ
โ (r, s, v) โโ โ
โ โผโผ โ
โ โโโโโโโโโโโโโโโโโ โ
โ โ ecrecover โ โ
โ โ (precompile) โ โ
โ โโโโโโโโโโโโโโโโโ โ
โ โ โ
โ v โ
โ [Recovered Address] โ
โ โ โ
โ v โ
โ โโโโโโโโโโโโโโโโโ โ
โ โ isOwner[addr] โ โโโ> true/false โ
โ โโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Owner Quorum Visualization
MULTI-SIG QUORUM EXAMPLES
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
2-of-3 MULTISIG (Common for small teams)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ
โ Alice โ โ Bob โ โ Charlie โ
โ (Owner) โ โ (Owner) โ โ (Owner) โ
โโโโโโฌโโโโโ โโโโโโฌโโโโโ โโโโโโฌโโโโโ
โ โ โ
โ โโโโโโโโโโโโโโผโโโโโโโโโโโโโ โ
โโโโโค โ โโโโโ
โ โ โ
v v v
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2 of 3 Required โ
โ โ
โ Alice + Bob โโโ> โ โ
โ Alice + Charlie โโโ> โ โ
โ Bob + Charlie โโโ> โ โ
โ Alice alone โโโ> โ โ
โ Bob alone โโโ> โ โ
โ Charlie alone โโโ> โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
3-of-5 MULTISIG (Enterprise treasury)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโ โโโโโโโโโ โโโโโโโโโ โโโโโโโโโ โโโโโโโโโ
โ CEO โ โ CFO โ โ CTO โ โ Legal โ โ Board โ
โโโโโฌโโโโ โโโโโฌโโโโ โโโโโฌโโโโ โโโโโฌโโโโ โโโโโฌโโโโ
โ โ โ โ โ
โโโโโโโโโโโดโโโโโฌโโโโโดโโโโโโโโโโดโโโโโโโโโโ
โ
v
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3 of 5 Required โ
โ โ
โ Possible combos: 10 โ
โ (5 choose 3) โ
โโโโโโโโโโโโโโโโโโโโโโโโโ
OWNER CHANGE FLOW (Adding Dave, Removing Bob)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Before: 2-of-3 [Alice, Bob, Charlie]
Step 1: addOwner(Dave, threshold=2)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ
โ Alice โ โ Bob โ โ Charlie โ โ Dave โ
โ [โ] โ โ [โ] โ โ โ โ NEW โ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ
Now: 2-of-4
Step 2: removeOwner(Bob, threshold=2)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ
โ Alice โ โ Charlie โ โ Dave โ
โ [โ] โ โ [โ] โ โ โ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ
Now: 2-of-3 [Alice, Charlie, Dave]
โ ๏ธ DANGER: Simultaneous operations!
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
What if threshold change and owner removal happen together?
Bad: changeThreshold(3) THEN removeOwner(Bob)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
After step 1: 3-of-4 [Alice, Bob, Charlie, Dave]
After step 2: 3-of-3 [Alice, Charlie, Dave]
All owners must agree for EVERY tx!
Safe: Include new threshold in removeOwner
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
removeOwner(Bob, newThreshold=2)
Result: 2-of-3 [Alice, Charlie, Dave] โ
Self-Assessment Checklist
Before moving to the next project, verify:
Conceptual Understanding
- I can explain why M-of-N multi-sig is more secure than a single private key
- I understand how
ecrecoverworks and why it returns address(0) on failure - I can explain EIP-712โs domain separator and why it prevents cross-contract replay
- I understand why nonces must be unique and how they prevent replay attacks
- I can trace through a complete transaction from proposal to execution
Implementation Skills
- My multi-sig correctly verifies ECDSA signatures using
ecrecover - Iโve implemented EIP-712 typed data signing
- My nonce management prevents both same-nonce and cross-chain replay
- Owner management (add/remove) works safely without locking funds
- Batch transactions execute atomically
Security Knowledge
- I know how to prevent signature malleability attacks
- I understand reentrancy risks in multi-sig execution and have mitigated them
- Iโve considered front-running and MEV in my design
- My implementation prevents threshold from exceeding owner count
- I understand the trade-offs of on-chain vs off-chain signature collection
Real-World Understanding
- I can explain Gnosis Safeโs proxy architecture and why itโs gas-efficient
- I understand what Modules and Guards are and when to use them
- I know the historical attacks on multi-sigs (Parity, etc.) and how to prevent them
- I can design a social recovery mechanism for a multi-sig
Whatโs Next?
With multi-signature wallets mastered, you now understand one of the most critical security primitives in Web3. Multi-sigs protect billions of dollars in protocol treasuries, bridge contracts, and corporate wallets.
In Project 17: Flash Loan Implementation, youโll explore the other end of the trust spectrumโtransactions that must complete atomically within a single block. Youโll learn how flash loans enable arbitrage, liquidations, and complex DeFi operations without upfront capital. This is where youโll see why multi-sigs are essential: they protect against the very attacks that flash loans enable.
Remember: In Web3, you donโt just write codeโyou write code that controls money. A bug in a multi-sig isnโt a 500 error; itโs potentially millions of dollars lost forever. Test thoroughly, audit carefully, and never deploy to mainnet without professional review.