Raisebox Faucet

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

Block timestamp manipulation risk

Author Revealed upon completion

Root + Impact

Description

Expected behavior:

Time-based logic (cooldowns and daily resets) should be robust to small, realistic variations in block timestamps and should not allow meaningful early bypass of rate limits or resets due to miner/validator timestamp adjustments.

Actual behavior:

The contract uses block.timestamp for the per-user cooldown (CLAIM_COOLDOWN) and for daily reset logic. Miners/validators can shift block timestamps slightly (on the order of seconds to a minute) which could be leveraged to marginally accelerate claims or cause earlier daily resets. While minor, this can be used repeatedly or combined with other conditions to gain advantage, especially on testnets where blocks and timestamps are less strictly enforced.


Root cause summary: relying directly on block.timestamp for critical timing without accounting for small timestamp manipulation or using coarse-grained comparisons (e.g., integer-day arithmetic) leaves the contract susceptible to small-but-real timing anomalies.

// Root cause in the codebase with @> marks to highlight the relevant section
// Per-user cooldown check
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
@> revert RaiseBoxFaucet_ClaimCooldownOn();
}
// Daily reset (example from code)
if (block.timestamp > lastFaucetDripDay + 1 days) {
@> lastFaucetDripDay = block.timestamp;
@> dailyClaimCount = 0;
}

Risk

Likelihood

Moderate: Manipulation by miners/validators is not trivial on mainnet but is realistic on testnets. Because faucet operations are time-based and frequent, the condition is plausible in practice.

Impact

1.Small timing manipulations could allow a claimant to make a claim slightly earlier than intended (seconds/minutes), or cause the faucet daily counters to reset earlier than real-world 24-hour boundaries.

2.On a testnet (where miner/validator control is lower and block behavior can be noisier), this can be used to claim marginal extra advantage repeatedly.

3.Impact is functional fairness (not immediate fund loss), but repeated small advantages can cumulate.


Proof of Concept


Explanation: the test first performs a legitimate claim, then simulates a small timestamp advance. For large cooldowns (3 days) a single small warp won't bypass, but the test demonstrates the contract's dependency on block.timestamp and that a miner could, in principle, shift timestamps. On shorter cooldowns or when combined with other conditions, these shifts can enable earlier claims.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../contracts/RaiseBoxFaucet.sol"; // adjust path if needed
contract TimestampPoC is Test {
RaiseBoxFaucet faucet;
address user = address(0xDEAD);
function setUp() public {
faucet = new RaiseBoxFaucet("RaiseBox", "RBT", 1000 ether, 0.005 ether, 1 ether);
// Fund faucet with tokens and ETH for claims
// (assumes faucet mints to itself in constructor)
vm.deal(address(faucet), 1 ether);
vm.deal(user, 1 ether);
}
function test_small_timestamp_advance_allows_early_claim() public {
// First claim by user
vm.prank(user);
faucet.claimFaucetTokens();
// Immediately try to claim again before cooldown -> should revert
vm.prank(user);
vm.expectRevert(); // cooldown active
faucet.claimFaucetTokens();
// Simulate a miner/validator advancing block timestamp by a small margin (e.g., +1 minute)
// In practice a miner could set the timestamp slightly forward within consensus limits.
vm.warp(block.timestamp + 60); // advance 60 seconds (simulates miner shift)
// If CLAIM_COOLDOWN was very short, this could allow an earlier claim.
// For the faucet with 3 days cooldown this single warp won't bypass, but repeated small shifts
// or shorter cooldowns could be exploited. This PoC demonstrates the timestamp dependency.
vm.prank(user);
bool reverted = false;
try faucet.claimFaucetTokens() {
// if not reverted, test reports success (rare for 3-day cooldown)
} catch {
reverted = true;
}
// assert that a tiny advance doesn't bypass a 3-day cooldown, but shows where the attack surface is
assertTrue(reverted, "Small timestamp advance should not bypass long cooldown — demo shows dependency on block.timestamp");
}
}

Recommended Mitigation

Use coarse-grained time units for resets and/or rely on block number for short windows. For daily resets, use integer-day arithmetic; for cooldowns consider using block numbers or document the acceptable trust model.


- remove this code
+ add this code
- // Per-user cooldown check using raw timestamp
- if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
- revert RaiseBoxFaucet_ClaimCooldownOn();
- }
+ // Prefer using block-based cooldown or add small tolerance if timestamp is used.
+ // Option A: use integer days for daily resets (already recommended elsewhere)
+ uint256 currentDay = block.timestamp / 1 days;
+ if (currentDay <= lastClaimDay[faucetClaimer]) {
+ revert RaiseBoxFaucet_ClaimCooldownOn();
+ }
+
+ // Option B: for exact cooldowns, compare block numbers (blocksPerDay = ~6500 on mainnet)
+ // uint256 blocksPerCooldown = blocksPerDay * 3; // example for 3 days
+ // require(block.number >= lastClaimBlock[faucetClaimer] + blocksPerCooldown, "Cooldown not elapsed");

Support

FAQs

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