From 25e0bc60e8f846f019246d859aef3dbcc20cc06d Mon Sep 17 00:00:00 2001 From: DevRedious Date: Tue, 16 Jun 2026 15:52:33 +0200 Subject: [PATCH] Add skill: auditing-foundry-smart-contract-security Pre-deployment security audit skill for Solidity contracts in Foundry projects. Complements analyzing-ethereum-smart-contract-vulnerabilities (which it is based on) with a dev-side, Foundry-first workflow and full key-hygiene coverage. Layers four independent techniques: - Static analysis: Slither (90+ detectors) + Aderyn (Cyfrin) - Symbolic execution: Mythril (optional) - Property-based testing: forge fuzz + invariant tests (handler pattern) - Manual review checklist + secrets/keystore audit Includes scripts/agent.py (orchestrator aggregating Slither/Aderyn/Mythril/forge test + coverage + private-key scan into a JSON report with a PASS/FAIL deploy gate) and three references (tool cheat-sheets, SWC vulnerability checklist, secure deployment & key hygiene with cast keystore / multisig). Passes tools/validate-skill.py. Slither, Aderyn, forge test/coverage parsing and the gate logic were verified end-to-end against a reentrancy-vulnerable contract. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../LICENSE | 201 +++++++++++ .../SKILL.md | 170 ++++++++++ .../references/api-reference.md | 181 ++++++++++ .../references/secure-deployment-and-keys.md | 90 +++++ .../references/vulnerability-checklist.md | 78 +++++ .../scripts/agent.py | 319 ++++++++++++++++++ 6 files changed, 1039 insertions(+) create mode 100644 skills/auditing-foundry-smart-contract-security/LICENSE create mode 100644 skills/auditing-foundry-smart-contract-security/SKILL.md create mode 100644 skills/auditing-foundry-smart-contract-security/references/api-reference.md create mode 100644 skills/auditing-foundry-smart-contract-security/references/secure-deployment-and-keys.md create mode 100644 skills/auditing-foundry-smart-contract-security/references/vulnerability-checklist.md create mode 100644 skills/auditing-foundry-smart-contract-security/scripts/agent.py diff --git a/skills/auditing-foundry-smart-contract-security/LICENSE b/skills/auditing-foundry-smart-contract-security/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/auditing-foundry-smart-contract-security/LICENSE @@ -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. diff --git a/skills/auditing-foundry-smart-contract-security/SKILL.md b/skills/auditing-foundry-smart-contract-security/SKILL.md new file mode 100644 index 00000000..3841d4bd --- /dev/null +++ b/skills/auditing-foundry-smart-contract-security/SKILL.md @@ -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 && solc-select use ` +- **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 --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. diff --git a/skills/auditing-foundry-smart-contract-security/references/api-reference.md b/skills/auditing-foundry-smart-contract-security/references/api-reference.md new file mode 100644 index 00000000..09b63d20 --- /dev/null +++ b/skills/auditing-foundry-smart-contract-security/references/api-reference.md @@ -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 # 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/ diff --git a/skills/auditing-foundry-smart-contract-security/references/secure-deployment-and-keys.md b/skills/auditing-foundry-smart-contract-security/references/secure-deployment-and-keys.md new file mode 100644 index 00000000..61bf7b02 --- /dev/null +++ b/skills/auditing-foundry-smart-contract-security/references/secure-deployment-and-keys.md @@ -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 +forge script script/Deploy.s.sol --account deployer --rpc-url --broadcast --verify + +# Mainnet (prefer a Ledger): +forge script script/Deploy.s.sol --ledger --hd-paths "m/44'/60'/0'/0/0" --rpc-url --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/ diff --git a/skills/auditing-foundry-smart-contract-security/references/vulnerability-checklist.md b/skills/auditing-foundry-smart-contract-security/references/vulnerability-checklist.md new file mode 100644 index 00000000..505b053b --- /dev/null +++ b/skills/auditing-foundry-smart-contract-security/references/vulnerability-checklist.md @@ -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). diff --git a/skills/auditing-foundry-smart-contract-security/scripts/agent.py b/skills/auditing-foundry-smart-contract-security/scripts/agent.py new file mode 100644 index 00000000..378f4666 --- /dev/null +++ b/skills/auditing-foundry-smart-contract-security/scripts/agent.py @@ -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()