recon-registry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/bin/recon-registry.mjs +239 -0
- package/package.json +30 -0
- package/schema/entry.schema.json +40 -0
- package/templates/Harness.sol +38 -0
- package/templates/Rvm.sol +44 -0
- package/templates/recon-registry.toml +16 -0
- package/templates/registry-README.md +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# recon-registry
|
|
2
|
+
|
|
3
|
+
A community catalog of **single self-contained fuzzing harnesses and mocks**, deployable **by
|
|
4
|
+
name** into the [Recon operator](https://github.com/Recon-Fuzz/recon-fuzzer). Package a whole
|
|
5
|
+
protocol (or a standalone tester like an ERC-4626 vault or a weird ERC-20) into **one
|
|
6
|
+
self-contained contract** whose constructor stands the project up — then anyone can fuzz it.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
operator (Rust) recon-registry (this repo) you (a Foundry project)
|
|
10
|
+
deploy_from_registry(name) ──▶ entries/<name>.json ◀── PR ── npx recon-registry pack/publish
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use an entry (consumer)
|
|
14
|
+
From the operator: `deploy_from_registry("ERC4626Tester", as_actor: true)` — fetches the entry
|
|
15
|
+
and stands the project up at a deterministic address. Browse: `npx recon-registry list`.
|
|
16
|
+
|
|
17
|
+
## Contribute an entry (author)
|
|
18
|
+
In your Foundry project:
|
|
19
|
+
```
|
|
20
|
+
npx recon-registry init # scaffold recon-registry.toml + registry/Harness.sol + Rvm.sol
|
|
21
|
+
# write your harness (its constructor news+wires the whole project; see registry/Harness.sol)
|
|
22
|
+
npx recon-registry pack # forge build + extract → recon-registry-out/<name>.json
|
|
23
|
+
npx recon-registry publish # write token → auto-PR; else opens a prefilled issue (one Ctrl+V)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Model (intentionally simple)
|
|
27
|
+
- **One entry = one contract.** A "project" is a single self-contained Setup-style harness whose
|
|
28
|
+
creation bytecode embeds and deploys its deps. No multi-contract manifests, no dependency
|
|
29
|
+
ordering.
|
|
30
|
+
- **Entry schema** (`schema/entry.schema.json`): `name, description, tags, abi, creationBytecode,
|
|
31
|
+
source, solc`. Source is inlined (humans + the LLM read behavior); no heavy provenance.
|
|
32
|
+
- **Formats:** manifest = TOML (`recon-registry.toml`, human-edited); entries = JSON
|
|
33
|
+
(`entries/*.json`, machine-generated). Same split as `foundry.toml` ↔ `out/*.json`.
|
|
34
|
+
|
|
35
|
+
## Trust = reproducibility, enforced in CI
|
|
36
|
+
On every PR, CI recompiles the entry's `source` with its `solc` and asserts the **bytecode
|
|
37
|
+
matches**, validates the schema, and smoke-deploys. On merge to `main`, `registry.json` is
|
|
38
|
+
rebuilt and the entry is immediately available to the operator (merge = publish).
|
|
39
|
+
|
|
40
|
+
## Layout
|
|
41
|
+
```
|
|
42
|
+
entries/*.json published entries (machine-generated)
|
|
43
|
+
src/*.sol the harness/mock sources (for browsing + CI recompile)
|
|
44
|
+
schema/ the entry JSON schema
|
|
45
|
+
registry.json the catalog index (CI-generated on merge)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See `CONTRIBUTING.md` for the full author guide.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// recon-registry — package a Foundry harness/mock into a registry entry and publish it.
|
|
3
|
+
//
|
|
4
|
+
// npx recon-registry init scaffold manifest + harness template (run in a Foundry project)
|
|
5
|
+
// npx recon-registry pack forge build + extract a schema-valid entry JSON
|
|
6
|
+
// npx recon-registry publish open a PR to the registry repo with the entry
|
|
7
|
+
// npx recon-registry list list published entries
|
|
8
|
+
//
|
|
9
|
+
// Zero runtime deps: Node builtins + shelling out to `forge` and `gh`.
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, readdirSync } from "node:fs";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const TEMPLATES = resolve(__dir, "..", "templates");
|
|
18
|
+
const DEFAULT_REGISTRY = "Recon-Fuzz/recon-registry";
|
|
19
|
+
|
|
20
|
+
const die = (m) => { console.error(`recon-registry: ${m}`); process.exit(1); };
|
|
21
|
+
const ok = (m) => console.log(`✓ ${m}`);
|
|
22
|
+
|
|
23
|
+
// --- tiny flat-TOML reader (enough for recon-registry.toml: [section] key = "..." | [..]) ---
|
|
24
|
+
function readToml(path) {
|
|
25
|
+
const out = {};
|
|
26
|
+
let section = "";
|
|
27
|
+
for (const raw of readFileSync(path, "utf8").split("\n")) {
|
|
28
|
+
const line = raw.replace(/#.*$/, "").trim();
|
|
29
|
+
if (!line) continue;
|
|
30
|
+
const sec = line.match(/^\[(.+)\]$/);
|
|
31
|
+
if (sec) { section = sec[1]; out[section] = out[section] || {}; continue; }
|
|
32
|
+
const kv = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/);
|
|
33
|
+
if (!kv) continue;
|
|
34
|
+
let [, k, v] = kv;
|
|
35
|
+
v = v.trim();
|
|
36
|
+
let val;
|
|
37
|
+
if (v.startsWith("[")) val = v.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
38
|
+
else val = v.replace(/^["']|["']$/g, "");
|
|
39
|
+
(section ? out[section] : out)[k] = val;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sh(cmd, args, opts = {}) {
|
|
45
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...opts });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- cross-platform helpers (macOS / Linux / Windows) ---
|
|
49
|
+
function copyToClipboard(text) {
|
|
50
|
+
const p = process.platform;
|
|
51
|
+
const cands =
|
|
52
|
+
p === "darwin" ? [["pbcopy", []]]
|
|
53
|
+
: p === "win32" ? [["clip", []]]
|
|
54
|
+
: [["xclip", ["-selection", "clipboard"]], ["wl-copy", []], ["xsel", ["--clipboard", "--input"]]];
|
|
55
|
+
for (const [cmd, args] of cands) {
|
|
56
|
+
try { execFileSync(cmd, args, { input: text, shell: p === "win32" }); return true; } catch {}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function openUrl(url) {
|
|
61
|
+
const p = process.platform;
|
|
62
|
+
try {
|
|
63
|
+
if (p === "darwin") execFileSync("open", [url], { stdio: "ignore" });
|
|
64
|
+
else if (p === "win32") execFileSync("cmd", ["/c", "start", "", url], { stdio: "ignore" });
|
|
65
|
+
else execFileSync("xdg-open", [url], { stdio: "ignore" });
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
// Reveal a file in the OS file manager, selected/highlighted so it's ready to drag.
|
|
69
|
+
function revealFile(p) {
|
|
70
|
+
const plat = process.platform;
|
|
71
|
+
try {
|
|
72
|
+
if (plat === "darwin") execFileSync("open", ["-R", p], { stdio: "ignore" });
|
|
73
|
+
else if (plat === "win32") execFileSync("explorer", [`/select,${p}`], { stdio: "ignore" });
|
|
74
|
+
else execFileSync("xdg-open", [dirname(p)], { stdio: "ignore" }); // most Linux FMs lack --select
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------- init
|
|
79
|
+
function init() {
|
|
80
|
+
if (!existsSync("foundry.toml")) die("run inside a Foundry project (no foundry.toml found)");
|
|
81
|
+
mkdirSync("registry", { recursive: true });
|
|
82
|
+
const files = [
|
|
83
|
+
["recon-registry.toml", "recon-registry.toml"],
|
|
84
|
+
[join("registry", "Harness.sol"), "Harness.sol"],
|
|
85
|
+
[join("registry", "Rvm.sol"), "Rvm.sol"],
|
|
86
|
+
[join("registry", "README.md"), "registry-README.md"],
|
|
87
|
+
];
|
|
88
|
+
for (const [dst, tpl] of files) {
|
|
89
|
+
if (existsSync(dst)) { console.log(`· skip ${dst} (exists)`); continue; }
|
|
90
|
+
copyFileSync(join(TEMPLATES, tpl), dst);
|
|
91
|
+
ok(`created ${dst}`);
|
|
92
|
+
}
|
|
93
|
+
// Best-effort: prefill author from git config (only replaces the untouched placeholder).
|
|
94
|
+
try {
|
|
95
|
+
const n = sh("git", ["config", "user.name"]).trim();
|
|
96
|
+
const e = sh("git", ["config", "user.email"]).trim();
|
|
97
|
+
const a = n ? (e ? `${n} <${e}>` : n) : "";
|
|
98
|
+
const t = existsSync("recon-registry.toml") ? readFileSync("recon-registry.toml", "utf8") : "";
|
|
99
|
+
if (a && t.includes("Your Name <you@example.com>")) {
|
|
100
|
+
writeFileSync("recon-registry.toml", t.replace("Your Name <you@example.com>", a));
|
|
101
|
+
ok(`author = ${a}`);
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
|
|
105
|
+
console.log("\nNext: edit recon-registry.toml + registry/Harness.sol, then `npx recon-registry pack`.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------- pack
|
|
109
|
+
function pack() {
|
|
110
|
+
if (!existsSync("recon-registry.toml")) die("no recon-registry.toml — run `npx recon-registry init` first");
|
|
111
|
+
const m = readToml("recon-registry.toml");
|
|
112
|
+
const name = m.entry?.name || die("recon-registry.toml: [entry].name required");
|
|
113
|
+
const harness = m.entry?.harness || die("recon-registry.toml: [entry].harness required");
|
|
114
|
+
const skip = (m.build?.skip || []).flatMap((s) => ["--skip", s]);
|
|
115
|
+
|
|
116
|
+
console.log(`Building (forge build ${skip.join(" ")}) ...`);
|
|
117
|
+
sh("forge", ["build", ...skip], { stdio: ["ignore", "inherit", "inherit"] });
|
|
118
|
+
|
|
119
|
+
// Locate the concrete artifact for the harness contract.
|
|
120
|
+
const artifact = findArtifact("out", harness) || die(`artifact for ${harness} not found under out/`);
|
|
121
|
+
const a = JSON.parse(readFileSync(artifact, "utf8"));
|
|
122
|
+
const bytecode = a.bytecode?.object || "";
|
|
123
|
+
if (bytecode.replace(/^0x/, "").length === 0)
|
|
124
|
+
die(`${harness} has empty bytecode (abstract contract or basename collision — check [build].skip)`);
|
|
125
|
+
if (bytecode.includes("__$")) die(`${harness} has unlinked libraries — link them before packing`);
|
|
126
|
+
|
|
127
|
+
// Inline the FLATTENED source — self-contained (full dependency tree in one file), so CI can
|
|
128
|
+
// recompile it standalone to verify the bytecode, and humans/LLM see everything.
|
|
129
|
+
const sourcePath = m.entry?.source || guessSource(harness);
|
|
130
|
+
let source = "";
|
|
131
|
+
if (sourcePath && existsSync(sourcePath)) {
|
|
132
|
+
try { source = sh("forge", ["flatten", sourcePath]); }
|
|
133
|
+
catch { source = readFileSync(sourcePath, "utf8"); }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const entry = {
|
|
137
|
+
name,
|
|
138
|
+
description: m.entry?.description || "",
|
|
139
|
+
author: m.entry?.author || "",
|
|
140
|
+
tags: m.entry?.tags || [],
|
|
141
|
+
abi: a.abi,
|
|
142
|
+
creationBytecode: bytecode,
|
|
143
|
+
source,
|
|
144
|
+
solc: m.build?.solc || readToml("foundry.toml").profile?.default?.solc || detectSolc(),
|
|
145
|
+
};
|
|
146
|
+
mkdirSync("recon-registry-out", { recursive: true });
|
|
147
|
+
const out = join("recon-registry-out", `${name}.json`);
|
|
148
|
+
writeFileSync(out, JSON.stringify(entry, null, 1));
|
|
149
|
+
ok(`packed ${out} (${(bytecode.length / 2) | 0} bytes bytecode, ${entry.abi.length} abi items)`);
|
|
150
|
+
if (!source) console.log("· note: no source inlined — set [entry].source in the manifest for transparency");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------- publish
|
|
154
|
+
// Two tiers, no `gh`/`git`, cross-platform:
|
|
155
|
+
// 1. If a write token is available (GH_TOKEN/GITHUB_TOKEN), fire a `repository_dispatch` →
|
|
156
|
+
// the registry Action validates + opens the PR fully automatically (no UI step).
|
|
157
|
+
// 2. Otherwise, copy the entry JSON to the clipboard and open the prefilled "Submit a
|
|
158
|
+
// registry entry" issue — the user clicks the field, Ctrl/Cmd+V, Submit; the Action
|
|
159
|
+
// turns the issue into a PR.
|
|
160
|
+
async function publish() {
|
|
161
|
+
const built = existsSync("recon-registry-out") && readdirSync("recon-registry-out").find((f) => f.endsWith(".json"));
|
|
162
|
+
if (!built) die("nothing packed — run `npx recon-registry pack` first");
|
|
163
|
+
const m = existsSync("recon-registry.toml") ? readToml("recon-registry.toml") : {};
|
|
164
|
+
const repo = m.registry?.repo || DEFAULT_REGISTRY;
|
|
165
|
+
const name = built.replace(/\.json$/, "");
|
|
166
|
+
const file = join("recon-registry-out", built);
|
|
167
|
+
const jsonText = readFileSync(file, "utf8");
|
|
168
|
+
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
169
|
+
|
|
170
|
+
// Tier 1: repository_dispatch (needs write access).
|
|
171
|
+
if (token) {
|
|
172
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/dispatches`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Bearer ${token}`,
|
|
176
|
+
Accept: "application/vnd.github+json",
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
"User-Agent": "recon-registry",
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({ event_type: "submit-entry", client_payload: { entry: JSON.parse(jsonText) } }),
|
|
181
|
+
});
|
|
182
|
+
if (res.status === 204) {
|
|
183
|
+
ok(`dispatched '${name}' — the registry Action will validate it and open a PR.`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log(`· repository_dispatch not permitted (HTTP ${res.status}); falling back to the issue flow.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Tier 2: open the markdown issue page AND reveal the entry file in the file manager so the
|
|
190
|
+
// user just drags it into the issue body and submits. The Action downloads + parses the
|
|
191
|
+
// attached file (drag-dropped .json works) and opens the PR. No clipboard/token/gh needed.
|
|
192
|
+
const url = `https://github.com/${repo}/issues/new?template=submit-entry.md&title=${encodeURIComponent(`[entry] ${name}`)}`;
|
|
193
|
+
const abs = resolve(file);
|
|
194
|
+
console.log(`\nSubmit '${name}':`);
|
|
195
|
+
console.log(` 1. Opening the issue page + your file manager (the entry file is highlighted).`);
|
|
196
|
+
console.log(` 2. Drag this file into the issue body, then click "Submit new issue":`);
|
|
197
|
+
console.log(` ${abs}`);
|
|
198
|
+
console.log(` 3. A bot downloads it, validates it, and opens the PR.\n`);
|
|
199
|
+
openUrl(url);
|
|
200
|
+
revealFile(abs);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------- list
|
|
204
|
+
async function list() {
|
|
205
|
+
const m = existsSync("recon-registry.toml") ? readToml("recon-registry.toml") : {};
|
|
206
|
+
const repo = m.registry?.repo || DEFAULT_REGISTRY;
|
|
207
|
+
const url = `https://raw.githubusercontent.com/${repo}/main/registry.json`;
|
|
208
|
+
const res = await fetch(url, { headers: { "User-Agent": "recon-registry" } });
|
|
209
|
+
if (!res.ok) die(`could not fetch ${url} (HTTP ${res.status})`);
|
|
210
|
+
const { entries = [] } = await res.json();
|
|
211
|
+
if (!entries.length) return console.log("(registry is empty)");
|
|
212
|
+
for (const e of entries) console.log(`${e.name}\t[${(e.tags || []).join(", ")}]\t${e.description || ""}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------- helpers
|
|
216
|
+
function findArtifact(root, contract) {
|
|
217
|
+
if (!existsSync(root)) return null;
|
|
218
|
+
for (const d of readdirSync(root, { withFileTypes: true })) {
|
|
219
|
+
const p = join(root, d.name);
|
|
220
|
+
if (d.isDirectory()) { const r = findArtifact(p, contract); if (r) return r; }
|
|
221
|
+
else if (d.name === `${contract}.json`) return p;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function guessSource(contract) {
|
|
226
|
+
for (const root of ["registry", "src", "test"]) {
|
|
227
|
+
const r = findArtifact.call(null, root, contract); // reuse: look for <contract>.sol
|
|
228
|
+
if (existsSync(join(root, `${contract}.sol`))) return join(root, `${contract}.sol`);
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function detectSolc() {
|
|
233
|
+
try { return (sh("forge", ["--version"]).match(/solc\s+([0-9.]+)/) || [])[1] || ""; } catch { return ""; }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const [, , cmd] = process.argv;
|
|
237
|
+
const fn = { init, pack, publish, list }[cmd];
|
|
238
|
+
if (!fn) die(`unknown command '${cmd ?? ""}'. usage: recon-registry <init|pack|publish|list>`);
|
|
239
|
+
await fn();
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "recon-registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Package a Foundry project into a single self-contained fuzzing harness/mock and publish it to the Recon contract registry, deployable by name from the operator.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"recon-registry": "bin/recon-registry.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"templates/",
|
|
12
|
+
"schema/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ethereum",
|
|
19
|
+
"foundry",
|
|
20
|
+
"forge",
|
|
21
|
+
"fuzzing",
|
|
22
|
+
"invariant-testing",
|
|
23
|
+
"recon",
|
|
24
|
+
"registry"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": { "type": "git", "url": "git+https://github.com/Recon-Fuzz/recon-registry.git" },
|
|
28
|
+
"homepage": "https://github.com/Recon-Fuzz/recon-registry#readme",
|
|
29
|
+
"bugs": { "url": "https://github.com/Recon-Fuzz/recon-registry/issues" }
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/Recon-Fuzz/recon-registry/schema/entry.schema.json",
|
|
4
|
+
"title": "Recon registry entry",
|
|
5
|
+
"description": "One self-contained deployable contract (a project harness or a standalone mock). Single contract only — a project is one self-contained Setup-style harness, never a multi-contract manifest.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["name", "description", "tags", "abi", "creationBytecode", "source", "solc"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^[A-Za-z0-9_.-]+$",
|
|
13
|
+
"description": "Unique entry name (also the file name and the deploy-by-name key)."
|
|
14
|
+
},
|
|
15
|
+
"description": { "type": "string", "description": "One-line summary of what it stands up / does." },
|
|
16
|
+
"author": { "type": "string", "description": "Optional attribution, e.g. \"Jane Doe <jane@example.com>\" (name required, email optional)." },
|
|
17
|
+
"tags": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "type": "string" },
|
|
20
|
+
"description": "Discovery tags, e.g. erc4626, vault, lending, oracle, harness, mock."
|
|
21
|
+
},
|
|
22
|
+
"abi": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"description": "Standard Solidity ABI (JSON). Needed to encode constructor args and interact."
|
|
25
|
+
},
|
|
26
|
+
"creationBytecode": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"pattern": "^0x[0-9a-fA-F]*$",
|
|
29
|
+
"description": "Linked creation (init) bytecode. Self-contained — embeds any `new`'d deps. Must be non-empty and fully library-linked (no __$ placeholders)."
|
|
30
|
+
},
|
|
31
|
+
"source": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Inlined Solidity source so humans and the LLM operator can see exactly what the contract does."
|
|
34
|
+
},
|
|
35
|
+
"solc": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "solc version the bytecode was built with. CI recompiles `source` with this and asserts the bytecode matches (reproducibility = trust; no per-entry provenance)."
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {rvm} from "./Rvm.sol";
|
|
5
|
+
|
|
6
|
+
// import {YourProtocol} from "src/YourProtocol.sol";
|
|
7
|
+
// import {MockToken} from "src/mocks/MockToken.sol";
|
|
8
|
+
|
|
9
|
+
/// A registry harness packages an ENTIRE project into ONE self-contained contract: its
|
|
10
|
+
/// constructor `new`s and wires the whole protocol, funds the operator's actors, and sets up
|
|
11
|
+
/// initial state. Deploying this one artifact (the operator does so with `as_actor: true`)
|
|
12
|
+
/// stands the project up — `new`'d contracts are embedded in this harness's creation bytecode.
|
|
13
|
+
///
|
|
14
|
+
/// After deploy, you attach handlers / properties / ghosts at runtime; this harness is just the
|
|
15
|
+
/// SUT setup. Expose addresses via public getters so those can wire to them.
|
|
16
|
+
contract MyHarness {
|
|
17
|
+
// YourProtocol public protocol;
|
|
18
|
+
// MockToken public token;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
// The operator's actor set (includes this harness when deployed as_actor).
|
|
22
|
+
address[] memory actors = rvm.getActors();
|
|
23
|
+
|
|
24
|
+
// 1. Deploy + wire your protocol and mocks here:
|
|
25
|
+
// token = new MockToken("Test", "TST", 18);
|
|
26
|
+
// protocol = new YourProtocol(address(token));
|
|
27
|
+
|
|
28
|
+
// 2. Fund / approve every actor (no hardcoded users):
|
|
29
|
+
for (uint256 i; i < actors.length; i++) {
|
|
30
|
+
address a = actors[i];
|
|
31
|
+
// token.mint(a, type(uint128).max);
|
|
32
|
+
// rvm.prank(a); token.approve(address(protocol), type(uint256).max);
|
|
33
|
+
a; // remove once used
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// function protocolAddr() external view returns (address) { return address(protocol); }
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {Vm} from "forge-std/Vm.sol";
|
|
5
|
+
|
|
6
|
+
/// Recon extended VM — inherits ALL Foundry cheatcodes (via `is Vm`) and adds the operator's
|
|
7
|
+
/// actor / asset / ghost / generic-registry extensions, served at the canonical cheatcode
|
|
8
|
+
/// address. The handle is named `rvm` so it never collides with forge-std's `vm`.
|
|
9
|
+
interface IRvm is Vm {
|
|
10
|
+
// --- actors (the operator's actor set; includes the harness/deployer when deployed as_actor) ---
|
|
11
|
+
function getActor() external view returns (address);
|
|
12
|
+
function getActor(uint256 index) external view returns (address);
|
|
13
|
+
function getActors() external view returns (address[] memory);
|
|
14
|
+
function addActor() external returns (address);
|
|
15
|
+
function addActor(address actor) external;
|
|
16
|
+
function removeActor(address actor) external;
|
|
17
|
+
function switchActor(uint256 entropy) external;
|
|
18
|
+
|
|
19
|
+
// --- assets (operator-managed mock-token registry) ---
|
|
20
|
+
function getAsset() external view returns (address);
|
|
21
|
+
function getAssets() external view returns (address[] memory);
|
|
22
|
+
function newAsset(uint8 decimals) external returns (address);
|
|
23
|
+
function addAsset(address token) external;
|
|
24
|
+
function switchAsset(uint256 entropy) external;
|
|
25
|
+
|
|
26
|
+
// --- ghosts ---
|
|
27
|
+
function ghosts() external view returns (address[] memory);
|
|
28
|
+
function ghostGet(string calldata key) external view returns (bytes memory);
|
|
29
|
+
function ghostSet(string calldata key, bytes calldata value) external;
|
|
30
|
+
|
|
31
|
+
// --- generic named registry ---
|
|
32
|
+
function push(string calldata name, bytes calldata item) external;
|
|
33
|
+
function pop(string calldata name) external returns (bytes memory);
|
|
34
|
+
function removeAt(string calldata name, uint256 index) external;
|
|
35
|
+
function store(string calldata name, uint256 index, bytes calldata item) external;
|
|
36
|
+
function pick(string calldata name, uint256 entropy) external returns (bytes memory);
|
|
37
|
+
function current(string calldata name) external view returns (bytes memory);
|
|
38
|
+
function at(string calldata name, uint256 index) external view returns (bytes memory);
|
|
39
|
+
function all(string calldata name) external view returns (bytes[] memory);
|
|
40
|
+
function count(string calldata name) external view returns (uint256);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Recon VM handle. `rvm.*` — distinct from forge-std's `vm`.
|
|
44
|
+
IRvm constant rvm = IRvm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# recon-registry entry manifest. Edit, then `npx recon-registry pack && npx recon-registry publish`.
|
|
2
|
+
|
|
3
|
+
[entry]
|
|
4
|
+
name = "MyHarness" # unique entry name (also the published file name)
|
|
5
|
+
description = "One-line description of what this harness/mock stands up"
|
|
6
|
+
author = "Your Name <you@example.com>"
|
|
7
|
+
tags = ["protocol", "harness"]
|
|
8
|
+
harness = "MyHarness" # the contract `pack` extracts (single self-contained harness)
|
|
9
|
+
# source = "registry/Harness.sol" # optional: defaults to the harness's .sol; inlined into the entry
|
|
10
|
+
|
|
11
|
+
[build]
|
|
12
|
+
solc = "0.8.24" # stamped into the entry; CI recompiles with this to verify bytecode
|
|
13
|
+
skip = ["test/recon/**"] # collision guard: skip dirs with clashing basenames
|
|
14
|
+
|
|
15
|
+
[registry]
|
|
16
|
+
repo = "Recon-Fuzz/recon-registry" # PR target
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# registry/ — your fuzzing harness entry
|
|
2
|
+
|
|
3
|
+
This folder was scaffolded by `npx recon-registry init`. It packages your project into a single
|
|
4
|
+
self-contained harness for the [recon-registry](https://github.com/Recon-Fuzz/recon-registry),
|
|
5
|
+
deployable by name from the Recon operator.
|
|
6
|
+
|
|
7
|
+
- `Harness.sol` — your harness: its constructor stands up the whole project (see the TODOs).
|
|
8
|
+
- `Rvm.sol` — the `rvm` cheatcode interface the harness uses (`getActors`, `prank`, …).
|
|
9
|
+
- `../recon-registry.toml` — the entry manifest (name, description, tags, harness, solc).
|
|
10
|
+
|
|
11
|
+
## Flow
|
|
12
|
+
```
|
|
13
|
+
# edit Harness.sol + recon-registry.toml
|
|
14
|
+
npx recon-registry pack # forge build + extract entry → recon-registry-out/<name>.json
|
|
15
|
+
npx recon-registry publish # open a PR to the registry → merge = live for everyone
|
|
16
|
+
```
|