A public dApp on Base that demonstrates every feature of B20 — Base's native token standard — in a guided, gamified flow.
Deploy two B20 tokens (Asset + Stablecoin) via the B20Factory precompile, exercise all 40 capabilities one by one, and call renounceLastAdmin to seal the run as immutable onchain evidence.
| Network | Upgrade | Timestamp | Date (UTC) |
|---|---|---|---|
| Base Sepolia | Beryl | 1781805600 |
2026-06-18 18:00 UTC |
| Base Mainnet | Beryl | 1782410400 |
2026-06-25 18:00 UTC |
All code is complete and ready. The UI shows a countdown banner until activation. Nothing needs to change — the app detects activation automatically by calling eth_getCode at the B20Factory address (0xB20f000000000000000000000000000000000000).
| Tool | Version | Install |
|---|---|---|
| Node.js | ≥ 20 | nodejs.org |
| Foundry | latest | curl -L https://foundry.paradigm.xyz | bash && foundryup |
| git | any | — |
/contracts
src/Speedrun.sol ← Onchain scorekeeper (40 steps, uint64 bitmap)
script/Deploy.s.sol ← forge script deploy
test/Speedrun.t.sol ← Foundry tests (mock precompile via vm.etch)
foundry.toml
remappings.txt
/web ← Next.js 14 (App Router)
app/
layout.tsx
page.tsx ← Landing + activation banner + leaderboard
run/page.tsx ← Active speedrun dashboard (all 40 steps)
profile/[addr]/page.tsx ← Public per-runner view + step checklist
components/
Providers.tsx ← wagmi + RainbowKit providers
ActivationBanner.tsx ← Live countdown to Beryl activation
StepCard.tsx ← Individual step with Execute + Mark Done
LevelGrid.tsx ← 5-level grid of StepCards
PauseProbe.tsx ← Simulates paused op and captures revert proof
lib/
abis/
B20.ts ← IB20 ABI (both token variants)
B20Asset.ts ← IB20Asset extensions (announce, multiplier, etc.)
B20Factory.ts ← IB20Factory ABI + B20Variant enum
PolicyRegistry.ts ← IPolicyRegistry ABI + PolicyType enum
Speedrun.ts ← Speedrun ABI + bytecode (populated by make sync-abi)
addresses.ts ← Precompile addresses + Beryl activation timestamps
wagmi.ts ← wagmi config (Base Sepolia + Mainnet)
steps.ts ← All 40 step definitions with prereqs, gas, doc links
hooks.ts ← useActivationStatus, useBerylCountdown, useSpeedrunProgress
/scripts
sync-abi.js ← Copies ABI+bytecode from forge out/ to web/lib/abis/Speedrun.ts
Makefile ← install, build, test, sync-abi, deploy-*, web-dev
.env.example ← Required env vars (never commit .env)
# 1. Clone
git clone <this-repo>
cd Speedrun
# 2. Copy env and fill in your values
cp .env.example .env
# Edit .env: set BASE_SEPOLIA_RPC_URL, PRIVATE_KEY, NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
# 3. Install everything (forge-std + npm deps)
make install
# 4. Build contracts + sync ABI to the web app
make sync-abi
# 5. Run tests
make test
# 6. Start the frontend
make web-dev
# → http://localhost:3000| Variable | Required | Description |
|---|---|---|
BASE_MAINNET_RPC_URL |
Contracts | RPC for Base mainnet (default: https://mainnet.base.org) |
BASE_SEPOLIA_RPC_URL |
Contracts | RPC for Base Sepolia (default: https://sepolia.base.org) |
PRIVATE_KEY |
Not needed | Key is stored in Foundry encrypted keystore (cast wallet import), not in env. |
BASESCAN_API_KEY |
Optional | For --verify on deploy. Get free at basescan.org. |
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID |
Frontend | Free at cloud.walletconnect.com |
NEXT_PUBLIC_SPEEDRUN_ADDRESS |
Optional | Pre-fill the Speedrun contract address in the UI |
The private key never appears in any CLI argument, env var, log file, or shell history.
Foundry's encrypted keystore (cast wallet import) is used instead.
# ── One-time setup (run once, key stored AES-256 encrypted) ──
cast wallet import speedrun --interactive
# → paste your private key (hidden input, like sudo)
# → set a keystore password
# → key stored at ~/.foundry/keystores/speedrun
# ── Deploy ────────────────────────────────────────────────────
make deploy-sepolia
# → prompts for keystore password only (key never in CLI args)
# → prints Speedrun contract address + ready-to-copy cast send command
# ── Call initTokens() ─────────────────────────────────────────
# Salts are arbitrary bytes32 — pick any two different values.
cast send <SPEEDRUN_ADDR> \
"initTokens(bytes32,bytes32,string)" \
$(cast --from-utf8 asset | cast --to-bytes32) \
$(cast --from-utf8 stable | cast --to-bytes32) \
"USD" \
--rpc-url $BASE_SEPOLIA_RPC_URL \
--account speedrunmake deploy-mainnet # prompts for confirmation| # | Operation | Token | Gas est. |
|---|---|---|---|
| 1 | Deploy Asset token (decimals=12) via initTokens() |
Asset | 180k |
| 2 | Deploy Stablecoin token (immutable currency code) | Stable | 160k |
| 3 | grantRole(MINT_ROLE, deployer) |
Asset | 45k |
| 4 | grantRole(BURN_ROLE, deployer) |
Asset | 45k |
| 5 | grantRole(BURN_BLOCKED_ROLE, deployer) |
Asset | 45k |
| 6 | grantRole(PAUSE_ROLE, deployer) |
Asset | 45k |
| 7 | grantRole(UNPAUSE_ROLE, deployer) |
Asset | 45k |
| 8 | grantRole(METADATA_ROLE, deployer) |
Asset | 45k |
| 9 | grantRole(OPERATOR_ROLE, deployer) on Asset |
Asset | 45k |
| # | Operation | Target | Gas est. |
|---|---|---|---|
| 10 | PolicyRegistry.createPolicy(admin, BLOCKLIST) → blocklistId |
PolicyRegistry | 60k |
| 11 | PolicyRegistry.createPolicy(admin, ALLOWLIST) → allowlistId |
PolicyRegistry | 60k |
| 12 | updateBlocklist(blocklistId, true, [victim]) |
PolicyRegistry | 55k |
| 13 | updateAllowlist(allowlistId, true, [addr]) |
PolicyRegistry | 55k |
| 14 | updatePolicy(TRANSFER_SENDER_POLICY, blocklistId) |
Asset | 50k |
| 15 | updatePolicy(TRANSFER_RECEIVER_POLICY, allowlistId) |
Asset | 50k |
| 16 | updatePolicy(TRANSFER_EXECUTOR_POLICY, policyId) |
Asset | 50k |
| 17 | updatePolicy(MINT_RECEIVER_POLICY, allowlistId) |
Asset | 50k |
| 18 | stageUpdateAdmin(policyId, newAdmin) + finalizeUpdateAdmin(policyId) |
PolicyRegistry | 80k |
| # | Operation | Token | Gas est. |
|---|---|---|---|
| 19 | mint(to, amount) |
Asset | 55k |
| 20 | mintWithMemo(to, amount, memo) |
Asset | 60k |
| 21 | transfer(to, amount) |
Asset | 55k |
| 22 | transferWithMemo(to, amount, memo) |
Asset | 60k |
| 23 | approve(spender, amount) + transferFrom(from, to, amount) |
Asset | 70k |
| 24 | transferFromWithMemo(from, to, amount, memo) |
Asset | 75k |
| 25 | burn(amount) |
Asset | 50k |
| 26 | burnWithMemo(amount, memo) |
Asset | 55k |
| 27 | Mint to victim → burnBlocked(victim, amount) |
Asset | 80k |
| 28 | updateSupplyCap(newCap) |
Asset | 45k |
| 29 | EIP-712 off-chain sign → permit(owner, spender, value, deadline, v, r, s) |
Asset | 65k |
| # | Operation | Token | Gas est. |
|---|---|---|---|
| 30 | updateMultiplier(newMultiplier) — UI shows scaledBalanceOf before/after |
Asset | 50k |
| 31 | announce([batchMint(…)], id, description, uri) — posts disclosure + mints atomically |
Asset | 150k |
| 32 | updateExtraMetadata("twitter", "@…") then updateExtraMetadata("twitter", "") to delete |
Asset | 60k |
| 33 | updateName(newName) — emits NameUpdated + EIP712DomainChanged |
Asset | 50k |
| 34 | updateSymbol(newSymbol) — emits SymbolUpdated (no EIP712DomainChanged) |
Asset | 50k |
| 35 | updateContractURI(newURI) — emits parameterless ContractURIUpdated per ERC-7572 |
Asset | 50k |
| # | Operation | Notes |
|---|---|---|
| 36 | pause([TRANSFER]) |
UI auto-probes transfer → captures ContractPaused(TRANSFER) revert |
| 37 | pause([MINT]) |
UI auto-probes mint → captures ContractPaused(MINT) revert |
| 38 | pause([BURN]) |
UI auto-probes burn → captures ContractPaused(BURN) revert |
| 39 | unpause([TRANSFER, MINT, BURN]) |
Unpauses all three in one call |
| 40 | renounceLastAdmin() on both tokens |
IRREVERSIBLE — triple-confirm modal. Caller must be sole admin. |
Total estimated cost: ~$1–3 at typical Base fees (1–5 gwei).
Verified from base/base-std and eth_getCode on both networks:
| Precompile | Address |
|---|---|
| B20Factory | 0xB20f000000000000000000000000000000000000 |
| ActivationRegistry | 0x8453000000000000000000000000000000000001 |
| PolicyRegistry | 0x8453000000000000000000000000000000000002 |
Struct layouts (version must be 1):
// Asset
abi.encode(uint8(1), name, symbol, initialAdmin, uint8(decimals))
// Stablecoin
abi.encode(uint8(1), name, symbol, initialAdmin, currency)Divergences from original spec:
announce(internalCalls, id, description, uri)—idisstring, notbytes32pause/unpausetakeuint8[](feature array), not a single featurerenounceLastAdminrequires caller to be sole DEFAULT_ADMIN holder (check before calling)- ERC-2612
permit: EOA signatures only — ERC-1271 contract signatures not supported
- Single-player per deployment: each runner deploys their own
Speedrun.sol. No factory pattern. - Progress stored onchain as
uint64bitmap: biti= stepicomplete. 40 steps fit with headroom. markStepis honor-system: the runner provides the proving tx hash. The event log is the permanent evidence.- Speedrun.sol does NOT forward B20 calls: the player calls B20 tokens and PolicyRegistry directly from their wallet.
markStepis a separate write. - Activation detection:
useActivationStatus()callseth_getCodeat the factory address. If empty → shows banner. Auto-clears when Beryl activates.
make test
# or verbose:
make test-verboseThe test file deploys a MockB20Factory at 0xB20f000000000000000000000000000000000000 using Foundry's vm.etch, then tests all Speedrun.sol logic without needing live precompiles.
# Push to GitHub, then in Vercel:
# Root directory: web
# Build command: npm run build
# Output directory: .next
# Environment variables in Vercel dashboard:
# NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=...
# NEXT_PUBLIC_SPEEDRUN_ADDRESS=... (after you deploy the contract)-
forge buildsucceeds -
make test→ all tests pass (uses mock precompile) -
make sync-abi→ bytecode populated inweb/lib/abis/Speedrun.ts - 2026-06-18 18:00 UTC → Beryl activates on Base Sepolia
- Deploy Speedrun.sol to Sepolia via
make deploy-sepolia - Call
initTokens()with your chosen salts + currency - Run all 40 steps via the UI
-
renounceLastAdminon both tokens (step 40) - 2026-06-25 18:00 UTC → Beryl activates on Base Mainnet
- Repeat on mainnet for the permanent run
- Add deployed addresses to this README
- Deploy frontend to Vercel
To be filled in after Beryl activates.
| Network | Speedrun contract | Asset token | Stablecoin token |
|---|---|---|---|
| Base Sepolia | — | — | — |
| Base Mainnet | — | — | — |