MultiSig Timelock

First Flight #55
Beginner FriendlyWallet
100 EXP
Submission Details
Impact: high
Likelihood: low

L02. Unrestricted renounceRole Enables Signers to Bypass Governance Controls and Corrupt Quorum Accounting

Author Revealed upon completion

Root + Impact

Unrestricted renounceRole Allows Signers to Corrupt Signer Accounting and Permanently Break Quorum

Description

  • The protocol relies on OpenZeppelin AccessControl to manage signing permissions and maintains its own internal signer accounting (s_signerCount and signer array) to enforce quorum and execution rules.

  • Signer removal is expected to occur exclusively through the revokeSigningRole function, which enforces invariants such as preventing the removal of the last signer and maintaining internal counters.

  • The issue is that signers can independently call renounceRole(SIGNING_ROLE, msg.sender), bypassing revokeSigningRole.

  • This causes the AccessControl role state to change without updating internal signer tracking, leading to inconsistent signer counts, broken quorum logic, and potential permanent denial of service.

// @> OpenZeppelin allows self-removal without restriction
function renounceRole(bytes32 role, address account) public virtual override {
require(account == _msgSender(), "AccessControl: can only renounce roles for self");
_revokeRole(role, account);
}
// @> Internal signer accounting is NOT updated
uint256 private s_signerCount;
address[] private s_signers;
// @> Invariants enforced only here, not in renounceRole
function revokeSigningRole(address _account) external onlyOwner {
...
s_signerCount -= 1;
}

Risk

Likelihood:

  • Signers may voluntarily exit governance or rotate keys during normal protocol operation.

  • Multisig participants can renounce roles without coordination or owner approval.

Impact:

  • Internal signer count diverges from actual signer set, corrupting quorum enforcement.

  • Transactions become permanently unexecutable due to unreachable confirmation thresholds.


Proof of Concept

This PoC demonstrates that a signer can renounce their signing role without updating internal signer tracking:

  1. A multisig is initialized with multiple signers.

  2. One signer calls renounceRole to remove themselves.

  3. The contract still believes the signer exists due to unchanged internal counters.

  4. New transactions fail to reach quorum despite all active signers confirming.

This results in a permanent governance deadlock without owner intervention.

PoC Code

function testSignerCanRenounceRoleAndBreakQuorum() public grantSigningRoles {
// Initial signer count is 5
assertEq(multiSigTimelock.getSignerCount(), 5);
// Signer renounces role directly
vm.prank(SIGNER_FIVE);
multiSigTimelock.renounceRole(
multiSigTimelock.getSigningRole(),
SIGNER_FIVE
);
// AccessControl no longer recognizes signer
assertFalse(
multiSigTimelock.hasRole(
multiSigTimelock.getSigningRole(),
SIGNER_FIVE
)
);
// Internal signer count remains unchanged
assertEq(multiSigTimelock.getSignerCount(), 5);
// Propose new transaction
vm.deal(address(multiSigTimelock), 10 ether);
vm.prank(OWNER);
uint256 txnId =
multiSigTimelock.proposeTransaction(SPENDER_ONE, 1 ether, hex"");
// Only 4 real signers can confirm
vm.prank(OWNER);
multiSigTimelock.confirmTransaction(txnId);
vm.prank(SIGNER_TWO);
multiSigTimelock.confirmTransaction(txnId);
vm.prank(SIGNER_THREE);
multiSigTimelock.confirmTransaction(txnId);
vm.prank(SIGNER_FOUR);
multiSigTimelock.confirmTransaction(txnId);
// Execution fails permanently due to broken quorum assumptions
vm.prank(OWNER);
vm.expectRevert(
MultiSigTimelock.MultiSigTimelock__InsufficientConfirmations.selector
);
multiSigTimelock.executeTransaction(txnId);
}

Recommended Mitigation

Signer removal must be fully controlled and state-synchronized.

Disable Self-Removal for Signers

Function: renounceRole(bytes32,address)

Function renounceRole
- allow renounceRole for SIGNING_ROLE
+ revert when role == SIGNING_ROLE and instruct to use revokeSigningRole

This ensures all signer removals go through invariant-preserving logic and prevents silent quorum corruption.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!