MultiSig Timelock

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

Bug Timelock Bypass via Transaction Value Fragmentation in MultiSigTimelock.sol

Author Revealed upon completion

Root + Impact

File in Scope: "src/MultiSigTimelock.sol"

Description

  • Normally, transactions above certain ETH thresholds are enforced to wait through a timelock before execution.

  • However, the timelock is calculated only once at execution time, based solely on txn.value, not on intent or bundled execution.

  • A malicious proposer can split a large fund movement into multiple sub-1 ETH transactions, each with zero timelock, effectively bypassing the intended delay mechanism.

function _getTimelockDelay(uint256 value) internal pure returns (uint256) {
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; // @> Exploit surface
}
}

Risk

Likelihood:

File in scope: MultiSigTimelock.sol

  • Large withdrawals can be fragmented into smaller chunks without restriction

  • No aggregation or cumulative tracking of withdrawals exists

Impact:

File in scope: MultiSigTimelock.sol

  • Timelock guarantees are completely bypassed

  • Defeats the protocol’s primary security assumption


PoC

// Attacker proposes 150 transactions of 0.9 ETH
for (uint256 i = 0; i < 150; i++) {
multisig.proposeTransaction(attacker, 0.9 ether, "");
multisig.confirmTransaction(i);
multisig.confirmTransaction(i);
multisig.confirmTransaction(i);
multisig.executeTransaction(i); // Executes instantly
}

Mitigation

File in scope: MultiSigTimelock.sol

❌ Remove

uint256 requiredDelay = _getTimelockDelay(txn.value);

✅ Add (cumulative withdrawal tracking)

mapping(uint256 => uint256) private s_cumulativeValue;
function _confirmTransaction(uint256 txnId) internal {
...
s_cumulativeValue[txnId] += s_transactions[txnId].value;
}

And calculate delay using cumulative value:

uint256 requiredDelay = _getTimelockDelay(s_cumulativeValue[txnId]);

Support

FAQs

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

Give us feedback!