@zigrivers/scaffold 3.26.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +21 -7
  2. package/content/knowledge/core/ai-memory-management.md +17 -0
  3. package/content/knowledge/core/claude-md-patterns.md +2 -2
  4. package/content/knowledge/core/coding-conventions.md +2 -2
  5. package/content/knowledge/core/task-decomposition.md +4 -4
  6. package/content/knowledge/core/task-tracking.md +120 -29
  7. package/content/knowledge/core/user-stories.md +1 -1
  8. package/content/knowledge/execution/multi-agent-coordination.md +118 -0
  9. package/content/knowledge/execution/task-claiming-strategy.md +15 -3
  10. package/content/knowledge/execution/worktree-management.md +5 -3
  11. package/content/knowledge/web3/web3-access-control.md +189 -0
  12. package/content/knowledge/web3/web3-architecture.md +162 -0
  13. package/content/knowledge/web3/web3-audit-workflow.md +151 -0
  14. package/content/knowledge/web3/web3-common-vulnerabilities.md +171 -0
  15. package/content/knowledge/web3/web3-conventions.md +162 -0
  16. package/content/knowledge/web3/web3-deployment-and-verification.md +216 -0
  17. package/content/knowledge/web3/web3-dev-environment.md +150 -0
  18. package/content/knowledge/web3/web3-gas-optimization.md +165 -0
  19. package/content/knowledge/web3/web3-oracles-and-external-data.md +155 -0
  20. package/content/knowledge/web3/web3-project-structure.md +212 -0
  21. package/content/knowledge/web3/web3-requirements.md +152 -0
  22. package/content/knowledge/web3/web3-security.md +163 -0
  23. package/content/knowledge/web3/web3-testing.md +180 -0
  24. package/content/knowledge/web3/web3-upgradeability.md +189 -0
  25. package/content/methodology/web3-overlay.yml +40 -0
  26. package/content/pipeline/build/multi-agent-resume.md +27 -7
  27. package/content/pipeline/build/multi-agent-start.md +35 -7
  28. package/content/pipeline/build/new-enhancement.md +8 -1
  29. package/content/pipeline/build/quick-task.md +9 -0
  30. package/content/pipeline/build/single-agent-resume.md +11 -4
  31. package/content/pipeline/build/single-agent-start.md +13 -4
  32. package/content/pipeline/consolidation/workflow-audit.md +1 -1
  33. package/content/pipeline/environment/git-workflow.md +2 -2
  34. package/content/pipeline/foundation/beads.md +148 -22
  35. package/content/pipeline/foundation/coding-standards.md +1 -1
  36. package/content/tools/post-implementation-review.md +6 -6
  37. package/content/tools/prompt-pipeline.md +1 -1
  38. package/content/tools/release.md +5 -5
  39. package/content/tools/review-code.md +347 -3
  40. package/content/tools/review-pr.md +349 -7
  41. package/content/tools/version-bump.md +5 -5
  42. package/dist/cli/commands/observe.d.ts +2 -0
  43. package/dist/cli/commands/observe.d.ts.map +1 -1
  44. package/dist/cli/commands/observe.js +9 -1
  45. package/dist/cli/commands/observe.js.map +1 -1
  46. package/dist/cli/commands/observe.test.js +36 -0
  47. package/dist/cli/commands/observe.test.js.map +1 -1
  48. package/dist/config/schema.d.ts +672 -126
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/config/schema.js +8 -1
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/config/schema.test.js +2 -2
  53. package/dist/config/schema.test.js.map +1 -1
  54. package/dist/config/validators/index.d.ts.map +1 -1
  55. package/dist/config/validators/index.js +2 -0
  56. package/dist/config/validators/index.js.map +1 -1
  57. package/dist/config/validators/web3.d.ts +4 -0
  58. package/dist/config/validators/web3.d.ts.map +1 -0
  59. package/dist/config/validators/web3.js +15 -0
  60. package/dist/config/validators/web3.js.map +1 -0
  61. package/dist/e2e/project-type-overlays.test.js +76 -0
  62. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  63. package/dist/observability/adapters/beads.d.ts +4 -0
  64. package/dist/observability/adapters/beads.d.ts.map +1 -1
  65. package/dist/observability/adapters/beads.js +25 -2
  66. package/dist/observability/adapters/beads.js.map +1 -1
  67. package/dist/observability/adapters/beads.test.js +40 -2
  68. package/dist/observability/adapters/beads.test.js.map +1 -1
  69. package/dist/observability/engine/ledger-writer.d.ts +11 -1
  70. package/dist/observability/engine/ledger-writer.d.ts.map +1 -1
  71. package/dist/observability/engine/ledger-writer.js +6 -0
  72. package/dist/observability/engine/ledger-writer.js.map +1 -1
  73. package/dist/observability/engine/llm-dispatcher.d.ts.map +1 -1
  74. package/dist/observability/engine/llm-dispatcher.js +36 -5
  75. package/dist/observability/engine/llm-dispatcher.js.map +1 -1
  76. package/dist/observability/engine/llm-dispatcher.test.js +23 -0
  77. package/dist/observability/engine/llm-dispatcher.test.js.map +1 -1
  78. package/dist/project/adopt.d.ts.map +1 -1
  79. package/dist/project/adopt.js +3 -1
  80. package/dist/project/adopt.js.map +1 -1
  81. package/dist/project/detectors/coverage.test.js +3 -2
  82. package/dist/project/detectors/coverage.test.js.map +1 -1
  83. package/dist/project/detectors/disambiguate.js +1 -1
  84. package/dist/project/detectors/disambiguate.js.map +1 -1
  85. package/dist/project/detectors/index.d.ts.map +1 -1
  86. package/dist/project/detectors/index.js +2 -0
  87. package/dist/project/detectors/index.js.map +1 -1
  88. package/dist/project/detectors/resolve-detection.test.js +57 -0
  89. package/dist/project/detectors/resolve-detection.test.js.map +1 -1
  90. package/dist/project/detectors/types.d.ts +6 -2
  91. package/dist/project/detectors/types.d.ts.map +1 -1
  92. package/dist/project/detectors/types.js.map +1 -1
  93. package/dist/project/detectors/web3.d.ts +4 -0
  94. package/dist/project/detectors/web3.d.ts.map +1 -0
  95. package/dist/project/detectors/web3.js +37 -0
  96. package/dist/project/detectors/web3.js.map +1 -0
  97. package/dist/project/detectors/web3.test.d.ts +2 -0
  98. package/dist/project/detectors/web3.test.d.ts.map +1 -0
  99. package/dist/project/detectors/web3.test.js +75 -0
  100. package/dist/project/detectors/web3.test.js.map +1 -0
  101. package/dist/types/config.d.ts +8 -1
  102. package/dist/types/config.d.ts.map +1 -1
  103. package/dist/wizard/copy/core.d.ts.map +1 -1
  104. package/dist/wizard/copy/core.js +4 -0
  105. package/dist/wizard/copy/core.js.map +1 -1
  106. package/dist/wizard/copy/index.d.ts.map +1 -1
  107. package/dist/wizard/copy/index.js +2 -0
  108. package/dist/wizard/copy/index.js.map +1 -1
  109. package/dist/wizard/copy/types.d.ts +5 -1
  110. package/dist/wizard/copy/types.d.ts.map +1 -1
  111. package/dist/wizard/copy/types.test-d.js +7 -0
  112. package/dist/wizard/copy/types.test-d.js.map +1 -1
  113. package/dist/wizard/copy/web3.d.ts +3 -0
  114. package/dist/wizard/copy/web3.d.ts.map +1 -0
  115. package/dist/wizard/copy/web3.js +15 -0
  116. package/dist/wizard/copy/web3.js.map +1 -0
  117. package/dist/wizard/questions.d.ts +2 -1
  118. package/dist/wizard/questions.d.ts.map +1 -1
  119. package/dist/wizard/questions.js +8 -1
  120. package/dist/wizard/questions.js.map +1 -1
  121. package/dist/wizard/questions.test.js +14 -0
  122. package/dist/wizard/questions.test.js.map +1 -1
  123. package/dist/wizard/wizard.d.ts.map +1 -1
  124. package/dist/wizard/wizard.js +1 -0
  125. package/dist/wizard/wizard.js.map +1 -1
  126. package/package.json +1 -1
@@ -0,0 +1,212 @@
1
+ ---
2
+ name: web3-project-structure
3
+ description: Opinionated Foundry project layout for smart-contract teams — src, test, script, lib, broadcast, foundry.toml, remappings — covering test naming, deploy provenance, and what belongs in git
4
+ topics: [web3, project-structure, foundry, solidity]
5
+ ---
6
+
7
+ A smart-contract repository is read by more adversarial eyes than almost any other kind of codebase: auditors, MEV searchers, frontrunners, and the occasional regulator. Structure carries weight that goes beyond developer ergonomics. An auditor opening the repo for the first time should be able to find the contract under review, its tests, its deploy script, and its on-chain deployment receipts within thirty seconds. Gas snapshots and fuzz seeds need a fixed home so regressions are diffable. Broadcast logs are the audit trail tying a verified contract address back to a commit SHA — losing or muddling them turns "which version is mainnet running?" into a forensic exercise. Foundry's conventions answer most of these questions; this doc records the opinionated version a team should adopt before the first PR lands.
8
+
9
+ ## Summary
10
+
11
+ A Foundry project has six top-level directories, each answering one question: `src/` (contracts under audit), `test/` (Foundry tests mirroring `src/`), `script/` (deploy and management scripts inheriting `Script`), `lib/` (forge-installed dependencies, committed as submodules or git-trees), `broadcast/` (deploy logs keyed by chain ID and script name), and `docs/` (NatSpec-generated or hand-written architecture notes). `foundry.toml` at the root configures the compiler, fuzz/invariant runners, and formatter; `remappings.txt` aliases `lib/` paths so imports stay readable. The `.gitignore` excludes `cache/`, `out/`, and broadcast directories for ephemeral local chains (anvil, chain ID 31337), but **keeps** broadcast artifacts for canonical chain IDs (1, 10, 8453, 42161, ...) because they are the deploy provenance. Tests follow strict naming — `test_`, `testFuzz_`, `invariant_` — so the runner can pick them up and reviewers can read intent off the function name.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Top-level layout
16
+
17
+ ```
18
+ project-root/
19
+ ├── src/ # Contracts under audit — the deliverable
20
+ │ ├── interfaces/ # I*.sol interfaces, importable by consumers
21
+ │ ├── libraries/ # Pure libraries (no state)
22
+ │ └── tokens/ # Domain-grouped subdirectories
23
+ ├── test/ # Foundry tests — mirrors src/
24
+ │ ├── unit/ # Per-contract unit tests
25
+ │ ├── integration/ # Multi-contract / forked-mainnet tests
26
+ │ ├── invariant/ # Invariant suites + Handler contracts
27
+ │ └── utils/ # Shared test helpers, mocks, base contracts
28
+ ├── script/ # Deploy + management scripts (forge script)
29
+ │ ├── Deploy.s.sol
30
+ │ └── Upgrade.s.sol
31
+ ├── lib/ # forge install dependencies (submodules)
32
+ │ ├── forge-std/
33
+ │ ├── openzeppelin-contracts/
34
+ │ └── solmate/
35
+ ├── broadcast/ # Deploy artifacts keyed by script + chain ID
36
+ │ └── Deploy.s.sol/
37
+ │ ├── 1/ # Mainnet — KEEP, this is provenance
38
+ │ ├── 10/ # Optimism — KEEP
39
+ │ └── 31337/ # Anvil — gitignored
40
+ ├── docs/ # NatSpec output or hand-written architecture
41
+ ├── foundry.toml # Project config
42
+ ├── remappings.txt # Import aliases for lib/
43
+ ├── .gitignore
44
+ └── README.md
45
+ ```
46
+
47
+ One-liners:
48
+ - `src/` — every contract that ships; group by domain, never by Solidity language feature
49
+ - `test/` — mirrors `src/` so reviewers can find the test for any contract in one jump
50
+ - `script/` — every deploy, upgrade, and ops script (parameter tuning, role grants); each inherits `forge-std/Script.sol`
51
+ - `lib/` — third-party Solidity tracked via `forge install` (git submodules under the hood)
52
+ - `broadcast/` — receipts from `forge script --broadcast`; partition by chain ID; **keep** canonical chains, gitignore ephemeral ones
53
+ - `docs/` — `forge doc` output or hand-written `architecture.md`, `threat-model.md`, `invariants.md`
54
+
55
+ ### `foundry.toml`
56
+
57
+ `foundry.toml` is the project config — compiler version, optimizer settings, fuzz/invariant runner knobs, formatter rules, and per-profile overrides. A minimal but production-shaped config:
58
+
59
+ ```toml
60
+ [profile.default]
61
+ src = "src"
62
+ out = "out"
63
+ libs = ["lib"]
64
+ test = "test"
65
+ script = "script"
66
+ solc_version = "0.8.26"
67
+ evm_version = "cancun"
68
+ optimizer = true
69
+ optimizer_runs = 200
70
+ via_ir = false
71
+ bytecode_hash = "none" # Reproducible builds (no metadata hash)
72
+ cbor_metadata = false
73
+ gas_reports = ["*"]
74
+
75
+ [profile.ci]
76
+ fuzz = { runs = 10_000 }
77
+ invariant = { runs = 256, depth = 128 }
78
+ verbosity = 3
79
+
80
+ [fmt]
81
+ line_length = 120
82
+ tab_width = 4
83
+ bracket_spacing = true
84
+ int_types = "long" # uint256 not uint, by name
85
+ quote_style = "double"
86
+ number_underscore = "thousands" # 1_000_000
87
+
88
+ [fuzz]
89
+ runs = 256 # Local default; CI overrides via [profile.ci]
90
+ max_test_rejects = 65_536
91
+
92
+ [invariant]
93
+ runs = 64
94
+ depth = 32
95
+ fail_on_revert = false
96
+ ```
97
+
98
+ `optimizer_runs` is the only setting non-obvious enough to justify thinking. The number is **not** "how many times to run the optimizer" — it's the expected number of times each opcode will run over the contract's lifetime. Higher values trade deploy-time bytecode size for cheaper runtime gas. Defaults:
99
+ - `200` (Solidity default) — balanced; right for most contracts including infrequently-called governance
100
+ - `1_000_000` — protocol contracts with hot paths (AMMs, lending pools, perp engines); pay extra deploy cost once, save gas on every swap forever
101
+ - `1` — one-shot contracts (deploy scripts, factories that deploy once and self-destruct conceptually); minimize deploy cost
102
+
103
+ The `ci` profile overrides fuzz runs to 10k for thorough coverage on PRs; locally 256 keeps `forge test` snappy.
104
+
105
+ ### Test naming
106
+
107
+ Foundry's runner discovers tests by function-name prefix. Three prefixes matter, and the convention is load-bearing — auditors read intent off the name:
108
+
109
+ - `test_<unit>_<behavior>` — concrete unit tests; e.g. `test_transfer_revertsWhenInsufficientBalance`
110
+ - `testFuzz_<unit>_<property>` — property tests with fuzzed inputs; e.g. `testFuzz_deposit_creditsExactAmount(uint256 amount)`
111
+ - `invariant_<property>` — invariant tests run by the invariant engine over a stateful handler; e.g. `invariant_totalSupplyEqualsSumOfBalances`
112
+
113
+ File mirror: a contract `src/Vault.sol` gets its tests at `test/unit/Vault.t.sol` (the `.t.sol` suffix is convention, not required, but make it consistent across the repo). Invariant suites live in `test/invariant/Vault.invariants.t.sol` with their `Handler` contract alongside.
114
+
115
+ ```solidity
116
+ // test/unit/Vault.t.sol
117
+ contract VaultTest is Test {
118
+ Vault vault;
119
+
120
+ function setUp() public {
121
+ vault = new Vault();
122
+ }
123
+
124
+ function test_deposit_creditsBalance() public {
125
+ vault.deposit{value: 1 ether}();
126
+ assertEq(vault.balanceOf(address(this)), 1 ether);
127
+ }
128
+
129
+ function testFuzz_deposit_creditsExactAmount(uint96 amount) public {
130
+ vm.deal(address(this), amount);
131
+ vault.deposit{value: amount}();
132
+ assertEq(vault.balanceOf(address(this)), amount);
133
+ }
134
+ }
135
+ ```
136
+
137
+ Use `uint96` (or another bounded type) for fuzz parameters tied to ETH amounts — full `uint256` blows the available balance and triggers `max_test_rejects` exhaustion.
138
+
139
+ ### Deploy scripts and `broadcast/`
140
+
141
+ Deploy scripts inherit `forge-std/Script.sol` and read environment-specific addresses via `vm.envAddress`. Hard-coded addresses in script bodies are a category of bug — they survive a `git mv` from staging to mainnet and you find out at $4 gwei.
142
+
143
+ ```solidity
144
+ // script/Deploy.s.sol
145
+ import {Script} from "forge-std/Script.sol";
146
+ import {Vault} from "../src/Vault.sol";
147
+
148
+ contract Deploy is Script {
149
+ function run() external returns (Vault vault) {
150
+ address admin = vm.envAddress("ADMIN_ADDRESS");
151
+ uint256 pk = vm.envUint("DEPLOYER_PK");
152
+
153
+ vm.startBroadcast(pk);
154
+ vault = new Vault(admin);
155
+ vm.stopBroadcast();
156
+ }
157
+ }
158
+ ```
159
+
160
+ Running `forge script script/Deploy.s.sol --rpc-url $RPC --broadcast --verify` writes a receipt to `broadcast/Deploy.s.sol/<chainId>/run-latest.json` and a timestamped copy. That JSON contains the deployed address, transaction hash, block number, constructor args, and a SHA tying it back to the commit. **This is the provenance artifact.** Keep it for canonical chains; without it, "which commit deployed the mainnet Vault at 0xabc..." becomes archaeology. Verify on Etherscan in the same command (`--verify`) so the audit trail extends to the public block explorer.
161
+
162
+ ### `lib/` and remappings
163
+
164
+ Solidity dependencies are installed via `forge install`, which adds the upstream repo as a git submodule under `lib/`:
165
+
166
+ ```bash
167
+ forge install OpenZeppelin/openzeppelin-contracts@v5.0.0
168
+ forge install transmissions11/solmate
169
+ forge install foundry-rs/forge-std
170
+ ```
171
+
172
+ Pin to a tag (`@v5.0.0`), never to `main`. Submodule SHAs are recorded in `.gitmodules` and the commit, so a fresh `forge install` after clone produces the same dependency set.
173
+
174
+ Imports through `lib/openzeppelin-contracts/contracts/...` are ugly. `remappings.txt` aliases them:
175
+
176
+ ```
177
+ @openzeppelin/=lib/openzeppelin-contracts/
178
+ @openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/
179
+ solmate/=lib/solmate/src/
180
+ forge-std/=lib/forge-std/src/
181
+ ```
182
+
183
+ Then `import "@openzeppelin/contracts/token/ERC20/ERC20.sol";` resolves cleanly. Keep `remappings.txt` checked in; some IDE plugins and `forge` itself read it.
184
+
185
+ ### `.gitignore`
186
+
187
+ ```gitignore
188
+ # Build artifacts
189
+ cache/
190
+ out/
191
+
192
+ # Coverage
193
+ lcov.info
194
+
195
+ # Node (Hardhat carryovers, JS tooling)
196
+ node_modules/
197
+
198
+ # Environment / secrets
199
+ .env
200
+ .env.*
201
+ !.env.example
202
+
203
+ # Anvil / ephemeral local chains — chain ID 31337
204
+ broadcast/*/31337/
205
+ broadcast/**/dry-run/
206
+
207
+ # Editor
208
+ .vscode/
209
+ .idea/
210
+ ```
211
+
212
+ The load-bearing line is `broadcast/*/31337/`. Anvil's chain ID is 31337 and every local script run leaves a receipt; those are noise. Mainnet (`1`), Optimism (`10`), Base (`8453`), Arbitrum (`42161`), and other canonical chain IDs are **not** in the ignore list because their broadcast artifacts are the on-chain deploy provenance. Treat them with the same care as the contracts themselves: commit, review, and tag at release.
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: web3-requirements
3
+ description: Problem framing, invariants, threat model, trust assumptions, and success metrics for shipping smart contracts and protocols to EVM chains
4
+ topics: [web3, requirements, invariants, threat-model, security]
5
+ ---
6
+
7
+ A smart contract shipped to an EVM chain without a written invariant set, a threat model, and an explicit list of trust assumptions is a guessing game with adversarial counterparties and irreversible state. This document defines the acceptance spec for a contract or protocol going to Ethereum mainnet, an L2 (Optimism, Arbitrum, Base), or a compatible sidechain. The audience is a senior Solidity engineer or protocol architect who can ship code but has not yet hardened it against funded adversaries. The goal is to force, in writing, the questions an auditor will ask on day one.
8
+
9
+ ## Summary
10
+
11
+ A web3 requirements doc states what the contract does for which users (problem framing), enumerates the invariants that must hold across every reachable state, names the threat model in terms of capabilities and time horizon, lists each trust assumption as a documented failure mode, and defines success in concrete economic, gas, or capability terms. State invariants up front and write them as Foundry invariant tests before you finish the implementation. If you cannot enumerate your trust assumptions, you have not designed a protocol — you have written code that happens to compile.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Problem framing
16
+
17
+ The framing answers one question: what does this contract do for which users so that what becomes possible. Write it before you open `forge init`. The template below forces concrete nouns and verbs; if you find yourself writing "leverage" or "unlock value" you are not ready.
18
+
19
+ ```
20
+ This contract does <user-visible action> for <user class> so that <decision/outcome>.
21
+ ```
22
+
23
+ Worked example — a yield vault: "This contract lets monthly retail depositors deposit ETH and earn variable yield routed through Aave v3, so that they can hold a yield-bearing position without managing positions themselves." That sentence implies the whole surface area: deposit, withdraw, accounting for accrued yield, an external adapter to Aave, an admin for adapter rotation. Everything else is implementation.
24
+
25
+ Drop the framing into a `docs/spec.md` block at the top of the repo. Keep out-of-scope explicit — out-of-scope is where audits find missing checks.
26
+
27
+ ```markdown
28
+ # docs/spec.md
29
+ user_action: deposit ETH; withdraw ETH + accrued yield
30
+ user_class: retail depositors holding their own keys (EOAs and Safe multisigs)
31
+ outcome: passive yield routed through Aave v3 on Optimism
32
+ out_of_scope:
33
+ - non-ETH assets
34
+ - leveraged positions
35
+ - cross-chain bridging (vault is single-chain only)
36
+ - permissioned access (anyone with ETH can deposit)
37
+ ```
38
+
39
+ ### Invariants
40
+
41
+ Invariants are properties that must hold across every reachable state and every call sequence. They are not unit-test assertions about one transaction — they are global truths the protocol exposes. Write them in English first, then in a Foundry invariant test before you finish the implementation. If an invariant is hard to state in one sentence, decompose it; if it cannot be tested with a fuzzer, it is not really an invariant.
42
+
43
+ Opinionated defaults for a vault-style contract:
44
+
45
+ - **Conservation**: `totalAssets()` is greater than or equal to the sum of user-owed principal. The vault never owes more than it can pay.
46
+ - **Solvency**: at any block, a full sequential withdrawal of every depositor at their current share would not revert for accounting reasons.
47
+ - **Monotonicity of share price**: in the absence of admin-triggered loss recognition, `convertToAssets(1e18)` is non-decreasing. A surprise drop indicates a bug or an external loss the protocol failed to flag.
48
+ - **Access control**: only `admin` can call functions tagged `onlyAdmin`. Stated as `forall caller != admin, call to onlyAdmin function reverts`.
49
+ - **No reentrancy reachable state**: no external call to user-controlled code happens before state writes are finalized in any deposit/withdraw path.
50
+
51
+ Encode them as Foundry invariant tests. The handler is the load-bearing part — a weak handler proves nothing.
52
+
53
+ ```solidity
54
+ // test/invariant/VaultInvariants.t.sol
55
+ pragma solidity ^0.8.24;
56
+
57
+ import {Test} from "forge-std/Test.sol";
58
+ import {Vault} from "src/Vault.sol";
59
+ import {VaultHandler} from "./VaultHandler.sol";
60
+
61
+ contract VaultInvariants is Test {
62
+ Vault internal vault;
63
+ VaultHandler internal handler;
64
+
65
+ function setUp() public {
66
+ vault = new Vault();
67
+ handler = new VaultHandler(vault);
68
+ targetContract(address(handler));
69
+ }
70
+
71
+ /// Conservation: assets under management cover all outstanding shares.
72
+ function invariant_solvency() public view {
73
+ uint256 owed = vault.convertToAssets(vault.totalSupply());
74
+ assertGe(vault.totalAssets(), owed, "vault is insolvent");
75
+ }
76
+
77
+ /// Share price is monotonic in the absence of admin loss recognition.
78
+ function invariant_sharePriceMonotonic() public view {
79
+ assertGe(vault.convertToAssets(1e18), handler.lastSharePrice());
80
+ }
81
+ }
82
+ ```
83
+
84
+ Run with `forge test --match-contract VaultInvariants --fuzz-runs 50000`. A passing invariant suite at 50k runs is a precondition for audit submission — not a substitute for it.
85
+
86
+ ### Threat model
87
+
88
+ A threat model names who attacks, with what capabilities, on what time horizon, and — critically — who you do not defend against. Skipping the last part is how teams ship contracts that are "secure" against an unspecified adversary and break against a real one.
89
+
90
+ Rubric, in order of how often each is skipped:
91
+
92
+ - **Capabilities**: what can the attacker do on-chain. Concretely: deploy arbitrary contracts, hold arbitrary token balances, call any public function in any order, observe and reorder mempool transactions (front-running), pay arbitrary gas, hold flash-loan-scale capital (think hundreds of millions of dollars for a single transaction on mainnet), submit governance proposals if your protocol has on-chain governance, manipulate spot prices on thin DEX pools.
93
+ - **Time horizon**: are we defending across one block (atomic flash-loan attack), one transaction batch (sandwich), a governance timelock window (typically 24-72 hours), or months (slow-roll oracle drift). Different defenses apply at each horizon.
94
+ - **Out of scope**: name them. Common honest answers: nation-state actors capable of reorgs on L1, the chain itself going down or censoring, supply-chain compromise of a dependency outside the audit boundary, social-engineering of the multisig signers, an exploit in the underlying L2 sequencer.
95
+
96
+ Concrete attack scenarios to write down before audit:
97
+
98
+ - **Oracle manipulation**: an adversary moves the price reported by a feed (spot DEX TWAP on a thin pool, a stale Chainlink feed during a fast move) and exploits a function that trusts it. Defense: use a Chainlink feed with a heartbeat check and a deviation circuit-breaker, never a single-block DEX spot price.
99
+ - **Governance proposal abuse**: an adversary accumulates governance tokens (or buys voting power via a flash loan against a vulnerable token design) and pushes a malicious proposal — for example, upgrading the vault implementation to one that drains funds. Defense: timelock all upgrade and parameter changes; size the timelock to the off-chain alerting and response window.
100
+ - **Reentrancy via callbacks**: any function that makes an external call into user-controlled code before finalizing state is vulnerable. Defense: checks-effects-interactions, plus `nonReentrant` on every function that touches accounting. Treat ERC-777, ERC-1363, and any token with transfer hooks as user-controlled code for this purpose.
101
+ - **Front-running and MEV**: on a public mempool, any profitable action you broadcast is visible. Defense: commit-reveal for sensitive ordering, private mempools (Flashbots Protect) for admin actions, or making the action order-independent. Sandwich-resistance for swaps means a strict slippage cap on every quote, sourced from the caller and not from an oracle the attacker can move.
102
+ - **Donation / inflation attack**: an attacker deposits a tiny first share, then transfers a large amount directly to the vault address to inflate share price and round the next depositor's shares to zero. Defense: virtual shares / dead shares in the ERC-4626 implementation, or an initial deposit by the deployer that is permanently locked.
103
+
104
+ ### Trust assumptions
105
+
106
+ Every contract trusts something. The job here is to make each trust explicit, paired with the failure mode if the trust is misplaced. The list below is the minimum for a DeFi vault — extend it for your specific design.
107
+
108
+ - **Price oracle (Chainlink ETH/USD on the target chain)**: trusted to be correct within the configured deviation and heartbeat. If it fails — feed goes stale during a fast move, or the off-chain operators collude — the vault can mis-price deposits and withdrawals. Mitigation: pause-on-stale check (`updatedAt` is within the heartbeat window) and an emergency pause that the multisig can trigger.
109
+ - **Upgrade admin (3-of-5 Safe multisig)**: trusted to act in users' interest and to keep keys secure. If three signers are compromised, they can upgrade the vault to drain funds. Mitigation: 48-hour timelock on every upgrade; off-chain monitoring with on-call alerting; a separate guardian role with veto power on suspicious upgrades but no positive authority.
110
+ - **Underlying protocol (Aave v3)**: trusted to honor its own accounting and not to grief depositors. If Aave's pool is compromised, the vault inherits the loss. Mitigation: cap exposure per adapter; document the inherited risk in the user-facing README.
111
+ - **Solidity compiler and toolchain**: trusted to compile source to faithful bytecode. Mitigation: pin a specific `solc` version (e.g. `0.8.24`), reproducible builds via Foundry's `forge build --deterministic`, and verified source on Etherscan.
112
+ - **The chain itself**: trusted not to reorg meaningfully or to censor the contract. For L2s, also trusted to honor its withdrawal-window guarantees. Document the chain-specific risk (e.g. "this protocol is deployed on Base; a Base sequencer outage halts deposits and withdrawals until it recovers").
113
+
114
+ Each line is a documented risk. Surface them in the user-facing docs — not just in the audit report — so depositors can size their exposure.
115
+
116
+ ### Success metrics
117
+
118
+ State success in concrete numbers, written before launch. Vague goals ("be secure", "have lots of users") let the team declare victory after any outcome. Three categories to name:
119
+
120
+ - **Economic security**: a $-TVL bar paired with a no-exploit time horizon. Example: `$50M TVL with no successful exploit in the first 6 months post-launch`. The dollar figure forces the team to size the bug bounty (a useful default: 10% of TVL up to a cap), and the time horizon forces a monitoring commitment.
121
+ - **Gas budget**: per-function ceilings on the target chain, measured by `forge snapshot`. Example: `deposit costs less than 200k gas on Optimism; withdraw less than 250k`. Encode these as snapshot tests in CI so a regression breaks the build, not just makes things slightly more expensive.
122
+ - **Capability counts**: scale targets that drive design. Example: `supports 10,000 unique depositors without unbounded loops`, or `supports adapter rotation without migrating user balances`. Each capability target rules out a class of naive implementations (no `address[] depositors` you iterate over).
123
+
124
+ ```solidity
125
+ // test/gas/Snapshot.t.sol
126
+ pragma solidity ^0.8.24;
127
+ import {Test} from "forge-std/Test.sol";
128
+ import {Vault} from "src/Vault.sol";
129
+
130
+ contract GasSnapshot is Test {
131
+ Vault internal vault;
132
+ address internal user = address(0xBEEF);
133
+
134
+ function setUp() public { vault = new Vault(); vm.deal(user, 10 ether); }
135
+
136
+ function test_gas_deposit() public {
137
+ vm.prank(user);
138
+ uint256 g = gasleft();
139
+ vault.deposit{value: 1 ether}();
140
+ uint256 used = g - gasleft();
141
+ assertLt(used, 200_000, "deposit exceeds gas budget");
142
+ }
143
+ }
144
+ ```
145
+
146
+ A few more disciplines that pay off late but cost little up front:
147
+
148
+ - **Deployment script as code**: the canonical deployment lives in `script/Deploy.s.sol`, not in a one-off REPL session. Anyone should be able to reproduce mainnet bytecode from a tagged commit.
149
+ - **Pause-and-recover drill**: before mainnet, simulate the full incident response on a fork — the multisig signs, the contract pauses, an exploit is contained, an upgrade is queued through timelock, and depositors are made whole. If the runbook is not exercised, it does not exist.
150
+ - **Post-deploy verification**: verified source on Etherscan or the L2 explorer, a published address registry (one line per chain) committed to the repo, and a public read-only dashboard for the invariant metrics so anyone can re-derive solvency from on-chain state.
151
+
152
+ Taken together — framing, invariants, threat model, trust assumptions, success metrics — these five sections are what an auditor will ask for in the kickoff call. Write them before you ship, commit them next to the contracts, and treat any drift as a scope change that requires re-auditing.
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: web3-security
3
+ description: Layered security practices for smart contracts heading to mainnet — defense-in-depth, Checks-Effects-Interactions, pull payments, OpenZeppelin primitives, pause + multisig, and input validation discipline
4
+ topics: [web3, security, solidity, openzeppelin, defense-in-depth]
5
+ ---
6
+
7
+ A smart contract is a public, immutable bank vault that anyone on the planet can call. The mempool is hostile, the bytecode is permanent, and the only patch deployment is a redeploy + migration that your users may or may not follow. Most exploits aren't novel cryptography — they're missed standard patterns: a state update after an external call, a `tx.origin` check that a phishing contract bypassed, a `transfer` to a contract that reverts and bricks an auction. Layered defense beats clever one-off mitigations, every time.
8
+
9
+ ## Summary
10
+
11
+ Layer your defenses: a written spec with invariants, secure-by-construction patterns, static analysis and forge tests, an external audit, then a bug bounty — each catches what the previous layer missed. Use `Checks-Effects-Interactions` religiously and wrap any function that touches an external contract with `ReentrancyGuard` from OpenZeppelin. Prefer `pull payments` (users withdraw) over push (contract sends) so a malicious or buggy recipient cannot stall everyone else. Treat `OpenZeppelin` as your baseline — audited, well-known primitives (`AccessControl`, `Pausable`, `SafeERC20`) have fewer surprises than hand-rolled equivalents. Wire a `Pausable` kill-switch to a 2-of-N or 3-of-N Safe multisig and time-lock the truly dangerous operations.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Defense-in-depth layered model
16
+
17
+ Treat security as five distinct layers, each independently valuable. The point of layering is that any single layer will miss something — the protocol survives because the next layer catches it.
18
+
19
+ 1. **Spec and invariants** — Before any Solidity, write down what must always be true: "total supply equals sum of balances," "no user can withdraw more than they deposited," "only governance can change fee tiers," "the protocol is solvent at every block." These become the oracle for tests, fuzzing, and audit conversations. A contract without a written invariant set is a contract whose author has not decided what "correct" means, and reviewers cannot tell you whether the code is correct against an undefined target.
20
+ 2. **Secure-by-construction patterns** — CEI, pull-payments, `ReentrancyGuard`, custom errors with parameters, `immutable` where possible, explicit visibility. Most exploits target contracts that skipped one of these — the pattern existed, the author didn't reach for it.
21
+ 3. **Tooling** — Slither for static analysis on every PR, Foundry fuzz and invariant tests aimed directly at the spec from layer 1, Echidna or Halmos for deeper property testing, mythril or symbolic execution where it earns its keep. CI fails the build on new Slither high/medium findings; coverage gates keep the test suite from rotting.
22
+ 4. **Audit** — One or two reputable firms (Trail of Bits, OpenZeppelin, Spearbit, Code4rena contest). Audit late enough that the code is frozen, early enough that fixes don't push the launch. Code freeze before audit, no scope creep during. Treat every finding — even "informational" — as a real signal about your design.
23
+ 5. **Bug bounty** — Immunefi or Cantina, sized to the TVL at risk. A $50k bounty on a $50M protocol is an insult to whitehats and an invitation to blackhats; scale rewards to the prize. Run the bounty continuously, not for a launch week and then off.
24
+
25
+ See `web3-audit-workflow.md` for the tooling integration details and `web3-common-vulnerabilities.md` for the SWC-level checklist that audits work through.
26
+
27
+ ### Checks-Effects-Interactions
28
+
29
+ The single most important pattern in Solidity. Order every state-changing function as: (1) **Check** preconditions and inputs, (2) update **Effects** in storage, (3) make external **Interactions** last. Reentrancy exploits work by letting an attacker re-enter your function before step 2 has run; CEI removes that window.
30
+
31
+ Vulnerable:
32
+
33
+ ```solidity
34
+ function withdraw() external {
35
+ uint256 amount = balances[msg.sender];
36
+ (bool ok, ) = msg.sender.call{value: amount}(""); // INTERACTION first
37
+ require(ok, "send failed");
38
+ balances[msg.sender] = 0; // EFFECT after — exploitable
39
+ }
40
+ ```
41
+
42
+ Corrected with CEI plus `ReentrancyGuard` as a belt-and-braces backstop:
43
+
44
+ ```solidity
45
+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
46
+
47
+ contract Vault is ReentrancyGuard {
48
+ mapping(address => uint256) public balances;
49
+ error NothingToWithdraw();
50
+ error TransferFailed();
51
+
52
+ function withdraw() external nonReentrant {
53
+ uint256 amount = balances[msg.sender]; // CHECK
54
+ if (amount == 0) revert NothingToWithdraw();
55
+ balances[msg.sender] = 0; // EFFECT (before interaction)
56
+ (bool ok, ) = msg.sender.call{value: amount}(""); // INTERACTION last
57
+ if (!ok) revert TransferFailed();
58
+ }
59
+ }
60
+ ```
61
+
62
+ `nonReentrant` is cheap insurance for the case where you, or a future maintainer, miss a CEI ordering somewhere subtle (cross-function reentrancy, view-function reentrancy on read-after-write).
63
+
64
+ ### Pull-payments over push
65
+
66
+ Never proactively send funds to N users in a loop. One reverting recipient bricks the whole batch — a single griefing contract can stall every auction, raffle, or dividend you ship. Instead, credit each user's balance in storage and let them pull:
67
+
68
+ ```solidity
69
+ mapping(address => uint256) public pending;
70
+
71
+ function _credit(address user, uint256 amount) internal {
72
+ pending[user] += amount;
73
+ }
74
+
75
+ function claim() external nonReentrant {
76
+ uint256 amount = pending[msg.sender];
77
+ if (amount == 0) revert NothingToWithdraw();
78
+ pending[msg.sender] = 0;
79
+ (bool ok, ) = msg.sender.call{value: amount}("");
80
+ if (!ok) revert TransferFailed();
81
+ }
82
+ ```
83
+
84
+ The contract's job is bookkeeping; the user's job is taking custody. If their withdrawal fails — recipient is a contract that reverts, ran out of gas, or hit a blacklist — only they are affected, and they can fix it on their side without coordinating with everyone else in the queue. OpenZeppelin's `PullPayment` contract is a drop-in implementation if you want one less thing to write.
85
+
86
+ The rare exceptions where push is acceptable: payouts to a single, trusted address you control (your treasury); ERC-20 `transfer` calls inside `SafeERC20` where you've already validated the recipient. Even then, never wrap a push in a user-iterating loop.
87
+
88
+ ### OpenZeppelin as baseline
89
+
90
+ Pin a specific OpenZeppelin Contracts version in `foundry.toml` remappings and inherit from their primitives instead of hand-rolling. The core set worth knowing cold:
91
+
92
+ - `ReentrancyGuard` — the `nonReentrant` modifier shown above. Tracks a `_status` slot to reject reentrant calls cheaply.
93
+ - `Pausable` — `whenNotPaused` modifier on user-facing entry points; emergency `_pause()` callable by a privileged role. Pair with `AccessControl` so the pauser role is granular and revocable.
94
+ - `AccessControl` — role-based permissions instead of a single `owner`. Lets you separate `PAUSER_ROLE`, `UPGRADER_ROLE`, and `TREASURER_ROLE` to different keys, with `DEFAULT_ADMIN_ROLE` controlling grants. Deep dive in `web3-access-control.md`.
95
+ - `SafeERC20` — `safeTransfer`, `safeTransferFrom`, `forceApprove`. Handles non-standard tokens that don't return a bool (USDT) and the approve-race-condition pattern. Use it for every ERC-20 interaction without exception.
96
+ - `EIP712` and `Nonces` — typed structured signatures and replay-protected nonces when you need permit-style or meta-tx flows.
97
+
98
+ These are audited, widely deployed, and reviewed continuously by the ecosystem. A hand-rolled `nonReentrant` will pass review once and then rot as the team rotates; OZ's gets re-validated every release and every external audit of every protocol that uses it. Pin the version (`@openzeppelin/contracts@5.0.2`) in `package.json` and the remapping so an `npm update` cannot silently swap your security primitives, and read the changelog before upgrading — major versions occasionally change defaults (initializer patterns, role bytes32 encoding) in ways that matter.
99
+
100
+ ### Pause + emergency multisig
101
+
102
+ Wire a kill-switch on every entry point that moves funds, and put the pause key behind a Safe multisig — never an EOA. Two-of-three for small protocols, three-of-five for serious TVL, with signers on different hardware in different physical locations.
103
+
104
+ ```solidity
105
+ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
106
+ import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
107
+
108
+ contract Market is AccessControl, Pausable {
109
+ bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
110
+
111
+ constructor(address multisig) {
112
+ _grantRole(DEFAULT_ADMIN_ROLE, multisig); // multisig owns role management
113
+ _grantRole(PAUSER_ROLE, multisig);
114
+ }
115
+
116
+ function pause() external onlyRole(PAUSER_ROLE) { _pause(); }
117
+ function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); }
118
+
119
+ function trade(uint256 amount) external whenNotPaused {
120
+ // ... real logic ...
121
+ }
122
+ }
123
+ ```
124
+
125
+ Anything irreversible — upgrading an implementation, draining the treasury, raising fee caps, changing oracle sources — goes behind a `TimelockController` with a 24–72 hour delay so users have time to exit if the multisig is compromised or coerced. The pause itself stays instantaneous (you can't time-lock an emergency); the dangerous knobs are slow.
126
+
127
+ Two adjacent practices worth wiring up at the same time:
128
+
129
+ - Publish a public dashboard or `roles.md` of who holds which role and where the timelock points, so users can verify the kill-switch exists and is wired correctly. An invisible kill-switch is a trust claim, not a security control.
130
+ - Rehearse a pause-and-recover with the multisig signers at least once before mainnet so the first time someone signs is not the night of an incident. Practice the unhappy path: signer unavailable, hardware wallet bricked, mis-typed function selector.
131
+ - Subscribe the multisig to an on-chain monitoring service (OpenZeppelin Defender, Forta, Tenderly Alerts) that pages on anomalous activity — large withdrawals, oracle deviation, paused-state changes. The kill-switch is only useful if someone is watching.
132
+
133
+ ### Input validation and visibility
134
+
135
+ Validate every input at the public boundary with custom errors that carry the offending values — they're cheaper than revert strings and far more useful when triaging a failed tx:
136
+
137
+ ```solidity
138
+ error AmountZero();
139
+ error AmountExceedsCap(uint256 requested, uint256 cap);
140
+ error RecipientZero();
141
+
142
+ uint256 public immutable cap; // immutable: set in constructor, never changes
143
+ uint8 public constant DECIMALS = 18; // constant: known at compile time
144
+
145
+ function deposit(address to, uint256 amount) external whenNotPaused {
146
+ if (to == address(0)) revert RecipientZero();
147
+ if (amount == 0) revert AmountZero();
148
+ if (amount > cap) revert AmountExceedsCap(amount, cap);
149
+ // ...
150
+ }
151
+ ```
152
+
153
+ Mark functions `external` rather than `public` unless you call them internally — `external` is cheaper and signals "this is an entry point, audit the inputs." Use `immutable` for constructor-set values (deploy-time addresses, supply caps, the multisig address) and `constant` for compile-time literals; both save gas and tell the reader "this cannot change," which is itself a security property. Default state variables to `private` or `internal` and expose specific getters where you need them, rather than leaning on `public` which generates a getter you may not have meant to commit to as part of the ABI.
154
+
155
+ Custom errors with parameters are strictly better than `require(cond, "string")`: they're cheaper, they survive ABI decoding so off-chain tools can show the failing values, and they force you to name the failure mode rather than describe it. Reserve `require` strings for one-off scripts and tests.
156
+
157
+ ### What NOT to use
158
+
159
+ - **`tx.origin` for authorization** — `tx.origin` is the EOA that started the transaction, which a phishing contract can trick a user into being. Always check `msg.sender` (the immediate caller). `tx.origin` is acceptable only for narrow checks like "refuse to be called by any contract" (`require(tx.origin == msg.sender)`), and even that is fragile in a post-EIP-3074 / account-abstraction world.
160
+ - **Raw low-level `call` without checking return data** — `(bool ok, ) = addr.call(...)` ignores the actual return payload; an unverified `ok` says "the call returned," not "the call succeeded as intended." Use `SafeERC20` for tokens and check `ok` plus decode return data for everything else. Bubble up the revert reason where the caller cares.
161
+ - **Unbounded loops over user-supplied arrays** — gas grows with N, an attacker submits a million-element array, the function reverts forever and locks whatever it gated. Cap input lengths or paginate. The same hazard applies to iterating over a `users` mapping that anyone can grow.
162
+ - **Calling arbitrary user-supplied addresses** — every external call is a trust boundary. Whitelist the targets you call (routers, oracles, your own modules) via an admin-managed allowlist; never `target.call(data)` where `target` came from `msg.sender`. If the protocol genuinely needs to integrate with arbitrary external contracts, isolate the interaction in a sandbox contract with no privileges and no funds beyond the immediate call's value.
163
+ - **`block.timestamp` for fine-grained ordering or randomness** — miners (or proposers post-merge) have a small window to nudge it. Fine for "did 24 hours pass since deploy"; not fine for "who got the millisecond-precise winning bid" or as an entropy source.