The bug in one sentence

The multisig hashed the transaction parameters and the nonce — but not the chain ID — so a signature collected on mainnet was a valid signature everywhere.

// what was signed
keccak256(abi.encode(to, value, data, nonce));

// what should have been signed
keccak256(abi.encode(to, value, data, nonce, block.chainid));

Why it matters

Multisig owners often deploy the same wallet address across many chains. If an owner ever signs a payout on chain A, an attacker who controls a deployment on chain B can replay those exact signatures to authorize the same transfer there — draining a wallet the owners never intended to touch.

A signature is only as scoped as the data inside it. Leave out the domain and you've signed a blank cheque for every chain at once.

Reproducing it

  1. Collect a threshold of valid signatures for a transfer on a fork.
  2. Replay the identical calldata and signatures against the same wallet address on the live chain.
  3. The contract recovers the same signers, hits threshold, and executes.

The fix

Adopt EIP-712 with a domain separator that binds chainId and the verifying contract address. Recompute the separator if block.chainid changes (post-fork) rather than caching it at deploy time. That single field collapses the entire cross-chain replay surface.

Takeaway

When you review any signing scheme, the first question is always: what is actually inside the signed bytes? Everything not bound there is replayable.