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:

  1. Master ECDSA signature verification understanding how ecrecover extracts signer addresses from signatures and why this is fundamental to blockchain authentication
  2. Implement EIP-712 typed data signing creating human-readable, phishing-resistant signature requests that work across all major wallets
  3. Design robust nonce management preventing replay attacks across chains and within transactions
  4. Build owner management systems enabling secure addition, removal, and rotation of signers without compromising funds
  5. Understand threshold cryptography concepts learning why M-of-N schemes eliminate single points of failure
  6. Master gas estimation for delegate calls enabling accurate execution cost prediction for arbitrary transactions
  7. 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 curve
  • s: A value computed from the message hash, private key, and random point
  • v: 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:

  1. Defining a structured data format
  2. Including domain separation (contract address, chain ID)
  3. 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:

  1. Proxy Pattern: Each Safe is a minimal proxy delegating to a singleton
  2. Modules: Optional extensions (recurring payments, social recovery)
  3. Guards: Transaction validation hooks (spending limits, allowlists)
  4. 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 ecrecover returns address(0) on failure
  • Signature malleability and the low-S requirement
  • The difference between personal_sign and signTypedData

2. EIP-712 Typed Data Signing

EIP-712 is the foundation for secure off-chain signing. Without it, users blindly sign hashes.

Key reading:

What to understand:

  • How domain separators prevent cross-contract replay attacks
  • How type hashes are computed
  • How nested structs are encoded
  • The \x19\x01 prefix 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.sender and 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

  1. 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?
  2. 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?
  3. How will you track confirmations?
    • Mapping from txId to confirmer addresses?
    • Counter per transaction?
    • How do you prevent double-confirmation?

Signature Verification

  1. 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
  2. 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)?
  3. How will you prevent signature malleability?
    • Require low-S values?
    • Use compact signature format?
    • Validate v values strictly?

Transaction Lifecycle

  1. 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?
  2. Who can propose transactions?
    • Only owners?
    • Anyone, with owner approval required?
    • Different tiers for different transaction types?
  3. How do you handle failed executions?
    • Revert entire transaction?
    • Mark as failed, allow retry?
    • What about partial failures in batch transactions?

Owner Management

  1. How do you add/remove owners securely?
    • Same threshold as regular transactions?
    • Higher threshold for governance changes?
    • Time-lock for owner changes?
  2. 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?
  3. How do you change the threshold?
    • Must new threshold be <= owner count?
    • What about in-flight transactions?
    • Time-lock considerations?

Security Considerations

  1. How do you prevent replay attacks?
    • Nonce per transaction?
    • Chain ID in signature?
    • Contract address in domain separator?
  2. 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?
  3. 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:

  1. Alice proposes the transaction. What data is stored? What events are emitted?
  2. Bob reviews and signs off-chain. What does he sign exactly (EIP-712 format)?
  3. Alice collects Bobโ€™s signature and submits for execution. What verification happens?
  4. The execution succeeds. What state changes? What events?
  5. Charlie tries to confirm after execution. What happens?

Exercise 2: Invalid Signature Scenarios

For each scenario, determine what should happen and why:

  1. Signature from non-owner address
  2. Valid signature but wrong nonce
  3. Valid signature but for different chain
  4. Duplicate signature from same owner
  5. Signature with high-S value (malleability)
  6. Empty signature bytes
  7. 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:

  1. Whatโ€™s the safest order of operations?
  2. Can this be done in one transaction or multiple?
  3. What happens to Bobโ€™s pending confirmations?
  4. What if Bob has signed a transaction that hasnโ€™t executed yet?
  5. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Signature Verification Mechanics

  1. Explain how ecrecover works. What does it return on failure?
    • ecrecover is 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 returns address(0), NOT revert. This is why you must check require(recovered != address(0)).
  2. 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.
  3. 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.
  4. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. 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.
  2. 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.
  3. 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

  1. Why is gas estimation for multi-sig transactions challenging?
    • The multi-sig executes arbitrary transactions via delegatecall or call. 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.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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

Security Audits (Learn from the experts)

Tutorials and Guides

Research Papers


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 ecrecover works 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.