MultiSig Timelock

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

Timelock Bypass via Transaction Value Manipulation

Author Revealed upon completion

Root + Impact

Description

The timelock duration is calculated exclusively from the value (ETH amount) of a proposed transaction. Transactions with value < 1 ETH are categorized as low-risk and are therefore executable without any delay once the 3-signer quorum is met. The contract does not evaluate the economic or security impact of the calldata, meaning external calls that move large amounts of value (ERC20 transfers, approvals, internal ETH forwarding, upgrades) are treated as low-risk when value == 0. This design flaw allows high-impact actions to be executed immediately, bypassing the intended protection of the dynamic timelock.

// File: MultiSigTimelock.sol
function _getTimelockDelay(uint256 value) internal pure returns (uint256) {
if (value < 1 ether) {
return 0;
// @> Transactions with value < 1 ETH bypass the timelock entirely
} else if (value < 10 ether) {
return 1 days;
} else if (value < 100 ether) {
return 2 days;
} else {
return 7 days;
}
}

Risk

Likelihood:

  • The issue occurs whenever a signer proposes a transaction with value = 0 and non-empty calldata, which is a standard pattern for interacting with ERC20 tokens and external contracts.

  • Any group of 3 authorized signers can propose, confirm, and execute such a transaction without delay, making the scenario realistic in normal governance operation.

Impact:

  • Unlimited token approvals can be granted to attacker-controlled addresses.

  • High-value ERC20 token balances held by the wallet can be instantly drained.

Proof of Concept

This transaction satisfies:

  • Minimum 3 confirmations

  • value < 1 ETH → zero timelock

Yet results in total loss of assets.

// Assume the multisig holds a large ERC20 balance
IERC20 token = IERC20(tokenAddress);
// Step 1: Propose a transaction with zero ETH value
// value = 0 ETH → no timelock enforced
// data encodes a high-value token transfer
bytes memory data = abi.encodeWithSignature(
"transfer(address,uint256)",
attacker,
token.balanceOf(address(multisig))
);
// Step 2: Three signers confirm the transaction
// Step 3: Execute immediately with no delay
// Result: entire token balance is drained instantly

Recommended Mitigation

Previously, timelock delays were based only on ETH value, so transactions with no ETH but containing function calls could execute immediately.

The fix enforces a minimum delay for any transaction with calldata:

All external calls now respect a minimum delay.

  • Prevents attackers from bypassing governance using zero-value but high-risk function calls.

  • Strengthens overall timelock security without affecting normal ETH-based transactions.

// Timelock determined only by ETH value
- uint256 delay = getDelayBasedOnValue(tx.value);
+ // Enforce minimum delay for any external call
+ uint256 delay = getDelayBasedOnValue(tx.value);
+ if (tx.data.length > 0) {
+ delay = max(delay, MIN_CALL_DELAY);
+ }

Support

FAQs

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

Give us feedback!