Battle-test EVVM services locally before you spend a wei of testnet or mainnet gas.
📚 Documentation | 🌐 Website | 📖 How to make an EVVM service
Scaffold-EVVM is the iteration loop for anyone shipping an EVVM service. Drop your .sol file, run one command, and you get:
- A local EVVM stack (all 6 protocol contracts deployed on Anvil/Hardhat).
- Your service auto-deployed and wired in — addresses written to the frontend
.env, ABI persisted, no manual plumbing. - A fully generated UI at
/services/<your-service>— read panel, write panel with the right form per function role (admin / publicAction / publicPay), live event tail, EVVM dual-signature signing handled for you. - A block explorer (
/evvmscan) that decodes your service's calls and events as first-class citizens alongside Core / Staking / NameService / etc. - 23+ signature constructors for every EVVM operation so you can stress-test how your service interacts with payments, staking, names, and P2P swaps in the same UI.
🛡️ Why this matters: every signature you generate against a service ABI you wrote yesterday is one fewer testnet deploy, one fewer testnet faucet round-trip, one fewer "wait, did I send the right struct?" thirty minutes after pushing. Failing fast locally is the whole point.
git clone https://github.com/EVVM-org/scaffold-evvm.git
cd scaffold-evvm
npm install
# 1. Drop services/<YourService>/<YourService>.sol
npm run wizard
# 2. Open http://localhost:3000/services/<YourService>
# 3. Hit functions, watch events tail, iterate.That's it. Edit the contract, re-run the wizard, the UI rebuilds itself from your new ABI.
Scaffold-EVVM understands two kinds of services:
| Kind | Inherits | Use when |
|---|---|---|
| Plain contract | Nothing | One-off demos / tooling that doesn't need gasless UX (the bundled Counter example) |
| EVVM service | EvvmService |
Real services — gasless dual-signature flows where users sign, fishers execute |
The protocol's canonical guide is How to make an EVVM service. The rest of this section is the scaffold-evvm shorthand for it.
services/
└── Tipjar/
├── Tipjar.sol ← required: one main contract
├── manifest.json ← optional: classifies functions for the auto-UI
└── Deploy.s.sol ← optional: custom Foundry deploy script
The folder name becomes the slug (/services/Tipjar) and the env var (NEXT_PUBLIC_CUSTOM_TIPJAR_ADDRESS).
For a real gasless service, extend EvvmService. The constructor takes the deployed Core and Staking addresses — scaffold-evvm fills them in automatically when it sees those types.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import "@scaffold-evvm/testnet-contracts/library/EvvmService.sol";
contract Tipjar is EvvmService {
error Unauthorized();
address public immutable owner;
mapping(address => uint256) public received;
event Tipped(address indexed from, address indexed to, uint256 amount);
constructor(address _core, address _staking, address _owner)
EvvmService(_core, _staking)
{
owner = _owner;
}
function tip(
// ── action params ───────────────────────────────────
address user,
address to,
uint256 amount,
// ── canonical EVVM action plumbing ──────────────────
address senderExecutor, // the contract delivering this call to Core (usually address(this))
address originExecutor, // address(0) = anyone can execute; else restricts
uint256 nonce, // service nonce (sync or async)
bool isAsyncExec, // matches the nonce mode
bytes memory signature, // user-signed action authorization
// ── canonical EVVM payment plumbing ─────────────────
uint256 priorityFeePay, // fisher reward (in the chosen token)
uint256 noncePay, // payment nonce (independent of action nonce)
bool isAsyncExecPay,
bytes memory signaturePay
) external {
// (1) Verify the user signed the action + consume the nonce atomically.
//
// Action hash = keccak256(abi.encode("opName", ...domain args only)).
// The plumbing (executor pair, nonce, isAsyncExec, evvmId) is added
// by the unified envelope inside Core.validateAndConsumeNonce — it
// does NOT belong in this inner hash.
core.validateAndConsumeNonce(
user,
senderExecutor,
keccak256(abi.encode("tip", to, amount)),
originExecutor,
nonce,
isAsyncExec,
signature
);
// (2) Pull the user's payment via Core.pay — separate signature, separate nonce.
requestPay(
user,
getPrincipalTokenAddress(), // or core.getChainHostCoinAddress() for native
amount,
priorityFeePay,
originExecutor,
noncePay,
isAsyncExecPay,
signaturePay
);
// (3) Reward the fisher (only meaningful if this contract is a registered staker).
if (core.isAddressStaker(address(this))) {
makeCaPay(msg.sender, getPrincipalTokenAddress(), priorityFeePay);
}
// (4) Domain logic + events.
received[to] += amount;
emit Tipped(user, to, amount);
}
}The four invariants every EVVM service must honor:
- Validate before side effects.
core.validateAndConsumeNonce(...)is the first line of the function. Skip it and the same signature can be replayed. - Action hash holds the action, nothing else. Only the function name + domain arguments go inside
keccak256(abi.encode(...)). The executor pair, nonce,isAsyncExec, andevvmIdare added by Core's unified envelope automatically. - The pay signature is separate.
requestPaytakes its own nonce and signature — a compromised pay nonce doesn't invalidate the action. - Use
originExecutordeliberately. Passaddress(0)to let any fisher execute. Pass a specific address only if you want to lock execution to one EOA.
manifest.json next to your .sol file tells the auto-UI how to classify each function. It's optional — without it everything works, the UI just classifies by Solidity mutability.
{
"name": "Tipjar",
"description": "Send a gasless tip to any EVVM user, paid in MATE.",
"tags": {
"admin": ["transferOwnership"],
"publicPay": ["tip"],
"publicAction": [],
"hidden": []
},
"actions": {
"tip": { "actionPayload": ["address to", "uint256 amount"] }
}
}| Tag | What the auto-UI does |
|---|---|
admin |
Renders under "Admin actions", behind a "connected wallet must hold the admin role" hint |
publicPay |
Renders a dual-signature form (action + EVVM-pay). Both nonces + signatures handled by the UI |
publicAction |
Renders a single-signature form (action only, no fee transfer) |
hidden |
Doesn't render |
actions.<name>.actionPayload tells the UI which types to encode inside the action hash. The shape matches what your contract's keccak256(abi.encode(...)) expects — operation name plus domain args.
npm run wizardThe wizard:
- Re-compiles via
forge build --via-ir. - Reads your service's ABI from
packages/foundry/out/<File>.sol/<Contract>.json. - Resolves constructor arguments —
addressparameters whose name matches a known role (Core, Staking, owner) auto-fill; anything else prompts you. - Deploys, writes the address to
deployments/customcontracts.jsonandNEXT_PUBLIC_CUSTOM_<NAME>_ADDRESSin the frontend.env. - Persists the ABI to
packages/nextjs/public/customservices.jsonso the frontend + EVVMScan pick it up.
- Open
http://localhost:3000/services/Tipjar→ read panel shows live reads, write panel shows yourtipform, events panel tailsTipped. - Open
http://localhost:3000/evvmscan→ every call and log against Tipjar is decoded with your ABI. - Edit
Tipjar.sol, re-runnpm run wizard→ UI rebuilds itself.
When you visit /services/<your-service>, scaffold-evvm walks your ABI + manifest and produces three panels:
Every view / pure function is invoked on mount. Return types render with the right widget:
| Solidity return | Rendered as |
|---|---|
address |
Monospace address + copy button |
uint256 (large) |
Both raw wei and a human-formatted MATE-style value |
bool |
Yes/No badge |
bytes |
Hex with collapse toggle |
string |
Plain text |
| Tuple / struct | Two-column key-value table |
| Array | Bullet list (or table if elements are tuples) |
Functions that take arguments live in the read panel too, with an inline form so you can call them on demand.
Every state-changing function gets a form. The form type depends on the manifest tag:
| Tag | What the form does |
|---|---|
| (none) — plain | One input per argument, "Submit" calls client.writeContract directly via wagmi/viem |
publicAction |
Same fields, but the canonical action plumbing (senderExecutor, originExecutor, nonce, isAsyncExec, signature) is pre-filled and the action signature is built before submission |
publicPay |
Action plus EVVM-pay fields (priorityFeePay, noncePay, isAsyncExecPay, signaturePay, tokenAddress, amount). Both signatures are produced and submitted together |
admin |
Same as plain, grouped under "Admin actions" with a yellow header reminding you the call needs the admin role |
address inputs accept either a hex address or @username and resolve via NameService on submit. Numeric inputs accept human-readable decimals (10.5) and convert to wei.
Every event declared in your ABI is tailed live using the same decoder EVVMScan uses. Each entry shows the event name, decoded arguments with the same per-type widgets, the emitting block + relative timestamp, and a link to the emitting transaction.
- It doesn't sequence multi-step flows (no "init → lockNumber → finalize" wizard). Each function is one form.
- It doesn't handle ERC-20 approvals before writes — if you need them, instruct the user manually or write a custom page.
- It doesn't poll reads on a tight interval — reads refresh on mount and after a successful write.
When you outgrow the auto-UI, drop a regular Next.js page under packages/nextjs/src/app/your-service/ using the same components/ui/ primitives. The ABI, address, and EVVMScan hookup still work; only the page is custom.
These ship turned on — you don't configure them.
| Feature | What it does |
|---|---|
EVVMScan Explorer (/evvmscan) |
Etherscan-style live feed. Decodes Core, Staking, Estimator, NameService, Treasury, P2PSwap, and your custom services. Transaction page breaks out senderExecutor / originExecutor / signatures. Address page tags contracts vs EOAs with EVVM-aware direction (Core.addBalance reads as IN even when you're tx.from). |
| 23+ signature constructors | Pre-built forms for pay, dispersePay, all three staking modes, NameService lifecycle, P2PSwap order book, treasury bridge. Use them to stress-test how your service composes with the rest of the protocol. |
| ABI-decoded monitor | npm run monitor streams every block + tx in a separate terminal, ABI-decoded against Foundry build output and the evvm-js SDK. |
| UI Pro Max design system | Fira Sans + Fira Code, dark/light token scale, shared components/ui/ primitives, a11y-ready (skip link, focus rings, reduced-motion). See design-system/scaffold-evvm/MASTER.md. |
| Sandbox-tuned contracts | All protocol timelocks patched to 30 seconds so you can test governance flows in real time. |
Requirements: Node.js v18+, Foundry, Git.
git clone https://github.com/EVVM-org/scaffold-evvm.git
cd scaffold-evvm
npm install
npm run wizardThe wizard checks prerequisites, auto-clones Testnet-Contracts if missing, prompts for framework (Foundry / Hardhat) + admin addresses, compiles, deploys all 6 protocol contracts plus any services in services/, writes the frontend .env, and launches http://localhost:3000.
💡 Tip: Keep the terminal open to maintain the local chain.
Ctrl+Cshuts everything down cleanly.
For iterating on a service without restarting the frontend every time:
# Terminal 1
npm run cli deploy # spawns chain + deploys contracts + services
# Terminal 2
npm run frontend # next dev on :3000
# Terminal 3 (optional)
npm run monitor # ABI-decoded live block/tx feed
⚠️ Local deployment only. Testnet deployment is on the roadmap.
npm run cli flush # clear all caches, kill servers
npm run wizard # fresh startCommon issues solved by flush:
- Nonce errors ("Nonce too high/low")
- Port 8545 already in use
- Transaction reverted / deployment failed
Stuck transactions after redeploying: when the local chain resets, your wallet still remembers the old nonce.
npm run cli flush- In your wallet, clear activity:
- MetaMask: Settings → Advanced → Clear activity tab data
- Rabby: Settings → Clear pending transactions
npm run wizard
WalletConnect doesn't work with localhost — import the test private key directly into MetaMask/Rabby (see "Local network" below).
Scaffold-EVVM deploys 6 protocol contracts. Your services compose with these.
| Contract | What it does |
|---|---|
| Core | Holds per-user balances. Verifies signatures + consumes nonces via validateAndConsumeNonce. Single entry point for pay, dispersePay, batchPay, caPay, disperseCaPay. |
| Staking | Era-based reward system for MATE token holders. Three flows (golden / presale / public). EvvmService provides _makeStakeService helpers for contracts that want to stake themselves. |
| Estimator | Pure reward-calculation engine, called by Staking. Stateless. |
| NameService | Username registration, renewal, marketplace offers, custom metadata. Core resolves @username → address inside pay() etc. |
| Treasury | ETH / ERC-20 vault. No signatures — direct caller balance ops. |
| P2PSwap | Peer-to-peer order book. Two fill models (proportional / fixed fee). |
Full per-contract reference: evvm.info/docs/Contracts.
Both Anvil and Hardhat Network use the same defaults:
| Port | 8545 |
| Chain ID | 31337 |
| Test address | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 |
| Test private key | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 |
⚠️ Publicly known test keys. Never use on networks with real value.
scaffold-evvm/
├── cli/ # Interactive CLI wizard (this is where the wizard lives)
├── services/ # ← YOU DROP YOUR .sol FILES HERE
│ └── Counter/ # Bundled minimal example
├── packages/
│ ├── foundry/ # Foundry package
│ │ ├── testnet-contracts/ # Bundled snapshot of all protocol contracts
│ │ └── contracts/services/ # Symlinked target for services/
│ ├── hardhat/ # Hardhat package (delegates compilation to forge)
│ └── nextjs/ # Frontend + EVVMScan + auto-UI for custom services
├── Testnet-Contracts/ # Auto-cloned at deploy time (git ignored)
├── input/ # Generated deployment inputs (Inputs.*.sol, address.json)
└── deployments/ # Deployment summaries + customcontracts.json
All scaffold-evvm documentation lives at evvm.info/docs/LibrariesAndTools/ScaffoldEvvm alongside the rest of the EVVM protocol docs — one canonical home, no second site to maintain.
Start here:
- Scaffold-EVVM overview — what it is, the iteration loop, when to use it
- Custom services walkthrough — folder convention, manifest, examples
- Auto-UI reference — what the generated page renders for each function shape
- Getting started — install, wizard, troubleshooting
Protocol references:
- How to make an EVVM service — the protocol's canonical service-authoring guide (this README mirrors it for the scaffold-evvm workflow)
- EVVM Documentation — full protocol docs
- Signature Structures — EIP-191 payload formats
- EVVM Website
- Foundry Book · Hardhat Docs
- Fork the repository.
- Create a feature branch:
git checkout -b feature/amazing-feature. - Commit using the joelparkerhenderson git-commit-message convention (imperative subject, body explains why).
- Push:
git push origin feature/amazing-feature. - Open a Pull Request.
EVVM Noncommercial License v1.0 — see LICENSE for details. For commercial use, contact: g@evvm.org.
Made with 🧉 for the EVVM ecosystem
Inspired by 🏗️ Scaffold-ETH