Solidity, for LeetCode.
SleetCode is a LeetCode-style competitive coding platform built specifically for Solidity and smart contract development. It gives developers a curated set of Solidity challenges — ranging from basic storage patterns to gas optimization and security-vulnerability fixes — and grades submissions against hidden Foundry test suites, the same way LeetCode grades against hidden test cases.
If you've ever wanted to practice Solidity the way you practice DSA — bite-sized problems, instant feedback, a difficulty ladder, and a judge that doesn't lie to you — that's what this is.
- Why SleetCode
- How It Works
- Architecture
- Tech Stack
- Monorepo Structure
- Problem Format
- Execution & Sandboxing
- Getting Started
- Environment Variables
- Seed Problems
- Roadmap
- Notes on Cross-Platform Dev
- Deployment
- Contributing
- License
Most Solidity learning resources are either full tutorials (too slow) or full audits (too advanced). There's no equivalent of "just give me 50 small, testable problems and let me grind." SleetCode fills that gap:
- Problem-first, not project-first. Every challenge is small, self-contained, and solvable in one sitting.
- Foundry as the judge. Correctness is defined by
forge test, not by a human reviewing your PR. - Difficulty ladder. Easy → Medium → Hard, mirroring the LeetCode mental model developers already have.
- Security-aware problems. Reentrancy guards, flash loan callbacks, and other real-world vulnerability patterns are first-class problem categories, not an afterthought.
- You pick a problem from the problem list (e.g. "SimpleVault — deposit/withdraw ETH with balance tracking").
- You write your Solidity solution in an in-browser Monaco editor (the same editor that powers VS Code).
- On submit, your code is sent to the backend, queued as a job, and dropped into an isolated Docker sandbox running Foundry.
- Inside the sandbox: your contract is compiled (
forge build --json) and tested against a hidden test contract (forge test --json) that the problem author wrote. - Results — pass/fail per test, gas used, and compiler errors with line numbers — are streamed back and rendered in the UI.
The hidden test contract is the specification. There's no ambiguity about what "correct" means — if it passes the tests, it's correct.
┌─────────────────────────────────────────────────────────┐
│ Next.js Frontend │
│ Problem List │ Code Editor (Monaco) │ Test Results UI │
└─────────────────────────┬───────────────────────────────┘
│ REST / WebSocket
┌─────────────────────────▼───────────────────────────────┐
│ Fastify API (Node.js / TypeScript) │
│ Job Queue (BullMQ + Redis) → Docker Execution │
└─────────────────────────┬───────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────┐
│ Docker Container (per submission) │
│ forge build --json → forge test --json → JSON result │
└─────────────────────────────────────────────────────────┘
Submission flow:
POST /submit → enqueue job, return jobId
GET /result/:jobId → poll for status (or WebSocket push)
Submissions are never executed synchronously and never run on the host — every submission gets its own throwaway, network-isolated, resource-capped container.
| Layer | Technology |
|---|---|
| Frontend | Next.js, Monaco Editor, Tailwind CSS |
| Editor | @monaco-editor/react + Solidity syntax highlighting |
| Backend | Fastify (Node.js / TypeScript) |
| Job Queue | BullMQ + Redis |
| Execution Sandbox | Docker + Foundry image |
| ORM / Database | Drizzle ORM + PostgreSQL |
| Monorepo Tooling | Turborepo / npm workspaces |
| Deployment | Render (API + workers), Upstash Redis |
sleetcode/
├── apps/
│ ├── web/ # Next.js frontend
│ │ ├── app/
│ │ │ ├── problems/
│ │ │ │ └── [slug]/ # Problem detail + editor page
│ │ │ └── ...
│ │ └── components/
│ └── api/ # Fastify backend
│ ├── src/
│ │ ├── routes/ # submit, result, problems, auth
│ │ ├── queue/ # BullMQ producers/consumers
│ │ ├── sandbox/ # Docker execution engine
│ │ └── index.ts
│ └── Dockerfile # Bakes Foundry into the API image
├── packages/
│ ├── db/ # Drizzle schema, migrations, client
│ ├── shared/ # Shared TypeScript types (Problem, SubmissionResult, etc.)
│ └── ui/ # Shared UI components (optional)
├── foundry-sandbox/ # Standalone execution image (if run separately from API)
│ ├── Dockerfile
│ └── entrypoint.sh
├── docker-compose.yml
├── turbo.json
└── package.json
Every problem is a row in Postgres with this shape:
type Problem = {
id: string
title: string
slug: string
difficulty: 'Easy' | 'Medium' | 'Hard'
description: string // markdown, shown in the left panel
starterCode: string // what the user sees when they open the problem
solutionTemplate: string // scaffold with function signatures
testContract: string // hidden Foundry test file
tags: string[] // e.g. ['ERC20', 'Storage', 'Gas']
}The testContract is never sent to the client. It's injected server-side at execution time, alongside the user's submission, into the sandbox.
Example hidden test:
contract VaultTest is Test {
Vault vault;
function setUp() public {
vault = new Vault();
}
function test_deposit() public {
vault.deposit{value: 1 ether}();
assertEq(vault.balanceOf(address(this)), 1 ether);
}
function test_withdraw() public {
// ...
}
}The execution engine is the core of the whole project — everything else is standard web development.
Per-submission container constraints:
NetworkMode: 'none'— no network access, ever- Memory capped (e.g. 256MB)
- CPU capped (e.g. 1 vCPU)
- Hard timeout (e.g. 30s) — treated as a "Time Limit Exceeded" verdict, same as an online judge
AutoRemove: true— containers never persist after finishing
Pipeline inside the container:
- Write user code to
src/Solution.sol - Write the hidden test to
test/Solution.t.sol - Run
forge build --json— if this fails, return structured compiler errors (with line numbers, for highlighting in Monaco) - Run
forge test --json— parse pass/fail per test, plus gas usage - Return a single structured
SubmissionResultback through the queue
type SubmissionResult = {
passed: boolean
tests: {
name: string
passed: boolean
reason: string | null
gasUsed: number
}[]
compileError: string | null
}The API's own Docker image has Foundry baked in directly, so forge is shelled out to as a subprocess rather than requiring a separate sandbox image round-trip — simplifying the deployment story while keeping submissions isolated per-job.
- Node.js 20+
- Docker (with Foundry image support)
- PostgreSQL
- Redis
# clone
git clone https://github.com/<your-username>/sleetcode.git
cd sleetcode
# install dependencies (monorepo root)
npm install
# copy env files
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env
# run database migrations
npm run db:migrate --workspace=packages/db
# seed problems
npm run db:seed --workspace=packages/db
# start everything (frontend, API, worker) via Turborepo
npm run devdocker compose up --buildThis brings up Postgres, Redis, the API (with Foundry baked in), and the frontend together.
| Variable | Description | Where |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | apps/api |
REDIS_URL |
Redis connection string (BullMQ) | apps/api |
NEXT_PUBLIC_API_URL |
Base URL of the Fastify API | apps/web |
AUTH_SECRET / provider keys |
Auth provider credentials | apps/api, apps/web |
Note: Any script run directly with
tsxmust explicitlyimport "dotenv/config"at the top — env vars are not auto-loaded outside the Next.js/Fastify runtime bootstraps.
The initial problem set, ordered roughly by difficulty:
- Hello Solidity (Easy) — store and return a
uint - SimpleVault (Easy) — deposit/withdraw ETH with balance tracking
- ERC20 From Scratch (Medium) — implement
transfer/approve/transferFrom - Reentrancy Guard (Medium) — fix a vulnerable withdraw function
- Gas Golf (Hard) — optimize a loop to stay under a target gas limit
- Flash Loan Receiver (Hard) — correctly implement
IFlashLoanReceiver
- Monorepo scaffold (apps/web, apps/api, packages/db, packages/shared)
- Drizzle schema + migrations for problems/submissions/users
- Docker-based Foundry execution engine
- Monaco editor problem page with submit → poll → results flow
- Problem list page with difficulty/tag filtering
- Auth + per-user submission history
- Leaderboard / streak tracking
- Discussion / editorial per problem
This project is developed across both Windows and WSL. A few gotchas worth documenting for contributors:
- Never share
node_modulesacross Windows and WSL filesystems — binaries compiled for one will silently corrupt on the other. Always runnpm installfresh inside WSL if you're developing there. - Monorepo hoisting can cause Drizzle ORM version mismatches between
packages/dband consumers — pin versions explicitly if you hit schema/type errors that don't match your actualdrizzle-ormversion. drizzle-kithas known quirks generating SQL for array-typed columns with defaults — double-check generated migrations for array columns before applying them.- If you ever run
forge initinside a package that's already part of the monorepo's git tree, remove the nested.gitit creates — otherwise you'll get submodule-like conflicts.
- API + worker: deployed to Render, with Foundry baked directly into the API's Docker image (no separate sandbox image to manage in production).
- Redis: Upstash (serverless Redis, works well with Render's networking model).
- Database: any managed Postgres (Render Postgres, Supabase, Neon, etc.).
Anvil/on-chain deployment of user submissions is intentionally out of scope — forge test is the source of truth for correctness, so there's no need to deploy anything to a live or local chain.
Contributions of new problems are especially welcome. A new problem is just:
- A markdown description
- A starter code scaffold
- A hidden Foundry test contract
- A difficulty + tag set
Open a PR against packages/db/seed/problems/ following the existing problem shape.
MIT