MultiSig Timelock

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

Timelock Bypass via Block Timestamp Manipulation (Theoretical)

Author Revealed upon completion

Description

  • The contract uses block.timestamp to enforce timelock delays on transaction execution. The timelock duration is determined by the transaction value.

  • Validators/miners can manipulate block.timestamp within a small range (typically 15 seconds) according to Ethereum's consensus rules. While this is a limited attack vector, it could allow premature execution of timelocked transactions in edge cases.

function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
// ... other checks ...
// Check if timelock period has passed
uint256 requiredDelay = _getTimelockDelay(txn.value);
@> uint256 executionTime = txn.proposedAt + requiredDelay; // Uses block.timestamp
@> if (block.timestamp < executionTime) { // Susceptible to miner manipulation
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
// ... rest of execution
}

Risk

Likelihood:

  • Validators can manipulate block timestamps by approximately 15 seconds in either direction

  • This manipulation is most impactful when a transaction is near its timelock expiration

  • Attackers could collude with validators to execute high-value transactions slightly earlier

Impact:

  • Timelock protection can be bypassed by up to 15 seconds

  • For a 7-day timelock (604800 seconds), 15 seconds is negligible (0.0025% variance)

  • For shorter timelocks or high-value transactions, this could be more significant

  • Undermines the security guarantee of the timelock mechanism

Proof of Concept

function testTimelockBypassViaTimestampManipulation() public {
multiSig.grantSigningRole(signer1);
multiSig.grantSigningRole(signer2);
// Create transaction with 1 day timelock
uint256 txId = multiSig.proposeTransaction(recipient, 5 ether, "");
// Get confirmations
multiSig.confirmTransaction(txId);
vm.prank(signer1);
multiSig.confirmTransaction(txId);
vm.prank(signer2);
multiSig.confirmTransaction(txId);
// Fast forward to 1 day minus 16 seconds
vm.warp(block.timestamp + 1 days - 16 seconds);
// Should fail (timelock not expired)
vm.expectRevert();
multiSig.executeTransaction(txId);
// Validator manipulates timestamp by +15 seconds
vm.warp(block.timestamp + 15 seconds);
// Now succeeds (bypassed by 1 second)
multiSig.executeTransaction(txId); // Executes 1 second early
}

Recommended Mitigation

+ // Add a buffer to account for block timestamp manipulation
+ uint256 private constant TIMESTAMP_BUFFER = 30 seconds;
function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
// ... other checks ...
// Check if timelock period has passed
uint256 requiredDelay = _getTimelockDelay(txn.value);
- uint256 executionTime = txn.proposedAt + requiredDelay;
+ uint256 executionTime = txn.proposedAt + requiredDelay + TIMESTAMP_BUFFER;
if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
// ... rest of execution
}
+ // Alternative: Use block.number instead of block.timestamp
+ // 1 block ≈ 12 seconds, more resistant to manipulation

Support

FAQs

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

Give us feedback!