All audits
PassedEthereumIndependent

Gnosis Safe Multisig

Smart contract security audit by TierZero · 2023-06-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

0
Critical
0
High
2
Medium
4
Low
5
Informational

What it is

Gnosis Safe (now shipped as Safe{Wallet}, contracts at safe-global/safe-contracts) is the de facto standard smart contract wallet on Ethereum and every major EVM chain. It is not a cryptographic threshold signature scheme — there is no distributed key generation and no single aggregated signature. It is an n-of-m quorum contract: a fixed owner list, a threshold count, and an execTransaction entry point that accepts a batch of individually-produced ECDSA (or contract) signatures, checks that at least threshold of them come from distinct, registered owners, and only then executes the call.

Around that core sit two extension points that account for most of the ecosystem built on top of Safe: modules, which are external contracts pre-authorized to call execTransactionFromModule and execute without collecting owner signatures at all, and guards, which hook checkTransaction before execution and checkAfterExecution after, letting an integrator impose policy (spending limits, allowlists, timelocks) without touching the core signature-verification path. This separation — verification in the singleton, policy in modules/guards — is the part of the design worth studying, independent of Safe's own track record.

Threat model

The properties an attacker wants to break:

  1. Forge or reduce quorum — execute a transaction with fewer than threshold valid, distinct owner signatures.
  2. Replay a valid signature set — reuse a signed payload on another chain, another Safe instance, or against the same Safe after state has changed.
  3. Abuse the module path — get a module enabled without going through the owner quorum, or get an already-enabled module to execute something outside its intended scope.
  4. Neutralize a guard — cause checkTransaction/checkAfterExecution to be skipped or to fail open.
  5. Corrupt the proxy — collide storage between the minimal proxy and the singleton implementation it delegates to, or hijack the singleton pointer.

Why the design holds

Replay protection is domain-separated, not just nonce-based. The signed struct is an EIP-712 typed hash that includes chainId and the Safe's own address (verifyingContract) inside the domain separator, plus a monotonically incrementing nonce inside the message. That combination kills three replay vectors at once: cross-chain (chainId bound), cross-contract (verifyingContract bound), and same-chain replay after the transaction already executed (nonce bound and incremented atomically in the same call that consumes the signatures).

// simplified from GnosisSafe.checkSignatures
for (i = 0; i < threshold; i++) {
    currentOwner = recoverSigner(dataHash, signatures, i);
    require(currentOwner > lastOwner, "signatures not sorted / duplicate");
    require(owners[currentOwner] != address(0), "not an owner");
    lastOwner = currentOwner;
}

The strictly-ascending ordering requirement is the quiet workhorse here: it is an O(threshold) way to reject duplicate signers without a set data structure, and it is the reason "just resubmit the same signature twice" doesn't satisfy quorum.

Modules bypass signatures by design, but enabling a module does not. enableModule is itself gated behind execTransaction, i.e., behind the owner threshold. Once enabled, a module can call execTransactionFromModule with no owner signature — that's the entire point of a module (session keys, recovery, automation) — but the attack surface is pushed to "can this module be tricked into calling something it shouldn't," which is a scoping problem for the module author, not a gap in the Safe core. This is exactly the same trust-boundary shape as external call authorization in other execution environments — we've written about the analogous problem of validating the calling context in cross-program invocation risk on Solana DeFi programs, and the lesson transfers directly: the core contract's job is to gate the grant, not to police everything the grantee later does.

Guards fail closed, not open. In the current major version, if a guard is set and its checkTransaction or checkAfterExecution call reverts, the whole outer transaction reverts. A guard that isn't there at all is a legitimate no-op state; a guard that reverts stops execution rather than silently passing it through.

Signature type disambiguation avoids malleability and mixed-mode confusion. The v byte of each signature entry is overloaded to select among ecrecover'd ECDSA (v >= 27), pre-approved on-chain hashes (v == 1, checked against approvedHashes[owner][hash]), and EIP-1271 contract signatures (v == 0, delegates to isValidSignature on the claimed signer contract). Each path is fully separated in the verification loop, so a signature crafted for one mode cannot be reinterpreted as another.

Where implementers get it wrong

None of this is a knock on Safe's core — it's the second- and third-order integration decisions that produce incidents:

  • Overbroad module scoping. A module written to "let this relayer pay gas for the user" that doesn't constrain to/value/data is a full owner-equivalent backdoor. Zodiac's Roles Modifier exists specifically because raw execTransactionFromModule access is too coarse for anything beyond a fully trusted automation contract.
  • Guards added as an afterthought without reentrancy analysis. A guard that reads state the transaction itself is about to mutate can be checking against a value that's already stale by the time checkAfterExecution runs.
  • Fallback handler sprawl. Extending DefaultCallbackHandler to support more token standards is common; each added selector is new attack surface reachable without going through execTransaction at all.
  • delegatecall operations in MultiSend batches. The operation field allowing delegatecall is powerful (atomic batched calls) and dangerous — a delegatecall target controls Safe storage directly. Treat any delegatecall target the same way you'd treat a proposed core-logic upgrade, at the same review depth we apply in a smart contract audit.
  • Off-chain nonce races in multi-proposer setups. Two owners proposing transactions concurrently against the same nonce is a UX and signature-invalidation problem more than a security hole, but it produces "why did my transaction disappear" support tickets that get miscategorized as bugs.

Takeaways

Safe's core is narrow and has stayed narrow on purpose: sorted-signature quorum checks, a domain-separated EIP-712 hash, and a hard authorization gate in front of every extension point. The incidents that do happen in the ecosystem cluster almost entirely in the modules, guards, and fallback handlers that projects bolt on afterward — components that get far less scrutiny than the core and that inherit full owner-equivalent power if scoped carelessly. If you're shipping a custom module, guard, or signing flow around a Safe deployment — including EIP-712/permit-based flows adjacent to it, the kind we cover in gasless approvals and EVM sniping bot exposure with Permit2 — that's the code that needs independent eyes, not the audited core it plugs into. That's the kind of review our code review and audit service is built around, and it's where we'd point you before mainnet.

Reach out through our smart contract audit service if you want a second set of eyes on a Safe module, guard, or integration before it goes live.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit