MultiSig Timelock

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

Medium [M-2] Timelock based only on ETH value

Author Revealed upon completion

Medium [M-2] Timelock based only on ETH value

Description

  • Timelock delay derives solely from txn.value; 0-ETH but high-impact admin/governance/upgrade calls execute immediately.

  • Recommendation: add per-transaction minDelay/severity or base delay; or destination-aware delay list.

// Root cause in the codebase with @> marks to highlight the relevant section
function _getTimelockDelay(uint256 value) internal pure returns (uint256) {
uint256 sevenDaysTimeDelayAmount = 100 ether;
uint256 twoDaysTimeDelayAmount = 10 ether;
2
uint256 oneDayTimeDelayAmount = 1 ether;
if (value >= sevenDaysTimeDelayAmount) {
return SEVEN_DAYS_TIME_DELAY;
} else if (value >= twoDaysTimeDelayAmount) {
return TWO_DAYS_TIME_DELAY;
} else if (value >= oneDayTimeDelayAmount) {
return ONE_DAY_TIME_DELAY;
} else {
return NO_TIME_DELAY;
}
}

Risk

Likelihood:

  • many critical admin/upgrade/governance calls are 0-ETH, so they bypass delay; operators may assume timelock applies.

Impact:

  • time-delay safety net absent for highest-impact operations; hostile upgrades/transfers can execute without notice.

Proof of Concept

// Propose a 0-ETH call to an upgrade/admin contract; with 3 confirmations, it executes immediately (no delay tier triggered).

Recommended Mitigation

// Add a configurable minDelay per transaction or a global base delay.
// Consider marking sensitive destinations with higher required delay.
+ add these codes below
// Example: add a per-transaction minDelay
struct Transaction {
address to;
uint256 value;
bytes data;
uint256 confirmations;
uint256 proposedAt;
uint256 minDelay; // new: explicit minimum delay for this tx
bool executed;
}
function _proposeTransaction(
address to,
uint256 value,
bytes memory data,
uint256 minDelay
) internal returns (uint256) {
uint256 transactionId = s_transactionCount;
s_transactions[transactionId] = Transaction({
to: to,
value: value,
data: data,
confirmations: 0,
proposedAt: block.timestamp,
minDelay: minDelay,
executed: false
});
s_transactionCount++;
emit TransactionProposed(transactionId, to, value);
return transactionId;
}
function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
uint256 requiredDelay = _getTimelockDelay(txn.value);
uint256 executionTime = txn.proposedAt + (requiredDelay > txn.minDelay ? requiredDelay : txn.minDelay);
if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
// ... rest of existing execution logic ...
}

Support

FAQs

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

Give us feedback!