The contract should use block numbers for time-based validations to prevent miner manipulation of timing-dependent functionality.
@> if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
@> uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
@> if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
contract TimestampManipulationAttack {
RaiseBoxFaucet public faucet;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
function demonstrateTimestampBypass() external pure returns (bool) {
uint256 lastClaimTime = 1000000;
uint256 claimCooldown = 259200;
uint256 normalCurrentTime = 1259180;
bool normalCheck = normalCurrentTime >= (lastClaimTime + claimCooldown);
uint256 manipulatedTime = normalCurrentTime + 15;
bool manipulatedCheck = manipulatedTime >= (lastClaimTime + claimCooldown);
return manipulatedCheck && !normalCheck;
}
function demonstrateDailyResetManipulation() external pure returns (uint256, uint256) {
uint256 deployTime = 1704151800;
uint256 normalMidnight = 1704067200;
uint256 normalDay = normalMidnight / 86400;
uint256 manipulatedTime = normalMidnight + 15;
uint256 manipulatedDay = manipulatedTime / 86400;
return (normalDay, manipulatedDay);
}
}
The mitigation involves replacing timestamp-based logic with block number-based logic to eliminate miner manipulation. Block numbers cannot be manipulated and provide consistent timing that's resistant to attacks.
- uint256 public constant CLAIM_COOLDOWN = 3 days;
+ uint256 public constant CLAIM_COOLDOWN_BLOCKS = 17280; // ~3 days at 15s/block average
- mapping(address => uint256) private lastClaimTime;
+ mapping(address => uint256) private lastClaimBlock;
- uint256 public lastDripDay;
- uint256 public lastFaucetDripDay;
+ uint256 public lastResetBlock;
function claimFaucetTokens() public nonReentrant {
faucetClaimer = msg.sender;
- if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
+ if (block.number < (lastClaimBlock[faucetClaimer] + CLAIM_COOLDOWN_BLOCKS)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
// Other checks...
// Replace daily reset logic with block-based system
- uint256 currentDay = block.timestamp / 24 hours;
- if (currentDay > lastDripDay) {
- lastDripDay = currentDay;
- dailyDrips = 0;
- }
-
- if (block.timestamp > lastFaucetDripDay + 1 days) {
- lastFaucetDripDay = block.timestamp;
- dailyClaimCount = 0;
- }
+ uint256 blocksPerDay = 5760; // ~24 hours at 15s/block
+ uint256 currentPeriod = block.number / blocksPerDay;
+ uint256 lastPeriod = lastResetBlock / blocksPerDay;
+
+ if (currentPeriod > lastPeriod) {
+ lastResetBlock = block.number;
+ dailyDrips = 0;
+ dailyClaimCount = 0;
+ }
// Update state with block numbers
- lastClaimTime[faucetClaimer] = block.timestamp;
+ lastClaimBlock[faucetClaimer] = block.number;
// Rest of function logic...
}
// Add getter function for compatibility
+ function getUserLastClaimBlock(address user) external view returns (uint256) {
+ return lastClaimBlock[user];
+ }
// Update constructor to initialize with block numbers
constructor(
string memory name_,
string memory symbol_,
uint256 faucetDrip_,
uint256 sepEthDrip_,
uint256 dailySepEthCap_
) ERC20(name_, symbol_) Ownable(msg.sender) {
faucetDrip = faucetDrip_;
sepEthAmountToDrip = sepEthDrip_;
dailySepEthCap = dailySepEthCap_;
+ lastResetBlock = block.number;
_mint(address(this), INITIAL_SUPPLY);
}