Merge pull request #81 from DevRedious/add-foundry-smart-contract-security-skill

Add skill: auditing-foundry-smart-contract-security
This commit is contained in:
Mahipal
2026-06-20 16:44:21 +02:00
committed by GitHub
6 changed files with 1039 additions and 0 deletions
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please do not remove or change
the license header comment from a contributed file except when
necessary.
Copyright 2026 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,170 @@
---
name: auditing-foundry-smart-contract-security
description: >-
Pre-deployment security audit of Solidity smart contracts in a Foundry project.
Combines static analysis (Slither, Aderyn), symbolic execution (Mythril), and
property-based testing (forge fuzz + invariant tests with handlers) to catch
reentrancy, access-control, oracle/price manipulation, and arithmetic bugs
BEFORE deploying to an EVM chain. Also enforces key hygiene (no plaintext
private keys, encrypted cast keystore) and a secure deploy workflow. Use when
writing, reviewing, testing, or deploying Solidity/Foundry contracts, building
a dApp, or working with forge/cast/anvil, MetaMask, or Web3/DeFi code.
domain: cybersecurity
subdomain: blockchain-security
tags:
- solidity
- foundry
- forge
- smart-contract
- slither
- aderyn
- mythril
- reentrancy
- defi
- web3
- invariant-testing
- audit
version: "1.0"
author: devredious
license: Apache-2.0
based_on: mukul975/analyzing-ethereum-smart-contract-vulnerabilities
swc_registry: https://swcregistry.io/
mitre_attack:
- T1190
- T1059
---
# Auditing Foundry Smart Contract Security
## Overview
Deployed smart contracts are **immutable** and custody **real funds**, so a bug
shipped to mainnet cannot be patched — it can only be exploited. Most catastrophic
DeFi losses come from a small set of recurring classes: reentrancy, broken access
control, oracle/price manipulation, and unchecked arithmetic or external calls.
This skill runs a **defense-in-depth, pre-deployment audit** of a Foundry project,
layering four independent techniques that each catch what the others miss:
1. **Static analysis**`slither` (90+ detectors) and `aderyn` (Cyfrin, Rust) scan
the AST/IR in seconds for known anti-patterns.
2. **Symbolic execution**`mythril` (optional, slow) explores execution paths and
SMT-solves for deep arithmetic/reentrancy bugs.
3. **Property-based testing**`forge test` with **fuzzing** (`testFuzz_*`) and
**invariant tests** (`invariant_*` + handler contracts with ghost variables)
proves protocol-level properties hold across millions of random sequences.
4. **Manual review + key hygiene** — a structured checklist (see
`references/vulnerability-checklist.md`) and a secrets/keystore audit so no
private key ever lives in plaintext and deployment goes through an encrypted
`cast` keystore (see `references/secure-deployment-and-keys.md`).
The skill is **dev-side and pre-deployment** — it is run by the engineer building
the contract, not by a SOC after an incident. Findings gate the deploy: any
high/critical static finding, failing test, leaked key, or low coverage = **FAIL**.
## When to Use
- Before deploying any Solidity contract to a testnet or mainnet EVM chain.
- When writing or reviewing a Foundry project (`foundry.toml`, `src/`, `test/`, `script/`).
- When a contract handles value: tokens (ERC-20/721/1155), vaults, staking, AMMs, bridges, governance.
- When adding fuzz or invariant tests, or when coverage of value-moving functions is unknown.
- When wiring deployment scripts — to verify keys are in an encrypted keystore, not `.env` plaintext.
- When integrating a price oracle, external call, `delegatecall`, or upgradeable proxy.
- When triaging a Slither/Aderyn report and needing to separate real bugs from false positives.
## Prerequisites
- **Foundry** installed (`forge`, `cast`, `anvil`): `curl -L https://foundry.paradigm.xyz | bash && foundryup`
- **Slither** + solc: `pip install slither-analyzer` and `solc-select install <ver> && solc-select use <ver>`
- **Aderyn** (recommended): `cargo install aderyn` (or `npm i -g @cyfrin/aderyn`)
- **Mythril** (optional, slow symbolic exec): `pip install mythril`
- **gitleaks** (key-leak scan): see the companion `implementing-secret-scanning-with-gitleaks` skill
- A Foundry project that **compiles** (`forge build` succeeds) — analyzers need build artifacts.
- Solidity ^0.8.x is assumed (built-in overflow checks); pre-0.8 contracts need extra SafeMath review.
> Install the Python tools in a virtualenv (recommended on externally-managed distros). Never run
> analysis against untrusted contract source on a machine with funded wallets unlocked.
## Steps
### Step 1: Build and sanity-check the project
```bash
forge build # analyzers require fresh artifacts
forge fmt --check # style gate (optional)
cat foundry.toml # note solc version, optimizer, remappings, evm_version
```
### Step 2: Static analysis (fast, run every time)
```bash
# Slither — full project (uses foundry.toml + remappings automatically)
slither . --json slither-report.json
# Aderyn — Cyfrin Rust analyzer, complementary detectors
aderyn . -o aderyn-report.json
```
Or run the bundled orchestrator that runs both, deduplicates, and gates the result:
```bash
python3 scripts/agent.py --project . --output audit-report.json
```
### Step 3: Symbolic execution on critical contracts (optional, slow)
```bash
# Only on the highest-value contract(s) — Mythril is path-explosive
myth analyze src/Vault.sol --solc-json mythril.config.json --execution-timeout 300 -o json
# or: python3 scripts/agent.py --project . --mythril src/Vault.sol
```
### Step 4: Property-based testing — fuzz + invariants
```bash
forge test -vvv # unit + fuzz tests
forge coverage --report summary # coverage of value-moving code
forge test --match-test invariant_ -vvv # invariant suite (handler-based)
```
Every value-moving contract should have **invariant tests with a handler** (bounded
inputs, ghost variables, `targetContract(handler)`) — not just unit tests. See
`references/api-reference.md` for the handler pattern, and write a
`test_RevertWhen_*` (with `vm.expectRevert`) for each access-control guard.
### Step 5: Manual review against the checklist
Walk `references/vulnerability-checklist.md` for every contract: reentrancy
(checks-effects-interactions / `nonReentrant`), access control, oracle manipulation,
`delegatecall`/proxy storage layout, unchecked return values, `tx.origin`, weak
randomness, DoS, front-running/MEV, and ERC-specific pitfalls (approve race,
fee-on-transfer, rebasing).
### Step 6: Key hygiene & secure deploy
```bash
gitleaks detect --no-banner # no private keys / mnemonics / .env committed
git ls-files | grep -E '\.env$|keystore' && echo "WARN: secrets tracked by git"
# Import the deploy key ONCE into an encrypted keystore — never a plaintext PRIVATE_KEY env
cast wallet import deployer --interactive
# Deploy via the keystore account (testnet first), simulate before --broadcast
forge script script/Deploy.s.sol --account deployer --rpc-url <testnet> --broadcast --verify
```
See `references/secure-deployment-and-keys.md` for the full hardening rules
(MetaMask hygiene, hardware wallet for mainnet, RPC trust, post-deploy verification).
### Step 7: Triage and report
Combine Slither + Aderyn + Mythril + test results, deduplicate by (file, line),
drop confirmed false positives, rank by exploitability × financial impact, and map
each to its SWC id. The orchestrator emits `audit-report.json` with a PASS/FAIL gate.
## Expected Output
A JSON audit report listing findings with **SWC identifiers**, severity, tool source,
affected contract/function/line, and remediation; plus the test/coverage summary and a
single **PASS / FAIL** deploy gate. FAIL on any high/critical static finding, failing
test, leaked secret, or coverage below the configured threshold on value-moving code.
@@ -0,0 +1,181 @@
# API Reference — Tooling
## Slither (static analysis)
```bash
slither . # whole project (reads foundry.toml + remappings)
slither . --json slither-report.json # machine-readable
slither . --json - # JSON to stdout (used by agent.py)
slither . --print human-summary # quick overview
slither . --print inheritance-graph # inheritance / proxy layout
slither . --detect reentrancy-eth,unprotected-upgrade # specific detectors
slither --list-detectors # all 90+ detectors
slither . --exclude-informational --exclude-low # focus high/medium
slither . --triage-mode # interactively suppress false positives -> slither.db.json
```
Severity matrix (impact × confidence):
| Impact | Confidence | Example detectors |
|--------|------------|-------------------|
| High | High | `reentrancy-eth`, `suicidal`, `arbitrary-send-eth` |
| High | Medium | `controlled-delegatecall`, `reentrancy-no-eth` |
| Medium | High | `locked-ether`, `incorrect-equality`, `tx-origin` |
| Medium | Medium | `uninitialized-state`, `shadowing-state`, `unchecked-transfer` |
| Low | High | `naming-convention`, `solc-version`, `low-level-calls` |
| Informational | High | `pragma`, `dead-code`, `assembly` |
## Aderyn (Cyfrin, Rust static analyzer — complementary to Slither)
```bash
aderyn . # markdown report.md by default
aderyn . -o aderyn-report.json # JSON (used by agent.py)
aderyn . --scope src/ # limit scope
```
## Mythril (symbolic execution — slow, use on critical contracts only)
```bash
myth analyze src/Vault.sol -o json
myth analyze src/Vault.sol --execution-timeout 300 --max-depth 50 -o json
myth analyze --address 0x... --rpc <url> # deployed bytecode (read-only)
```
## Foundry — testing
```bash
forge build # required before static analysis
forge test -vvv # unit + fuzz; -vvvv shows traces
forge test --match-contract VaultTest
forge test --match-test invariant_ # invariant suite only
forge coverage --report summary # line/branch coverage table
forge coverage --report lcov # for CI / tooling
forge snapshot # gas snapshots (DoS-by-gas review)
forge fmt --check # style gate
```
### Fuzz test (property over random inputs)
```solidity
function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
```
### Revert test (replaces deprecated testFail)
```solidity
function test_RevertWhen_Unauthorized() public {
vm.prank(attacker);
vm.expectRevert("Not authorized"); // or vm.expectRevert(MyError.selector)
target.adminOnly();
}
```
### Key cheatcodes (`vm.*`)
| Cheatcode | Use |
|-----------|-----|
| `vm.prank(addr)` / `vm.startPrank` | impersonate caller (test access control) |
| `vm.warp(ts)` / `vm.roll(n)` | manipulate `block.timestamp` / `block.number` |
| `vm.deal(addr, amt)` | set ETH balance |
| `vm.store(addr, slot, val)` | overwrite storage (test invariants under hostile state) |
| `vm.expectRevert(...)` | assert a call reverts (with msg / custom error selector) |
| `vm.expectEmit(...)` | assert events |
| `bound(x, lo, hi)` | constrain fuzz inputs in handlers |
| `makeAddr("name")` | deterministic labelled actor |
### Invariant testing — handler pattern (the important one)
A handler wraps the target, **bounds inputs**, rotates **actors**, and tracks
**ghost variables**; `targetContract(handler)` makes the fuzzer drive only the
handler so sequences stay realistic.
```solidity
// test/Invariant.t.sol
contract VaultInvariantTest is Test {
Vault vault;
VaultHandler handler;
function setUp() public {
vault = new Vault();
handler = new VaultHandler(vault);
targetContract(address(handler)); // fuzz the handler, not the vault directly
}
function invariant_ConservationOfDeposits() public view {
assertEq(address(vault).balance,
handler.ghost_depositSum() - handler.ghost_withdrawSum());
}
function invariant_Solvency() public view {
assertGe(address(vault).balance, vault.totalDeposits());
}
}
```
```solidity
// test/handlers/VaultHandler.sol
contract VaultHandler is Test {
Vault public vault;
uint256 public ghost_depositSum;
uint256 public ghost_withdrawSum;
address[] public actors;
address internal currentActor;
modifier useActor(uint256 seed) {
currentActor = actors[bound(seed, 0, actors.length - 1)];
vm.startPrank(currentActor); _; vm.stopPrank();
}
constructor(Vault _v) {
vault = _v;
for (uint256 i; i < 10; i++) { actors.push(makeAddr(string(abi.encodePacked("actor", i)))); vm.deal(actors[i], 100 ether); }
}
function deposit(uint256 amt, uint256 seed) external useActor(seed) {
amt = bound(amt, 0.01 ether, 10 ether);
vault.deposit{value: amt}(); ghost_depositSum += amt;
}
function withdraw(uint256 amt, uint256 seed) external useActor(seed) {
uint256 bal = vault.balanceOf(currentActor);
if (bal == 0) return;
amt = bound(amt, 1, bal);
vault.withdraw(amt); ghost_withdrawSum += amt;
}
}
```
Tune in `foundry.toml`:
```toml
[invariant]
runs = 256
depth = 128
fail_on_revert = false # set true once the handler fully constrains inputs
[fuzz]
runs = 10000
```
## SWC Registry (key entries)
| SWC | Title | Detected by |
|-----|-------|-------------|
| SWC-101 | Integer Overflow/Underflow | Mythril (pre-0.8 only) |
| SWC-104 | Unchecked Call Return | Slither + Mythril |
| SWC-105 | Unprotected Ether Withdrawal | Slither + Mythril |
| SWC-106 | Unprotected SELFDESTRUCT | Slither + Mythril |
| SWC-107 | Reentrancy | Slither + Mythril |
| SWC-112 | Delegatecall to Untrusted Callee | Slither |
| SWC-114 | Transaction Order Dependence (front-running) | manual |
| SWC-115 | tx.origin Authentication | Slither |
| SWC-116 | Block Timestamp Dependence | Mythril |
| SWC-120 | Weak Randomness | Slither + manual |
## References
- Slither: https://github.com/crytic/slither
- Aderyn: https://github.com/Cyfrin/aderyn
- Mythril: https://github.com/Consensys/mythril
- Foundry Book: https://getfoundry.sh/
- SWC Registry: https://swcregistry.io/
- Solidity security: https://docs.soliditylang.org/en/latest/security-considerations.html
- Solodit (audit findings DB): https://solodit.xyz/
@@ -0,0 +1,90 @@
# Secure Deployment & Key Hygiene
The contract code can be flawless and you still lose everything if a **private key
leaks** or you sign a malicious transaction. This is the part most smart-contract
guides skip. Treat keys as the highest-severity asset.
## Golden rules
1. **A real private key or seed phrase NEVER touches a file, env var, shell history, or git.**
2. Plaintext `PRIVATE_KEY=0x...` in `.env` is the #1 leak vector — use an **encrypted keystore** instead.
3. **Separate wallets**: a throwaway dev wallet (testnet only) ≠ the mainnet deployer ≠ your personal MetaMask with real funds.
4. **Hardware wallet (Ledger/Trezor) for any mainnet deploy or admin action** that controls funds.
5. Simulate before broadcasting; verify after.
## Foundry encrypted keystore (`cast wallet`)
Import the key once into an encrypted, password-protected keystore — then reference
it by name. The raw key never appears in commands or files again.
```bash
# Import interactively (key is typed, not in argv/history), set a strong password
cast wallet import deployer --interactive
# Or generate a fresh dev key directly into the keystore
cast wallet new
# List / inspect (addresses only)
cast wallet list
```
Deploy by **account name**, never by `--private-key`:
```bash
# Testnet first — simulate (no --broadcast) then broadcast + verify
forge script script/Deploy.s.sol --account deployer --rpc-url <testnet_rpc>
forge script script/Deploy.s.sol --account deployer --rpc-url <testnet_rpc> --broadcast --verify
# Mainnet (prefer a Ledger):
forge script script/Deploy.s.sol --ledger --hd-paths "m/44'/60'/0'/0/0" --rpc-url <mainnet_rpc> --broadcast --verify
```
In deploy scripts, use `vm.startBroadcast()` with **no argument** (it uses the
`--account`/`--ledger` signer). Avoid `vm.envUint("PRIVATE_KEY")`.
## Anti-leak controls (wire into the project)
```bash
# 1. .gitignore the usual suspects
printf '.env\n.env.*\n*.key\nkeystore/\nbroadcast/\n' >> .gitignore
# 2. Scan history + working tree for secrets (see the implementing-secret-scanning-with-gitleaks skill)
gitleaks detect --no-banner
gitleaks detect --no-banner --log-opts="--all" # full git history
# 3. Confirm nothing sensitive is tracked
git ls-files | grep -E '\.env$|\.key$|keystore' && echo "REMOVE THESE FROM GIT"
```
If a key was ever committed (even and then deleted): **consider it compromised**
generate a new one, move funds, and purge history (BFG / `git filter-repo`).
## MetaMask / wallet operational security
- Dedicated browser profile for Web3; review every signature — **read what you sign**.
- Beware **blind signing** and `eth_sign`/`personal_sign` phishing; reject opaque hex.
- Token **approval hygiene**: avoid unlimited `approve`; periodically revoke (revoke.cash); prefer `permit` with deadlines.
- Verify the **contract address and chain id** before interacting; bookmark dApps, don't follow links.
- Add networks/RPCs only from trusted sources — a malicious RPC can lie about state and simulate fake balances.
## RPC & dependency trust
- Pin a reputable RPC (your own node, or a known provider); a hostile RPC can feed false data to scripts and frontends.
- Pin dependency versions (`forge install` with a tag/commit; lock OpenZeppelin version). Re-audit on bumps.
- Verify deployed bytecode matches source on the explorer (`forge verify-contract` / `--verify`).
## Post-deploy checklist
- [ ] Source verified on the block explorer.
- [ ] Ownership/admin transferred to a **multisig** (Safe), not an EOA, for anything controlling funds.
- [ ] Timelock on privileged upgrades/parameter changes.
- [ ] Monitoring/alerting on critical events (large withdrawals, ownership changes, pause).
- [ ] Emergency runbook: pause + emergency-withdraw path tested on testnet.
- [ ] Deploy key rotated/retired if it ever touched a less-trusted machine.
## References
- Foundry deploying guide: https://getfoundry.sh/guides/deploying
- `cast wallet`: https://getfoundry.sh/cast/reference/wallet
- OpenZeppelin Contracts: https://docs.openzeppelin.com/contracts
- Safe (multisig): https://safe.global/
- revoke.cash (approval management): https://revoke.cash/
@@ -0,0 +1,78 @@
# Manual Review Checklist — Solidity / EVM
Walk this for **every** contract that moves value. Each item: what to look for →
how to confirm (Foundry test / Slither detector) → fix. Tools catch the known
patterns; this catches the logic bugs they can't.
## 1. Reentrancy (SWC-107)
- [ ] External call (`call`, `transfer`, ERC-777 hooks, ERC-721 `onERC...Received`, arbitrary token) **before** state updates?
- [ ] Cross-function / read-only reentrancy: a view used by another protocol mid-call?
- **Confirm:** Slither `reentrancy-eth`/`reentrancy-no-eth`; write an attacker contract test that re-enters in its `receive()`.
- **Fix:** Checks-Effects-Interactions order; `nonReentrant` (OpenZeppelin `ReentrancyGuard`); pull-over-push payments. ("OZ" = OpenZeppelin throughout.)
## 2. Access control (SWC-105/106/115)
- [ ] Every state-changing/admin function gated (`onlyOwner`, roles, custom modifier)?
- [ ] `initialize()` on upgradeable contracts protected against re-init and front-running?
- [ ] No `tx.origin` for auth (phishable) — use `msg.sender`.
- [ ] `selfdestruct` / `delegatecall` reachable only by trusted roles?
- **Confirm:** a `test_RevertWhen_*` with `vm.prank(attacker)` + `vm.expectRevert` for each guard.
- **Fix:** OZ `Ownable2Step` / `AccessControl`; `initializer` modifier; remove `tx.origin`.
## 3. Oracle / price manipulation (DeFi #1 exploit class)
- [ ] Price from spot `getReserves()` / a single AMM pool? (flash-loan manipulable)
- [ ] Using Chainlink: checked `updatedAt` staleness, `answeredInRound`, min/max bounds, L2 sequencer uptime?
- **Confirm:** fork test that manipulates the pool within one tx and asserts your protocol stays solvent.
- **Fix:** TWAP / Chainlink with staleness+deviation checks; never trust spot for pricing.
## 4. Arithmetic & rounding (SWC-101)
- [ ] Solidity ^0.8 (built-in checked math) — and any `unchecked{}` block justified?
- [ ] Division before multiplication (precision loss)? Rounding always in protocol's favor?
- [ ] Share-inflation / first-depositor attack on ERC-4626-style vaults?
- **Confirm:** `testFuzz_*` over amounts; invariant `assertGe(assets, shares-implied)`.
- **Fix:** mulDiv (OZ `Math.mulDiv`); virtual shares/offset for vaults; explicit rounding direction.
## 5. Unchecked external calls / return values (SWC-104)
- [ ] Return value of low-level `call`/`send` and ERC-20 `transfer`/`transferFrom` checked?
- [ ] Non-standard ERC-20s (no return, fee-on-transfer, rebasing) handled?
- **Confirm:** Slither `unchecked-transfer`, `unchecked-lowlevel`.
- **Fix:** OZ `SafeERC20`; measure balance delta for fee-on-transfer; require success.
## 6. delegatecall / proxy storage (SWC-112)
- [ ] Storage layout identical across implementation upgrades (no reordered/removed vars)?
- [ ] No `delegatecall` to user-supplied address; implementation can't be re-initialized/self-destructed?
- **Confirm:** Slither `controlled-delegatecall`, `unprotected-upgrade`; storage-layout diff between versions.
- **Fix:** OZ `UUPS`/`Transparent` proxies + storage gaps; `_disableInitializers()` in constructor.
## 7. Front-running / MEV / ordering (SWC-114)
- [ ] Approve race (ERC-20 `approve` from non-zero to non-zero)?
- [ ] Slippage / deadline params on swaps and mints? Commit-reveal where needed?
- **Fix:** `increaseAllowance`/`permit`; enforce `minOut` + `deadline`; commit-reveal for sensitive ordering.
## 8. Randomness (SWC-120)
- [ ] Any `block.timestamp`/`blockhash`/`prevrandao` used as randomness for value?
- **Fix:** Chainlink VRF; never on-chain pseudo-randomness for payouts.
## 9. Denial of service (SWC-113/128)
- [ ] Unbounded loops over user-growable arrays? Push payments that can revert and brick the contract?
- [ ] Single external dependency whose revert blocks all users?
- **Fix:** pull payments; bounded iteration / pagination; isolate failures.
## 10. Token / standard pitfalls
- [ ] ERC-20: decimals assumptions, fee-on-transfer, rebasing, missing return.
- [ ] ERC-721/1155: safe-transfer reentrancy hooks; approval scope.
- [ ] Permit (EIP-2612): replay, deadline, signature malleability.
## 11. General hygiene
- [ ] `pragma` pinned (`pragma solidity 0.8.26;` not `^`)? Latest stable solc?
- [ ] Events emitted for every state-changing action (off-chain monitoring)?
- [ ] No leftover `console.log` / test backdoors / hardcoded addresses?
- [ ] Pausability + emergency withdraw for value-holding contracts?
- [ ] Invariants written for every conservation law (total supply, solvency, accounting)?
## Severity triage (impact × likelihood)
- **Critical/High** → direct loss/lock of funds, unauthorized mint/withdraw, broken access control. **Blocks deploy.**
- **Medium** → loss under specific conditions, griefing, precision drift.
- **Low/Info** → best-practice, gas, style.
Cross-check real-world findings on **Solodit** (https://solodit.xyz/) for the contract type
you're shipping (vault, AMM, staking, bridge, governance).
@@ -0,0 +1,319 @@
#!/usr/bin/env python3
"""Foundry Smart Contract Security Agent.
Pre-deployment audit orchestrator for a Foundry project. Runs static analysis
(Slither, Aderyn), optional symbolic execution (Mythril), Foundry tests/coverage,
and a key-leak scan, then aggregates everything into a single JSON report with a
PASS/FAIL deploy gate.
Design constraints (mirrors the upstream repo's style, hardened):
- subprocess always called with an ARGUMENT LIST, never shell=True
- no outbound network calls; every tool runs locally on local source
- every external tool guarded by timeout and graceful degradation if absent
- read-only with respect to the project (only writes the report file)
"""
import os
import re
import json
import shutil
import argparse
import logging
import subprocess
from collections import defaultdict
from datetime import datetime, timezone
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
SWC_REGISTRY = {
"SWC-101": "Integer Overflow and Underflow",
"SWC-104": "Unchecked Call Return Value",
"SWC-105": "Unprotected Ether Withdrawal",
"SWC-106": "Unprotected SELFDESTRUCT",
"SWC-107": "Reentrancy",
"SWC-110": "Assert Violation",
"SWC-112": "Delegatecall to Untrusted Callee",
"SWC-113": "DoS with Failed Call",
"SWC-114": "Transaction Order Dependence (front-running)",
"SWC-115": "Authorization through tx.origin",
"SWC-116": "Block values as a proxy for time",
"SWC-120": "Weak Sources of Randomness",
"SWC-128": "DoS with Block Gas Limit",
}
SEVERITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3, "informational": 4, "optimization": 5}
# Directories that are dependencies / build output, not the audited code.
SKIP_DIRS = {"lib", "out", "cache", "node_modules", ".git", "broadcast", "artifacts"}
# A raw 32-byte hex private key (with or without 0x). High-precision signal: a
# 64-hex literal in source is almost always a key. Broader secret detection
# (mnemonics, API tokens, generic secrets) is intentionally delegated to gitleaks
# (see references/secure-deployment-and-keys.md) rather than reinvented noisily here.
PRIVKEY_RE = re.compile(r"\b(0x)?[0-9a-fA-F]{64}\b")
def _which(tool):
return shutil.which(tool) is not None
def _run(cmd, timeout):
"""Run a command (list args), return (returncode, stdout, stderr). Never raises."""
try:
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return p.returncode, p.stdout, p.stderr
except subprocess.TimeoutExpired:
logger.warning("Timeout: %s", " ".join(cmd))
return 124, "", "timeout"
except FileNotFoundError:
return 127, "", "not found"
# --------------------------------------------------------------------------- #
# Static analysis
# --------------------------------------------------------------------------- #
def run_slither(project):
if not _which("slither"):
logger.warning("slither not installed - skipping static analysis")
return None
rc, out, err = _run(["slither", project, "--json", "-"], timeout=300)
# Slither exits non-zero when it finds issues; JSON still lands on stdout.
if not out:
logger.error("slither produced no JSON (%s)", err.strip()[:200])
return {}
try:
return json.loads(out)
except json.JSONDecodeError:
logger.error("slither JSON parse failed")
return {}
def analyze_slither(slither_output):
findings, by_severity, by_detector = [], defaultdict(int), defaultdict(int)
for det in (slither_output or {}).get("results", {}).get("detectors", []):
severity = det.get("impact", "informational").lower()
by_severity[severity] += 1
name = det.get("check", "unknown")
by_detector[name] += 1
loc = ""
elems = det.get("elements", [])
if elems:
sm = elems[0].get("source_mapping", {})
lines = sm.get("lines") or [0]
loc = f"{sm.get('filename_short', '')}:L{lines[0]}"
findings.append({
"source": "slither", "detector": name, "severity": severity,
"confidence": det.get("confidence", ""), "location": loc,
"description": (det.get("description", "") or "").strip()[:240],
})
return {
"total": len(findings),
"by_severity": dict(by_severity),
"top_detectors": dict(sorted(by_detector.items(), key=lambda x: -x[1])[:15]),
"findings": sorted(findings, key=lambda f: SEVERITY_RANK.get(f["severity"], 9)),
}
def run_aderyn(project):
if not _which("aderyn"):
logger.info("aderyn not installed - skipping (recommended: cargo install aderyn)")
return None
report = os.path.join(project, "aderyn-report.json")
rc, out, err = _run(["aderyn", project, "-o", report], timeout=300)
try:
with open(report) as fh:
data = json.load(fh)
return data
except (OSError, json.JSONDecodeError):
logger.error("aderyn report not readable")
return {}
def analyze_aderyn(aderyn_output):
findings, by_severity = [], defaultdict(int)
if not aderyn_output:
return {"total": 0, "by_severity": {}, "findings": []}
for sev_key, sev in (("high_issues", "high"), ("low_issues", "low")):
block = aderyn_output.get(sev_key, {}) or {}
for issue in block.get("issues", []) if isinstance(block, dict) else []:
by_severity[sev] += 1
inst = (issue.get("instances") or [{}])[0]
loc = f"{inst.get('contract_path', '')}:L{inst.get('line_no', 0)}"
findings.append({
"source": "aderyn", "detector": issue.get("title", ""), "severity": sev,
"location": loc, "description": (issue.get("description", "") or "").strip()[:240],
})
return {
"total": len(findings),
"by_severity": dict(by_severity),
"findings": sorted(findings, key=lambda f: SEVERITY_RANK.get(f["severity"], 9)),
}
# --------------------------------------------------------------------------- #
# Symbolic execution (optional)
# --------------------------------------------------------------------------- #
def run_mythril(target, timeout):
if not _which("myth"):
logger.info("mythril not installed - skipping symbolic execution")
return None
rc, out, err = _run(
["myth", "analyze", target, "--execution-timeout", str(timeout), "-o", "json"],
timeout=timeout + 60,
)
if not out:
return {}
try:
return json.loads(out)
except json.JSONDecodeError:
logger.error("mythril JSON parse failed")
return {}
def analyze_mythril(mythril_output):
findings, by_swc = [], defaultdict(int)
for issue in (mythril_output or {}).get("issues", []):
swc = f"SWC-{issue.get('swc-id')}" if issue.get("swc-id") else "unknown"
by_swc[swc] += 1
findings.append({
"source": "mythril", "swc_id": swc,
"swc_title": SWC_REGISTRY.get(swc, issue.get("title", "")),
"severity": issue.get("severity", "Medium").lower(),
"location": f"{issue.get('contract', '')}:L{issue.get('lineno', 0)}",
"description": (issue.get("description", "") or "").strip()[:240],
})
return {"total": len(findings), "by_swc": dict(by_swc), "findings": findings}
# --------------------------------------------------------------------------- #
# Foundry tests + coverage
# --------------------------------------------------------------------------- #
def run_forge_tests(project):
if not _which("forge"):
logger.warning("forge not installed - skipping tests")
return {"available": False}
rc, out, err = _run(["forge", "test"], timeout=900)
text = out + err
passed = sum(int(n) for n in re.findall(r"(\d+)\s+passed", text))
failed = sum(int(n) for n in re.findall(r"(\d+)\s+failed", text))
return {"available": True, "exit_code": rc, "passed": passed,
"failed": failed, "all_passed": rc == 0}
def run_forge_coverage(project):
if not _which("forge"):
return {"available": False}
rc, out, err = _run(["forge", "coverage", "--report", "summary"], timeout=1200)
m = re.search(r"^\|\s*Total\s*\|\s*([\d.]+)%", out, re.M)
lines_pct = float(m.group(1)) if m else None
return {"available": True, "lines_pct": lines_pct}
# --------------------------------------------------------------------------- #
# Key hygiene
# --------------------------------------------------------------------------- #
def scan_key_leaks(project):
"""Heuristic scan for plaintext private keys / mnemonics in source-controlled files."""
hits = []
for root, dirs, files in os.walk(project):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for f in files:
# Match source/config files plus all dotenv variants (.env, .env.local, .env.prod...)
if not (f.endswith((".sol", ".env", ".json", ".js", ".ts", ".toml", ".txt", ".md", ".sh", ".yaml", ".yml"))
or f.startswith(".env")):
continue
path = os.path.join(root, f)
try:
with open(path, encoding="utf-8", errors="ignore") as fh:
content = fh.read()
except OSError:
continue
for m in PRIVKEY_RE.finditer(content):
# Exclude the well-known Anvil/Hardhat test mnemonic-derived keys & all-zero.
val = m.group(0).lower().removeprefix("0x")
if val == "0" * 64 or "test test test" in content[max(0, m.start() - 80):m.start()]:
continue
hits.append({"file": os.path.relpath(path, project), "type": "possible_private_key"})
break
return {"leaked_secret_candidates": len(hits), "hits": hits[:20],
"note": "high-precision private-key scan only; run gitleaks for full secret coverage"}
# --------------------------------------------------------------------------- #
# Aggregation + gate
# --------------------------------------------------------------------------- #
def deduplicate(*finding_lists):
seen, combined = set(), []
for lst in finding_lists:
for f in lst:
key = (f.get("location", ""), f.get("detector", f.get("swc_id", "")))
if key not in seen:
seen.add(key)
combined.append(f)
return combined
def build_report(project, slither, aderyn, mythril, tests, coverage, keys, min_coverage):
combined = deduplicate(slither["findings"], aderyn["findings"], mythril["findings"])
crit_high = sum(1 for f in combined if f.get("severity") in ("critical", "high"))
gate_fail = []
if crit_high > 0:
gate_fail.append(f"{crit_high} high/critical static finding(s)")
if tests.get("available") and not tests.get("all_passed"):
gate_fail.append(f"{tests.get('failed', '?')} failing test(s)")
if keys["leaked_secret_candidates"] > 0:
gate_fail.append(f"{keys['leaked_secret_candidates']} possible leaked secret(s)")
cov = coverage.get("lines_pct")
if cov is not None and cov < min_coverage:
gate_fail.append(f"line coverage {cov}% < {min_coverage}% threshold")
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"project": os.path.abspath(project),
"static_analysis": {
"slither": {"total": slither["total"], "by_severity": slither["by_severity"],
"top_detectors": slither["top_detectors"]},
"aderyn": {"total": aderyn["total"], "by_severity": aderyn["by_severity"]},
},
"symbolic_execution": {"mythril": {"total": mythril["total"], "by_swc": mythril.get("by_swc", {})}},
"testing": {"forge_test": tests, "coverage": coverage},
"key_hygiene": keys,
"combined_findings": len(combined),
"critical_high_findings": crit_high,
"deploy_gate": "PASS" if not gate_fail else "FAIL",
"gate_failures": gate_fail,
"findings": combined[:40],
}
def main():
ap = argparse.ArgumentParser(description="Foundry Smart Contract Security Audit Agent")
ap.add_argument("--project", default=".", help="Path to the Foundry project root")
ap.add_argument("--mythril", metavar="FILE", help="Run Mythril symbolic execution on this .sol file")
ap.add_argument("--mythril-timeout", type=int, default=300)
ap.add_argument("--min-coverage", type=float, default=80.0, help="Min line coverage %% for PASS")
ap.add_argument("--output", default="audit-report.json")
args = ap.parse_args()
logger.info("Auditing Foundry project: %s", os.path.abspath(args.project))
slither = analyze_slither(run_slither(args.project))
aderyn = analyze_aderyn(run_aderyn(args.project))
mythril = analyze_mythril(run_mythril(args.mythril, args.mythril_timeout) if args.mythril else {})
tests = run_forge_tests(args.project)
coverage = run_forge_coverage(args.project)
keys = scan_key_leaks(args.project)
report = build_report(args.project, slither, aderyn, mythril, tests, coverage, keys, args.min_coverage)
with open(args.output, "w") as fh:
json.dump(report, fh, indent=2, default=str)
logger.info("Audit: %d findings (%d high/critical) | gate=%s",
report["combined_findings"], report["critical_high_findings"], report["deploy_gate"])
if report["gate_failures"]:
logger.warning("Gate failures: %s", "; ".join(report["gate_failures"]))
print(json.dumps(report, indent=2, default=str))
if __name__ == "__main__":
main()