| name | target-onboarding |
| description | Create and execute onboarding for a new scfuzzbench benchmark target end-to-end, including target repo setup, validation, manifest registration, and /start payload. |
| metadata | {"short-description":"Onboard a benchmark target end-to-end"} |
Target Onboarding Skill
Use this skill when onboarding a new benchmark target for scfuzzbench/scfuzzbench.
This skill covers:
- creating/maintaining
pre-target and main branches in the target repo
- porting recon harness/config files
- running local validation
- opening and merging a
harness -> main PR
- registering the merged commit and preparing an exact
/start request
Branch convention (mandatory)
Every target repo lives as a fork under the scfuzzbench GitHub org and uses exactly
two long-lived branches:
pre-target: the pristine upstream state at the vulnerable baseline commit,
before any harness is added. Never rebased or amended.
main (default branch): pre-target plus the harness. After the harness PR
merges, resolve main to an immutable commit SHA and register that SHA in
benchmarks/targets.json in scfuzzbench/scfuzzbench.
Seeing what the harness adds is always compare pre-target...main on GitHub — no other
branch/ref bookkeeping is allowed. Work branches (e.g. harness) are temporary and merge
into main via PR. Benchmark requests consume the registered commit, not the mutable
branch name.
Inputs
Required:
upstream_target_repo_url: upstream project URL
vulnerable_baseline_commit_sha: baseline commit for pre-target
recon_harness_source_repo_url: source repo containing recon harness
recon_harness_source_ref_for_test_recon: source branch/commit to copy harness from
destination_repo_url: https://github.com/scfuzzbench/<repo>-scfuzzbench
benchmark_type: property or optimization
Optional:
- requester notes and constraints
Non-negotiable constraints
- Keep target code at the vulnerable point in time.
- Port the full harness (e.g. from
test/recon/), not partial files.
- Validate locally before opening PR.
- Keep global defaults in
scfuzzbench generic; use per-target overrides only when needed.
- Do not leak secrets in issues/PRs.
- Every benchmark target must include canary checks:
- one canary assertion failure
- one canary global invariant failure prefixed with
invariant_
- Naming rule:
invariant_* functions must not have parameters
- if a global check has parameters, it must be prefixed
global_*
- apply this naming rule across the full inheritance tree, not only
Properties.sol (for example files under test/recon/properties/** and inherited bases)
- Assertion reason centralization rule:
- every assertion failure reason must be declared in
Properties.sol as a string constant
- no inline assertion reason literals in harness files (for example
CryticToFoundry.sol)
- every assertion failure reason value should start with
!!! (keeps assertion canary extraction consistent across fuzzers)
- Assertion naming normalization rule:
- assertion handler functions must use
targetFunctionName_ASSERTION_<ASSERTION_CONSTANT_SUFFIX>(...)
ASSERTION_CONSTANT_SUFFIX must exactly match the referenced ASSERTION_* constant suffix
- example:
ASSERTION_WITHDRAW_DOS -> iSpoke_withdraw_ASSERTION_WITHDRAW_DOS(...)
- each assertion handler function must reference exactly one
ASSERTION_* constant
- if a property needs multiple checks (
gte/lte/eq/t/...) in the same handler, all checks must use that same single ASSERTION_* constant
- canonical cross-fuzzer identifier for assertions is always
targetFunctionName (strip _ASSERTION_<ASSERTION_CONSTANT_SUFFIX>)
- example:
iSpoke_withdraw_ASSERTION_WITHDRAW_DOS -> iSpoke_withdraw
- Scope control rule:
- prefer minimal, rename-first edits when applying naming normalization
- preserve existing harness behavior and side effects
- if helper methods, handler splits, or action/assertion refactors are potentially ambiguous/confusing, ask the user first and apply their case-by-case preference
- Invariant signature compatibility rule:
- every
invariant_* function across test/recon/** and inherited bases must be declared returns (bool)
- invariant functions must be nonpayable (not
view/pure) for Medusa property compatibility
- include an explicit boolean return at function end (for example
return true;)
- Single-harness rule:
- the exact same harness must run unmodified on Foundry, Echidna, Medusa, and Recon
- no per-fuzzer shims: no
_isAssertion(...), no assertionFailures mapping, no invariant_assertion_failure_* wrappers, no fuzzer-conditional code paths
- Fuzzer-magic signal ban:
- never declare or emit an
AssertionFailed event (any signature) anywhere in the harness tree, including helper bases like a t(bool, string) implementation
- Echidna treats
AssertionFailed(...) events as their own generic failure identity in addition to detecting the assert panic, so the belt-and-braces "emit + assert" pattern produces a phantom Echidna-exclusive bug named AssertionFailed that Foundry/Medusa/Recon can never observe (seen on the Origin Dollar and Drips targets before this rule)
- surface assertion failures only via a plain
assert(false) (Panic 0x01); all four fuzzers detect it and the failure identity dedups to the calling function name
- the same ban applies to any other single-fuzzer magic signal (special events, fuzzer-specific sentinel calls, or cheatcode-based failure reporting)
- No agent-instruction files:
- target repos must not contain
AGENTS.md, CLAUDE.md, or similar agent-instruction files; they go stale and do not belong in benchmark targets
Workflow
1) Create target repo and baseline branches
- Fork/copy the upstream project into the scfuzzbench org as
scfuzzbench/<repo>-scfuzzbench.
- Checkout the vulnerable baseline commit.
- Create
pre-target at that commit and push it. Never touch it again.
- Create
main at the same commit, push it, and set it as the default branch.
2) Create harness branch and port harness
- Create a work branch (e.g.
harness) from main.
- Port full recon setup from source ref.
Minimum files/directories to port:
test/recon/ (full tree)
foundry.toml
echidna.yaml
medusa.json
- Required helpers/remappings/scripts used by recon tests
3) Ensure benchmark-compatible config
foundry.toml must include benchmark-compatible values:
[profile.default]
assertions_revert = false
[invariant]
runs = 500000000
depth = 100
include_storage = true
show_solidity = true
show_metrics = true
fail_on_revert = false
corpus_dir = "corpus/foundry"
4) Foundry assertion mode
Use Foundry assertion mode directly. Compatibility-shim modes are not supported.
Upstream Foundry (the commit pinned in infrastructure/variables.tf) reports handler-side
assertion failures during invariant campaigns by default and keeps the campaign running
(foundry-rs/foundry#14275 + #14482) — no fork-specific foundry.toml keys are needed.
Do not add fail_on_assert or continuous_run to foundry.toml; those were custom-fork
keys and no longer exist upstream.
Required:
- keep all assertion reason strings in
Properties.sol as string constant ASSERTION_* = "!!! ...";
- set
assertions_revert = false under [profile.default] in foundry.toml
- name assertion handlers as
targetFunctionName_ASSERTION_<ASSERTION_CONSTANT_SUFFIX>(...)
ASSERTION_CONSTANT_SUFFIX must exactly match the referenced ASSERTION_* constant suffix
- examples:
iHub_mintFeeShares_ASSERTION_MINT_FEE_SHARES_PPS_CHANGE, iSpoke_withdraw_ASSERTION_WITHDRAW_DOS, assert_canary_ASSERTION_CANARY
- each assertion handler must reference exactly one
ASSERTION_* constant
- if multiple checks are required in one handler, reuse the same single
ASSERTION_* constant across those checks
- do not add Foundry-only wrapper invariants (
invariant_assertion_failure_*)
- do not add
_isAssertion, assertionFailures, or overridden assert helpers in CryticToFoundry.sol
setUp() must include handler routing (targetContract, multiple targetSender values)
- include
invariant_noop() public returns (bool) in CryticToFoundry.sol for assertion-focused smoke checks
- local review must confirm canonical identifier compatibility:
- Echidna/Medusa report handler name (
targetFunctionName(...))
- Foundry failure traces include handler name (
targetFunctionName_ASSERTION_*)
- canonical dedup key is
targetFunctionName
5) Canary requirement for every target
Add these canaries to each target harness:
- Assertion canary:
- helper/action function:
assert_canary_ASSERTION_CANARY(uint256 entropy)
- assertion reason string:
!!! canary assertion
- canonical assertion identifier:
assert_canary
- Global invariant canary:
- invariant function name must start with
invariant_
- canary invariant must take no parameters
- canary invariant must use signature
function invariant_canary() public returns (bool)
- use
invariant_canary and make it fail immediately (Canary invariant)
Reference implementation:
// Properties.sol
string constant ASSERTION_CANARY = "!!! canary assertion";
string constant INVARIANT_CANARY_GLOBAL_INVARIANT_FAILURE = "Canary invariant";
function assert_canary_ASSERTION_CANARY(uint256 entropy) public {
t(entropy > 0, ASSERTION_CANARY);
}
function invariant_canary() public returns (bool) {
t(false, INVARIANT_CANARY_GLOBAL_INVARIANT_FAILURE);
return true;
}
// CryticToFoundry.sol
function setUp() public override {
setup();
targetContract(address(this));
targetSender(address(0x10000));
targetSender(address(0x20000));
targetSender(address(0x30000));
}
// Upstream Foundry reports handler assertion failures natively:
// - do not add _isAssertion(...)
// - do not add assertionFailures mapping
// - do not add invariant_assertion_failure_* wrappers
Both canaries are intentional failures used to verify:
- all fuzzers emit failures on the target
- the analysis/parser pipeline is capturing failures correctly
- normalized assertion id consistency across fuzzers (
assert_canary)
6) Fuzzer-specific path rules
Echidna:
- usually use
test/recon/CryticTester.sol
- use
tests/... only for target-specific exceptions
- use the
echidna binary in commands and docs (do not use echidna-test)
- enforce naming + config split:
- apply naming rules to
Properties.sol and all inherited recon contracts
- global checks in harness code must never use
property_ or crytic_
- use
invariant_ only for no-arg globals
- if a global check has parameters, prefix it
global_
echidna.yaml should use testMode: "assertion"
echidna.yaml should use prefix: "echidna_"
- rationale: in assertion mode, Echidna should catch assertion failures plus global properties in one run, so cannot use prefix "invariant"
Medusa:
- use concrete compilation target file (not
".")
- usually
test/recon/CryticTester.sol
medusa.json property prefix should stay invariant_
- rationale: Medusa can run property and assertion testing at the same time
- if gas-floor errors occur, raise gas limits
Example:
"compilation": {
"platform": "crytic-compile",
"platformConfig": {
"target": "test/recon/CryticTester.sol"
}
}
7) Local validation before PR
Run all:
forge test --match-contract CryticToFoundry --list
- Echidna smoke run
- Medusa smoke run
- Foundry invariant smoke run
- 2-minute canary trial for each fuzzer
- Ensure
CryticToFoundry.sol has no test_* repro/unit tests
- Canary smoke checks must fail within the smoke trial window:
FOUNDRY_INVARIANT_RUNS=1 forge test --match-contract CryticToFoundry --match-test invariant_canary -vv
FOUNDRY_INVARIANT_RUNS=20000 FOUNDRY_INVARIANT_DEPTH=1 forge test --match-contract CryticToFoundry --match-test invariant_noop -vv
- Acceptance gate: each fuzzer must report at least 2 bugs within 2 minutes:
- one bug for
invariant_canary (Canary invariant)
- one bug for the assertion canary (
!!! canary assertion via assert_canary_ASSERTION_CANARY), with canonical id assert_canary
Suggested 2-minute commands:
timeout 120 echidna test/recon/CryticTester.sol --contract CryticTester --config echidna.yaml --format text --disable-slither
timeout 120 medusa fuzz --config medusa.json --timeout 120
timeout 120 forge test --match-contract CryticToFoundry --match-test 'invariant_' -vv
Completion is tied to the 2-minute 2-canary acceptance gate above.
Debug-only fallback for Foundry output inspection:
FOUNDRY_INVARIANT_RUNS=100 forge test --match-contract CryticToFoundry --match-test 'invariant_' -vv
8) Open PR from harness branch to main
Create PR harness -> main (with main still at the pre-target baseline, so the PR
diff is the full harness).
PR description must include:
- vulnerable baseline ref used for
pre-target
- recon harness source ref
- files copied/changed
- local smoke test summary
- 2-minute canary trial summary per fuzzer (must show both canaries found)
- canary validation summary (assertion canary + global invariant canary)
- proposed target manifest metadata and
/start request template for scfuzzbench
- any target-specific overrides and why
9) Register the target and prepare the final /start request
After the harness PR merges, resolve the target repository's main branch to its
full commit SHA. Add or update its entry in
scfuzzbench/scfuzzbench/benchmarks/targets.json, run make targets-validate,
and use that same SHA and properties_path in the final request.
Typical fields:
target_repo_url: destination repo URL (under the scfuzzbench org)
target_commit: full commit SHA resolved from main after the harness PR is merged
benchmark_type: property or optimization
instance_type
instances_per_fuzzer
timeout_hours
fuzzers: ["echidna","medusa","foundry","recon-fuzzer"]
- optional
fuzzer_env_json only when target-specific override is necessary
- omit
foundry_version (it is local-only) and leave foundry_git_repo and foundry_git_ref
empty — the infrastructure defaults build upstream foundry-rs/foundry at the commit pinned
in infrastructure/variables.tf
Common failures and fixes
- Echidna:
tests/recon/CryticTester.sol does not exist
- fix target path to
test/recon/CryticTester.sol unless repo is a known exception
- Medusa: target
"." treated as directory
- use explicit Solidity file target
- Medusa:
insufficient gas for floor data gas cost
- raise
transactionGasLimit and blockGasLimit
- Foundry failures not surfaced
- verify
foundry.toml sets assertions_revert = false under [profile.default]
- verify the Foundry build is at least the pinned upstream commit (needs foundry-rs/foundry#14275 + #14482)
- verify assertion reasons are constants in
Properties.sol (recommended !!! prefix)
- remove leftover compatibility shim code (
_isAssertion, assertionFailures, invariant_assertion_failure_*)
- Foundry unrealistically fast/all bugs immediate
- remove any
test_* functions in CryticToFoundry
- Echidna returns 0 issues unexpectedly
- enforce
testMode: "assertion" with prefix: "echidna_" in echidna.yaml
- enforce naming rule across inherited recon properties too:
invariant_* must be no-arg, parameterized globals must be global_*
- keep global checks out of
property_ and crytic_
- Broken-invariant overlap shows assertion bugs as Foundry-only
- ensure assertion handler is named
targetFunctionName_ASSERTION_<ASSERTION_CONSTANT_SUFFIX>
- ensure
ASSERTION_CONSTANT_SUFFIX exactly matches the referenced ASSERTION_* constant suffix
- ensure each handler references exactly one
ASSERTION_* constant; split legacy multi-assert handlers when needed
- Recon:
Invalid hex address / Odd number of digits at config load
- Recon rejects Echidna-style short hex addresses (
"0x10000") in echidna.yaml
- always write zero-padded 20-byte addresses (
"0x0000000000000000000000000000000000010000"); Echidna accepts both
- Onboarding a pre-existing suite whose handlers are named
test* (or invariant* without underscore)
- Foundry would run
test* functions as standalone unit tests and treat invariant* as predicates
- do NOT rename the upstream handlers; instead make
CryticToFoundry a Test-only contract that deploys CryticTester and calls targetContract(address(tester)) — handlers stay stateful-fuzz-only and failures still dedup to the same handler names across all fuzzers
- Stateful suites that depend on time passing between calls (streams, vesting, cycles)
- Echidna/Medusa advance time natively (
maxTimeDelay / blockTimestampDelayMax)
- mirror them for Foundry with
[invariant] max_time_delay and max_block_delay in foundry.toml
Completion checklist
Done means all are true:
- destination repo is created/updated in the
scfuzzbench org
pre-target and main branches are pushed (main is the default branch)
- harness PR is merged with required validation details
- no
AGENTS.md/CLAUDE.md or other agent-instruction files exist in the target repo
- canary assertion + canary
invariant_ global failure are present and intentionally failing
- no parameterized function is prefixed
invariant_ (use global_* for parameterized globals)
- naming rules are satisfied across inherited recon property contracts, not only
Properties.sol
- each fuzzer reports at least 2 canary bugs (assertion + global invariant) within 2 minutes
- the pinned target is registered in
benchmarks/targets.json and exact /start JSON is provided
- PR URL is recorded in final report; include tracking issue URL only if one was explicitly requested
- all assertion failure reasons are constants in
Properties.sol; !!! prefix is recommended for consistent parser extraction
- every assertion handler
targetFunctionName_ASSERTION_<ASSERTION_CONSTANT_SUFFIX> has exactly one referenced ASSERTION_* constant
- assertion failures normalize to
targetFunctionName across Echidna, Medusa, and Foundry