Raisebox Faucet

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

Gas Limit Vulnerabilities

Author Revealed upon completion

Gas Limit Vulnerabilities

Description

The contract should optimize gas usage and consider splitting complex operations to prevent transaction failures under high network congestion.

The claimFaucetTokens() function performs multiple storage operations, external calls, and complex calculations in a single transaction, potentially consuming excessive gas that could cause failures during network congestion or high gas price periods.

function claimFaucetTokens() public {
@> // Multiple storage reads (~2,100 gas each)
@> // Multiple storage writes (~20,000 gas each)
@> // Complex conditional logic with nested checks
@> // External ETH transfer (~21,000+ gas)
@> // Multiple event emissions (~375+ gas each)
@> // ERC20 token transfer (~3,000+ gas)
// All operations combined in single transaction create gas risk
}

Risk

Likelihood: Medium

  • Network congestion regularly increases gas prices and can reduce effective block gas limits

  • Complex transactions fail when gas requirements exceed user-set limits during peak usage

  • Users experience failed transactions during high network activity periods (NFT drops, DeFi events)

Impact: Medium

  • Legitimate users cannot claim tokens during high gas price periods, reducing faucet utility

  • Failed transactions waste user gas fees without providing any service or tokens

  • Faucet becomes unusable during network congestion events, hurting user experience

  • User experience degradation reduces protocol adoption and trust

Proof of Concept

// Demonstrate gas consumption analysis
contract GasConsumptionAnalysis {
RaiseBoxFaucet public faucet;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
// Measure gas consumption of claimFaucetTokens
function measureGasConsumption() external returns (
uint256 gasUsedNormal,
uint256 gasUsedFirstTime,
uint256 gasUsedWithReset
) {
// Scenario 1: Normal claim (no ETH, no reset)
uint256 gasStart = gasleft();
try faucet.claimFaucetTokens() {
gasUsedNormal = gasStart - gasleft();
} catch {
gasUsedNormal = type(uint256).max; // Failed due to gas
}
// Scenario 2: First-time claim (includes ETH transfer)
gasStart = gasleft();
// Simulate first-time claimer
gasUsedFirstTime = gasStart - gasleft() + 21000; // Add ETH transfer cost
// Scenario 3: Claim with daily reset (maximum operations)
gasStart = gasleft();
// Simulate daily reset scenario
gasUsedWithReset = gasStart - gasleft() + 40000; // Add reset operations cost
return (gasUsedNormal, gasUsedFirstTime, gasUsedWithReset);
}
// Show gas consumption breakdown
function analyzeGasBreakdown() external pure returns (
uint256 storageReads,
uint256 storageWrites,
uint256 externalCall,
uint256 eventEmissions,
uint256 tokenTransfer,
uint256 totalEstimate
) {
// Conservative estimates based on EVM costs
storageReads = 8 * 2100; // ~8 SLOAD operations
storageWrites = 4 * 20000; // ~4 SSTORE operations
externalCall = 21000; // ETH transfer call
eventEmissions = 3 * 375; // ~3 event emissions
tokenTransfer = 3000; // ERC20 transfer
totalEstimate = storageReads + storageWrites + externalCall + eventEmissions + tokenTransfer;
// Total: ~110,125 gas in worst case
return (storageReads, storageWrites, externalCall, eventEmissions, tokenTransfer, totalEstimate);
}
// Demonstrate failure scenarios
function simulateGasFailures() external pure returns (
string memory scenario1,
string memory scenario2,
string memory scenario3
) {
scenario1 = "User sets 80k gas limit, transaction fails on first-time claim with ETH";
scenario2 = "Network congestion raises gas price, users can't afford full transaction";
scenario3 = "Block gas limit pressure causes intermittent failures";
return (scenario1, scenario2, scenario3);
}
}

Real gas failure scenarios:

  1. Network congestion during popular NFT mint

  2. User sets conservative gas limit (75,000)

  3. claimFaucetTokens() requires 110,000+ gas for first-time user

  4. Transaction fails with "out of gas" error

  5. User pays gas fees but receives no tokens

  6. User tries again with higher limit, pays more fees

  7. During peak congestion, even high limits may fail

  8. Faucet becomes unusable when needed most

Recommended Mitigation

The mitigation splits the complex function into smaller, gas-efficient operations and implements gas optimization techniques to ensure reliable operation even during network congestion periods.

// Split complex operations into separate functions
+ function claimTokensOnly() external nonReentrant whenNotPaused {
+ address claimer = msg.sender;
+
+ // Simplified checks for token-only claim
+ require(claimer != address(0) && claimer != address(this) && claimer != owner(), "Invalid claimer");
+ require(block.timestamp >= lastClaimTime[claimer] + CLAIM_COOLDOWN, "Cooldown active");
+ require(balanceOf(address(this)) > faucetDrip, "Insufficient contract balance");
+ require(dailyClaimCount < dailyClaimLimit, "Daily limit reached");
+
+ // Handle daily reset efficiently
+ _handleDailyReset();
+
+ // Update state
+ lastClaimTime[claimer] = block.timestamp;
+ dailyClaimCount++;
+
+ // Transfer tokens
+ _transfer(address(this), claimer, faucetDrip);
+ emit Claimed(claimer, faucetDrip);
+ }
+
+ function claimFirstTimeEth() external nonReentrant whenNotPaused {
+ address claimer = msg.sender;
+
+ // Check eligibility for ETH
+ require(!hasClaimedEth[claimer], "Already claimed ETH");
+ require(!sepEthDripsPaused, "ETH drips paused");
+ require(address(this).balance >= sepEthAmountToDrip, "Insufficient ETH balance");
+
+ // Handle ETH daily limits
+ _handleEthDailyLimits();
+
+ // Update state before external call
+ hasClaimedEth[claimer] = true;
+ dailyDrips += sepEthAmountToDrip;
+
+ // External call
+ (bool success,) = claimer.call{value: sepEthAmountToDrip}("");
+ require(success, "ETH transfer failed");
+
+ emit SepEthDripped(claimer, sepEthAmountToDrip);
+ }
// Optimize the main function by caching storage reads
function claimFaucetTokens() public nonReentrant whenNotPaused {
+ // Cache storage reads to reduce gas
+ address claimer = msg.sender;
+ uint256 lastClaim = lastClaimTime[claimer];
+ uint256 currentBalance = balanceOf(address(this));
+ uint256 currentDailyCount = dailyClaimCount;
+ bool hasClaimedEthBefore = hasClaimedEth[claimer];
// Checks (optimized)
- faucetClaimer = msg.sender;
-
- if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
+ if (block.timestamp < (lastClaim + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
- if (faucetClaimer == address(0) || faucetClaimer == address(this) || faucetClaimer == Ownable.owner()) {
+ if (claimer == address(0) || claimer == address(this) || claimer == owner()) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
- if (balanceOf(address(this)) <= faucetDrip) {
+ if (currentBalance <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
- if (dailyClaimCount >= dailyClaimLimit) {
+ if (currentDailyCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
+ // Cache and update state efficiently
+ _updateClaimState(claimer);
// Handle ETH drip for first-time claimers (optimized)
- if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
+ if (!hasClaimedEthBefore && !sepEthDripsPaused) {
_handleFirstTimeEthDrip(claimer);
}
- // Remove redundant daily reset logic (moved to helper)
- if (block.timestamp > lastFaucetDripDay + 1 days) {
- lastFaucetDripDay = block.timestamp;
- dailyClaimCount = 0;
- }
-
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
// Interactions
- _transfer(address(this), faucetClaimer, faucetDrip);
- emit Claimed(msg.sender, faucetDrip);
+ _transfer(address(this), claimer, faucetDrip);
+ emit Claimed(claimer, faucetDrip);
}
// Add gas-efficient helper functions
+ function _updateClaimState(address claimer) internal {
+ _handleDailyReset();
+ lastClaimTime[claimer] = block.timestamp;
+ dailyClaimCount++;
+ }
+
+ function _handleDailyReset() internal {
+ uint256 currentDay = block.timestamp / 1 days;
+ if (currentDay > lastResetDay) {
+ lastResetDay = currentDay;
+ dailyDrips = 0;
+ dailyClaimCount = 0;
+ }
+ }
+
+ function _handleEthDailyLimits() internal {
+ uint256 currentDay = block.timestamp / 24 hours;
+ if (currentDay > lastDripDay) {
+ lastDripDay = currentDay;
+ dailyDrips = 0;
+ }
+
+ require(dailyDrips + sepEthAmountToDrip <= dailySepEthCap, "Daily ETH cap reached");
+ }
+
+ function _handleFirstTimeEthDrip(address claimer) internal {
+ if (address(this).balance >= sepEthAmountToDrip) {
+ _handleEthDailyLimits();
+
+ hasClaimedEth[claimer] = true;
+ dailyDrips += sepEthAmountToDrip;
+
+ (bool success,) = claimer.call{value: sepEthAmountToDrip}("");
+ if (success) {
+ emit SepEthDripped(claimer, sepEthAmountToDrip);
+ } else {
+ revert RaiseBoxFaucet_EthTransferFailed();
+ }
+ } else {
+ emit SepEthDripSkipped(claimer, "Faucet out of ETH");
+ }
+ }
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.