function _proposeTransaction(address to, uint256 value, bytes memory data) internal returns (uint256) {
uint256 transactionId = s_transactionCount;
s_transactions[transactionId] = Transaction({
to: to,
value: value,
data: data,
confirmations: 0,
@> proposedAt: block.timestamp,
executed: false
});
}
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);
}
}
pragma solidity ^0.8.19;
import {MultiSigTimelock} from "./MultiSigTimelock.sol";
contract TimelockPreAgingExploit {
function testPreAgeTransaction() external {
MultiSigTimelock wallet = new MultiSigTimelock();
payable(address(wallet)).transfer(100 ether);
address signer1 = address(0x1);
address signer2 = address(0x2);
wallet.grantSigningRole(signer1);
wallet.grantSigningRole(signer2);
uint256 txId = wallet.proposeTransaction(address(0x9999), 100 ether, "");
vm.warp(block.timestamp + 7 days + 1);
wallet.confirmTransaction(txId);
vm.prank(signer1);
wallet.confirmTransaction(txId);
vm.prank(signer2);
wallet.confirmTransaction(txId);
wallet.executeTransaction(txId);
assert(address(0x9999).balance == 100 ether);
}
}
struct Transaction {
address to;
uint256 value;
bytes data;
uint256 confirmations;
uint256 proposedAt;
+ uint256 approvedAt; // Track when threshold was reached
bool executed;
}
function _confirmTransaction(uint256 txnId) internal {
if (s_signatures[txnId][msg.sender]) {
revert MultiSigTimeLock__UserAlreadySigned();
}
s_signatures[txnId][msg.sender] = true;
// Increase counter
s_transactions[txnId].confirmations++;
+
+ // Set approvedAt timestamp when threshold is first reached
+ if (s_transactions[txnId].confirmations == REQUIRED_CONFIRMATIONS &&
+ s_transactions[txnId].approvedAt == 0) {
+ s_transactions[txnId].approvedAt = block.timestamp;
+ }
emit TransactionConfirmed(txnId, msg.sender);
}
function _revokeConfirmation(uint256 txnId) internal {
if (!s_signatures[txnId][msg.sender]) {
revert MultiSigTimeLock__UserHasNotSigned();
}
// Remove their signature
s_signatures[txnId][msg.sender] = false;
// Decrease counter
s_transactions[txnId].confirmations--;
+
+ // Clear approvedAt if confirmations drop below threshold
+ if (s_transactions[txnId].confirmations < REQUIRED_CONFIRMATIONS) {
+ s_transactions[txnId].approvedAt = 0;
+ }
emit TransactionRevoked(txnId, msg.sender);
}
function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
// CHECKS
if (txn.confirmations < REQUIRED_CONFIRMATIONS) {
revert MultiSigTimelock__InsufficientConfirmations(REQUIRED_CONFIRMATIONS, txn.confirmations);
}
+
+ // Require approvedAt to be set
+ if (txn.approvedAt == 0) {
+ revert("Transaction not approved");
+ }
+
// Check if timelock period has passed since approval
uint256 requiredDelay = _getTimelockDelay(txn.value);
- uint256 executionTime = txn.proposedAt + requiredDelay;
+ uint256 executionTime = txn.approvedAt + requiredDelay;
if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
// ... rest of function
}