A full rewrite of the original faucet into a modern, abuse-resistant architecture:
- Hardened Solidity faucet contract
- API service that issues short-lived EIP-712 claim tickets
- React dashboard for wallet auth, claims, and donations
- Clean monorepo layout with explicit setup and ops docs
Legacy faucet implementations (single function + static frontend) are easy to drain with scripted wallets and botnets. Modern public faucets now combine:
- Wallet ownership proof (signed challenge)
- Eligibility rules and rate limits
- Short-lived signed claim authorization
- On-chain replay/cooldown enforcement
- Operational observability and explicit runbooks
This repository now follows that model.
The legacy GitHub Pages frontend deployment was removed. This project is now run as a full-stack application (apps/api + apps/web) and deployed via your own hosting/runtime.
apps/web (React + viem)
|
| 1) POST /challenge
| 2) sign challenge with wallet
| 3) POST /ticket
v
apps/api (Fastify + viem signer)
|
| validates: signature, captcha (optional), ip/address cooldown,
| on-chain claim eligibility, faucet balance
| issues EIP-712 ticket (nonce + deadline + signature)
v
contracts/contracts/ModernEthFaucet.sol
|
| verifies ticket signer + nonce uniqueness + cooldown + wallet balance cap
| transfers drip amount and records next claim window
v
recipient wallet
.
├── apps/
│ ├── api/ # Fastify backend issuing claim tickets
│ └── web/ # React frontend
├── contracts/
│ ├── contracts/ # Solidity contracts
│ ├── scripts/ # Deployment scripts
│ └── test/ # Hardhat tests
├── package.json # Workspace scripts
└── tsconfig.base.json
contracts/contracts/ModernEthFaucet.sol
Key protections:
claim(bytes32 nonce, uint256 deadline, bytes signature)- Verifies EIP-712 signature from trusted
claimSigner - Enforces one-time nonce usage (
usedNonces) - Enforces per-wallet cooldown (
nextEligibleAt) - Rejects over-funded recipient wallets (
maxRecipientBalanceWei) - Uses
call+nonReentrantfor payout safety
- Verifies EIP-712 signature from trusted
- Owner controls:
setClaimSignersetFaucetConfigpause/unpausewithdraw
- Donation support:
receive()anddonate()emitDonationReceived
apps/api/src/index.ts
GET /api/v1/healthGET /api/v1/configPOST /api/v1/challenge- Input: wallet address
- Output: challenge message + challengeId + expiry
POST /api/v1/ticket- Input: address, challengeId, wallet signature, optional captcha token
- Output: short-lived claim ticket (
nonce,deadline,signature)
- Challenge signature validation (
verifyMessage) - Optional Turnstile verification
- Local IP + address cooldown gate
- On-chain recipient cooldown check (
nextEligibleAt) - Recipient wallet balance ceiling
- Faucet liquidity check
apps/web/src/App.tsx
Features:
- Wallet connection (EIP-1193)
- Live faucet + wallet state
- Challenge signing flow
- Claim ticket request + on-chain
claimexecution - Donation transaction flow
- Mobile/desktop responsive UI
- Node.js 20.x or 22.x LTS (Hardhat does not support Node 25)
- npm 10+ or Bun 1.3+
- A Sepolia-compatible RPC endpoint
npm installcp contracts/.env.example contracts/.env
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.envFill all required variables, especially:
contracts/.envSEPOLIA_RPC_URLDEPLOYER_PRIVATE_KEYFAUCET_OWNERTICKET_SIGNER_ADDRESS
apps/api/.envRPC_URLFAUCET_CONTRACT_ADDRESSTICKET_SIGNER_PRIVATE_KEY
apps/web/.envVITE_API_BASE_URLVITE_FAUCET_ADDRESS
npm run contracts:testnpm run contracts:deployThe deployment script can seed initial faucet balance via INITIAL_FUNDING_WEI.
npm run dev- API: http://localhost:8787
- Web: http://localhost:5173
| Variable | Description |
|---|---|
SEPOLIA_RPC_URL |
RPC endpoint used for deployment/verification. |
HOLESKY_RPC_URL |
Optional alternate network RPC endpoint. |
DEPLOYER_PRIVATE_KEY |
Deployer wallet private key. |
ETHERSCAN_API_KEY |
Optional contract verification key. |
FAUCET_OWNER |
Owner address for admin controls. |
TICKET_SIGNER_ADDRESS |
Address allowed to sign claim tickets. |
DRIP_AMOUNT_WEI |
Drip amount paid per successful claim. |
COOLDOWN_SECONDS |
Minimum wait between claims per wallet. |
MAX_RECIPIENT_BALANCE_WEI |
Wallet balance ceiling for eligibility. |
INITIAL_FUNDING_WEI |
Optional deploy-time faucet seed amount. |
| Variable | Description |
|---|---|
HOST / PORT |
API bind address/port. |
WEB_ORIGIN |
Allowed CORS origin for frontend. |
RPC_URL |
RPC endpoint for on-chain checks. |
FAUCET_CHAIN_ID |
Chain ID used in typed-data domain. |
FAUCET_CONTRACT_ADDRESS |
Deployed faucet contract address. |
FAUCET_DOMAIN_NAME / FAUCET_DOMAIN_VERSION |
EIP-712 domain values (must match contract). |
FAUCET_DRIP_WEI |
Expected on-chain drip amount. |
FAUCET_COOLDOWN_SECONDS |
Expected on-chain cooldown. |
FAUCET_MAX_RECIPIENT_BALANCE_WEI |
Eligibility balance cap. |
TICKET_SIGNER_PRIVATE_KEY |
Private key used to sign claim tickets. |
CHALLENGE_TTL_SECONDS |
Signature challenge expiry window. |
TICKET_TTL_SECONDS |
Claim ticket expiry window. |
ADDRESS_COOLDOWN_SECONDS / IP_COOLDOWN_SECONDS |
Local anti-abuse cooldown windows. |
TURNSTILE_SECRET_KEY |
Optional Cloudflare Turnstile secret. |
TURNSTILE_VERIFY_URL |
Turnstile verification endpoint. |
| Variable | Description |
|---|---|
VITE_API_BASE_URL |
Base URL of faucet API. |
VITE_CHAIN_ID |
Target chain ID for wallet switching. |
VITE_RPC_URL |
Public RPC used for read operations. |
VITE_FAUCET_ADDRESS |
Faucet contract address. |
VITE_EXPLORER_BASE_URL |
Explorer base URL for tx/address links. |
VITE_TURNSTILE_SITE_KEY |
Optional key for frontend Turnstile integration. |
npm run dev
npm run build
npm run test
npm run lint
npm run formatPrimary defense layers:
- Off-chain challenge-response proves wallet control before ticket issuance.
- Ticket signatures are EIP-712 typed data bound to recipient + nonce + deadline.
- Contract-level nonce replay protection prevents ticket reuse.
- Contract cooldown and max-balance checks provide deterministic anti-drain rules.
- Optional captcha adds bot friction for public internet deployments.
- Owner pause switch enables emergency shutdown.
- Replace in-memory challenge/rate-limit storage with Redis.
- Add reputation inputs (attestations, account age, allowlists, etc.) to API eligibility.
- Move signer key into KMS/HSM; never keep long-lived plaintext secrets on disk.
- Add structured logs + metrics (latency, deny reasons, claim success rates).
- Add CI checks (lint, tests, static analysis, contract size limits).
- Add multi-region RPC fallback and health probes.
This rewrite follows patterns used by current public faucets and standards docs:
- Ethereum test networks + faucet references: ethereum.org testnets
- Chainlink faucet wallet-first UX: faucets.chain.link
- Alchemy faucet eligibility and cooldown model: Alchemy Sepolia Faucet
- QuickNode faucet anti-abuse FAQ and timing limits: QuickNode Faucet
- Typed signatures standard: EIP-712
- Contract security primitives (ReentrancyGuard/Pausable): OpenZeppelin Contracts docs
- EIP-712 implementation helper: OpenZeppelin EIP712 docs
- Captcha verification API: Cloudflare Turnstile server-side validation
MIT