Language Rule
- Always respond in the same language the user is using. If the user asks in Chinese, respond in Chinese. If in English, respond in English.
Coding Principles
- Pragma: Use
pragma solidity ^0.8.19;— keep consistent across all files in the project - Dependencies: OpenZeppelin Contracts 4.9.x, manage imports via
remappings.txt - Error Handling: Prefer custom errors over
requirestrings — saves gas and is more expressive- Define:
error InsufficientBalance(uint256 available, uint256 required); - Use:
if (balance < amount) revert InsufficientBalance(balance, amount);
- Define:
- Documentation: All
public/externalfunctions must have NatSpec (@notice,@param,@return) - Event Indexing: Only add
indexedtoaddresstype parameters — add comment if indexing other types - Special Keywords:
immutable/constant/unchecked/assemblymust have inline comment explaining why
Initialization Strategy
When a contract involves state initialization (external addresses, business parameters, role assignments), DO NOT default to any single approach. Follow this workflow:
Workflow
- Evaluate: Is the contract upgradeable? Are all dependencies known at deploy time? Are there cross-contract deployment order constraints?
- Present options: Show the developer the three initialization schemes below with their trade-offs
- Let the developer choose: Do not assume a scheme — different projects and contracts have different needs
- Implement: Apply the chosen scheme with the safety rules listed below
Scheme Comparison
| Scheme A: Constructor Only | Scheme B: Initializer Only | Scheme C: Hybrid | |
|---|---|---|---|
| Approach | All init in constructor params | Deploy empty, call initialize() after |
Constructor handles known params; initialize() handles post-deploy dependencies |
| Atomicity | Deploy fails = nothing exists, no dangerous half-init state | Risk of "deployed but not initialized" window | Partially atomic — constructor part is safe, initializer part has the window |
| Front-run risk | None — constructor cannot be front-run | initialize() can be front-run if unprotected |
Only the initializer portion has risk |
| Gas | One tx for deploy + init | Deploy tx + N init txs | Deploy tx + 1 init tx |
| Flexibility | Low — all params must be known at deploy time | High — can set dependencies after other contracts deploy | Medium — split between deploy-time and post-deploy |
| Upgradeable | Not compatible with proxy patterns | Required for UUPS / Transparent Proxy | Partial — constructor sets proxy-safe params, initializer for the rest |
When to Suggest Each Scheme
| Scenario | Suggested Scheme |
|---|---|
| Non-upgradeable, all dependencies known at deploy | A |
| Upgradeable contract (UUPS / Transparent Proxy) | B |
| Non-upgradeable, but some dependencies deploy later (e.g., LP pair created after token deploy) | C |
| Multi-contract system with circular references | C |
| Simple utility / library-style contract | A |
| Contract needs post-deploy configuration by multisig / DAO | B or C |
Safety Rules (Apply Regardless of Scheme)
- One-shot lock: Any setter that should only be called once must enforce
require(currentValue == address(0))or abool initializedflag — prevent re-initialization - No unprotected initializers:
initialize()functions must have access control (onlyOwner,initializermodifier, or deployer-only check) - Immutable preference: Values known at deploy time should use
immutableover regular storage — saves gas on every read - No complex external calls in constructor: Constructor should not call external contracts for state-changing operations (swap, addLiquidity) — the contract is not fully constructed yet
- Deploy script responsibility: When using Scheme C, the deploy script must call
initialize()in the same transaction batch or immediately after — document the required call sequence
Event Design Patterns
Reason-Annotated Events
When an event represents a conditional/branching outcome (the same action can happen for different reasons), add a string reason parameter to make on-chain data self-documenting:
// GOOD — on-chain data is self-explanatory
event WithdrawalFailed(address indexed user, uint256 amount, string reason);
emit WithdrawalFailed(user, amount, "insufficient balance");
emit WithdrawalFailed(user, amount, "cooldown not expired");
event ProposalRejected(uint256 indexed proposalId, string reason);
emit ProposalRejected(id, "quorum not reached");
emit ProposalRejected(id, "voting period ended");
// BAD — must read contract source to understand why
event WithdrawalFailed(address indexed user, uint256 amount);
Applicable scenarios:
- Fund destination branching: the same funds flow to different addresses under different conditions
- Operation rejected/downgraded: the user's request is not fully satisfied, with a reason attached
- State transitions: the same state change may be caused by different trigger conditions
- Admin operations: record why (e.g.
"security incident","parameter tuning")
Non-applicable scenarios:
- The event has only one trigger path (no branching) → no reason needed
- High-frequency events (every swap/transfer) → a string consumes extra gas; use
uint8 reasonCodeinstead
Choosing a Reason implementation approach
| Scenario | Approach | Gas cost |
|---|---|---|
| Few and fixed reason types (≤5) | string reason literal |
Low (compiler optimizes short strings) |
| Many or dynamic reason types | uint8 reasonCode + frontend mapping table |
Lowest |
| Dynamic data needs to be attached | string reason concatenation |
Relatively high, use with caution |
// 方式 A:string literal(推荐,可读性最好)
emit WithdrawalFailed(user, amount, "cooldown not expired");
// 方式 B:uint8 code(高频场景省 gas)
event SwapRejected(address indexed user, uint256 amount, uint8 reasonCode);
// 0 = slippage, 1 = insufficient liquidity, 2 = paused
emit SwapRejected(user, amount, 0);
General Event Design Rules
- Every state-changing
external/publicfunctionmust emit at least one event - Event parameters should contain enough information for the frontend/indexer without additional RPC callsto reconstruct the complete context
- NatSpec
@parammust list all possible values for the reason parameter
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Contract / Library | PascalCase | MyToken, StakingPool |
| Interface | I + PascalCase |
IMyToken, IStakingPool |
| State variable / Function | lowerCamelCase | totalSupply, claimDividend |
| Constant / Immutable | UPPER_SNAKE_CASE | MAX_SUPPLY, ROUTER_ADDRESS |
| Event | PascalCase (past tense) | TokenTransferred, PoolCreated |
| Custom Error | PascalCase | InsufficientBalance, Unauthorized |
| Function parameter | prefix _ for setter |
function setFee(uint256 _fee) |
- Forbidden: Pinyin names, single-letter variables (except
i/j/kin loops), excessive abbreviations - Return value naming: When a return variable name conflicts with a state variable, use a semantic prefix (
is/can/has/current/total) instead of appending_— keeps ABI clean for frontend consumers
Code Organization Rules
| Situation | Rule |
|---|---|
| Cross-contract constants | Place in src/common/Const.sol |
| Interface definitions | Place in src/interfaces/I<Name>.sol, separate from implementation |
| Shared stateful behavior | Place in src/abstract/<Name>.sol and inherit it instead of copying the implementation |
| Simple on-chain queries | Use Foundry cast CLI (call / send) |
| Complex multi-step operations | Use Foundry script (*.s.sol) |
| Import style | Use named imports: import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; |
Shared Administrative Functions
When the same administrative operation is needed by multiple contracts, keep one canonical function name, parameter order, event, errors, NatSpec, and implementation. Search the repository before adding a new variant.
Use a shared abstract contract for stateful behavior or modifiers, an interface when external consumers need a stable ABI, and a library only for stateless logic. Business contracts MUST inherit the shared implementation instead of copying it.
For the project's canonical owner-controlled asset recovery function and inheritance rules, read references/shared-contract-behaviors.md. The project policy intentionally treats the owner as the highest authority and permits recovery of every ERC20 token and native currency held by the contract.
Complex Function Refactoring
When a function coordinates multiple business stages, mixes validation with several state changes or external calls, or approaches compiler stack limits, read references/complex-function-refactoring.md before modifying it.
Do not split small functions merely to increase function count. Preserve the external ABI, execution order, revert behavior, state-transition ordering, emitted events, and fund-flow invariants unless the user explicitly requests a behavioral change.
Project Directory Structure
src/ — Contract source code
interfaces/ — Interface definitions (I*.sol)
abstract/ — Shared stateful base contracts (AssetRescue.sol, etc.)
common/ — Shared constants, types, errors (Const.sol, Types.sol)
test/ — Test files (*.t.sol)
script/ — Deployment & interaction scripts (*.s.sol)
config/ — Network config, parameters (*.json)
deployments/ — Deployment records (latest.env)
docs/ — Documentation, changelogs
lib/ — Dependencies (managed by Foundry)
Configuration Management
config/*.json— network RPC URLs, contract addresses, business parametersdeployments/latest.env— latest deployed contract addresses, must update after each deploymentfoundry.toml— compiler version, optimizer settings, remappings- Important config changes must be documented in the PR description
OpenZeppelin Library Selection Standards
When writing Solidity contracts, prioritize using battle-tested OpenZeppelin libraries over custom implementations. Select the appropriate library based on the scenario:
Access Control
| Scenario | Library | Import Path |
|---|---|---|
| Single owner management | Ownable |
@openzeppelin/contracts/access/Ownable.sol |
| Owner transfer needs safety | Ownable2Step |
@openzeppelin/contracts/access/Ownable2Step.sol |
| Multi-role permission (admin/operator/minter) | AccessControl |
@openzeppelin/contracts/access/AccessControl.sol |
| Need to enumerate role members | AccessControlEnumerable |
@openzeppelin/contracts/access/AccessControlEnumerable.sol |
| Governance with timelock delay | TimelockController |
@openzeppelin/contracts/governance/TimelockController.sol |
Ownership Model Selection
For a single-owner contract, the developer MUST choose the ownership model before code generation:
| Choice | Behavior | Use When |
|---|---|---|
Ownable |
Ownership transfer completes in one transaction | Simplicity and immediate transfer are explicitly preferred |
Ownable2Step |
New owner must call acceptOwnership() |
Protection against transferring ownership to the wrong address is preferred |
Do not silently force Ownable2Step. If the project has no recorded choice, present both options and let the developer choose. Once selected, use that model consistently across the project, including shared base contracts, deployment scripts, tests, and ownership-transfer commands. Generate the selected inheritance and imports from the start; do not generate Ownable2Step and leave the developer to replace it manually.
Rule: 1 role → developer chooses Ownable or Ownable2Step; 2+ roles → AccessControl; governance/DAO → TimelockController
Security Protection
| Scenario | Library | Usage |
|---|---|---|
| External call / token transfer | ReentrancyGuard |
Add nonReentrant modifier |
| Emergency pause needed | Pausable |
Add whenNotPaused to user-facing functions; keep admin functions unpaused |
| ERC20 token interaction | SafeERC20 |
Use safeTransfer / safeTransferFrom / safeApprove instead of raw calls |
Rule: Any contract that transfers tokens or ETH MUST use ReentrancyGuard + SafeERC20
Token Standards
| Scenario | Library | Notes |
|---|---|---|
| Fungible token | ERC20 |
Base standard |
| Token with burn mechanism | ERC20Burnable |
Adds burn() and burnFrom() |
| Token with max supply cap | ERC20Capped |
Enforces totalSupply <= cap |
| Gasless approval (EIP-2612) | ERC20Permit |
Saves users approve tx gas |
| Governance voting token | ERC20Votes |
Snapshot-based voting power |
| NFT | ERC721 |
Base NFT standard |
| NFT with enumeration | ERC721Enumerable |
Supports tokenOfOwnerByIndex queries |
| Multi-token (FT + NFT mixed) | ERC1155 |
Game items, batch operations |
Utility Libraries
| Scenario | Library | Usage |
|---|---|---|
| Whitelist / airdrop verification | MerkleProof |
Gas-efficient Merkle tree verification |
| Signature verification | ECDSA + EIP712 |
Off-chain sign + on-chain verify |
| Auto-increment IDs | Counters |
Token ID, order ID generation |
| Batch function calls | Multicall |
Multiple operations in one tx |
| Address set / uint set | EnumerableSet |
Iterable sets with O(1) add/remove/contains |
| Revenue sharing | PaymentSplitter |
Split ETH/token payments by shares |
| Standardized yield vault | ERC4626 |
DeFi vault standard |
Contract Upgrade
| Scenario | Library | Notes |
|---|---|---|
| Upgradeable contract (gas efficient) | UUPSUpgradeable |
Upgrade logic in implementation contract |
| Upgradeable contract (admin separated) | TransparentUpgradeableProxy |
Upgrade logic in proxy, higher gas |
| Initializer (replace constructor) | Initializable |
Use initializer modifier instead of constructor |
Rule: New projects prefer UUPSUpgradeable; always use Initializable for upgradeable contracts
Oracle and Off-Chain Services
| Scenario | Library | Notes |
|---|---|---|
| Token price data | AggregatorV3Interface |
Only for tokens with supported oracle data feeds |
| Verifiable randomness (lottery/NFT) | VRFConsumerBaseV2 |
On-chain provably fair random numbers |
| Automated execution (cron jobs) | AutomationCompatible |
Replace centralized keepers |
| Cross-chain messaging | CCIP |
Cross-chain token/message transfer |
Library Selection Decision Flow
Does contract handle user funds/tokens?
├── YES → Add ReentrancyGuard + SafeERC20
│ Does it need emergency stop?
│ ├── YES → Add Pausable
│ └── NO → Skip
└── NO → Skip
How many admin roles needed?
├── 1 role → Ask developer to choose Ownable or Ownable2Step
├── 2+ roles → AccessControl
└── DAO/governance → TimelockController
Does contract need price data?
├── Token has oracle feed → AggregatorV3Interface
├── No oracle feed → Custom TWAP with min-liquidity check
└── No price needed → Skip
Will contract need upgrades?
├── YES → UUPSUpgradeable + Initializable
└── NO → Standard deployment (immutable)
Anti-Patterns (Do NOT)
- Do NOT write custom
transferwrappers — useSafeERC20 - Do NOT write custom access control modifiers — use
Ownable/AccessControl - Do NOT write custom pause logic — use
Pausable - Do NOT use
SafeMathon Solidity >= 0.8.0 — overflow checks are built-in - Do NOT use
require(token.transfer(...))— usetoken.safeTransfer(...)viaSafeERC20 - Do NOT use
tx.originfor auth — usemsg.senderwithOwnable/AccessControl
MCP-Assisted Contract Generation (if available)
When OpenZeppelinContracts MCP is configured, prefer using it to generate base contracts instead of writing from scratch:
| Contract Type | MCP Tool | When to Use |
|---|---|---|
| Fungible token | solidity-erc20 |
Any new ERC20 token contract |
| NFT | solidity-erc721 |
Any new NFT contract |
| Multi-token | solidity-erc1155 |
Game items, batch operations |
| Stablecoin | solidity-stablecoin |
Stablecoin with ERC20 compliance |
| Real-world assets | solidity-rwa |
Asset tokenization |
| Smart account | solidity-account |
ERC-4337 account abstraction |
| Governance | solidity-governor |
DAO voting and proposals |
| Custom | solidity-custom |
Non-standard contracts with OZ patterns |
Workflow: MCP generates base → apply this skill's naming/structure rules → customize business logic → apply /solidity-security rules
Why MCP over manual: MCP output is validated against the same rule-set as OZ Contracts Wizard — imports, modifiers, security checks are guaranteed correct. Manual coding risks missing imports or using wrong OZ versions.
When NOT to use MCP: Heavily custom contracts with non-standard patterns, contracts that don't fit any OZ template, or when you need fine-grained control from line 1.
Graceful degradation: If MCP is not configured, fall back to the Library Selection Standards above and write contracts manually following all rules in this skill.
Foundry Quick Reference
| Operation | Command |
|---|---|
| Create new project | forge init <project-name> |
| Install dependency | forge install openzeppelin-contracts |
| Build contracts | forge build |
| Format code | forge fmt |
| Update remappings | forge remappings |