Web3 Faucet Service
Ubiquity · 2023 · Gasless onboarding solution using OpenZeppelin Defender Relay in Cloudflare Workers with abuse-resistant verification. Fully functional but never deployed to production due to shifting priorities.
Problem
New bounty hunters completing their first task at Ubiquity DAO encountered “not enough gas to claim” errors when attempting to redeem payment permits on Gnosis Chain. This critical onboarding barrier prevented crypto-inexperienced developers from accessing earned rewards, effectively blocking legitimate contributors at the moment of first success.
Approach
- Secure gas relay: OpenZeppelin Defender Relay to sponsor minimal xDAI transfers without private key custody
- Edge deployment: Cloudflare Workers for global availability with custom fetch-based patches for Defender compatibility
- Multi-layer validation: Supabase integration checking registered wallet existence and permit history to prevent abuse
- Intelligent preflight: Balance verification for both user eligibility and relayer funding before transaction execution
- Developer experience: Simple POST endpoint with JSON-RPC structured responses for consistent tooling integration
System diagram
flowchart LR Client[Client] --> POST[POST /faucet?address] POST --> Worker[Cloudflare Worker] Worker --> DB[Database Validation] DB --> RelayBal[Relay Balance Check] RelayBal --> Transfer[Gas Transfer Transaction] Transfer --> Response[Response with TX Hash]
Outcome
- Technical success: Fully functional gasless onboarding solution eliminating first-transaction barriers
- Security achieved: Zero private key custody with abuse-resistant validation via existing database schema
- Production readiness: QA endpoints with verifiable on-chain transactions and comprehensive error handling
- Never deployed: Complete implementation that never reached production due to shifting organizational priorities
Constraints
- Network specificity: Native xDAI required on Gnosis Chain (not WXDAI token) to solve actual gas payment problems
- Runtime limitations: Cloudflare Workers environment requiring custom Defender client patches (fetch over axios)
- Economic sustainability: Per-user subsidy must remain small and configurable (~$1 covering 10000 users)
- Abuse prevention: Database-driven duplicate subsidy resistance via permit history tracking
- Timeline pressure: Multiple task reassignments and tight delivery deadlines
Design choices
- Validation-first architecture: Database verification before any blockchain interaction to prevent failed transactions
- Deterministic preflight checks: Both user eligibility and relayer funding verification to avoid runtime failures
- Environment-driven configuration: Flexible claim fees, relay credentials, and network settings via environment variables
- Consistent API design: JSON-RPC response format for seamless integration with existing Ubiquity tooling
- Edge-first deployment: Global Cloudflare Workers distribution for predictable low-latency access
Proof
Code excerpt — core transaction flow
const userBal = await relayer.call("eth_getBalance", [ethAddress, "latest"]);
if (userBal.result > env.CLAIM_FEE) {
return makeRpcResponse({ error: { code: -32000, message: "Hunter has enough gas" } }, 400);
}
const relay_ = await relayer.getRelayer();
const relayBal = await relayer.call("eth_getBalance", [relay_.address, "latest"]);
if (relayBal.result < env.CLAIM_FEE) {
return makeRpcResponse({ error: { code: -32000, message: "Faucet has no funds" } }, 400);
}
const tx = await relayer.sendTransaction({
to: ethAddress,
value: toHex(env.CLAIM_FEE),
speed: "fast",
gasLimit: 21000,
});
Code excerpt — database validation
const { data } = await supabase.from("wallets").select("address").eq("address", ethAddress);
if (!data || data.length === 0) {
return makeRpcResponse({ error: { code: -32000, message: "Address not found" } }, 400);
}
const { data: permits } = await supabase
.from("permits")
.select("users(wallets(address))")
.not("users", "is", null)
.eq("users.wallets.address", ethAddress);
if (permits && permits.length > 0) {
return makeRpcResponse({ error: { code: -32000, message: "Has likely been subsidized before." } }, 400);
}
QA evidence — comprehensive scenario testing
# Already subsidized user (has permit)
res { jsonrpc: '2.0', error: { code: -32000, message: 'Has likely been subsidized before.' }, id: null }
# New user eligible for subsidy
res { jsonrpc: '2.0', result: { txHash: '0x3ca25068a7c59a190df4fb270e4e7348e46ec0b9e4b16f22ad19105b15c0dc58' }, id: null }
# User already has sufficient gas
res { jsonrpc: '2.0', error: { code: -32000, message: 'Hunter has enough gas' }, id: null }
# Verified transaction on-chain
https://mumbai.polygonscan.com/tx/0xfd938289d028e65be9ba38d34532d25c68569c7beb87034a3e0708f7f70d7f48
Deployment configuration — Cloudflare Workers
name = "ubq-gas-faucet"
main = "src/index.ts"
compatibility_date = "2023-02-01"
node_compat = true
# Custom patches for OpenZeppelin Defender compatibility
[build]
command = "npm run build"
# Environment variables for relay configuration
[env.production.vars]
CLAIM_FEE = "1000000000000000" # 0.001 xDAI
SUPABASE_URL = "https://your-project.supabase.co"
Implementation status — Complete but unused
- Development: ✅ Complete
- Testing: ✅ Comprehensive scenario coverage
- Security: ✅ Zero private key custody
- Deployment: ✅ Production-ready
- Production use: ❌ Never deployed
- Reason: Shifting organizational priorities
References
- Issue — #142 - Faucet service spec
- PR — #145 - Initial implementation
- Repo — ubiquity/faucet
- Defender Relay docs: https://docs.openzeppelin.com/defender/module/relayers