MultiSig Timelock

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

Quorum Deadlock

Root + Impact

Description

The MultiSigTimelock contract has a hardcoded requirement of 3 confirmations to execute any transaction (REQUIRED_CONFIRMATIONS = 3). However, the revokeSigningRole function allows the owner to remove signers until only 1 remains (s_signerCount <= 1 check).

If the number of signers is reduced below 3 (e.g., to 2), it becomes mathematically impossible to reach the required 3 confirmations. This effectively bricks the wallet, permanently freezing all funds and operations, as no new transaction can ever be executed. This violates the safety guarantee stated in the README ("prevents bricking the wallet").

/// File: src/MultiSigTimelock.sol:90
uint256 private constant REQUIRED_CONFIRMATIONS = 3;
/// File: src/MultiSigTimelock.sol:215-216
if (s_signerCount <= 1) {
revert MultiSigTimelock__CannotRevokeLastSigner();
}

Risk

Likelihood: Medium (Requires owner to revoke signers, but is a permitted action).
Impact: Critical (Permanent loss of funds/protocol freeze).

Proof of Concept

  1. Deploy MultiSigTimelock.

  2. Grant roles to Alice and Bob (Total signers = 3: Owner, Alice, Bob).

  3. Revoke Alice (Total = 2).

  4. Propose a transaction.

  5. Owner and Bob confirm (Total confirmations = 2).

  6. Try to execute. Parameters: executeTransaction(id).

  7. Transaction reverts with MultiSigTimelock__InsufficientConfirmations, and can never succeed.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {MultiSigTimelock} from "../../src/MultiSigTimelock.sol";
contract TestDeadlockAndPermissions is Test {
MultiSigTimelock public multiSig;
address public deployer;
address public alice;
address public bob;
address public charlie;
address public diana;
function setUp() public {
deployer = makeAddr("deployer");
alice = makeAddr("alice");
bob = makeAddr("bob");
charlie = makeAddr("charlie");
diana = makeAddr("diana");
vm.prank(deployer);
multiSig = new MultiSigTimelock();
}
function test_Deadlock_IfSignersReducedBelow3() public {
vm.startPrank(deployer);
// 1. Setup: Add signers up to 4 total (Deployer + Alice + Bob + Charlie)
multiSig.grantSigningRole(alice);
multiSig.grantSigningRole(bob);
multiSig.grantSigningRole(charlie);
assertEq(multiSig.getSignerCount(), 4);
// 2. Revoke Charlie and Bob. Count becomes 2 (Deployer + Alice)
multiSig.revokeSigningRole(charlie);
multiSig.revokeSigningRole(bob);
assertEq(multiSig.getSignerCount(), 2);
// 3. Propose a transaction
// Value 0.5 ETH (< 1 ETH) for 0 delay
uint256 txId = multiSig.proposeTransaction(address(0x123), 0.5 ether, "");
vm.stopPrank();
// 4. Confirm with all remaining signers (Deployer + Alice)
vm.prank(deployer);
multiSig.confirmTransaction(txId);
vm.prank(alice);
multiSig.confirmTransaction(txId);
// Check confirmations
uint256 confirmations = multiSig.getTransaction(txId).confirmations;
assertEq(confirmations, 2);
// 5. Try to execute - Should Fail
// We have 2 confirmations, but need 3. No one else can sign.
vm.deal(address(multiSig), 1 ether);
vm.prank(deployer);
vm.expectRevert(); // Expect insufficient confirmations
multiSig.executeTransaction(txId);
}
function test_PermissionMismatch_SignerCannotPropose() public {
vm.prank(deployer);
multiSig.grantSigningRole(alice);
vm.prank(alice);
// Expect revert because proposeTransaction is onlyOwner, despite README saying any signer
vm.expectRevert();
multiSig.proposeTransaction(address(0x123), 0.5 ether, "");
}
}

Recommended Mitigation

Update the check in revokeSigningRole to ensure the signer count does not drop below the required confirmations.

- if (s_signerCount <= 1) {
+ if (s_signerCount <= REQUIRED_CONFIRMATIONS) {
revert MultiSigTimelock__CannotRevokeLastSigner();
}
Updates

Lead Judging Commences

kelechikizito Lead Judge 4 days ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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

Give us feedback!