Why this doc exists. Solhunt-Duel's leaderboard claims that every "HARDENED" run passed four server-side gates the LLMs cannot see or modify. That's a load-bearing claim — if the gates are imaginary or LLM-graded, the whole project is theater. This document shows the actual gate code, with file paths and line numbers, so a skeptical reader can audit the claim without cloning the repo.
Source of truth:
src/sandbox/patch-harness.ts— the functionverifyPatch()is the entire verification pipeline. Everything below is a guided tour of that file.
Agents will say success is success if you let them. We caught Solhunt doing this on its own benchmark (67.7% curated → 13.7% random — same agent, same prompts, same model — see README §The numbers). For Solhunt-Duel we built the success/failure verdict OUTSIDE the LLM's reach.
The verdict is computed by Foundry's forge test against a real EVM fork. The LLM never sees the test code, never sees the bytecode comparison, never sees the storage layout diff. The harness writes the boolean. The boolean is the boolean.
// src/sandbox/patch-harness.ts:11-28
export interface PatchVerification {
/** Red's exploit test reverted/failed on the patched bytecode. */
exploitNeutralized: boolean;
/** Every benign/happy-path test passed. */
benignPassed: boolean;
/** Names of benign tests that regressed. Empty iff benignPassed is true. */
regressions: string[];
/** True if any existing storage slot/offset/type changed between original
* and patched source. Patches may append new storage; they may not reorder
* or resize existing storage. */
storageLayoutChanged: boolean;
/** True if the exploit also fails when run from a freshly-derived attacker
* EOA. Defeats "just ban the known attacker address" pseudo-patches. */
freshAttackerNeutralized: boolean;
/** Populated iff a structural problem (build failure, vm.etch incompat,
* missing contract name, ...) prevented a clean verdict. */
error?: string;
}A run is HARDENED iff:
exploitNeutralized && benignPassed && freshAttackerNeutralized && !storageLayoutChanged && !error
All four. Server-side. The agent's self-report is irrelevant.
Before any of the four gates fire, the harness runs Red's exploit against the ORIGINAL unpatched bytecode. The exploit must PASS here — if it doesn't, it means either Red's test is broken or our vm.etch is dropping constructor state (immutables, initializer storage). Either way, the four gates would be uninterpretable. So we abort with error set:
// src/sandbox/patch-harness.ts:139-160 (paraphrased for length)
const sanity = await runForgeTest(sandbox, args.sandboxId, {
projectRoot: args.patchedSourcePath,
testPath: args.exploitTestPath,
// ... env, fork URL, attacker label ...
});
if (!sanity.passed) {
result.error = "Baseline sanity failed: exploit did NOT pass on the ORIGINAL bytecode.";
return result;
}This catches a class of false-positive Red runs we kept hitting in early Phase 4: Red writes an exploit that passes locally on its own dev sandbox but fails on the harness fork because of how our cloned bytecode interacts with vm.etch. Without the sanity check, those would silently look like "patched runs neutralized everything!" — which would be a lie.
Re-run Red's exploit against the PATCHED bytecode. We expect it to FAIL (revert, panic, assertion fail). If it still passes, Blue's patch did not actually fix the bug.
// src/sandbox/patch-harness.ts:166-177
const exploitPatched = await runForgeTest(sandbox, args.sandboxId, {
projectRoot: args.patchedSourcePath,
testPath: args.exploitTestPath,
forkUrl: forgeForkUrl,
env: {
ETH_RPC_URL: args.rpcUrl,
DEXIBLE_FORK_BLOCK: String(args.forkBlockNumber),
DEXIBLE_RUNTIME_BYTECODE: patched.runtimeBytecode,
DEXIBLE_ATTACKER_LABEL: "attacker",
},
});
result.exploitNeutralized = !exploitPatched.passed;exploitNeutralized = NOT exploit_passed. Clean and obvious. The LLM never touches this; the harness reads the boolean from forge test exit code.
The trick gate. Run the SAME exploit a third time, but with DEXIBLE_ATTACKER_LABEL set to "fresh_attacker_0xf00d" (a different label deterministically derives a different attacker address via vm.addr).
If the patch only blocks the original attacker's hard-coded EOA, this run will PASS and we'll catch the bad patch. Real patches (allow-lists, role checks, input validation, state invariants) survive this re-run.
// src/sandbox/patch-harness.ts:187-198
const exploitFresh = await runForgeTest(sandbox, args.sandboxId, {
projectRoot: args.patchedSourcePath,
testPath: args.exploitTestPath,
forkUrl: forgeForkUrl,
env: {
ETH_RPC_URL: args.rpcUrl,
DEXIBLE_FORK_BLOCK: String(args.forkBlockNumber),
DEXIBLE_RUNTIME_BYTECODE: patched.runtimeBytecode,
DEXIBLE_ATTACKER_LABEL: "fresh_attacker_0xf00d", // ← different EOA
},
});
result.freshAttackerNeutralized = !exploitFresh.passed;Why does this gate exist? Because Blue, left to its own devices, will absolutely write require(msg.sender != ATTACKER_ADDR) and call it a day. We caught it doing this in the first 6 Phase 4 runs. The fresh-attacker gate makes that strategy fail mechanically.
Run the contract's normal-operation test suite against the patched bytecode. All tests must pass. Any regression — a previously-green test that now fails — gets surfaced in regressions[].
// src/sandbox/patch-harness.ts:204-216
const benignRun = await runForgeTest(sandbox, args.sandboxId, {
projectRoot: args.patchedSourcePath,
testPath: args.benignTestPath, // ← happy-path tests
forkUrl: forgeForkUrl,
env: {
ETH_RPC_URL: args.rpcUrl,
DEXIBLE_FORK_BLOCK: String(args.forkBlockNumber),
DEXIBLE_RUNTIME_BYTECODE: patched.runtimeBytecode,
DEXIBLE_PATCHED: "1",
},
});
result.benignPassed = benignRun.passed;
result.regressions = benignRun.failedTests;Why this matters: the trivial-correct patch for any vulnerability is "delete the function." That kills the exploit AND every legitimate user. The benign suite catches that.
The benign tests are written by hand, per-contract, alongside the original Red exploit. Blue does not see the benign suite source, only its name; if Blue patches things that touch unrelated function paths, this gate catches the collateral damage.
The most subtle gate. vm.etch swaps a contract's runtime bytecode but leaves the existing storage slots untouched. If Blue's patch reorders, resizes, or retypes a storage variable, the live contract's existing data gets reinterpreted under the new layout — silent corruption, possibly far worse than the original bug.
// src/sandbox/patch-harness.ts:109-115
result.storageLayoutChanged = compareStorageLayouts(
original.storageLayout,
patched.storageLayout
);The comparison uses the storage layout JSON that Solc 0.8.x emits for free during compile. Each entry is {label, slot, offset, type}; we compare each existing entry by index. The patch is allowed to APPEND new storage at the end (new state vars are fine) but not to mutate existing entries.
This catches a pattern Blue gravitates toward: "rewrite the contract more idiomatically, with a slightly different state shape." Looks great in isolation. Bricks every existing user. The gate kills it.
The agent calls verify_patch (a thin shell wrapper at src/bench/phase1-dexible-blue/docker-bin/verify_patch.sh) which invokes patch-harness-cli.ts, which runs the verifyPatch() function above and prints a JSON verdict.
The agent gets back:
{
"exploitNeutralized": true,
"benignPassed": true,
"freshAttackerNeutralized": true,
"storageLayoutChanged": false,
"regressions": [],
"error": null
}That's it. The agent does not see the forge test output, does not see the bytecode bytes, does not see the storage layout diff, does not see which test files were run. It cannot lie about whether exploitNeutralized is true because it does not produce that boolean — the harness does.
- The benign test suite source. If Blue could read benign tests, it could write surgical patches that ONLY satisfy the named tests (Goodhart's Law via test-mining). Hidden suite forces Blue to actually understand the contract's invariants.
- The fresh-attacker label or address. Blue cannot encode "permit any address except the ones the harness will use" because it does not know which addresses those are.
- The storage layout JSON. Blue cannot reverse-engineer "which fields look already-aligned and dodge the gate." It must respect the layout invariant intrinsically.
- The bytecode comparison. Blue cannot fake bytecode equivalence; the harness compiles both trees from source and compares the actual EVM output.
Clone the repo, look at the file, run a duel:
git clone https://github.com/claygeo/solhunt-duel.git
cd solhunt-duel
$EDITOR src/sandbox/patch-harness.ts # read line 89-223 yourself
npm install
# run the Dexible duel against your own fork (needs Anthropic API key + Etherscan)
npm run duel -- --target dexibleThe verify_patch.sh script that Blue calls is also readable in 14 lines — see file path above. There is no "actually it's a fancier verifier in production" hidden somewhere; what's in the repo is what runs.
Things the gates do NOT catch:
- Bugs the benign suite doesn't exercise. Out of scope by definition. We don't claim "patched contract is fully correct," we claim "patched contract neutralizes Red's exploit without breaking the named benign suite." Bugs outside that suite are outside the harness's claim.
- Race conditions / MEV. The gates run on a frozen fork with deterministic block timing. Real-world MEV / front-running is invisible to this verifier. We never claim otherwise; the leaderboard is for "is this exploit prevented" not "is this contract production-MEV-safe."
- Cross-contract attacks the harness doesn't model. Some exploits depend on liquidity in a specific Uniswap pool at a specific block. If we didn't pre-load that state in our fork, the exploit will fail on the harness fork even when it works on mainnet. That collapses to a Red-failed run, not a falsely-hardened one.
The gates falsify positive claims, not negative ones. A "HARDENED" verdict means "we ran the four checks and they all passed." It does not mean "the contract is now bulletproof." Different claim, different scope.
Solhunt-Duel is one project. The pattern in this doc — separating the verdict from the agent — generalizes:
- For coding agents: the harness should diff produced code against a hidden test suite the agent doesn't see.
- For tool-use agents: the harness should verify the side effects (database state, file system) match a hidden expected diff, not the agent's narration.
- For research / writing agents: the harness should grade against criteria the agent doesn't see, not against rubrics included in the prompt.
The general principle: agents that grade themselves regress to "I succeeded." External verifiers regress to the truth. The cost is engineering effort. The payoff is honest signal.
- Verifier core:
src/sandbox/patch-harness.ts - CLI wrapper:
src/sandbox/patch-harness-cli.ts - Shell shim:
src/bench/phase1-dexible-blue/docker-bin/verify_patch.sh - Blue's instructions about the gates:
src/agent/blue-prompt.md - Public benchmark leaderboard with all 4 gates per duel: solhunt-duel.netlify.app/leaderboard