@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,171 @@
1
+ ---
2
+ name: web3-common-vulnerabilities
3
+ description: SWC-style checklist of the most-exploited Solidity bugs — reentrancy, delegatecall hazards, signature replay, front-running, unchecked calls, unbounded loops, and the small set of patterns that catch them
4
+ topics: [web3, vulnerabilities, security, swc, solidity]
5
+ ---
6
+
7
+ The most-exploited contract bugs are usually the same handful, recycled across protocols by attackers who know exactly which patterns auditors and authors keep missing. Internalize this checklist; gate every PR with Slither and Foundry tests so the mechanical findings never reach review; require a dedicated reviewer pass for any change that touches the patterns below. Where this doc is the SWC-style enumeration of "what goes wrong," `web3-security.md` is the practices doc on "how to build so it doesn't" — read both, and treat `web3-audit-workflow.md` as the tooling glue.
8
+
9
+ ## Summary
10
+
11
+ There are roughly ten vulnerability classes that account for the overwhelming majority of historical losses, and almost every one has a standard mitigation. Slither catches a meaningful subset — unchecked low-level calls, `tx.origin` auth, obvious reentrancy — in CI, before a human ever sees the diff. Foundry fuzz and invariant tests catch another slice when the spec is written down: balance conservation, monotonic nonces, no-free-mint. Manual review catches the rest — the cross-function reentrancy, the subtle `delegatecall` target, the signature missing a chain ID. None of these layers is sufficient alone, and "we couldn't find a problem" is not the same as "there is no problem."
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Reentrancy
16
+
17
+ The classic exploit: an external call to a user-controlled address re-enters the same function (or a sibling) before storage is updated, letting the attacker drain a balance N times. Solve it structurally with Checks-Effects-Interactions, and defensively with OpenZeppelin's `nonReentrant`.
18
+
19
+ Vulnerable:
20
+
21
+ ```solidity
22
+ function withdraw() external {
23
+ uint256 amount = balances[msg.sender];
24
+ (bool ok, ) = msg.sender.call{value: amount}(""); // re-entry happens here
25
+ require(ok, "send failed");
26
+ balances[msg.sender] = 0; // too late
27
+ }
28
+ ```
29
+
30
+ Fixed:
31
+
32
+ ```solidity
33
+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
34
+
35
+ contract Vault is ReentrancyGuard {
36
+ mapping(address => uint256) public balances;
37
+ error Empty();
38
+ error TransferFailed();
39
+
40
+ function withdraw() external nonReentrant {
41
+ uint256 amount = balances[msg.sender];
42
+ if (amount == 0) revert Empty();
43
+ balances[msg.sender] = 0; // effect before
44
+ (bool ok, ) = msg.sender.call{value: amount}(""); // interaction last
45
+ if (!ok) revert TransferFailed();
46
+ }
47
+ }
48
+ ```
49
+
50
+ Cross-function and read-only reentrancy (a view function reads stale state during a reentrant call) both exist — `nonReentrant` on every state-touching external is cheap insurance.
51
+
52
+ ### Integer over/underflow
53
+
54
+ Solidity 0.8+ inserts overflow checks into every arithmetic op by default; the famous 2018 BatchOverflow-style bugs are no longer reachable in idiomatic code. The remaining risk is the `unchecked { ... }` escape hatch and importing legacy `<0.8` libraries unguarded. Use `unchecked` only where overflow is provably impossible (loop counters bounded by a checked length) and leave a comment:
55
+
56
+ ```solidity
57
+ for (uint256 i = 0; i < items.length; ) {
58
+ // ... body ...
59
+ unchecked { ++i; } // bounded by items.length, cannot overflow
60
+ }
61
+ ```
62
+
63
+ ### Front-running (MEV)
64
+
65
+ Every pending transaction is public; searchers and builders can reorder, sandwich, or front-run anything price-sensitive. For auctions, oracle updates, and competitive ops, use a commit-reveal scheme so bids are not visible until reveal:
66
+
67
+ ```solidity
68
+ mapping(address => bytes32) public commits;
69
+
70
+ function commitBid(bytes32 hash) external { commits[msg.sender] = hash; }
71
+
72
+ function revealBid(uint256 bid, bytes32 salt) external payable {
73
+ require(keccak256(abi.encode(bid, salt, msg.sender)) == commits[msg.sender], "bad reveal");
74
+ require(msg.value == bid, "value != bid");
75
+ // ... resolve auction ...
76
+ }
77
+ ```
78
+
79
+ For one-shot user txs that must not be sandwiched, route through Flashbots Protect or a private mempool RPC. Quote slippage tolerances and deadlines on every swap.
80
+
81
+ ### `delegatecall` hazards
82
+
83
+ `delegatecall` runs target bytecode against the caller's storage and `msg.sender` — the foundation of upgradeable proxies, and a foot-cannon if the target address is attacker-controlled. Never `delegatecall` to an unvalidated address:
84
+
85
+ ```solidity
86
+ // VULNERABLE — total takeover
87
+ function forward(address target, bytes calldata data) external {
88
+ (bool ok, ) = target.delegatecall(data);
89
+ require(ok);
90
+ }
91
+ ```
92
+
93
+ If you need a proxy, use OpenZeppelin's `ERC1967Proxy` / `UUPSUpgradeable` with an admin-gated `_authorizeUpgrade`, and pin the implementation address in `immutable` storage where the architecture allows.
94
+
95
+ ### Unchecked external calls
96
+
97
+ Low-level `.call`, `.delegatecall`, and `.staticcall` return `(bool ok, bytes memory)`. Ignoring `ok` silently swallows reverts:
98
+
99
+ ```solidity
100
+ // VULNERABLE
101
+ recipient.call{value: amount}("");
102
+
103
+ // FIXED
104
+ (bool ok, ) = recipient.call{value: amount}("");
105
+ if (!ok) revert TransferFailed();
106
+ ```
107
+
108
+ For ERC-20, use OpenZeppelin's `SafeERC20.safeTransfer` / `safeTransferFrom` — they handle non-standard tokens (USDT) that don't return a bool and revert on failure.
109
+
110
+ ### Signature replay (EIP-712 + nonces)
111
+
112
+ A signature without a nonce, chain ID, and contract address can be replayed forever, on every chain. Use EIP-712 typed data with a per-signer nonce that increments on every accepted signature:
113
+
114
+ ```solidity
115
+ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
116
+ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
117
+
118
+ contract Permit is EIP712 {
119
+ mapping(address => uint256) public nonces;
120
+ bytes32 private constant TYPEHASH =
121
+ keccak256("Claim(address user,uint256 amount,uint256 nonce,uint256 deadline)");
122
+
123
+ constructor() EIP712("MyProtocol", "1") {}
124
+
125
+ function claim(address user, uint256 amount, uint256 deadline, bytes calldata sig) external {
126
+ require(block.timestamp <= deadline, "expired");
127
+ bytes32 digest = _hashTypedDataV4(
128
+ keccak256(abi.encode(TYPEHASH, user, amount, nonces[user]++, deadline))
129
+ );
130
+ require(ECDSA.recover(digest, sig) == user, "bad sig");
131
+ // ... pay out ...
132
+ }
133
+ }
134
+ ```
135
+
136
+ The domain separator already includes `chainid` and `address(this)`, so the same signature cannot be replayed on another chain or fork.
137
+
138
+ ### DoS via unbounded arrays
139
+
140
+ Looping over user-controlled arrays grows gas with N; an attacker grows N until the function reverts forever, bricking whatever it gated. Anti-pattern:
141
+
142
+ ```solidity
143
+ // VULNERABLE — one griefer locks every payout
144
+ function distribute() external {
145
+ for (uint256 i; i < recipients.length; ++i) {
146
+ (bool ok, ) = recipients[i].call{value: shares[i]}("");
147
+ require(ok); // any reverting recipient halts everyone
148
+ }
149
+ }
150
+ ```
151
+
152
+ Convert to pull payments: credit each recipient in storage and let them withdraw individually. Same fix for any "iterate over all users" loop — cap input length, paginate, or invert control.
153
+
154
+ ### Approve-and-pull race
155
+
156
+ ERC-20 `approve(spender, N)` is vulnerable to a known race: a spender watching the mempool can spend the old allowance, then the new one, ending up with `old + new`. Mitigate one of three ways: (1) set allowance to `0` first, then to the new value in a second tx; (2) use `increaseAllowance` / `decreaseAllowance` on tokens that support them; (3) use OpenZeppelin's `SafeERC20.forceApprove` which handles the zero-reset under the hood. For new code, prefer EIP-2612 `permit` so allowance and use happen atomically in one tx.
157
+
158
+ ### `tx.origin`
159
+
160
+ `tx.origin` is the EOA that started the transaction tree; `msg.sender` is the immediate caller. A phishing contract that tricks a user into calling it can pass any `tx.origin` check while `msg.sender` is the malicious contract. Authorize with `msg.sender`, always:
161
+
162
+ ```solidity
163
+ modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } // correct
164
+ // require(tx.origin == owner) // WRONG — phishable
165
+ ```
166
+
167
+ The only narrow use is `require(tx.origin == msg.sender)` to refuse contract callers, and even that is increasingly fragile under account abstraction (EIP-4337) and EIP-3074.
168
+
169
+ ### Self-destruct deprecation
170
+
171
+ `SELFDESTRUCT` no longer deletes code or storage after EIP-6780 (Dencun, March 2024) — it only forwards the ETH balance, and only fully self-destructs if called in the same tx that created the contract. Audits and tooling still flag it; treat it as effectively removed, never rely on it for upgrade or wipe semantics, and migrate any legacy code that did.
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: web3-conventions
3
+ description: Solidity style and convention discipline for smart-contract teams — forge fmt as single formatter, NatSpec on public functions, pinned pragma, custom errors over string reverts, naming and ordering rules
4
+ topics: [web3, conventions, solidity, forge-fmt, natspec]
5
+ ---
6
+
7
+ Solidity is brittle. The compiler is fast and unforgiving, the gas model rewards terseness, and the deployment surface is immutable — every style drift you tolerate during development eventually becomes a bytecode-determinism question, an audit comment, or a bug nobody can patch. The conventions below are the ones enforced by `forge fmt` and code review at every serious shop; encode them as CI gates and they stop being judgment calls.
8
+
9
+ ## Summary
10
+
11
+ Use `forge fmt` as the single formatter for any Foundry project — it replaces prettier-solidity-plugin and is the only formatter that tracks the official Solidity style guide. Require `NatSpec` (`@notice`, `@dev`, `@param`, `@return`) on every public and external function; this is what wallets and block explorers render to users. Pin the `pragma` exactly (`pragma solidity 0.8.24;`) instead of carets — reproducible bytecode is a security property, not a preference. Prefer custom errors to string reverts: cheaper, introspectable, and trivially decoded by clients. Follow PascalCase contracts, camelCase functions/variables, UPPER_SNAKE_CASE constants, leading-underscore private vars, and past-tense event names — these match the [official style guide](https://docs.soliditylang.org/en/latest/style-guide.html) and what `forge fmt` enforces structurally.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Pragma and license
16
+
17
+ Every Solidity file starts with an SPDX license identifier and an exact pragma. The license header is required by `solc` and surfaces in verified-source explorers; the pinned pragma is what makes your deployed bytecode reproducible months later.
18
+
19
+ ```solidity
20
+ // SPDX-License-Identifier: MIT
21
+ pragma solidity 0.8.24;
22
+ ```
23
+
24
+ Do not use `pragma solidity ^0.8.0;` in deployable contracts. A caret range lets `solc` 0.8.24 and 0.8.27 produce different bytecode for the same source, which breaks deterministic deployments (CREATE2 addresses), audit reproduction, and source verification on chains with strict matching. Pin to a specific patch version, commit `foundry.toml` with the same `solc_version`, and bump deliberately. Use `// SPDX-License-Identifier: MIT` for permissive code and `// SPDX-License-Identifier: AGPL-3.0` for protocol code that should stay open.
25
+
26
+ ### NatSpec on public functions
27
+
28
+ NatSpec is not a documentation nicety — it is the contract the user sees. Wallets like Rabby, Frame, and Safe render `@notice` strings during transaction confirmation, and Etherscan surfaces NatSpec on verified contracts. Missing NatSpec on a public function means the user signs a transaction with no human-readable description.
29
+
30
+ ```solidity
31
+ /// @title Vault
32
+ /// @notice Holds user deposits and accrues yield.
33
+ contract Vault {
34
+ /// @notice Deposit `amount` of the underlying asset and mint vault shares.
35
+ /// @dev Pulls tokens via `transferFrom`; caller must approve first.
36
+ /// Reverts with `ZeroAmount` if `amount == 0`.
37
+ /// @param amount Amount of underlying to deposit, in token decimals.
38
+ /// @param receiver Address that will receive the minted shares.
39
+ /// @return shares Number of vault shares minted to `receiver`.
40
+ function deposit(uint256 amount, address receiver)
41
+ external
42
+ returns (uint256 shares)
43
+ {
44
+ if (amount == 0) revert ZeroAmount();
45
+ // ...
46
+ }
47
+ }
48
+ ```
49
+
50
+ Rules:
51
+
52
+ - `@notice` is mandatory on every `public`/`external` function — it is the end-user description.
53
+ - `@dev` is for integrators reading the source; explain invariants, side effects, and revert conditions here.
54
+ - `@param` and `@return` are mandatory whenever the function has parameters or named returns.
55
+ - `@inheritdoc` on overrides — do not copy-paste the parent's NatSpec.
56
+ - Internal/private functions can use `//` comments; reserve `///` for documented interfaces.
57
+
58
+ ### Naming and ordering
59
+
60
+ The Solidity style guide names the categories; `forge fmt` enforces the ordering within a contract.
61
+
62
+ ```solidity
63
+ // SPDX-License-Identifier: MIT
64
+ pragma solidity 0.8.24;
65
+
66
+ contract Vault {
67
+ // 1. State variables
68
+ uint256 public constant MAX_FEE_BPS = 500;
69
+ address public owner;
70
+ uint256 private _totalShares;
71
+
72
+ // 2. Events
73
+ event Deposited(address indexed user, uint256 amount, uint256 shares);
74
+
75
+ // 3. Errors
76
+ error ZeroAmount();
77
+ error InsufficientBalance(uint256 available);
78
+
79
+ // 4. Modifiers
80
+ modifier onlyOwner() {
81
+ if (msg.sender != owner) revert InsufficientBalance(0);
82
+ _;
83
+ }
84
+
85
+ // 5. Constructor
86
+ constructor(address _owner) {
87
+ owner = _owner;
88
+ }
89
+
90
+ // 6. receive / fallback (none here)
91
+
92
+ // 7. external -> public -> internal -> private functions
93
+ function deposit(uint256 amount) external returns (uint256) { /* ... */ }
94
+ function totalShares() public view returns (uint256) { return _totalShares; }
95
+ function _mintShares(address to, uint256 shares) internal { /* ... */ }
96
+ }
97
+ ```
98
+
99
+ Naming rubric:
100
+
101
+ - **Contracts / libraries / interfaces**: `PascalCase`. Interfaces prefixed with `I` (`IERC20`, `IVault`).
102
+ - **Functions / state variables / locals**: `camelCase` (`totalShares`, `computeFee`).
103
+ - **Constants and immutables**: `UPPER_SNAKE_CASE` (`MAX_FEE_BPS`, `WETH`).
104
+ - **Private / internal storage**: single leading underscore (`_totalShares`, `_pendingWithdrawals`). Function parameters that shadow state vars also take a leading underscore (`address _owner`).
105
+ - **Custom errors**: `PascalCase`, descriptive (`InsufficientBalance(uint256 available)` not `ERR_BAL`).
106
+ - **Events**: `PascalCase`, past-tense verb (`Deposited`, `OwnerUpdated`, `Withdrawn`) — events describe something that already happened.
107
+
108
+ ### `forge fmt` as single formatter
109
+
110
+ Foundry ships its own formatter: `forge fmt`. It is the only formatter that tracks the Solidity style guide in lockstep with `solc` releases, and it eliminates the `prettier` + `prettier-plugin-solidity` + `solhint` + `solhint-prettier` dependency stack that plagued older projects.
111
+
112
+ ```bash
113
+ forge fmt # format every .sol file in src/, test/, script/
114
+ forge fmt --check # CI mode: exit non-zero if anything is unformatted
115
+ forge fmt path/to/Vault.sol # single file
116
+ ```
117
+
118
+ Wire `forge fmt --check` into CI and a pre-commit hook. Configure it in `foundry.toml`:
119
+
120
+ ```toml
121
+ [fmt]
122
+ line_length = 120
123
+ tab_width = 4
124
+ bracket_spacing = true
125
+ int_types = "long" # uint256, not uint
126
+ quote_style = "double"
127
+ number_underscore = "thousands"
128
+ ```
129
+
130
+ If you want lint rules beyond formatting (e.g. flag `tx.origin`, missing visibility, reentrancy patterns), add `solhint` with a minimal `.solhint.json` — but do not duplicate formatting rules between them. Let `forge fmt` own layout; let `solhint` own semantics.
131
+
132
+ ### Custom errors
133
+
134
+ `require(condition, "string message")` was the only revert pattern before Solidity 0.8.4. Custom errors are strictly better: cheaper to deploy and call, encode arguments, and decode reliably in clients.
135
+
136
+ ```solidity
137
+ // Before — string revert
138
+ function withdraw(uint256 amount) external {
139
+ require(amount > 0, "Vault: zero amount");
140
+ require(balances[msg.sender] >= amount, "Vault: insufficient balance");
141
+ // ...
142
+ }
143
+
144
+ // After — custom errors
145
+ error ZeroAmount();
146
+ error InsufficientBalance(uint256 requested, uint256 available);
147
+
148
+ function withdraw(uint256 amount) external {
149
+ if (amount == 0) revert ZeroAmount();
150
+ uint256 bal = balances[msg.sender];
151
+ if (bal < amount) revert InsufficientBalance(amount, bal);
152
+ // ...
153
+ }
154
+ ```
155
+
156
+ Benefits:
157
+
158
+ - **Gas**: each unique revert string costs ~50 bytes of deployed bytecode plus runtime memory expansion; custom errors are a 4-byte selector.
159
+ - **Introspection**: clients (ethers, viem, foundry traces) decode `InsufficientBalance(100, 42)` directly. String reverts are opaque blobs.
160
+ - **Refactor safety**: rename the error type and the compiler finds every caller; rename a string and nothing breaks until production.
161
+
162
+ Reserve `require` for legacy interface compatibility or when you genuinely need the string in a chain explorer; everywhere else, define a typed error.
@@ -0,0 +1,216 @@
1
+ ---
2
+ name: web3-deployment-and-verification
3
+ description: Deploying smart contracts with forge script — broadcast artifacts as provenance, Etherscan verification, multi-chain flows, testnet rehearsals, mainnet pre-flight, post-deploy role hardening, and CREATE2 deterministic deploys
4
+ topics: [web3, deployment, verification, forge-script, etherscan]
5
+ ---
6
+
7
+ Shipping a contract to mainnet is the most irreversible thing a smart-contract team ever does. There is no rollback, no patch deploy, no `kubectl rollout undo` — once the bytecode is live and users start interacting with it, you live with what you wrote. The instinct from web2 — "deploy fast, fix forward" — produces drained protocols on-chain. Treat deployment as a release event: every privileged operation scripted (not hand-called), every artifact archived (not transient), every step rehearsed on a testnet that mirrors mainnet, and every contract verified on Etherscan before you tell anyone the address. The cost of the discipline is half a day of process; the cost of skipping it is the entire protocol.
8
+
9
+ ## Summary
10
+
11
+ Use `forge script` with a `Script`-extending contract for every deploy — never an ad-hoc `forge create` invocation, never a hand-pasted constructor argument. The script batches deploy + initial role grants + ownership transfer into one atomic broadcast so there is no intermediate state where the deployer EOA holds privileges. Commit the resulting `broadcast/Deploy.s.sol/<chainId>/run-latest.json` for every canonical chain — it is the provenance artifact tying a verified address back to a commit SHA. Verify on Etherscan inline (`--verify` on the script) or as a separate `forge verify-contract` step before announcing the address; an unverified mainnet contract is functionally invisible to users and auditors. Rehearse the full deploy on Sepolia first — same script, different `--rpc-url` — and only promote to mainnet once the testnet smoke test, role-hardening sequence, and (if upgradeable) upgrade burn-test all pass. Then run the mainnet pre-flight checklist before unlocking the hardware wallets.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### `forge script` deploys
16
+
17
+ Every mainnet deploy goes through a `Script`-extending contract in `script/` — not `forge create`, not a one-off REPL, not a `cast send` typed into a terminal at 2am. The script is reviewable, diffable, and re-runnable; it lives in the same repo as the contracts it deploys; and it captures every privileged operation in a single broadcast so there is no window where the deployer EOA holds powers it should not. A minimal but production-shaped deploy script:
18
+
19
+ ```solidity
20
+ // script/Deploy.s.sol
21
+ import {Script} from "forge-std/Script.sol";
22
+ import {Vault} from "../src/Vault.sol";
23
+
24
+ contract Deploy is Script {
25
+ function run() external returns (Vault vault) {
26
+ address adminMultisig = vm.envAddress("ADMIN_MULTISIG");
27
+ address minterMultisig = vm.envAddress("MINTER_MULTISIG");
28
+ address timelock = vm.envAddress("TIMELOCK");
29
+ uint256 pk = vm.envUint("DEPLOYER_PK");
30
+
31
+ vm.startBroadcast(pk);
32
+
33
+ // Deploy
34
+ vault = new Vault(adminMultisig);
35
+
36
+ // Grant operational roles
37
+ vault.grantRole(vault.MINTER_ROLE(), minterMultisig);
38
+ vault.grantRole(vault.UPGRADER_ROLE(), timelock);
39
+
40
+ // Renounce deployer's transient admin (if constructor granted it)
41
+ // — preferred pattern is to pass the multisig to the constructor
42
+ // so the deployer never holds DEFAULT_ADMIN_ROLE in the first place.
43
+
44
+ vm.stopBroadcast();
45
+ }
46
+ }
47
+ ```
48
+
49
+ `vm.startBroadcast(pk)` and `vm.stopBroadcast()` bracket the transactions that get sent on-chain — everything in between becomes a real transaction signed by the deployer key. Calls outside the broadcast block are simulation-only (useful for reading state to assert preconditions, e.g. `require(adminMultisig.code.length > 0, "admin must be a contract")`). Batching deploy + role grants in a single broadcast means the role-hardening steps cannot be forgotten mid-deploy; the contract goes live in its final access-control configuration.
50
+
51
+ `vm.startBroadcast()` has three forms worth distinguishing:
52
+
53
+ - `vm.startBroadcast()` — uses the default sender, typically the address derived from `--private-key`, `--account`, or `--ledger` passed on the command line.
54
+ - `vm.startBroadcast(uint256 pk)` — broadcasts as a specific private key, useful when the script needs to switch identities mid-run (rare; usually a smell).
55
+ - `vm.startBroadcast(address signer)` — broadcasts as a specific address; combined with `--unlocked` for local anvil testing, or used with `--sender` for prepare-only flows.
56
+
57
+ Prefer the no-argument form with `--account <name>` (encrypted keystore) or `--ledger` (hardware) at the CLI: the script stays identical between testnet and mainnet, and the signing identity is selected at invocation time. `vm.envUint("DEPLOYER_PK")` reads the private key into the script's memory, which is fine for a one-off mainnet deploy from a fresh ephemeral environment but is a footgun on a shared developer machine. The deployer EOA is privileged for exactly one transaction — its job is to produce the broadcast — and then it should be a dead account.
58
+
59
+ For deploys that originate from a Safe rather than an EOA, the `forge script` flow changes shape: the script is run with `--sender $SAFE_ADDRESS` and **not** `--broadcast`, producing an unsigned transaction batch (often via `--ffi` and a helper like `safe-tx-builder`) that gets posted to the Safe UI for signing. The broadcast artifact is then the Safe transaction hash plus the eventual execution receipt. This is the right pattern for any subsequent privileged operation after the initial deploy; the deployer EOA only ever produces the first transaction.
60
+
61
+ ### Broadcast artifacts as provenance
62
+
63
+ Running `forge script script/Deploy.s.sol --rpc-url $RPC --broadcast` writes a JSON receipt to `broadcast/Deploy.s.sol/<chainId>/run-latest.json` (plus a timestamped copy alongside). That JSON contains the deployed address, the transaction hash, the block number, the constructor arguments, the gas used, and the ABI of the deployed contracts. Combined with the git commit SHA at the time of the run, it is the provenance artifact for the deployment. **Commit it.** Without the broadcast log, "which commit deployed the mainnet Vault at 0xabc..." becomes archaeology — you are diffing bytecode against historical builds trying to reconstruct which branch shipped. See `web3-project-structure.md` for the directory layout and `.gitignore` rules that keep canonical-chain broadcasts (1, 10, 8453, 42161) tracked while excluding ephemeral anvil (31337) noise.
64
+
65
+ Tag the release commit (`git tag mainnet-v1.0.0`) so the deployment is permanently locatable by name, and link the tag from your README's deployment table. A year from now, when someone asks "what version is live?", the answer is one `git checkout` away rather than a forensic exercise. Pair this with a CI job that uploads the broadcast artifact to an immutable store (release assets, IPFS) for the canonical record outside the repo.
66
+
67
+ The broadcast JSON itself is structured. Key fields to know:
68
+
69
+ - `transactions[].contractAddress` — the deployed address. Always present for `CREATE`/`CREATE2` transactions.
70
+ - `transactions[].hash` — the on-chain transaction hash for cross-referencing with Etherscan.
71
+ - `transactions[].arguments` — the constructor args as a JSON array, useful for re-verifying or auditing what was passed.
72
+ - `receipts[]` — the post-execution receipts including gas used, block number, and emitted logs.
73
+ - `commit` — Foundry records the current git commit SHA at the time of the script run.
74
+
75
+ Downstream tooling (deploy dashboards, indexer config generators, partner integrations) can parse this JSON directly. Treat it as a stable interface — your deployment story should never require running the script again to "look up" what happened the first time.
76
+
77
+ ### Etherscan verification
78
+
79
+ An unverified mainnet contract is hostile UX: users see opaque bytecode, auditors cannot review the source, and Etherscan's "Read Contract" / "Write Contract" tabs are dead. Verify every deploy. Two paths:
80
+
81
+ **Inline during deploy** — pass `--verify` to `forge script` and Foundry verifies each deployed contract immediately after broadcast. This is the preferred flow because the verification happens in the same invocation that produced the address, eliminating the risk of forgetting:
82
+
83
+ ```bash
84
+ forge script script/Deploy.s.sol \
85
+ --rpc-url $MAINNET_RPC \
86
+ --broadcast \
87
+ --verify \
88
+ --etherscan-api-key $ETHERSCAN_API_KEY
89
+ ```
90
+
91
+ **Standalone after the fact** — if verification was skipped or failed, run `forge verify-contract` against the deployed address:
92
+
93
+ ```bash
94
+ forge verify-contract 0xVaultAddress src/Vault.sol:Vault \
95
+ --chain mainnet \
96
+ --etherscan-api-key $ETHERSCAN_API_KEY \
97
+ --constructor-args $(cast abi-encode "constructor(address)" $ADMIN_MULTISIG) \
98
+ --watch
99
+ ```
100
+
101
+ `--watch` polls Etherscan until the verification completes. Constructor args must match exactly — encode them with `cast abi-encode` rather than typing the hex by hand, because a single nibble off means a "bytecode mismatch" error and another round of debugging.
102
+
103
+ Two reproducibility traps cause more "verified contract" failures than anything else. First, the compiler settings must match exactly: `solc_version`, `optimizer_runs`, `via_ir`, `evm_version`, and `bytecode_hash` in `foundry.toml` must be identical to what was used at deploy time. Setting `bytecode_hash = "none"` in `foundry.toml` removes the metadata hash from the compiled bytecode, which makes verification deterministic across machines — strongly recommended. Second, library addresses must be linked the same way; if your contract uses a non-inlined library, pass `--libraries` to `forge verify-contract` with the deployed library address. When verification fails, the Etherscan diff view shows which bytes diverge — read it before guessing.
104
+
105
+ Beyond Etherscan, consider also publishing to Sourcify (`--verifier sourcify`), which is a decentralized verification network not controlled by any single explorer. Sourcify verification produces a permanent, IPFS-backed record that survives any future Etherscan policy change or rate limit. For protocols that care about long-term decentralized verifiability, dual-verification (Etherscan + Sourcify) is cheap insurance.
106
+
107
+ L2 explorers vary: Basescan and Optimistic Etherscan use the same Etherscan stack and the same `forge verify-contract` flags. Arbiscan is similar but occasionally has stricter handling of via-IR contracts. Blockscout-based explorers (Gnosis Chain, several appchains) take `--verifier blockscout` with the chain-specific instance URL. Document the verifier per chain in your deploy runbook so a new operator does not have to discover it under deploy pressure.
108
+
109
+ ### Multi-chain deploys
110
+
111
+ Modern protocols ship to several L2s alongside mainnet — Optimism, Base, Arbitrum, sometimes a half-dozen more. The same `forge script` runs against every chain; only the `--rpc-url` and chain-specific env vars change:
112
+
113
+ ```bash
114
+ # Sepolia rehearsal
115
+ forge script script/Deploy.s.sol --rpc-url $SEPOLIA_RPC --broadcast --verify
116
+
117
+ # Mainnet
118
+ forge script script/Deploy.s.sol --rpc-url $MAINNET_RPC --broadcast --verify
119
+
120
+ # Base
121
+ forge script script/Deploy.s.sol --rpc-url $BASE_RPC --broadcast --verify
122
+ ```
123
+
124
+ Pin the expected chain ID inside the script with `require(block.chainid == 1, "wrong chain")` for mainnet-only operations — a wrong RPC URL is the kind of mistake that lands a "mainnet" deploy on Goerli, or worse, the other way around. Etherscan API keys are per-chain (Etherscan, Basescan, Arbiscan, Optimistic Etherscan, ...) — keep them in `.env` and never paste them inline.
125
+
126
+ Maintain a `deployments.json` (or similar) keyed by chain ID that captures the canonical address of every contract on every chain. Reference it from `Deploy.s.sol` for cross-contract wiring (e.g. "deploy a new Router that knows about the existing Vault on this chain"), and update it via the same broadcast that produced the new address. The file is the source of truth for downstream consumers — frontends, indexers, partner integrations — and lives in version control alongside the contracts. Some teams use `foundry-deployments` or `forge-deploy` plugins to manage this automatically; others write a 50-line helper in `script/utils/`. Either is fine; what is not fine is humans copy-pasting addresses between chats.
127
+
128
+ A multi-chain deploy is also a multi-chain **operations** problem: every chain has its own gas market, its own block-explorer quirks, its own reorg risk. Mainnet's 12-second blocks and ~$5 confirmation cost differ wildly from Arbitrum's sub-second blocks and sub-cent confirmation cost. Budget time for each chain individually — do not assume "we did mainnet, the L2s are a copy-paste" because the same script will still take a different amount of wall-clock time and produce different gas receipts.
129
+
130
+ ### Testnet rehearsal
131
+
132
+ Sepolia is not optional. Every mainnet deploy is preceded by a full Sepolia run of the same script, against a real RPC, with real (testnet) ETH. The rehearsal must exercise the **entire** post-deploy path: deploy, role grants, ownership transfer, Etherscan verification, smoke-test script against the live address, and (if upgradeable) a burn-test of the upgrade flow through the proxy. Only after every step succeeds on testnet — and the team has reviewed the resulting broadcast artifact and the verified Etherscan page — does the same script run against mainnet. The discipline catches the boring failures: a missing env var, a constructor arg in the wrong order, a multisig address that is actually an EOA, an Etherscan API key with the wrong permissions. Catching them on Sepolia costs an hour; catching them on mainnet costs a redeploy and a public explanation.
133
+
134
+ The Sepolia smoke test is a separate `forge script` (`script/SmokeTest.s.sol`) that exercises the protocol's happy path against the just-deployed address: deposit, mint, transfer, withdraw, pause, unpause, role-grant. It runs unattended, asserts expected end states, and fails loudly on the first deviation. Treat it as part of the deploy pipeline, not an optional manual step. A "fork test" using `vm.createFork($MAINNET_RPC)` is **not** a substitute — forking simulates against historical state but does not exercise the real Etherscan verification, the real Safe-signing UX, or the real gas-pricing dynamics. Sepolia is closer to mainnet than any fork.
135
+
136
+ Be explicit about which testnet you target. Goerli is deprecated. Holesky and Sepolia are the supported Ethereum testnets; Sepolia is the default for most teams because faucets are accessible. L2 testnets (Sepolia-Optimism, Sepolia-Base, Sepolia-Arbitrum) all derive from Sepolia and use the same wallet/faucet flow. Pick once and stay consistent across your contract suite — splitting testnets across protocols means each deploy needs a fresh faucet trip.
137
+
138
+ Rehearse with the **real multisig**, not a placeholder EOA. The Safe UI on Sepolia is the same UI your signers will see on mainnet; their first encounter with the "Sign Transaction" button should be on testnet ETH. Walk through the Tenderly simulation step together, verify the call data matches the script's expected output, and have every signer practice rejecting a transaction (you want them to know what the cancel flow looks like before they ever need it). The rehearsal is as much a people-process check as a tooling check.
139
+
140
+ If the protocol is upgradeable, the testnet rehearsal must include an actual upgrade through the timelock: schedule the upgrade, wait the (compressed) delay, execute, and verify the new implementation. Skipping this step is how teams discover their `_authorizeUpgrade` is misgated only when they need to ship a hotfix.
141
+
142
+ ### Mainnet pre-flight checklist
143
+
144
+ Before the deploy transaction is signed, every item is checked off, in order, with a named owner:
145
+
146
+ 1. **All tests pass and Slither is clean** — `forge test`, `forge coverage`, and `slither .` all green on the exact commit being deployed. Cross-ref `web3-audit-workflow.md` for the full gate.
147
+ 2. **Audit report delivered and findings remediated** — every P0/P1 from the audit has either a fix commit or a written acceptance with sign-off from the audit firm. No "we'll address this in v2" handwaves.
148
+ 3. **Testnet rehearsal complete and verified** — Sepolia deploy succeeded end-to-end, including Etherscan verification and the smoke-test script. Broadcast artifact reviewed.
149
+ 4. **Multisig signers ready and hardware wallets unlocked** — every required signer is online, hardware wallet plugged in and unlocked, Safe UI loaded, Tenderly simulation tab open. Rehearse the signing ceremony beforehand so the first signature is not the night of the deploy.
150
+ 5. **Initial role assignments scripted** — every privileged role grant lives in `Deploy.s.sol` and runs inside the same broadcast as the deploy. **Never hand-call `grantRole` on mainnet.** A script is reviewable; a typed-out `cast send` at midnight is not. Cross-ref `web3-access-control.md`.
151
+ 6. **Gas budget estimated and funded** — deploy gas estimated via the testnet run, mainnet base fee checked at the time of deploy, deployer EOA funded with enough ETH (plus headroom for retries). Stuck transactions because of insufficient gas are an avoidable embarrassment.
152
+ 7. **Deploy window chosen with cost and contention in mind** — avoid known high-fee windows (NFT mints, market dislocations, hyped launches) unless the deploy is itself an event. A boring 4am-UTC Sunday deploy is the right kind of boring. Have a no-deploy list (Eth mainnet upgrades, scheduled L2 sequencer maintenance) so you do not ship in the middle of someone else's incident.
153
+ 8. **Source code frozen** — the commit being deployed is tagged, the working tree is clean, and there is no in-flight PR that "we'll fold in real quick." A clean tree is the precondition for a reproducible verification.
154
+
155
+ Additional items worth treating as gates even though they are not strictly pre-flight:
156
+
157
+ - **Monitoring wired before the deploy lands.** Tenderly Alerts, OpenZeppelin Defender, or a custom indexer should already be configured for the (currently zero) contract address; flip them on the moment the deploy confirms. Catching an exploit ten minutes in is much better than catching it ten hours in.
158
+ - **Incident-response runbook ready.** Who pauses? Who calls the auditor? Who tweets? Decide before the deploy, not during the incident.
159
+ - **Block explorer pages bookmarked.** A panicked search for "Etherscan Vault address" during an incident is how the wrong contract gets paused.
160
+
161
+ ### Post-deploy hardening
162
+
163
+ The minutes after a mainnet deploy are the most dangerous part of a deploy. The deployer EOA may still hold transient privileges; the contract may not yet be verified; the addresses are not yet published; the protocol may be paused-but-fundable or unpaused-but-untested. MEV searchers and on-chain attackers run scripts that watch for new deployments at known protocols and probe for misconfigurations within seconds. Run these steps immediately, ideally as part of the same `forge script` invocation:
164
+
165
+ 1. **Transfer ownership to the multisig and verify on-chain** — the deploy script should pass the multisig to the constructor, not the deployer. If for any reason the deployer held admin transiently, transfer and verify with `hasRole(DEFAULT_ADMIN_ROLE, multisig) == true` before doing anything else.
166
+ 2. **Renounce `DEFAULT_ADMIN_ROLE` from the deployer EOA** — `renounceRole` in the same broadcast as the deploy. After this transaction lands, the deployer key is no longer a risk to the protocol. Verify with `hasRole(DEFAULT_ADMIN_ROLE, deployerEOA) == false`.
167
+ 3. **Set the timelock as upgrade-admin** — if the protocol is upgradeable, the proxy admin / `UPGRADER_ROLE` goes to the `TimelockController`, not directly to the multisig. See `web3-upgradeability.md`.
168
+ 4. **Verify on Etherscan** — if `--verify` was not part of the deploy run, do it now with `forge verify-contract`. An unverified mainnet contract is not deployed in any meaningful sense.
169
+ 5. **Publish the addresses to the README, protocol docs, and any subgraph/indexer configs** — together with the block number, commit SHA, and a link to the verified Etherscan page. Users and auditors should not have to ask which address is canonical.
170
+ 6. **Run an on-chain assertion script.** A short `forge script PostDeployCheck.s.sol` that reads back every role assignment and configuration parameter and `require`s the expected value. Run it immediately, and again 24 hours later — if anything diverged in the meantime, you want to know before the community does. This is the on-chain analogue of a smoke test.
171
+
172
+ A common failure mode: a team intends to do all five steps but completes only the first three before "the deploy worked, we'll polish later." The unverified contract sits on Etherscan for a week, the deployer EOA still holds admin, and the addresses are scattered across Slack messages. The fix is to script all five steps as part of `Deploy.s.sol` so they happen atomically — the deploy is not "done" until role-hardening and verification have both succeeded.
173
+
174
+ Worked example of the verification step inside the script — Foundry's cheatcodes let you assert post-deploy invariants in the same broadcast:
175
+
176
+ ```solidity
177
+ vm.stopBroadcast();
178
+
179
+ // Outside the broadcast — these are simulation reads, not transactions
180
+ require(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), adminMultisig), "admin not granted");
181
+ require(!vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), msg.sender), "deployer still admin");
182
+ require(vault.hasRole(vault.UPGRADER_ROLE(), timelock), "timelock not upgrader");
183
+ ```
184
+
185
+ A failing `require` after `vm.stopBroadcast()` means the script reverts and `forge script` exits non-zero — so a deploy that produced a misconfigured contract surfaces as a CI failure rather than a silent success. Wire the deploy script into a CI pipeline that gates merge-to-main on a successful Sepolia run, and the worst classes of "we forgot a role grant" bugs disappear before they can reach mainnet.
186
+
187
+ ### CREATE2 deterministic deploys
188
+
189
+ For protocols that need the **same address on every chain** — typically cross-chain messaging contracts, omnichain tokens, or routers that other protocols hard-code — use `CREATE2` via a deterministic factory. `CREATE2` computes the deployed address as `keccak256(0xff ++ factory ++ salt ++ keccak256(initcode))`, so identical bytecode + identical salt + identical factory address produces the identical contract address across chains. The canonical factory is Safe's `CreateCall` or the widely-used deterministic deployment proxy at `0x4e59b44847b379578588920cA78FbF26c0B4956C` (which itself exists at the same address on most EVM chains, bootstrapped via a presigned transaction). Foundry supports `CREATE2` via `new Contract{salt: SALT}(args)` inside a script:
190
+
191
+ ```solidity
192
+ bytes32 salt = keccak256("Vault.v1.mainnet");
193
+ Vault vault = new Vault{salt: salt}(adminMultisig);
194
+ ```
195
+
196
+ The constraint is that the initcode (including constructor args) must be byte-identical across chains — any chain-specific configuration must come from post-deploy setter calls, not constructor parameters. The salt is part of the public deployment scheme: document it alongside the deployed address so downstream protocols can independently verify the contract was deployed via the expected factory + salt. Vanity addresses (e.g. starting with multiple zero bytes for tiny gas savings on every call) come from grinding the salt against an off-chain miner; budget hours-to-days of GPU time for serious patterns.
197
+
198
+ Treat `CREATE2` as a specialist tool: most protocols do not need cross-chain address parity, and the operational overhead of locking the initcode is real. If your first deploy ever might want a same-address counterpart on another chain, deploy from a `CREATE2` factory from day one — converting later means either redeploying at a new address (breaking integrations) or accepting that the cross-chain story is asymmetric.
199
+
200
+ A common pattern is to pair `CREATE2` with **proxy** deployment: the deterministic factory deploys a minimal proxy at the cross-chain address, and the implementation contract (which can differ per chain) sits behind it. Argument-driven differences end up in proxy initialization (`initialize(...)`) calls that occur after the cross-chain `CREATE2` deploy. This separates "the address everyone integrates with" from "the chain-specific config the protocol needs," giving you the best of both worlds at the cost of one extra contract per chain. See `web3-upgradeability.md` for the proxy patterns.
201
+
202
+ ### Common pitfalls
203
+
204
+ A short list of mistakes that have shipped to mainnet at well-funded protocols, each of which the above discipline prevents:
205
+
206
+ - **Deploying with a hard-coded admin address that turned out to be a test wallet from a stale `.env`.** Always read from `vm.envAddress` and require non-zero, plus assert `code.length > 0` if the admin is meant to be a contract.
207
+ - **Forgetting to verify on Etherscan because "we'll do it tomorrow."** A week later the deploy is in production, the verified flag is still off, and users are filing support tickets asking whether the protocol is a scam.
208
+ - **Granting `DEFAULT_ADMIN_ROLE` to the deployer EOA and never renouncing.** Six months later the EOA is on a former employee's laptop. The fix is in the constructor: grant admin to the multisig directly.
209
+ - **Multi-chain deploys producing different addresses because someone bumped the compiler between chains.** Pin everything in `foundry.toml`, commit the lockfile-equivalent (`lib/` submodule SHAs), and re-run from the tagged commit.
210
+ - **Discovering after the fact that the Sepolia RPC was actually pointed at Holesky.** Validate `block.chainid` in the script.
211
+ - **Constructor arg encoding off by one nibble.** Use `cast abi-encode` and pipe directly to `--constructor-args`; never type the hex.
212
+ - **Hand-calling `grantRole` from Etherscan's "Write Contract" tab as a "quick fix."** Scripts are reviewable; one-off Etherscan writes are not. If the deploy script needed a follow-up, the follow-up is itself a script.
213
+
214
+ The common thread is that every one of these failures was preventable by a script, a `require`, or a checklist. Mainnet deploys are not a place to be clever; they are a place to be boring.
215
+
216
+ See `web3-project-structure.md` for the broadcast directory layout, `web3-access-control.md` for role-assignment patterns inside deploy scripts, `web3-upgradeability.md` for proxy-aware deploys and the proxy-admin handoff, and `web3-audit-workflow.md` for the pre-deploy quality gates that feed the pre-flight checklist.