Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Permanent Denial of Execution via Irreversible Transaction Poisoning

Author Revealed upon completion

Root + Impact

Description

  • The multisig is designed to allow queued transactions to be executed once they receive sufficient confirmations.

However, a transaction that reverts during execution remains permanently executable-but-failing, with no mechanism to cancel, invalidate, or clean it up. This allows a single malicious or malformed transaction to irreversibly poison governance execution flow.

// @> Failed execution does not invalidate or remove the transaction
function executeTransaction(uint256 txId) external {
require(isConfirmed(txId));
transactions[txId].executed = true;
(bool success,) = tx.destination.call(tx.data);
require(success); // revert resets executed flag, tx stays live forever
}

Risk

Likelihood:

  • Occurs during normal multisig usage when interacting with external contracts

Common when targets change behavior, upgrade, or intentionally revert

Impact:

  • Permanent DoS of governance execution for affected transactions

Blocks treasury operations, upgrades, signer rotations, or emergency actions

Proof of Concept

  • Because failed executions do not transition the transaction into a terminal state (cancelled / failed), the multisig accumulates toxic transactions that cannot be resolved. Over time, this can completely halt operational governance.

// Attacker submits a transaction calling a contract that always reverts
submitTransaction(
revertingContract,
0,
abi.encodeWithSignature("alwaysRevert()")
);
// Honest signers confirm it (looks benign or disguised)
confirmTransaction(txId);
// Any execution attempt:
executeTransaction(txId); // always reverts
// txId can never be removed or skipped
// Governance tooling repeatedly fails or blocks batch execution

Recommended Mitigation

  • Introduce an explicit cancellation / failure finalization path

  • Allow governance-approved cancellation

Track failed attempts and auto-invalidate

- remove this code
+ add this code
struct Transaction {
address destination;
uint256 value;
bytes data;
bool executed;
+ bool cancelled;
}
function executeTransaction(uint256 txId) external {
require(!transactions[txId].cancelled, "cancelled");
require(isConfirmed(txId));
transactions[txId].executed = true;
(bool success,) = tx.destination.call{value: tx.value}(tx.data);
- require(success);
+ if (!success) {
+ transactions[txId].executed = false;
+ transactions[txId].cancelled = true;
+ }
}

Support

FAQs

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

Give us feedback!