The contract allows the owner to propose transactions regardless of the current signer count. However, execution requires exactly 3 confirmations (REQUIRED_CONFIRMATIONS = 3). If the signer count drops below 3 (e.g., via revokeSigningRole, possible down to 1), proposed transactions can accumulate but never reach the required confirmations, effectively "pinning" them in a pending state. This could lock funds if the contract holds ETH, as no withdrawals can occur until more signers are added.
Description: The contract uses a fixed confirmation threshold of 3 (REQUIRED_CONFIRMATIONS) for executing transactions, but does not prevent proposals when the current number of signers (s_signerCount) is below this threshold. Proposals can be made via proposeTransaction (restricted to the owner), incrementing s_transactionCount and storing the transaction details. Signers can attempt to confirm via confirmTransaction, but if fewer than 3 signers exist, the confirmation count can never reach 3.
In _executeTransaction, it checks if (txn.confirmations = REQUIRED_CONFIRMATIONS)
revokeSigningRole (allows reduction to 1 or 2 signers)
_executeTransaction (enforces fixed 3 confirmations)
Impact:
Funds in the contract become inaccessible if signers drop below 3 after proposals.
Wasted gas on futile confirmations/revocations for pinned transactions.
Reduced usability in a multisig setup, as it doesn't fail-fast on proposals that are doomed to fail.
Potential for denial-of-service if many invalid proposals are made, bloating storage (though limited by owner-only proposals).
Proof of Concept (Conceptual):
Deploy contract (1 signer: owner).
Grant 2 more signers (now 3).
Propose a transaction (ID 0).
Revoke 2 signers (now 1 signer).
Attempt to confirm/execute ID 0: Fails due to insufficient confirmations. Transaction is stuck.
Recommendation:
Add a check in proposeTransaction or _proposeTransaction: if (s_signerCount < REQUIRED_CONFIRMATIONS) revert MultiSigTimelock__InsufficientSignersForProposal();.
In revokeSigningRole, add: if (s_signerCount - 1 < REQUIRED_CONFIRMATIONS && _hasPendingTransactions()) revert ...; (implement _hasPendingTransactions to scan for unexecuted txns).
Alternatively, make REQUIRED_CONFIRMATIONS dynamic: e.g., uint256 required = min(REQUIRED_CONFIRMATIONS, s_signerCount); in an internal getter, ensuring it's at least 1. This maintains flexibility but requires careful testing.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.