DEV Community

0xrivet
0xrivet

Posted on

What Your Multisig Threshold Actually Protects

I've been digging into multisig configurations for bridge and protocol security reviews. The threshold gets all the attention — 3-of-5, 4-of-7, whatever. But after checking a few dozen Safes on mainnet, the threshold is rarely the weakest link. There are five other things that determine whether a Gnosis Safe actually protects funds, and most people only check the first one.

This post walks through all of them, with cast commands you can run yourself.


What the threshold does

The threshold sets the minimum number of owner signatures required to execute a transaction through execTransaction(). If threshold is 3 and you have 2 signatures, the call reverts. Simple.

# check threshold and owners
cast call <SAFE> "getThreshold()(uint256)"
cast call <SAFE> "getOwners()(address[])"
Enter fullscreen mode Exit fullscreen mode

This is the part everyone understlse.


What the threshold does NOT protect

1. Modules

This is the biggest blind spot in multisig security.

Safe modules are contracts authorFromModule()`. A module can execute*any transaction from the safe without a single owner signature*. The threshold is irrelevant. The module has its own authority.

`bash

if this returns anything other than an empty array, investigate

cast call "getModulesPagin[],address)" \
0x0000000000000000000000000000000000000001 10
`

Modules are legitimate — timelockation. But a malicious or compromisedmodule is a full bypass of every threshold. Your 7-of-10 means nothing if a module can move funds independently.

2. Guard

A guard contract implements checerExecution(). It adds validation on top of the threshold — restricting destinations, limiting values, blocking certain operations.

The guard address lives at a specific storage slot. If it's 0x00, there's no guard. No additional checks beyond threshold + signatu

`bash

guard storage slot (keccak256("guard_manager.guard.address"))

cast storage \
0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8

0x000...000 = no guard installe

`

A guard can enforce things like "no transfers above X without delay" or "no delegatecalls ever."
Without one, the threshold alone lly isn't enough.

3. Fallback handler

The fallback handler processes annize — EIP-1271 signaturevalidation, token callbacks (onERC721Received, onERC1155Received), anything routed through the fallback() function.

`bash

fallback handler slot (keccak256("fallback_manager.handler.address"))

cast storage \
0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5
`

A compromised fallback handler caion, manipulate token reception logic, or introduce re-entrancy vectors. It runs in the context of calls to the safe, not calls from it — a different attack surface terous.

4. Delegatecall (operation=1)

This is how Bybit lost $1.46B.

execTransaction takes an operaular call. 1` is a delegatecall — the target contract's code executes in the safe's storage context. The target can rewrite ownership, change the threshold, drain everyn.

The signers approved what appearehe transaction payload containedoperation: 1. The target contract ran DELEGATECALL in the safe's context, replacing the implementation with an attacker-chold was met. Every signature wasvalid. It just didn't matter — the signers didn't understand what they were signing.

# execTransaction signature — noteter
# execTransaction(address to, uint256 value, bytes data, uint8 operation,
#   uint256 safeTxGas, uint256 ba
#   address gasToken, address refundReceiver, bytes signatures)
#
# operation: 0 = Call, 1 = DelegateCall
# if your guard doesn't block openg
# set of signers can delegatecall arbitrary code
Enter fullscreen mode Exit fullscreen mode

Without a guard that explicitly bold-meeting quorum of signers —whether compromised, socially engineered, or just not reading the payload — can execute a delegatecall that rewrites the en

5. Organizational independence

Not on-chain, but worth more than

Ronin Bridge ran 5-of-9. Four keyfth was an allowlisted key from agas-free RPC arrangement in November 2021 — never revoked. One compromised developer machine yielded 5 of 9 signatures. The threshold s not.

You can't verify this with castwner addresses share a common fundingsource, were deployed by the same address, or interact with the same contracts. Same deployer funding multiple "independent" si

# check if owners share a funding origin
cast call <SAFE> "getOwners()(add
# for each owner, trace the first inbound ETH transfer
# common source → likely same org
Enter fullscreen mode Exit fullscreen mode

The Safe Safe Checklist

Five things to verify beyond the threshold. I run these on every safe I audit.

# Check Command Red flag
1 modules getModulesPaginated() any unknown contract
2 guard storage slot read
3 fallback storage slot read unverified contract
4 delegatecall guard configll restriction
5 independence owner funding trace common deployer/funder

The full check in one shot:

SAFE="<SAFE_ADDRESS>"

# threshold + owners
cast call $SAFE "getThreshold()(uint256)"
cast call $SAFE "getOwners()(address[])"

# modules (bypass threshold entirely)
cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" \
  0x0000000000000000000000000000000000000001 10

# guard (additional validation layer)
cast storage $SAFE \
  0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8

# fallback handler (processes unknown calls)
cast storage $SAFE \
  0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5
Enter fullscreen mode Exit fullscreen mode

The threshold is the door lock. Modules are the window you forgot to close. The guard is the deadbolt you never installed. The fallback handler is the mail slot big enough to reach through. And delegatecall is the landlord's master key — works even when every lock is engaged.

Check all five. Or don't, and hope nobody else does either.


More posts and tooling on 0xrivet.xyz. The cast snippets above and some other safe-audit scripts are on GitHub. If you're auditing bridge multisigs or building guard contracts, feel free to reach out — always happy to compare notes.
cast call $SAFE "getOwners()(addr

modules (bypass threshold entir

cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" \
0x00000000000000000000000000000

guard (additional validation la

cast storage $SAFE \
0x4a204f620c8c5ccdca3fd54d003ba3c34c8

fallback handler (processes unk

cast storage $SAFE \
0x6c9a6c4a39284e37ed1cf53d337579918d5



---                                                                                          
The threshold is the door lock. Modules are the window you forgot to close. The guard is the deadbolt you never installed. The slot big enough to reach through. And delegatecall is the landlord's master key — works even when every lock is engaged.

Check all five. Or don't, and hope nobody else does either.

---

*More posts and tooling on [0xrivet.xyz](https://0xrivet.xyz). The cast snippets above and some other safe-audit scripts are on [rivet). If you're auditing bridgemultisigs or building guard contracts, feel free to reach out — always happy to compare notes.*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)