MultiSig Timelock

First Flight #55
Beginner FriendlyWallet
100 EXP
View results
Submission Details
Severity: medium
Valid

ERC20 and arbitrary token transfer timelock bypass

Root + Impact

Description

  • Normal behavior: Timelock duration is determined solely by the value of ETH being sent.

  • Issue: Calls that transfer ERC20 or other tokens via data with value == 0 bypass the timelock entirely regardless of token amount.

function _getTimelockDelay(uint256 value) internal pure returns (uint256) {
// @> delay depends only on ETH 'value'
if (value >= 100 ether) return SEVEN_DAYS_TIME_DELAY;
else if (value >= 10 ether) return TWO_DAYS_TIME_DELAY;
else if (value >= 1 ether) return ONE_DAY_TIME_DELAY;
else return NO_TIME_DELAY;
}

Risk

Likelihood:

  • Reason 1 // Signers often transfer ERC20 with value == 0 via data

  • Reason 2 // High-value tokens (stablecoins) commonly held in multisigs

Impact:

  • Impact 1 // High-value token movements occur without intended delay controls

  • Impact 2 // Governance expectation violated; rushed large transfers possible

Proof of Concept

Explanation: The test testH1_DataMinDelayEnforced demonstrates that a transaction with value == 0 but non-empty data (simulating a token transfer) executes immediately in the original code. The mitigation enforces a minimum delay.

// Propose: value == 0, data calls ERC20.transfer(beneficiary, 1_000_000e6)
vm.prank(OWNER);
uint256 txnId = multiSigTimelock.proposeTransaction(
address(usdc), 0, abi.encodeWithSignature("transfer(address,uint256)", beneficiary, 1_000_000e6)
);
// Three confirmations
// Execute immediately: timelock delay == 0 because ETH value == 0
vm.prank(SIGNER_THREE);
multiSigTimelock.executeTransaction(txnId);

Recommended Mitigation

Explanation: Update _executeTransaction to check if data is non-empty and enforce a minimum delay (e.g., 1 day) even if value is 0.

- uint256 requiredDelay = _getTimelockDelay(txn.value);
+ // Apply timelock to non-ETH asset movements as well.
+ uint256 requiredDelay = bytes(txn.data).length > 0
+ ? ONE_DAY_TIME_DELAY
+ : _getTimelockDelay(txn.value);
Updates

Lead Judging Commences

kelechikizito Lead Judge 4 days ago
Submission Judgement Published
Validated
Assigned finding tags:

No validation of Tx calldata

Support

FAQs

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

Give us feedback!