Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: medium
Likelihood: high
Invalid

Block Timestamp Manipulation Vulnerability

Author Revealed upon completion

Block Timestamp Manipulation Vulnerability

Description

The contract should use block numbers for time-based validations to prevent miner manipulation of timing-dependent functionality.

The contract relies on block.timestamp for cooldown periods and daily resets, which miners can manipulate within a ±15 second window to bypass timing restrictions and claim tokens more frequently than intended.

@> 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;
}

Risk

Likelihood: High

  • Miners control block timestamps within a ±15 second window on Ethereum mainnet and most EVM chains

  • Attackers can coordinate with miners or be miners themselves to manipulate timestamps

  • The manipulation window allows bypassing short-term timing restrictions

Impact: Medium

  • Users can claim tokens slightly more frequently than intended, disrupting economic model

  • Daily reset mechanisms can be manipulated to extend or shorten daily periods

  • Cumulative effect allows significant deviation from intended claim frequency

Proof of Concept

// Demonstrate timestamp manipulation attack
contract TimestampManipulationAttack {
RaiseBoxFaucet public faucet;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
// Simulate the vulnerability
function demonstrateTimestampBypass() external pure returns (bool) {
// Normal scenario:
uint256 lastClaimTime = 1000000; // Some past timestamp
uint256 claimCooldown = 259200; // 3 days in seconds
uint256 normalCurrentTime = 1259180; // 20 seconds before cooldown expires
// Normal check would fail:
bool normalCheck = normalCurrentTime >= (lastClaimTime + claimCooldown);
// Returns false - user cannot claim yet
// With miner manipulation (+15 seconds):
uint256 manipulatedTime = normalCurrentTime + 15;
bool manipulatedCheck = manipulatedTime >= (lastClaimTime + claimCooldown);
// Returns true - user can now claim early!
return manipulatedCheck && !normalCheck; // True if attack works
}
// Demonstrate daily reset manipulation
function demonstrateDailyResetManipulation() external pure returns (uint256, uint256) {
// Deploy time: Jan 1, 2024 23:50:00
uint256 deployTime = 1704151800;
// Scenario 1: Normal timestamp (Jan 2, 00:00:00)
uint256 normalMidnight = 1704067200;
uint256 normalDay = normalMidnight / 86400; // Day calculation
// Scenario 2: Manipulated timestamp (23:59:45 -> 00:00:15 next day)
uint256 manipulatedTime = normalMidnight + 15;
uint256 manipulatedDay = manipulatedTime / 86400;
// Miner can force early daily reset by 15 seconds
return (normalDay, manipulatedDay);
}
}

Attack scenario:

  1. User has lastClaimTime = timestamp T

  2. User waits almost 3 days (T + 3 days - 30 seconds)

  3. User bribes miner to set block.timestamp = T + 3 days + 1

  4. Cooldown check passes, user claims 30 seconds early

  5. Repeat pattern allows claiming ~10 minutes early per month

  6. Over time, significant deviation from intended economics

Recommended Mitigation

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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 2 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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