Target repo: aru-evm · Pairs with: Invariant Test Suite, Deployment Ceremony
Three of the six findings in the initial review would have been caught by CI that ran slither + forge test --match-path test/invariant/* on every PR. The cost is one GitHub Actions workflow. The benefit is preventing a high-severity bug from reaching mainnet.
.github/workflows/ci.yml:
yamlname: CI
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: foundry-rs/foundry-toolchain@v1
with:
version: stable
- name: Forge build
run: forge build --sizes
- name: Forge tests
run: forge test -vvv
- name: Invariant tests (PR pass — 256 runs)
if: github.event_name == 'pull_request'
run: forge test --match-path 'test/invariant/*.t.sol' --invariant-runs 256
- name: Invariant tests (main — 1024 runs)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: forge test --match-path 'test/invariant/*.t.sol' --invariant-runs 1024
timeout-minutes: 30
- name: Gas snapshot diff
run: forge snapshot --check
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: foundry-rs/foundry-toolchain@v1
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Slither
run: pip install slither-analyzer
- name: Slither
run: slither . --filter-paths 'lib|test' --fail-high
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: foundry-rs/foundry-toolchain@v1
- name: Format check
run: forge fmt --check
In GitHub branch protection for main:
test must passstatic-analysis must passlint must passmain (no direct pushes)lib/openzeppelin-contracts must track a specific tag, not master. Pin in .gitmodules:
ini[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
branch = v5.0.2
Update policy: submodule bumps require their own PR, subject to the same CI, with a changelog note describing what's pulled in and why.
Pin solc explicitly (already done at 0.8.24). Add EVM version:
toml[profile.default]
solc = "0.8.24"
evm_version = "cancun"
optimizer = true
optimizer_runs = 200
bytecode_hash = "none" # reproducible bytecode
bytecode_hash = "none" makes compiled output deterministic across machines — auditors can reproduce the exact bytecode.
forge snapshot --check fails the PR if gas costs change. This catches:
while loops reintroducedWhen a gas change is intentional, the snapshot is regenerated in the same PR and the diff is reviewed.
slither.config.json:
json{
"filter_paths": "lib|test|script",
"exclude_informational": true,
"exclude_optimization": false,
"fail_on": "medium"
}
Any medium+ Slither finding fails CI. Low/informational findings are inspected on PR review but don't block.
Arbitrum inherits Ethereum's 24.576 KB contract size limit. forge build --sizes surfaces any contract approaching the limit — fail the build at 22 KB to leave headroom.
Add to CI:
yaml- name: Contract size check
run: |
forge build --sizes
forge build --sizes | awk '/^\|/ && $NF+0 > 22000 { print "Contract too large: " $2 " at " $NF " bytes"; exit 1 }'
Cutting a release tag (vX.Y.Z) requires:
main branch green on all CI jobsforge test --match-path 'test/invariant/*.t.sol' --invariant-runs 4096 --invariant-depth 200 — run on a CI worker with 60-minute budget| Change type | Reviewers required |
|---|---|
Contracts (src/*.sol) |
2 engineers + 1 auditor for mainnet-bound changes |
| Tests | 1 engineer |
Scripts (script/*.sol) |
2 engineers; script review before any mainnet run |
foundry.toml / .gitmodules |
2 engineers + explicit note in PR body on what's changing and why |
| CI workflows | 1 engineer + 1 from ops |
Auditor reviews are out-of-band (email / PDF) and referenced in the PR description.
.github/pull_request_template.md:
markdown## Summary
<one paragraph>
## Contract impact
- [ ] No contract changes
- [ ] Additive-only (new function, new event — no change to existing behavior)
- [ ] Behavior change to existing function — requires auditor review
- [ ] State layout change — **requires migration plan**
## Test plan
- [ ] Added/updated unit tests
- [ ] Added/updated invariant tests (for accounting or settlement changes)
- [ ] Gas snapshot diff reviewed
## Deployment notes
<only if this requires ops action at next deploy>
forge coverage is slow and coverage % is a vanity metric for a contract of this size — invariant tests are the real safety net. Optional for release process.CI is one layer. It catches regressions. The Deployment Ceremony catches ceremony failures. The Threat Model documents what neither layer catches. All three must be current; drift in any one defeats the others.