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];
uint256 requiredDelay = _getTimelockDelay(txn.value);
@> uint256 executionTime = txn.proposedAt + requiredDelay;
@> if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
}
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);
uint256 txId = multiSig.proposeTransaction(recipient, 5 ether, "");
multiSig.confirmTransaction(txId);
vm.prank(signer1);
multiSig.confirmTransaction(txId);
vm.prank(signer2);
multiSig.confirmTransaction(txId);
vm.warp(block.timestamp + 1 days - 16 seconds);
vm.expectRevert();
multiSig.executeTransaction(txId);
vm.warp(block.timestamp + 15 seconds);
multiSig.executeTransaction(txId);
}
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