Three Bugs That Nearly Killed Our Web3 Hackathon Project
CORS, commit-reveal race conditions, and Map size exceeded — surviving a 10-day hackathon on X Layer with Uniswap V4 hooks.
· DEV
We built Fanovo — a Uniswap V4 Hook protocol for the World Cup. Two hackathons at once, X Layer, 48 country tokens, 144 player tokens, bonding curves, commit-reveal packs. 10 days total. Sounded like an adventure. Spoiler: it was.
29 OpenCode sessions later, about half were about bugs. These three hurt the most.
Bug #1: Why Was the RPC Cursed?
Everything worked in MetaMask. Contracts deployed. Transactions went through. But the frontend loaded zero data. Wagmi just stayed silent. No errors. No logs.
Turns out X Layer RPC did not send CORS headers for browser requests. MetaMask, being a browser extension, does not care about CORS. But wagmi uses fetch, and the browser quietly blocked every response.
First attempt: Next.js rewrites pointing to the same RPC. Did not work. Rewrites did not forward POST bodies correctly, and the RPC returned 400.
// Before: Next.js rewrites
const nextConfig = {
async rewrites() {
return [{
source: "/api/rpc",
destination: "https://rpc.xlayer.tech/",
}];
},
};
We moved to a dedicated API route. Added User-Agent, CORS headers, and retry logic for 429 rate limits. After that, everything flew.
// After: dedicated API route
export async function POST(request: NextRequest) {
const body = await request.text();
const res = await fetch(RPC_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "fanovo-frontend/1.0",
},
body,
});
const data = await res.text();
return new NextResponse(data, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
Bug #2: Why Would the Commit-Reveal Not Open a Second Pack?
Pack opening used commit-reveal: user sends a hash, waits for confirmation, sends the original value. A fair scheme against frontrunning.
First pack opens fine. Second one reverts. The contract still remembered the previous commit — it thought the user was already opening a pack. The slot never got cleared after reveal.
// Problem: commit slot not cleared after reveal
function reveal(commitHash, value) {
require(commits[msg.sender] == commitHash);
// delete commits[msg.sender]; // this line was missing
_openPack(msg.sender, value);
}
Fix: one line — delete commits[msg.sender] after successful reveal. Plus emit a PackOpened event with the user address so the frontend can listen without reloading.
Bug #3: Why Did Map Maximum Size Get Exceeded?
The site worked. For about 10 minutes. Then Next.js crashed with "RangeError: Map maximum size exceeded". Not the browser — the server-side render just died.
useBlockNumber from wagmi watches every block by default. On X Layer a block comes every 2 seconds. In 10 minutes the V8 Map had 300+ entries and hit the hard limit.
// Before: watches every block
const { data: blockNumber } = useBlockNumber({ watch: true });
// After: polls every 15 seconds
const { data: blockNumber } = useBlockNumber({
watch: true,
query: { refetchInterval: 15_000 },
});
We also built a server-side indexer for burnt stats — the client could not aggregate thousands of events. Moved computation to the server, served pre-calculated numbers. BURNT LAST 24H stopped being forever zero.
What Was the Takeaway?
We shipped the project. Two hackathons, two contracts, Next.js frontend, all running on X Layer mainnet. The funny thing — none of these bugs were in the contract logic. CORS, a memory leak, a missing delete. This is what actually eats your time.
Web3 development is 20% smart contracts and 80% fighting the tools around them.