MultiSig Timelock

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

M01. Signer Revocation Does Not Invalidate Existing Transaction Confirmations

Author Revealed upon completion

Root + Impact

Description

  • Under normal operation, each signer may confirm a pending transaction, and the transaction becomes executable once the required confirmation threshold is reached.

  • Signers can be revoked by the owner to reflect governance changes, key rotations, or compromised accounts.

  • The issue is that revoking a signer does not invalidate or remove their past confirmations, allowing approvals from revoked signers to continue counting toward execution.

  • This breaks the assumption that only current signers influence transaction execution and allows outdated or unauthorized approvals to affect protocol state.

// @> Confirmation is stored per signer address
mapping(uint256 => mapping(address => bool)) private s_isConfirmed;
// @> Confirmation count is incremented but never tied to active signer set
s_transactions[_txId].confirmations += 1;
// @> Revoking a signer does not update existing confirmations
function revokeSigningRole(address _account) external onlyOwner {
...
s_signerCount -= 1;
// @> No cleanup or invalidation of s_isConfirmed for pending transactions
}

Risk

Likelihood:

  • Governance operations regularly remove or rotate signers over time.

  • Long-lived pending transactions accumulate confirmations across multiple governance epochs.

Impact:

  • Revoked signers continue to influence execution of pending transactions.

  • Transactions can execute without approval from the current signer set.


Proof of Concept

This PoC demonstrates that a signer’s approval remains valid even after their signing authority has been revoked:

  1. A transaction is proposed.

  2. A signer confirms the transaction.

  3. The signer is revoked by the owner.

  4. The transaction is executed successfully using the revoked signer’s confirmation.

This shows that execution logic does not validate confirmations against the current signer set.

PoC Code

function testRevokedSignerConfirmationStillCounts() public grantSigningRoles {
// Fund the multisig
vm.deal(address(multiSigTimelock), 10 ether);
// Propose a transaction
vm.prank(OWNER);
uint256 txnId =
multiSigTimelock.proposeTransaction(SPENDER_ONE, 1 ether, hex"");
// Collect confirmations
vm.prank(OWNER);
multiSigTimelock.confirmTransaction(txnId);
vm.prank(SIGNER_TWO);
multiSigTimelock.confirmTransaction(txnId);
vm.prank(SIGNER_THREE);
multiSigTimelock.confirmTransaction(txnId);
// Revoke one signer after confirmation
vm.prank(OWNER);
multiSigTimelock.revokeSigningRole(SIGNER_THREE);
// Execute transaction successfully
vm.prank(OWNER);
multiSigTimelock.executeTransaction(txnId);
}

Recommended Mitigation

Invalidate Confirmations on Revocation

Function: revokeSigningRole(address)

Function revokeSigningRole
- only decrement signer count
+ iterate over pending transactions and remove confirmations from revoked signer
Function confirmTransaction
- mapping(txId => mapping(address => bool))
+ mapping(txId => mapping(signerIndex => bool))

Support

FAQs

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

Give us feedback!