MultiSig Timelock

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

No transaction cancellation mechanism, no way to prevent transfer to a malicious address.

Author Revealed upon completion

Root + Impact

Description

The current statistics on protocol funds lost confirm that, even with multisig, such attacks succeed. Functionality for emergency blocking or cancellation of a transaction is required.

@> // function cancelTransaction(uint256 txnId) - does not exist

Risk

Likelihood:

  • Even though the likelihood is low by itself, human errors do happen.

  • Not every signer might be suspicious of every transaction; transactions might be confirmed without a check.

  • Also, an attack vector to trick a signer into transferring funds to a malicious address that resembles a previously verified one has become common and, in some cases, successful.

Impact:

  • Loss of funds intended for internal protocol transfers might cause severe damage to the protocol, as such transfers are by nature less suspicious.

  • Cannot recover from a compromised owner.

  • Erroneous transactions cannot be cancelled.

  • Must trust a single signer to never confirm malicious transactions.

  • Pending malicious transactions remain a permanent threat.

Proof of Concept

  1. One of the signers has been tricked into transferring funds to a malicious address that looks like the intended one.

  2. It gets 2 confirmations before detection.

  3. No way to cancel – must rely on the 3rd signer indefinitely.

  4. Accidents or errors in transaction parameters cannot be fixed.

Recommended Mitigation

Allow any signer to cancel suspicious transactions before they are fully confirmed.

+ function cancelTransaction(uint256 txnId)
+ external
+ onlyRole(SIGNING_ROLE)
+ transactionExists(txnId)
+ notExecuted(txnId)
+ {
+ require(
+ s_transactions[txnId].confirmations < REQUIRED_CONFIRMATIONS,
+ "Cannot cancel fully confirmed transaction"
+ );
+
+ // Clear all confirmations and signatures
+ for (uint256 i = 0; i < s_signerCount; i++) {
+ address signer = s_signers[i];
+ if (s_signatures[txnId][signer]) {
+ s_signatures[txnId][signer] = false;
+ }
+ }
+
+ s_transactions[txnId].confirmations = 0;
+ s_transactions[txnId].executed = true; // Mark as cancelled
+
+ // Release reserved funds if implemented
+ s_totalReserved -= s_reservedFunds[txnId];
+ delete s_reservedFunds[txnId];
+
+ emit TransactionCancelled(txnId);
+ }

Support

FAQs

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

Give us feedback!