Vanguard

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

Signer Set Drift Enables Permanent Governance Deadlock

Author Revealed upon completion

Root + Impact

Description

  • The multisig is designed to require a threshold number of confirmations from a dynamic signer set to execute transactions.

However, when the signer set or required confirmations are updated, existing pending transactions are not reconciled against the new signer configuration, causing transactions to become permanently unexecutable.

// @> Transaction confirmations are bound to historical signer state
mapping(uint256 => mapping(address => bool)) public confirmations;
// @> Signer set and required confirmations can change independently
function changeRequirement(uint256 _required) external onlyWallet {
required = _required;
}
function removeOwner(address owner) external onlyWallet {
isOwner[owner] = false;
}

Risk

Likelihood:

  • Occurs during normal governance operations such as signer rotation

Common when multisigs evolve over time (add/remove owners, adjust quorum)

Impact:

  • Permanent locking of already-approved transactions

Governance paralysis without fund loss recovery path

Proof of Concept

  • Confirmations are stored per-address without being invalidated or recalculated when ownership changes. This causes transactions to reference a non-existent quorum, permanently bricking them.

// Initial state
owners = [A, B, C];
required = 2;
// Transaction T submitted
A confirms T
B confirms T // T is now executable
// Governance update
removeOwner(B)
required = 2 // quorum now A + C
// Execution attempt
executeTransaction(T) // fails: confirmation from removed owner ignored
// T can never be re-confirmed or cleaned up

Recommended Mitigation

  • Invalidate or rebase confirmations when signer configuration changes.

- remove this code
+ add this code
function removeOwner(address owner) external onlyWallet {
isOwner[owner] = false;
+ _invalidatePendingTransactions();
}
function changeRequirement(uint256 _required) external onlyWallet {
required = _required;
+ _invalidatePendingTransactions();
}
function executeTransaction(uint256 txId) external {
+ require(isConfirmationStateValid(txId), "stale confirmations");
}

Support

FAQs

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

Give us feedback!