Description:
MultiSigTimelock keeps its own signer bookkeeping (s_signerCount, s_signers, s_isSigner) in addition to OpenZeppelin AccessControl role membership. Signer permissions are enforced with onlyRole(SIGNING_ROLE), but signer management (max 5 slots, s_signerCount, etc.) is enforced using the custom bookkeeping.
Because AccessControl.renounceRole() is publicly callable by any role-holder, a signer can renounce SIGNING_ROLE. This removes their role (so they can no longer confirm/execute), but does not update s_signerCount, s_signers, or s_isSigner. As a result:
The contract may believe it has 5 signers even when fewer actually have SIGNING_ROLE.
The owner cannot add replacements once s_signerCount == 5 (reverts with MultiSigTimelock__MaximumSignersReached()).
If enough signers renounce such that <3 remain, the wallet can become unexecutable, freezing ETH indefinitely.
Relevant code:
Role checks for confirmations/execution use onlyRole(SIGNING_ROLE)
Signer slot accounting uses s_signerCount/s_isSigner in grantSigningRole()
Impact:
A malicious or compromised signer can cause a permanent denial-of-service by renouncing, especially when the signer set is full (5/5). If fewer than 3 role-holders remain, no transaction can ever reach quorum, effectively freezing all funds. This is a single-signer DoS at maximum signer capacity.
Proof of Concept:
Mitigation:
Simplest (recommended): Remove s_signers, s_isSigner, s_signerCount entirely and rely on AccessControl only (keeping a separate enumerable list if needed).
Or: Override renounceRole() for SIGNING_ROLE to either:
revert (disallow renouncing), or
update s_signerCount/s_signers/s_isSigner consistently (swap-and-pop), freeing a slot.
Additionally, consider using AccessControlEnumerable if you need on-chain enumeration of role members.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.