Description
The MultisigTimelock contract calculates the execution delay based on txn.proposedAt. In _executeTransaction, the requirement is block.timestamp >= txn.proposedAt + requiredDelay ➡️ {_getTimelockDelay(txn.value)}.
In a MultiSig security model, a timelock is intended to provide a "veto" or "exit" window for stakeholders (SIGNERS) after a transaction has gained enough signatures to be valid. Because the current implementation starts the timer at the moment of proposal, the delay can expire while the transaction has zero or insufficient confirmations. If the quorum is reached after the delay has already passed, the transaction can be executed immediately, leaving zero time for SIGNERS to react to a fully authorized (and potentially malicious) action.
Risk (Impact)
Medium Severity. The security "buffer" provided by the dynamic timelock (1–7 days) is effectively neutralized.
A malicious or compromised OWNER can propose a 100 ETH transfer, wait 7 days, and then gather the required signatures(just 2 more) in the final minute of that week.
The transaction can be executed instantly upon the final confirmation.
This defeats the protocol's promise of transparency, as the community (EVERY SIGNER) never sees a fully authorized transaction sitting in a pending state before it is finalized.
PoC (Proof of Concept)
Monday 12:00 PM: Owner proposes a 100 ETH transfer. proposedAt is set. The requiredDelay is 7 days.
Tuesday - Sunday: The transaction sits with 0 confirmations. Users ignore it as it is not authorized.
Next Monday 11:55 AM: Required signers provide confirmations. txn.confirmations now meets REQUIRED_CONFIRMATIONS.
Next Monday 12:01 PM: block.timestamp is now greater than proposedAt + 7 days.
Result: The transaction is executed immediately. The "7-day delay" occurred while the transaction was unconfirmed, giving all the addresses with the SIGNING_ROLE 0 seconds of warning after the quorum was actually met.
Recommended Mitigation
Modify the Transaction struct to include a quorumReachedAt timestamp. The timer must start only when REQUIRED_CONFIRMATIONS is reached (>3) and must reset if a confirmation is revoked (REQUEST_CONFIRMATIONS --).
//UPDATE STRUCT (Adding quorumReachedAt)
Updated _confirmTransaction:
Solidity
function _confirmTransaction(uint256 txnId) internal {
if (s_signatures[txnId][msg.sender]) {
revert MultiSigTimeLock__UserAlreadySigned();
}
s_signatures[txnId][msg.sender] = true;
Transaction storage txn = s_transactions[txnId];
txn.confirmations++;
if (txn.confirmations >= REQUIRED_CONFIRMATIONS && txn.quorumReachedAt == 0) {
txn.quorumReachedAt = block.timestamp;
}
emit TransactionConfirmed(txnId, msg.sender);
}
Updated _revokeConfirmation:
Solidity
function _revokeConfirmation(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
txn.confirmations--;
if (txn.confirmations < REQUIRED_CONFIRMATIONS) {
txn.quorumReachedAt = 0;
}
emit TransactionRevoked(txnId, msg.sender);
}
Updated _executeTransaction:
Solidity
function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
require(txn.quorumReachedAt > 0, "Quorum not active");
uint256 requiredDelay = _getTimelockDelay(txn.value);
uint256 executionTime = txn.quorumReachedAt + requiredDelay;
if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
}
Why this update is critical:
Neutralizes "Stealth" Proposals: Without this fix, the 7-day delay can be "served" while the transaction has zero signatures. This allows a malicious quorum to execute high-value transfers instantly the moment they decide to sign, providing the public with zero warning that the transaction was actually authorized.
Enforces Continuous Quorum: By resetting the timer on **confirmation and revocation,** we ensure the protocol is protected by a continuous 7-day window of consensus. This prevents "Ghost Timers" where a transaction "pre-ages" and becomes a ticking time bomb that can be triggered months later without a fresh review period.
Aligns Code with Intent: The protocol advertises "granular delays" to protect funds. The current code only provides a "delay since creation," which offers no protection against a coordinated "flash-signature" attack.