MultiSig Timelock

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

Signer Set Changes Do Not Invalidate Existing Confirmations

Author Revealed upon completion

Root + Impact

Description

  • In a multisig system, confirmations are expected to reflect approval by a specific, known set of signers. If the signer set changes, previously collected confirmations should no longer be trusted under the new governance composition.

  • This contract allows the owner to add or remove signers after confirmations have already been collected, without invalidating or re-evaluating those confirmations. As a result, a transaction can be executed using approvals that were given under a different signer set, violating signer intent and governance assumptions.

// @> Signer management can change independently of transactions
function grantSigningRole(address _account) external onlyOwner {
s_signers[s_signerCount] = _account;
s_isSigner[_account] = true;
s_signerCount += 1;
}
// @> Confirmations are simple counters, not tied to signer set state
struct Transaction {
address to;
uint256 value;
bytes data;
uint256 confirmations; // @> No signer snapshot
uint256 proposedAt;
bool executed;
}

Risk

Likelihood:

  • Signer sets are expected to change during normal protocol operation (key rotation, new members, removals).

  • The owner can modify the signer set at any time, including while transactions are pending.

Impact:

  • Transactions may execute with approvals from a signer group that no longer represents current governance.

Enables stealth manipulation where malicious signers are added only to finalize execution.

  • Breaks signer intent and multisig trust guarantees.

Proof of Concept

  • The transaction executes even though signers A and B never approved execution under the new signer set.

// Initial signer set: A, B, C
// Required confirmations = 3
// Step 1: Transaction proposed
uint256 txnId = multisig.proposeTransaction(target, 0, maliciousData);
// Step 2: Two honest signers confirm
vm.prank(A);
multisig.confirmTransaction(txnId);
vm.prank(B);
multisig.confirmTransaction(txnId);
// confirmations = 2 (not executable)
// Step 3: Owner adds malicious signer D
multisig.grantSigningRole(D);
// Step 4: Malicious signer confirms
vm.prank(D);
multisig.confirmTransaction(txnId);
// confirmations = 3 → executable
// Step 5: Transaction executes
multisig.executeTransaction(txnId);

Recommended Mitigation

  • Invalidate or re-snapshot confirmations whenever the signer set changes.

  • Store a signerSetHash in each transaction and enforce equality at execution.

  • Reset confirmation count on any signer addition or removal.

  • Require re-confirmation if signer membership changes.

- remove this code
+ add this code
function grantSigningRole(address _account) external onlyOwner {
+ _invalidatePendingTransactions();
s_signers[s_signerCount] = _account;
s_isSigner[_account] = true;
s_signerCount += 1;
}

Support

FAQs

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

Give us feedback!