Back to blog
2026-07-148 min read

How a Multisig Wallet Actually Works

A walkthrough of the model behind Safe-style multisig wallets, a minimal Solidity implementation, and the pitfalls that make you not want to write your own.

MultisigSoliditySafeSecurity

The Problem a Multisig Solves

A single private key is a single point of failure. If it leaks, the wallet empties. If it is lost, the wallet freezes. A multisig spreads that risk across several keys: a wallet that requires 2 of 3 owners to sign any transaction.

The term comes from the signers, not the addresses. A 2-of-3 wallet is controlled by three keys. Moving funds requires signatures from any two of them.

The Model

A multisig wallet is a smart contract with two pieces of state:

  • Owners: the list of addresses that hold a key
  • Threshold: how many of them must sign for a transaction to execute

Every transfer goes through the same lifecycle: propose, collect signatures, execute. The contract does not move funds until the threshold is met.

A Minimal Implementation

This is the smallest multisig that does the job. It is educational, not production code. There is no replay protection and no way to change owners, and that matters later.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MinimalMultiSig {
  address[] public owners;
  uint256 public threshold;
  mapping(bytes32 => mapping(address => bool)) public signed;
  mapping(bytes32 => bool) public executed;

  constructor(address[] memory _owners, uint256 _threshold) {
    require(_owners.length >= _threshold, "threshold too high");
    owners = _owners;
    threshold = _threshold;
  }

  // Encode the transfer, hash it, collect signatures, then call this.
  function execute(
    address to,
    uint256 value,
    bytes calldata data,
    bytes[] calldata signatures
  ) external {
    bytes32 txHash = keccak256(abi.encode(to, value, data));
    require(!executed[txHash], "already executed");

    uint256 count = 0;
    for (uint256 i = 0; i < signatures.length; i++) {
      address signer = recoverSigner(txHash, signatures[i]);
      if (signed[txHash][signer]) continue; // no double counting
      if (!isOwner(signer)) continue;
      signed[txHash][signer] = true;
      count++;
    }
    require(count >= threshold, "not enough signatures");

    executed[txHash] = true;
    (bool ok, ) = to.call{value: value}(data);
    require(ok, "execution failed");
  }
}

To make a transfer, each signer signs the hash keccak256(abi.encode(to, value, data)) with their key. The last signer submits all signatures and the call executes. That is the whole mechanism.

Why You Use Safe Instead

The minimal version above has problems that are not obvious until money is in it:

  • Replay across chains: the same transaction hash can be valid on another chain that runs the same code. Production multisigs include a chain ID and nonce in the signed hash.
  • No owner changes: you cannot add an owner or change the threshold, so key rotation is impossible.
  • Phishing surface: an owner signs a hash without seeing the decoded transaction. Real wallets decode and display the transaction before signing.

Safe (formerly Gnosis Safe) solves these: EIP-712 typed data, a nonce per transaction, owner and threshold management, and modules for recovery and automation. It is audited, and its contract code runs on most EVM chains.

Creating a Safe With the SDK

The @safe-global/protocol-kit package handles deployment and transaction building:

import Safe from "@safe-global/protocol-kit";

const owners = [
  "0x1111111111111111111111111111111111111111",
  "0x2222222222222222222222222222222222222222",
  "0x3333333333333333333333333333333333333333",
];

const safe = await Safe.init({
  provider: "https://rpc.testnet.monad.xyz",
  signer: signerAddress,
  safeAddress: undefined, // undefined deploys a new one
});

const deployment = await safe.createSafe({ owners, threshold: 2 });
console.log("safe at", deployment.safeAddress);

Deploy on a testnet first. Send test funds, add an owner, remove an owner, make a 2-of-3 transfer. The failure modes are cheap to learn there.

What to Watch For

  • Threshold changes: lowering the threshold to 1 defeats the purpose. Some protocols require the threshold to never drop below 2.
  • Losing a key: with 2-of-3 you survive one lost key. With 3-of-5 you survive two. Keep the math honest about how many keys can fail.
  • Signer hygiene: a multisig is only as good as its weakest signer. A hardware key per signer is the standard setup.

Multisigs are boring by design. The contract does one thing, slowly and with checks. That slowness is the feature.