mcpField
S
solidity-coding

solidity-coding

github.com/0xlayerghost开发4134 次安装3 周前更新
代码仓库

关于 solidity-coding

面向编写 Solidity 合约的开发者,提供编码、初始化、安全性、事件设计和项目组织规范。它强调根据合约场景选择初始化方案与 OpenZeppelin 组件。

主要功能

  1. 01Solidity 版本与编码规范
  2. 02初始化方案比较及安全规则
  3. 03事件设计与命名约定
  4. 04合约目录和配置管理
  5. 05OpenZeppelin 组件选择

适用场景

  1. 01制定合约编码约定
  2. 02为合约选择初始化方式
  3. 03设计事件、错误和访问控制
  4. 04组织 Foundry 项目文件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/0xlayerghost/solidity-agent-kit --skill solidity-coding

安装后在 Claude Code 中输入/solidity-coding确认已加载。

附带文件references/complex-function-refactoring.mdreferences/shared-contract-behaviors.md

详细介绍

Solidity 编码规范

语言规则

  • 始终使用用户所用的语言回复。 如果用户用中文提问,就用中文回复;如果用英文提问,就用英文回复。

编码原则

  • Pragma:使用 pragma solidity ^0.8.19; — 在项目的所有文件中保持一致
  • 依赖项:使用 OpenZeppelin Contracts 4.9.x,通过 remappings.txt 管理导入
  • 错误处理:优先使用自定义错误,而非 require 字符串 — 这样可以节省 Gas,表达也更清晰
    • 定义:error InsufficientBalance(uint256 available, uint256 required);
    • 使用:if (balance < amount) revert InsufficientBalance(balance, amount);
  • 文档:所有 public / external 函数都必须包含 NatSpec(@notice、@param、@return)
  • 事件索引:仅对 address 类型参数添加 indexed — 若对其他类型添加索引,须附上注释
  • 特殊关键字:immutable / constant / unchecked / assembly 必须附带行内注释,说明使用原因

初始化策略

当合约涉及状态初始化(外部地址、业务参数、角色分配)时,不要默认采用任何单一方案。请遵循以下工作流:

工作流
  1. 评估:合约是否可升级?部署时是否已知所有依赖项?是否存在跨合约部署顺序约束?
  2. 展示选项:向开发者展示以下三种初始化方案及其权衡
  3. 让开发者选择:不要自行假定方案 — 不同项目和合约有不同需求
  4. 实施:按照下方列出的安全规则应用所选方案
方案对比
方案 A:仅构造函数方案 B:仅初始化器方案 C:混合
方式所有初始化参数都通过构造函数传入部署空合约,然后调用 initialize()构造函数处理已知参数;initialize() 处理部署后才能确定的依赖项
原子性部署失败 = 不会创建任何内容,不存在危险的部分初始化状态存在“已部署但未初始化”的风险窗口部分原子 — 构造函数部分安全,但初始化器部分存在风险窗口
抢跑风险无 — 构造函数无法被抢跑若未受保护,initialize() 可能被抢跑只有初始化器部分存在风险
Gas部署 + 初始化只需 1 笔交易部署交易 + N 笔初始化交易部署交易 + 1 笔初始化交易
灵活性低 — 部署时必须确定所有参数高 — 可在其他合约部署后设置依赖项中 — 在部署时和部署后之间拆分
可升级性与代理模式不兼容UUPS / Transparent Proxy 必须使用部分兼容 — 构造函数设置代理安全的参数,其余由初始化器处理
各方案的适用场景
场景建议方案
不可升级,且部署时已知所有依赖项A
可升级合约(UUPS / Transparent Proxy)B
不可升级,但部分依赖项稍后才部署(例如,LP 交易对在代币部署后创建)C
存在循环引用的多合约系统C
简单的工具类 / 库式合约A
需要在部署后由多签 / DAO 配置的合约B 或 C
安全规则(适用于所有方案)
  • 一次性锁定:任何仅应调用一次的 setter 都必须通过 require(currentValue == address(0)) 或 bool initialized 标志来强制执行——防止重新初始化
  • 禁止无保护的初始化函数:initialize() 函数必须设置访问控制(onlyOwner、initializer 修饰符或仅允许部署者调用的检查)
  • 优先使用不可变量:部署时已知的值应使用 immutable 而非普通存储——可节省每次读取的 Gas
  • 构造函数中不执行复杂的外部调用:构造函数不应调用外部合约执行状态变更操作(swap、addLiquidity)——此时合约尚未完全构造完成
  • 部署脚本的职责:使用方案 C 时,部署脚本必须在同一事务批次中或紧接着调用 initialize()——记录所需的调用顺序

事件设计模式

带原因标注的事件

当事件表示条件式/分支结果(同一操作可能因不同原因发生)时,添加 string reason 参数,让链上数据能够自我说明:

// 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);

适用场景:

  • 资金去向分叉:同一笔资金因不同条件流向不同地址
  • 操作被拒绝/降级:用户请求未完全满足,附带原因
  • 状态转换:同一个状态变更可能由不同触发条件引起
  • 管理员操作:记录原因(如 "security incident"、"parameter tuning")

不适用场景:

  • 事件只有一种触发路径(无分支)→ 不需要 reason
  • 高频事件(每笔 swap/transfer)→ string 会消耗额外 Gas,可用 uint8 reasonCode 代替
Reason 实现方式选择
场景方式Gas 成本
原因种类少且固定(≤5 种)string reason 字面量低(编译器会优化短字符串)
原因种类多或动态uint8 reasonCode + 前端映射表最低
需要附带动态数据拼接 string reason较高,慎用
// 方式 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);
通用事件设计规则
  • 每个改变状态的 external/public 函数必须至少触发一个事件
  • 事件参数应包含足够的信息,让前端/indexer 无需额外 RPC 调用即可重建完整上下文
  • NatSpec @param 必须列出 reason 参数的所有可能值

命名约定

元素约定示例
合约/库PascalCaseMyToken、StakingPool
接口I + PascalCaseIMyToken、IStakingPool
状态变量/函数lowerCamelCasetotalSupply、claimDividend
常量/不可变量UPPER_SNAKE_CASEMAX_SUPPLY、ROUTER_ADDRESS
事件PascalCase(过去时)TokenTransferred、PoolCreated
自定义错误PascalCaseInsufficientBalance、Unauthorized
函数参数setter 的参数以 _ 为前缀function setFee(uint256 _fee)
  • 禁止:拼音名称、单字母变量(循环中的 i/j/k 除外)、过度缩写
  • 返回值命名:当返回变量名与状态变量冲突时,使用语义前缀(is/can/has/current/total),而不是追加 _——让 ABI 对前端使用者保持简洁

代码组织规则

情况规则
跨合约常量放在 src/common/Const.sol
接口定义放在 src/interfaces/I<Name>.sol,与实现分开
共享的有状态行为放在 src/abstract/<Name>.sol 并通过继承复用,而不是复制实现
简单的链上查询使用 Foundry cast CLI(call/send)
复杂的多步骤操作使用 Foundry 脚本(*.s.sol)
导入风格使用具名导入:import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
共享管理功能

当多个合约需要执行相同的管理操作时,应保留一个规范的函数名称、参数顺序、事件、错误、NatSpec 和实现。添加新变体前,请先搜索代码库。

对于有状态行为或修饰符,应使用共享抽象合约;当外部使用者需要稳定的 ABI 时,应使用接口;仅对无状态逻辑使用库。业务合约 MUST 继承共享实现,而不是复制它。

有关项目规范的所有者控制资产追回函数及继承规则,请阅读 references/shared-contract-behaviors.md。项目政策有意将所有者视为最高权限方,并允许追回合约持有的所有 ERC20 代币和原生货币。

复杂函数重构

当一个函数协调多个业务阶段、将验证与多项状态更改或外部调用混在一起,或接近编译器的栈限制时,请先阅读 references/complex-function-refactoring.md,再进行修改。

不要仅为了增加函数数量而拆分小函数。除非用户明确要求改变行为,否则应保留外部 ABI、执行顺序、回滚行为、状态转换顺序、发出的事件和资金流不变量。

项目目录结构

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)

配置管理

  • config/*.json — 网络 RPC URL、合约地址、业务参数
  • deployments/latest.env — 最新部署的合约地址,每次部署后必须更新
  • foundry.toml — 编译器版本、优化器设置、重映射
  • 重要配置变更必须在 PR 描述中记录

OpenZeppelin 库选型标准

编写 Solidity 合约时,应优先使用久经实战检验的 OpenZeppelin 库,而不是自定义实现。根据场景选择合适的库:

访问控制
场景库导入路径
单一所有者管理Ownable@openzeppelin/contracts/access/Ownable.sol
所有权转移需要安全保障Ownable2Step@openzeppelin/contracts/access/Ownable2Step.sol
多角色权限(管理员/操作员/铸造者)AccessControl@openzeppelin/contracts/access/AccessControl.sol
需要枚举角色成员AccessControlEnumerable@openzeppelin/contracts/access/AccessControlEnumerable.sol
带时间锁延迟的治理TimelockController@openzeppelin/contracts/governance/TimelockController.sol
所有权模型选择

对于单一所有者合约,开发者 MUST 在生成代码前选择所有权模型:

选项行为适用情况
Ownable所有权转移在一笔交易中完成明确优先考虑简洁性和即时转移
Ownable2Step新所有者必须调用 acceptOwnership()优先考虑防止将所有权转移到错误地址

不要默认强制使用 Ownable2Step。如果项目尚未记录选择,请展示两个选项并让开发者选择。选定后,项目中的所有位置都应统一使用该模型,包括共享基类、部署脚本、测试和所有权转移命令。从一开始就生成所选模型对应的继承关系和导入,不要先生成 Ownable2Step 再让开发者手动替换。

规则:1 个角色 → 由开发者选择 Ownable 或 Ownable2Step;2 个或更多角色 → AccessControl;治理/DAO → TimelockController

安全防护
场景库用法
外部调用 / 代币转账ReentrancyGuard添加 nonReentrant 修饰符
需要紧急暂停Pausable为面向用户的函数添加 whenNotPaused;管理函数保持不暂停
ERC20 代币交互SafeERC20使用 safeTransfer / safeTransferFrom / safeApprove,不要使用原始调用

规则:任何转移代币或 ETH 的合约都必须使用 ReentrancyGuard + SafeERC20

代币标准
场景库说明
同质化代币ERC20基础标准
带销毁机制的代币ERC20Burnable添加 burn() 和 burnFrom()
带最大供应量上限的代币ERC20Capped强制执行 totalSupply <= cap
无 Gas 审批(EIP-2612)ERC20Permit节省用户审批交易的 Gas
治理投票代币ERC20Votes基于快照的投票权
NFTERC721基础 NFT 标准
带枚举功能的 NFTERC721Enumerable支持 tokenOfOwnerByIndex 查询
多代币(FT + NFT 混合)ERC1155游戏道具、批量操作
实用工具库
场景库用法
白名单 / 空投验证MerkleProof以低 Gas 成本验证 Merkle 树
签名验证ECDSA + EIP712链下签名 + 链上验证
自动递增 IDCounters生成代币 ID、订单 ID
批量函数调用Multicall在一笔交易中执行多项操作
地址集合 / uint 集合EnumerableSet可迭代集合,添加 / 删除 / 检查是否包含均为 O(1)
收益分成PaymentSplitter按份额拆分 ETH / 代币付款
标准化收益金库ERC4626DeFi 金库标准
合约升级
场景库说明
可升级合约(Gas 效率高)UUPSUpgradeable在实现合约中放置升级逻辑
可升级合约(管理员分离)TransparentUpgradeableProxy在代理合约中放置升级逻辑,Gas 成本较高
初始化器(替代构造函数)Initializable使用 initializer 修饰符代替构造函数

规则:新项目优先使用 UUPSUpgradeable;可升级合约始终使用 Initializable

预言机和链下服务
场景库说明
代币价格数据AggregatorV3Interface仅适用于具有受支持预言机数据馈送的代币
可验证随机数(彩票 / NFT)VRFConsumerBaseV2链上可证明公平的随机数
自动执行(定时任务)AutomationCompatible替代中心化 Keeper
跨链消息传递CCIP跨链代币 / 消息传输
库选择决策流程
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)
反面模式(请勿使用)
  • 请勿编写自定义 transfer 封装 — 使用 SafeERC20
  • 请勿编写自定义访问控制修饰符 — 使用 Ownable / AccessControl
  • 请勿编写自定义暂停逻辑 — 使用 Pausable
  • 请勿在 Solidity >= 0.8.0 中使用 SafeMath — 已内置溢出检查
  • 请勿使用 require(token.transfer(...)) — 通过 SafeERC20 使用 token.safeTransfer(...)
  • 请勿使用 tx.origin 进行身份验证 — 使用 Ownable / AccessControl 配合 msg.sender

MCP 辅助合约生成(如可用)

配置 OpenZeppelinContracts MCP 后,优先使用它生成基础合约,而不是从头编写:

合约类型MCP 工具使用场景
同质化代币solidity-erc20任何新的 ERC20 代币合约
NFTsolidity-erc721任何新的 NFT 合约
多代币solidity-erc1155游戏道具、批量操作
稳定币solidity-stablecoin符合 ERC20 标准的稳定币
现实世界资产solidity-rwa资产代币化
智能账户solidity-accountERC-4337 账户抽象
治理solidity-governorDAO 投票和提案
自定义solidity-custom使用 OZ 模式的非标准合约

工作流:MCP 生成基础合约 → 应用此技能的命名/结构规则 → 自定义业务逻辑 → 应用 /solidity-security 规则

为何使用 MCP 而非手动编写:MCP 输出经过验证,与 OZ Contracts Wizard 使用相同的规则集——导入、修饰符和安全检查均确保正确。手动编写可能遗漏导入或使用错误的 OZ 版本。

不应使用 MCP 的情况:采用大量自定义非标准模式的合约、不适用于任何 OZ 模板的合约,或需要从第一行开始精细控制的情况。

平稳降级:如果未配置 MCP,请回退到上述 Library Selection Standards,并遵循此技能的所有规则手动编写合约。

Foundry 快速参考

操作命令
创建新项目forge init <project-name>
安装依赖forge install openzeppelin-contracts
构建合约forge build
格式化代码forge fmt
更新 remappingsforge remappings

评论

支持 Markdown