MultiSig Timelock

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

[H-1] Quorum-Brick

Author Revealed upon completion

QUORUM-BRICK Vulnerability allows signer count to drop below the required 3-confirmation quorum without safeguards, enabling permanent execution blockage.

Description

  • The revokeSigningRole function allows the owner to remove a signer's role and access, reducing the signer count while preventing the complete elimination of all signers by reverting if the count would drop to zero, ensuring the wallet remains operational with at least one signer.

  • However, the function only checks against dropping to one or fewer signers, allowing revocations that reduce the count to two, which is below the required three confirmations for execution, permanently preventing any transaction from reaching the quorum and thus bricking all future executions without a way to recover unless signers are re-added, but risking total lockup if not addressed promptly.

function revokeSigningRole(address _account) external nonReentrant onlyOwner noneZeroAddress(_account) {
// CHECKS
if (!s_isSigner[_account]) {
revert MultiSigTimelock__AccountIsNotASigner();
}
// Prevent revoking the first signer (would break the multisig), moreover, the first signer is the owner of the contract(wallet)
if (s_signerCount <= 1) { // @> Only prevents drop to 0; allows drop to 2, below REQUIRED_CONFIRMATIONS (3)
revert MultiSigTimelock__CannotRevokeLastSigner();
}
// Find the index of the account in the array
uint256 indexToRemove = type(uint256).max; // Use max as "not found" indicator
for (uint256 i = 0; i < s_signerCount; i++) {
if (s_signers[i] == _account) {
indexToRemove = i;
break;
}
}
// Gas-efficient array removal: move last element to removed position
if (indexToRemove < s_signerCount - 1) {
// Move the last signer to the position of the removed signer
s_signers[indexToRemove] = s_signers[s_signerCount - 1];
}
// Clear the last position and decrement count
s_signers[s_signerCount - 1] = address(0);
s_signerCount -= 1; // @> Decrements without ensuring s_signerCount >= REQUIRED_CONFIRMATIONS post-revoke
s_isSigner[_account] = false;
_revokeRole(SIGNING_ROLE, _account);
}

Risk

Likelihood:

  • During routine role management when the owner revokes a signer from a minimal quorum setup of three or four signers.

  • In response to a compromised signer where quick revocation occurs without considering the impact on remaining quorum.

Impact:

  • Permanent lockup of all funds in the wallet as no transactions can achieve the required three confirmations, turning the contract into a receive-only address with no outflow capability.

  • Loss of wallet usability for the team or organization, potentially requiring a new deployment and fund migration if possible, leading to operational disruptions and potential financial losses from inaccessible assets.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./MultiSigTimelock.sol"; // Assume this is your contract file
contract QuorumBrickPoC {
MultiSigTimelock public wallet;
address public signer2 = address(0x2222222222222222222222222222222222222222);
address public signer3 = address(0x3333333333333333333333333333333333333333);
function setUp() external {
wallet = new MultiSigTimelock(); // Deployer (this) is owner and signer1
vm.deal(address(wallet), 100 ether); // Fund wallet (use vm.deal in Foundry)
wallet.grantSigningRole(signer2);
wallet.grantSigningRole(signer3); // Now 3 signers
}
function exploit() external {
wallet.revokeSigningRole(signer3); // Drops to 2 signers, below quorum
}
function checkBrick() external {
uint256 txnId = wallet.proposeTransaction(address(0x4444444444444444444444444444444444444444), 1 ether, "");
// Simulate max confirms: As owner (1), prank signer2 (2) — can't get 3
// wallet.confirmTransaction(txnId); // Owner confirms
// vm.prank(signer2); wallet.confirmTransaction(txnId);
// Attempt execute: Reverts with InsufficientConfirmations(3, 2)
}
}
// In Foundry: forge test --match-test checkBrick
// Expected: After exploit, any executeTransaction reverts due to max confirmations = 2 < 3.

Recommended Mitigation

- if (s_signerCount <= 1) {
+ if (s_signerCount <= REQUIRED_CONFIRMATIONS) {
revert MultiSigTimelock__CannotRevokeBelowQuorum(); // New custom error: error CannotRevokeBelowQuorum();
revert MultiSigTimelock__CannotRevokeLastSigner();
}

Support

FAQs

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

Give us feedback!