MultiSig Timelock

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

Missing Confirmation Validation After Signer Revocation

Author Revealed upon completion

Description

  • The contract allows proposing and executing transactions but does not provide any mechanism to cancel or delete a proposed transaction that hasn't been executed yet.

  • Once a transaction is proposed, it remains in the contract's storage forever, even if it becomes obsolete or unwanted. This could lead to storage bloat and potential confusion about which transactions are active.

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
onlyOwner
returns (uint256)
{
return _proposeTransaction(to, value, data);
}
@> // No function to cancel or delete a proposed transaction exists
@> // Transactions remain in storage forever after proposal

Risk

Likelihood:

  • When a transaction is proposed but later determined to be unnecessary or incorrect

  • Storage accumulates old, obsolete transactions that will never be executed

Impact:

  • Storage bloat over time with unused transactions

  • Potential confusion about which transactions are actively being considered

  • No way to clean up malicious or erroneous transaction proposals

  • Minor gas inefficiency from unused storage

Proof of Concept

function testCannotCancelProposedTransaction() public {
// Propose a transaction
uint256 txId = multiSig.proposeTransaction(recipient, 1 ether, "");
// Later realize this transaction is wrong or unwanted
// No function exists to cancel it!
// The transaction remains in storage forever
MultiSigTimelock.Transaction memory txn = multiSig.getTransaction(txId);
assertEq(txn.to, recipient); // Still exists in storage
// Only option is to never confirm/execute it, but it clutters storage
}

Recommended Mitigation

+ event TransactionCancelled(uint256 indexed transactionId);
+
+ error MultiSigTimelock__CannotCancelExecutedTransaction();
+ /**
+ * @dev Function to cancel a proposed transaction before execution
+ * @param txnId The ID of the transaction to cancel
+ */
+ function cancelTransaction(uint256 txnId)
+ external
+ nonReentrant
+ onlyOwner
+ transactionExists(txnId)
+ notExecuted(txnId)
+ {
+ Transaction storage txn = s_transactions[txnId];
+
+ // Mark as executed to prevent future execution
+ txn.executed = true;
+
+ // Optional: Reset confirmations to save gas on storage refunds
+ txn.confirmations = 0;
+
+ emit TransactionCancelled(txnId);
+ }

Support

FAQs

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

Give us feedback!