MultiSig Timelock

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

Timelock Countdown Starts at Proposal Instead of Quorum Approval

Author Revealed upon completion

Root + Impact

Description

  • A timelock is expected to begin after a transaction has received the required confirmations, ensuring a guaranteed reaction window once governance approval is finalized.

  • In this contract, the timelock countdown starts at transaction proposal time, not when the transaction reaches the required confirmation threshold. This allows signers to wait out the entire timelock period first, then collect confirmations and execute the transaction immediately, defeating the purpose of the timelock.

function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
// @> Timelock is calculated from proposal time, not approval time
uint256 requiredDelay = _getTimelockDelay(txn.value);
uint256 executionTime = txn.proposedAt + requiredDelay;
if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
}

Risk

Likelihood:

  • Transactions can remain unconfirmed for long periods during normal multisig usage.

Signers can intentionally delay confirmations until after the timelock duration has elapsed.

Impact:

  • Timelock provides no effective monitoring or reaction window.

High-risk transactions can be executed immediately after final confirmation.

  • Undermines governance transparency and user trust.

Proof of Concept

  • The transaction executes instantly after approval, despite being a high-risk action that should have allowed time for review.

// Required timelock: 7 days (value >= 100 ETH)
// Step 1: Propose transaction
uint256 txnId = multisig.proposeTransaction(target, 100 ether, data);
// Step 2: Wait 7 days WITHOUT confirming
vm.warp(block.timestamp + 7 days);
// Step 3: Collect confirmations
multisig.confirmTransaction(txnId);
multisig.confirmTransaction(txnId);
multisig.confirmTransaction(txnId);
// Step 4: Execute immediately
multisig.executeTransaction(txnId);

Recommended Mitigation

  • Start the timelock when the transaction reaches quorum approval, not when it is proposed.

- remove this code
+ add this code
struct Transaction {
address to;
uint256 value;
bytes data;
uint256 confirmations;
- uint256 proposedAt;
+ uint256 approvedAt;
bool executed;
}
function _confirmTransaction(uint256 txnId) internal {
if (!s_signatures[txnId][msg.sender]) {
s_transactions[txnId].confirmations++;
+ if (s_transactions[txnId].confirmations == REQUIRED_CONFIRMATIONS) {
+ s_transactions[txnId].approvedAt = block.timestamp;
+ }
}
}

Support

FAQs

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

Give us feedback!