A browser-first capture-the-flag for learning Move smart contract security. Inspect vulnerable Move modules, write an exploit in the run() path, and return the expected flag type to solve each level. Inspired by OpenZeppelin Ethernaut.
Play online: moveover.openzeppelin.com
- Runs entirely in the browser — Level execution uses WASM; no backend or API required.
- Static export — Build once and deploy to any static host (e.g. GitHub Pages, Netlify, Vercel).
- Local progress — Completion is stored in the browser and reflected in the header progress bar.
- Shareable links — Each level has a stable URL:
/{locale}/levels/{id}. - Multi-language — Level content is localized; English is the default.
- Keyboard shortcut —
Cmd(macOS) orCtrl(Windows/Linux) +Enterruns your solution from the editor.
npm install
npm run devOpen http://localhost:3000, pick a level, read the contract and instructions, then implement the exploit in the solution editor.
Level source of truth lives in public/contracts/*.move. The app:
- Loads contract source and level metadata (id, difficulty, content).
- Lets you edit a solution that defines a
run()function. - Compiles and runs your solution in the browser via the Move WASM runtime.
- Checks that the execution returns the expected flag type for that level.
Metadata is generated from the contract files and config: npm run sync:meta (or the prebuild hook) rebuilds src/data/levels/meta.ts. Progress is stored in local storage and is not sent to any server.
To preview the production static build locally:
npm run build
npx serve outThen open the URL shown (e.g. http://localhost:3000).
The app uses @openzeppelin/ui-builder-ui for the locale select, level tabs, contract-module tabs, header progress bar (with tooltip), run button (with loading state and tooltip), run result alerts (success/error), and tooltips. The rest of the UI (buttons, cards, typography) uses local components in src/components/ui/.
| Path | Purpose |
|---|---|
public/contracts/ |
Move modules (.move) — source of truth for level challenges |
src/data/levels/meta.config.json |
Level id, difficulty, and module mapping |
src/data/levels/meta.ts |
Generated metadata (do not edit by hand) |
src/data/levels/runConfig.ts |
Per-level runner module and expected return type |
src/data/levels/content/*.json |
Localized level names, descriptions, instructions, hints |
src/app/ |
Next.js routes (landing, levels, locale-aware pages) |
src/components/ |
UI (level picker, code editor, hints, etc.) |
| Command | Description |
|---|---|
npm run dev |
Sync metadata, watch public/contracts/*.move, start Next.js dev server |
npm run dev:next |
Start Next.js only (no sync or contract watch) |
npm run build |
Production build; runs sync:meta before building |
npm run start |
Run next start (for non-static hosting) |
npm run lint |
Run ESLint |
npm run sync:meta |
Regenerate src/data/levels/meta.ts from public/contracts |
npm run create:level |
Interactive flow to add a new level |
npm run delete:level |
Interactive flow to remove a level |
Use the automated flow:
npm run create:levelYou’ll be prompted for name, difficulty, instructions, and Move code. The script creates the contract file, updates config and content, and runs metadata sync. For manual steps and contract conventions, see ADD_LEVEL_README.md.
To remove a level:
npm run delete:levelYou’ll choose the level by id and confirm. For details, see DELETE_LEVEL_README.md.
Contributions are welcome — new levels, bug fixes, docs, and UX improvements. See CONTRIBUTING.md for how to fork, branch, run checks, and open a pull request.
This project is open source under the MIT License.