Skip to content

Commit 2590519

Browse files
authored
ci: run Echidna on PRs (#320)
1 parent 0a7a7c8 commit 2590519

5 files changed

Lines changed: 115 additions & 3 deletions

File tree

.github/workflows/echidna.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Echidna
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch: {}
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: echidna-${{ github.event.pull_request.number || github.ref }}
12+
cancel-in-progress: true
13+
14+
env:
15+
NODEJS_VERSION: '20'
16+
ECHIDNA_IMAGE: ghcr.io/crytic/echidna/echidna:v2.3.1
17+
18+
jobs:
19+
echidna:
20+
name: Echidna / ${{ matrix.contract }}
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 180
23+
strategy:
24+
fail-fast: false
25+
matrix:
26+
contract:
27+
- EchidnaStakeRegistryHarness
28+
- EchidnaPriceOracleHarness
29+
- EchidnaPostageStampHarness
30+
- EchidnaRedistributionHarness
31+
- EchidnaRedistributionClaimHarness
32+
- EchidnaSystemHarness
33+
34+
steps:
35+
- name: Checkout
36+
uses: actions/checkout@v4
37+
38+
- name: Setup Node.js and Yarn
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version: ${{ env.NODEJS_VERSION }}
42+
cache: yarn
43+
44+
- name: Install dependencies
45+
run: yarn install --frozen-lockfile
46+
47+
- name: Run Echidna
48+
env:
49+
ECHIDNA_CONTRACT: ${{ matrix.contract }}
50+
# 9000s leaves ~30 min for checkout, yarn, compile, and image pull
51+
# inside the 180-minute job cap so a timeout still uploads artifacts.
52+
ECHIDNA_TIMEOUT: '9000'
53+
run: yarn echidna
54+
55+
- name: Upload reproducers and logs
56+
if: failure()
57+
uses: actions/upload-artifact@v4
58+
with:
59+
name: echidna-${{ matrix.contract }}
60+
path: |
61+
echidna/logs/
62+
echidna/corpus/by-contract/
63+
crytic-export/
64+
if-no-files-found: warn
65+
retention-days: 14

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ tenderly.log
4242

4343
# Echidna fuzzing
4444
echidna/corpus/
45+
echidna/logs/
4546
crytic-export/
4647

4748
# Cursor
48-
.cursor/rules/audit-code.mdc
49+
.cursor/rules/audit-code.mdc

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ Harness layout, properties, and troubleshooting are documented in [echidna/READM
119119
yarn echidna
120120
```
121121

122+
CI runs the full Echidna campaign on every pull request. See [echidna/README.md](./echidna/README.md#ci).
123+
122124
## Run
123125

124126
### [Tests](./test)

echidna/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,26 @@ yarn echidna # all harnesses; needs Docker
125125
| `seqLen` | 320 | `ECHIDNA_SEQ_LEN` |
126126
| `maxBlockDelay` | 152 ||
127127
| workers | yaml | `ECHIDNA_WORKERS` |
128+
| seed | random | `ECHIDNA_SEED` |
129+
| timeout (seconds) | none | `ECHIDNA_TIMEOUT` |
128130

129131
Single harness: `ECHIDNA_CONTRACT=EchidnaRedistributionHarness yarn echidna` (also: `EchidnaStakeRegistryHarness`, `EchidnaPriceOracleHarness`, `EchidnaPostageStampHarness`, `EchidnaRedistributionClaimHarness`, `EchidnaSystemHarness`).
130132

131-
Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: `echidna/corpus/by-contract/<HarnessName>/` (gitignored). Crytic: `crytic-export/`.
133+
Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: `echidna/corpus/by-contract/<HarnessName>/` (gitignored). Logs: `echidna/logs/` (gitignored). Crytic: `crytic-export/`.
134+
135+
## CI
136+
137+
Workflow: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml). Runs on every pull request (and manual `workflow_dispatch`).
138+
139+
One matrix job per harness, using the same campaign as local `yarn echidna` (`echidna/echidna.yaml`: `testLimit` 60000, `seqLen` 320). Each job has a 180-minute cap and Echidna `--timeout` 9000s so setup plus fuzzing fit under the GitHub Actions limit and a timeout still uploads artifacts. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts.
140+
141+
CI does not pin `ECHIDNA_SEED` (a new seed each run). To replay a CI counterexample, copy `Seed: N` from the uploaded log and pass it locally:
142+
143+
```bash
144+
ECHIDNA_CONTRACT=EchidnaStakeRegistryHarness \
145+
ECHIDNA_SEED=N \
146+
yarn echidna
147+
```
132148

133149
## Extend
134150

scripts/echidna.sh

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ cd "$ROOT_DIR"
1313
IMAGE="${ECHIDNA_IMAGE:-ghcr.io/crytic/echidna/echidna:latest}"
1414
CONTRACT="${ECHIDNA_CONTRACT:-}"
1515
CONFIG="${ECHIDNA_CONFIG:-echidna/echidna.yaml}"
16+
LOG_DIR="${ECHIDNA_LOG_DIR:-echidna/logs}"
1617

1718
# Crytic-compile reads artifacts/build-info when using --hardhat-ignore-compile inside Docker (no Node/npx).
1819
# Stale build-info from deleted Solidity sources causes "Unknown file" failures.
@@ -40,6 +41,7 @@ fi
4041
# echidna/echidna.yaml: testLimit 60000, seqLen 320). Examples:
4142
# ECHIDNA_TEST_LIMIT=20000 ECHIDNA_SEQ_LEN=200 yarn echidna # faster smoke
4243
# ECHIDNA_WORKERS=8 ECHIDNA_CONTRACT=EchidnaSystemHarness yarn echidna
44+
# ECHIDNA_SEED=1 ECHIDNA_TIMEOUT=600 yarn echidna # reproducible / time-boxed CI run
4345
# Use a string (not an array) so `set -u` never trips on empty `${arr[*]}` on older Bash.
4446
ECHIDNA_EXTRA_CLI=""
4547
if [[ -n "${ECHIDNA_TEST_LIMIT:-}" ]]; then
@@ -51,26 +53,52 @@ fi
5153
if [[ -n "${ECHIDNA_WORKERS:-}" ]]; then
5254
ECHIDNA_EXTRA_CLI+=" --workers ${ECHIDNA_WORKERS}"
5355
fi
56+
if [[ -n "${ECHIDNA_SEED:-}" ]]; then
57+
ECHIDNA_EXTRA_CLI+=" --seed ${ECHIDNA_SEED}"
58+
fi
59+
if [[ -n "${ECHIDNA_TIMEOUT:-}" ]]; then
60+
ECHIDNA_EXTRA_CLI+=" --timeout ${ECHIDNA_TIMEOUT}"
61+
fi
62+
if [[ -n "${ECHIDNA_ARGS:-}" ]]; then
63+
ECHIDNA_EXTRA_CLI+=" ${ECHIDNA_ARGS}"
64+
fi
65+
66+
mkdir -p "${ROOT_DIR}/${LOG_DIR}"
5467

5568
for c in "${CONTRACTS_TO_RUN[@]}"; do
5669
echo "==> echidna: running contract $c" >&2
5770

5871
# One corpus + coverage tree per harness so saved sequences stay relevant to
5972
# that contract (shared corpus mixed unrelated call shapes and diluted learning).
6073
CORPUS_DIR="echidna/corpus/by-contract/${c}"
74+
if [[ -n "${ECHIDNA_SEED:-}" ]]; then
75+
CORPUS_DIR="${CORPUS_DIR}/seed-${ECHIDNA_SEED}"
76+
fi
6177
mkdir -p "${ROOT_DIR}/${CORPUS_DIR}"
6278

79+
LOG_FILE="${ROOT_DIR}/${LOG_DIR}/${c}.log"
80+
6381
# Drop stale Crytic output inside Docker (same uid as container root). A host
6482
# `rm -rf crytic-export` often fails after Docker created the dir as root.
83+
# Capture the fuzzer exit code while still teeing logs for CI artifacts.
84+
set +e
6585
docker run --rm \
6686
--entrypoint sh \
6787
-v "$ROOT_DIR":/src \
6888
-w /src \
6989
"$IMAGE" \
7090
-c "rm -rf crytic-export && echidna-test . --contract ${c} --config ${CONFIG} \
7191
--corpus-dir ${CORPUS_DIR} --coverage-dir ${CORPUS_DIR}/coverage${ECHIDNA_EXTRA_CLI} \
72-
--crytic-args '--hardhat-ignore-compile'"
92+
--crytic-args '--hardhat-ignore-compile'" \
93+
2>&1 | tee "${LOG_FILE}"
94+
ec=${PIPESTATUS[0]}
95+
set -e
7396

7497
yarn -s ts-node "${ROOT_DIR}/scripts/echidna-coverage-summary.ts" "${c}" \
7598
--coverage-dir "${ROOT_DIR}/${CORPUS_DIR}/coverage" || true
99+
100+
if [[ "${ec}" -ne 0 ]]; then
101+
echo "==> echidna: ${c} failed (exit ${ec}); log: ${LOG_FILE}" >&2
102+
exit "${ec}"
103+
fi
76104
done

0 commit comments

Comments
 (0)