How do I run slither effectively on a new codebase?
Basics: `slither .` (or `slither --compile-force-framework foundry` in a foundry repo). High-value detectors: `reentrancy-eth`, `reentrancy-no-eth`, `unchecked-transfer`, `tx-origin`, `uninitialized-state`, `arbitrary-send-eth`, `suicidal`, `controlled-delegatecall`. Useful extras: `slither . --print human-summary` (complexity + auth surface overview), `--print contract-summary`, `slither-check-upgradeability proxy.sol impl.sol` for proxy diffs, and `slither . --triage-mode` to persist dismissals. Honest limits: slither's reentrancy detector over-reports on `nonReentrant` functions (learn to triage fast); it can't see cross-protocol state (oracle staleness, LP pricing) or economic attacks (flash loans, rounding direction). Workflow: run it first for a cheap bug list, triage every finding to true/false positive in one pass, then spend human time on the economic layer slither can't model. Pin the version in CI — detector output churns between releases (as of early 2026, 0.10.x line).
What's the right foundry workflow for auditing a DeFi protocol?
Four layers. (1) Unit: `forge test -vvv` against the repo's suite — check what's NOT tested. (2) Fuzz: property-style tests with `function testFuzz_deposit(uint256 amount)` and `bound()` inputs; run deep with `forge test --fuzz-runs 100000` before reporting "tested." (3) Fork: `forge test --fork-url $MAINNET_RPC` against real USDT/oracles/pools — this is where token-quirk and integration bugs surface; use `vm.createSelectFork` and pin a block for reproducibility. (4) Invariant: `forge test --invariant-runs 256 --invariant-depth 30` with handlers. Practical flags: `--mt` to run one test, `forge coverage` to find untested branches, `forge snapshot` for gas regression, `cast call` for quick mainnet poking. Auditing tip: write your PoCs as foundry tests in the client's repo and attach them to findings — a runnable PoC that prints `stolen: 4123 ether` is worth more than two pages of prose. Also `forge inspect <Contract> storageLayout` for upgrade checks.
How do I write an invariant test for vault solvency?
Pattern: a handler contract wraps every state-changing entry point and the fuzzer calls them randomly: `function invariant_solvency() external { assertGe(token.balanceOf(address(vault)), vault.totalOwed()); }`. For an ERC-4626 vault, the core invariants: (1) `totalAssets() >=` sum claimable by all shares (track ghost variables in the handler: sum of shares minted per actor, sum withdrawn); (2) `convertToAssets(totalSupply()) <= totalAssets() + rounding tolerance`; (3) no actor can end a run with more assets than (deposited - withdrawn + donations it made). Setup details that matter: `targetContract(address(handler))` so fuzzing hits only your handlers; `targetSelector` to exclude or deliberately include admin functions; bound inputs with `bound(x, 1, 1e30)` to avoid trivial reverts; use ghost accounting (`ghost_totalDeposited`) because on-chain getters are what you're testing. Run deep: `--invariant-runs 512 --invariant-depth 50 --invariant-fail-on-revert false` — and when it breaks, `forge` prints the exact call sequence; shrink it into the finding PoC.
When should I use echidna or mythril instead of slither/foundry?
Echidna: property fuzzer for EVM; write `function echidna_never_insolvent() public returns (bool) { return token.balanceOf(address(vault)) >= vault.totalOwed(); }` and run `echidna . --contract EchidnaTest --config echidna.yaml`. It explores weirder sequences than hand-written handlers; weaker than foundry at developer ergonomics, better at long-horizon stateful search. Use it to attack invariants foundry invariant-testing can't express easily (cross-contract, many actors). Mythril: symbolic execution — `myth analyze contract.sol` finds reachable assertion failures, integer issues, and some access-control paths without writing tests; expect slow runs and false positives on anything non-trivial; good for small critical components (signature verifiers, math libs). Medusa (echidna successor in Go) is worth knowing as of early 2026. Honest division of labor: slither = static smoke screen (minutes), foundry = your PoC and regression engine, echidna/medusa = adversarial stateful search (hours), mythril/halmos = bounded formal-ish check on small targets. No tool finds oracle manipulation designs — that's reading.
How should I read an unfamiliar codebase in the first hours of an audit?
Trust boundaries first, code second. (1) Draw the actor map: EOAs, keepers, oracles, admins, other protocols — then mark every external function with who can call it and what value flows. Anything `onlyOwner` touching user funds is a centralization note; anything permissionless moving value is where exploits live. (2) Map the money paths: deposit → where do assets sit → who can move them → withdraw. Follow the balance, not the function names. (3) List integration assumptions: which tokens, which oracle, which chain — each assumption is a checklist item (decimals, hooks, staleness, blacklist). (4) Read admin/upgrade paths completely — small and disproportionately catastrophic. (5) Only then read core logic line-by-line, with slither output beside you. Keep a "questions for the devs" list — undocumented invariants ("totalAssets never decreases except on loss events") are gold; if the devs can't state their invariants, that itself is a finding about spec quality.
How do I write a finding that a client will actually fix?
Structure: title (imperative + impact, e.g. "Missing staleness check on Chainlink feed allows liquidations at outdated prices"), severity with justification, affected code (file:line), description, impact in money terms, runnable PoC, recommendation with actual code. Severity calibration: Critical = direct loss of user funds exploitable now; High = loss under plausible conditions; Medium = loss requiring specific states/timing, or griefing; Low = best practices, gas, minor deviations. The two things juniors skip: (1) quantified impact — "attacker steals up to X% of TVL, ~$N at current balances" beats "funds at risk"; (2) a fix the devs can paste — show the corrected require statement, don't say "add validation." State assumptions ("assumes keeper latency ≤ 1 block") so the client can accept risk explicitly instead of arguing severity. If your fix redesigns the protocol, also offer the minimal mitigation.
What separates a $5k audit report from a $50k one?
The $5k report is slither output with prose around it: known-pattern findings, no PoCs, severity inflation to look busy. The $50k report contains things tools can't produce: (1) a bespoke economic attack with numbers — a flash-loan path through the protocol's own math, simulated on a mainnet fork with a profit figure; (2) invariant violations the devs didn't know were invariants ("your health factor can go negative mid-liquidation because..."); (3) integration risk analysis across real deployments — actual oracle heartbeats, actual pool liquidity vs position size, actual multisig signers; (4) honest triage — false positives dismissed explicitly, so real findings aren't diluted; (5) fix review — a second pass verifying patches didn't introduce new bugs. Buyers pay for adversarial simulation and judgment, not checklists. If every finding could have come from a linter, the client notices. One deep, correct high-severity finding with a working fork PoC beats twenty lint-level mediums.
Which common 'findings' are actually false positives I shouldn't report?
Report-silencers that mark you as junior: (1) "Floating pragma" on a repo pinned by the build config — informational at best. (2) "`block.timestamp` can be manipulated by miners" on a 30-day timelock. (3) Slither's `reentrancy-eth` on a function already guarded by `nonReentrant` with correct CEI — triage it, don't paste it. (4) "Missing zero-address check" on a setter where zero is harmless or intended. (5) "Centralization: owner can pause" — pausing is the mitigation; report only when pause/upgrade powers can take user FUNDS. (6) Gas optimizations presented as vulnerabilities. (7) "No events emitted" on trivial setters. (8) Solidity version not latest — only matters if a specific compiler bug applies. (9) "Unchecked return value" where failure is impossible or irrelevant. Rule of thumb: every finding needs an attack path to real loss or a spec violation articulable in one sentence with numbers; otherwise it belongs in the informational appendix.