MultiSig Timelock

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

19Dec2025_AuditReport4_MultiSigTimelock

Author Revealed upon completion

Root + Impact

Description

  • Missing Transaction Cancellation Mechanism

  • Once a transaction is proposed, there is no way to cancel it even if it becomes malicious, outdated, or if the recipient address is discovered to be compromised. The transaction remains in the system indefinitely and can be executed at any point after the timelock expires if it gets enough confirmations. This is particularly problematic given that only the owner can propose transactions.

// Root cause in the codebase with @> marks to highlight the relevant section
// Owner proposes transaction to address that later becomes malicious
uint256 txId = multiSigTimelock.proposeTransaction(initiallyTrustedAddress, 10 ether, "");
// Address is discovered to be compromised
// But transaction cannot be cancelled
// If 3 signers already confirmed, it WILL execute after timelock
// No way to stop it

Risk

Likelihood:

  • Once a transaction is proposed, there is no way to cancel it even if it becomes malicious, outdated, or if the recipient address is discovered to be compromised. The transaction remains in the system indefinitely and can be executed at any point after the timelock expires if it gets enough confirmations. This is particularly problematic given that only the owner can propose transactions.

Impact:

  • Malicious or erroneous transactions cannot be stopped once proposed. If signers accidentally confirm a malicious transaction, or if a transaction becomes dangerous due to changed circumstances, there's no way to prevent execution except hoping signers revoke confirmations.

Proof of Concept

// No cancellation function exists
// Transactions stored permanently:
mapping(uint256 transactionId => Transaction) private s_transactions;
struct Transaction {
address to;
uint256 value;
bytes data;
uint256 confirmations;
uint256 proposedAt;
bool executed; // No 'cancelled' field
}

Recommended Mitigation

// Add to Transaction struct:
bool cancelled;
function cancelTransaction(uint256 txnId)
external
nonReentrant
transactionExists(txnId)
notExecuted(txnId)
onlyOwner // Or require multiple signers
{
s_transactions[txnId].cancelled = true;
emit TransactionCancelled(txnId);
}
// Update execution check:
modifier notCancelled(uint256 _transactionId) {
require(!s_transactions[_transactionId].cancelled, "Transaction cancelled");
_;
}

Support

FAQs

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

Give us feedback!