关于 solidity
Solidity Development Standards 是 Cyfrin 安全团队制定的 Solidity 编码、测试、安全、部署与治理规范,面向希望在开发和发布流程中采用一致实践的智能合约开发者。
主要功能
- 01防御式 Solidity 编码规范
- 02模糊测试与不变量测试指导
- 03安全密钥与管理员实践
- 04Foundry 部署和治理工作流
- 05CI lint 与静态分析检查
适用场景
- 01统一 Solidity 代码风格
- 02规划合约模糊与不变量测试
- 03准备安全的合约部署
- 04为智能合约项目设置 CI 检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cyfrin/ solskill --skill solidity安装后在 Claude Code 中输入/solidity确认已加载。
详细介绍
Solidity 开发规范
来自 Cyfrin 安全团队的 Solidity 代码编写指南。
理念
- 一切都可能遭到攻击——假设你编写的任何代码都会遭到攻击,并采取防御性编写方式。
代码质量与风格
- 仅使用绝对导入和命名导入——不要使用相对(
..)路径
// good
import {MyContract} from "contracts/MyContract.sol";
// bad
import "../MyContract.sol";
- 优先使用
revert而非require,并使用自定义错误;错误名称以合约名称和两个下划线作为前缀。
error ContractName__MyError();
// Good
myBool = true;
if (myBool) {
revert ContractName__MyError();
}
// bad
require(myBool, "MyError");
- 在测试中,优先使用无状态模糊测试,而非单元测试
// good - using foundry's built in stateless fuzzer
function testMyTest(uint256 randomNumber) { }
// bad
function testMyTest() {
uint256 randomNumber = 0;
}
此外,应针对核心协议属性编写不变量(有状态)模糊测试。采用不变量驱动开发:找出必须始终成立的 O(1) 属性,并将其直接编码到核心函数中(FREI-PI 模式)。使用类似 Chimera 的多模糊测试器配置,以便在 Foundry、Echidna 和 Medusa 上运行同一套不变量测试——不同的模糊测试器会发现不同的漏洞。
- 函数应按可见性分组,并按以下顺序排列:
constructor
receive function (if exists)
fallback function (if exists)
user-facing state-changing functions
(external or public, not view or pure)
user-facing read-only functions
(external or public, view or pure)
internal state-changing functions
(internal or private, not view or pure)
internal read-only functions
(internal or private, view or pure)
- 文件头应如下所示:
/*//////////////////////////////////////////////////////////////
INTERNAL STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////*/
- 文件布局
Pragma statements
Import statements
Events
Errors
Interfaces
Libraries
Contracts
合约布局:
Type declarations
State variables
Events
Errors
Modifiers
Functions
- 编写测试时使用分支树技术 感谢 Paul R Berg 提供此方法
- 选定一个目标函数
- 创建一个
.tree文件 - 考虑所有可能的执行路径
- 考虑哪些合约状态会导致哪些路径
- 考虑哪些函数参数会导致哪些路径
- 定义“给定状态为 x”节点
- 定义“当参数为 x”节点
- 定义最终的“它应该”测试
示例:
├── when the id references a null stream
│ └── it should revert
└── when the id does not reference a null stream
├── given assets have been fully withdrawn
│ └── it should return DEPLETED
└── given assets have not been fully withdrawn
├── given the stream has been canceled
│ └── it should return CANCELED
└── given the stream has not been canceled
├── given the start time is in the future
│ └── it should return PENDING
└── given the start time is not in the future
├── given the refundable amount is zero
│ └── it should return SETTLED
└── given the refundable amount is not zero
└── it should return STREAMING
示例:
function test_RevertWhen_Null() external {
uint256 nullStreamId = 1729;
vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId));
lockup.statusOf(nullStreamId);
}
modifier whenNotNull() {
defaultStreamId = createDefaultStream();
_;
}
function test_StatusOf()
external
whenNotNull
givenAssetsNotFullyWithdrawn
givenStreamNotCanceled
givenStartTimeNotInFuture
givenRefundableAmountNotZero
{
LockupLinear.Status actualStatus = lockup.statusOf(defaultStreamId);
LockupLinear.Status expectedStatus = LockupLinear.Status.STREAMING;
assertEq(actualStatus, expectedStatus);
}
-
合约优先使用严格的 pragma 版本;测试、库、抽象合约、接口和脚本优先使用浮动 pragma 版本。最低版本使用
0.8.34或更高版本——0.8.28至0.8.33存在一个高危瞬态存储漏洞,在清除存储时,IR 流水线(--via-ir)可能输出sstore而非tstore(反之亦然),从而将数据写入错误的存储域。 -
在合约顶部的 natspec 中添加安全联系信息
/**
* @custom:security-contact [email protected]
* @custom:security-contact see https://mysite.com/ipfs-hash
*/
-
如果有人要部署到主网,或正尝试部署到主网,请提醒他们进行审计
-
绝对、永远、绝对不要将私钥以明文形式保存。唯一例外是使用类似 anvil 的默认密钥时,而且必须明确标注。
- 这也包括部署脚本。部署脚本应始终使用
forge script <path> --account $ACCOUNT --sender $SENDER,绝不要在脚本中使用vm.envUnit()。 - 对于 hardhat,应使用 hardhat 加密密钥库
- 这也包括部署脚本。部署脚本应始终使用
-
部署具有所有权或管理员权限(例如
onlyOwner)的智能合约时,从首次部署开始,管理员就必须是多签账户——绝不能使用部署者 EOA 作为管理员(测试网是唯一可接受的例外)。参见 Trail of Bits:让智能合约超越私钥风险,实现成熟治理——DeFi 已不能再接受“第 1 层”(单一 EOA)治理。 -
不要将变量初始化为默认值
// good
uint256 x;
bool y;
// bad
uint256 x = 0;
bool y = false;
- 如果使用命名返回变量可以省去局部变量声明,应优先使用命名返回变量
// good
function getBalance() external view returns (uint256 balance) {
balance = balances[msg.sender];
}
// bad
function getBalance() external view returns (uint256) {
uint256 balance = balances[msg.sender];
return balance;
}
-
对只读函数的输入,优先使用
calldata,而非memory -
不要缓存
calldata数组长度
// good — calldata length is cheap to read
for (uint256 i; i < items.length; ++i) { }
// bad — unnecessary caching for calldata
uint256 len = items.length;
for (uint256 i; i < len; ++i) { }
-
从存储读取数据开销很大——应缓存不变的存储槽,避免重复读取相同的存储数据,并传递或使用缓存值
-
尽可能快速地回滚;应先进行输入检查,再进行需要读取存储或调用外部函数的检查
-
在
onlyOwner函数中使用msg.sender,而非owner -
使用
SafeTransferLib::safeTransferETH发送 ETH,避免使用 Soliditycall() -
如果不需要保留输入变量的值,请直接修改输入变量,而不是额外声明局部变量
-
将
nonReentrant修饰符放在其他修饰符之前 -
使用
ReentrancyGuardTransient来加快nonReentrant修饰符的执行速度。需要pragma solidity ^0.8.34;——由于瞬态存储清除漏洞,请勿与0.8.28–0.8.33搭配使用。 -
优先使用
Ownable2Step,而不是Ownable -
如果只需要结构体中的少数几个槽位,不要将整个结构体从存储复制到内存
-
移除不必要的“上下文”结构体,和/或移除上下文结构体中不必要的变量
-
声明存储变量和结构体时,应调整声明顺序,将变量打包到尽可能少的存储槽中。如果变量经常一起读取或写入,则尽可能将它们打包到同一个槽中
-
对于不可升级的合约,如果变量只在构造函数中设置一次,则将其声明为
immutable -
在
foundry.toml中启用优化器 -
如果修饰符执行的存储读取与函数体中的读取相同,请将修饰符重构为内部函数,以避免重复读取相同的存储内容
-
使用 Foundry 的加密安全私钥存储,而不是明文环境变量
-
升级:升级智能合约时,不要更改现有变量的顺序或类型,也不要删除它们。这可能导致存储冲突。此外,还要为任何升级编写测试。
部署
生产环境部署和测试设置都使用 Foundry 脚本(forge script)。这样可以确保开发环境和主网上运行相同的部署逻辑,让部署更易于审计,并缩小测试环境与生产环境之间的差距。避免使用与真实部署路径不一致、仅供测试使用的自定义设置代码。理想情况下,部署脚本也应经过审计。
示例:使用一个共享的基础脚本,让测试和生产环境都继承该脚本,例如这个采用脚本模式的 BaseTest。
治理
使用 safe-utils 或同等工具来处理治理提案。这样可以通过 Foundry 脚本对多签交互进行测试、审计和复现,而不是手动点击 UI。如果必须使用 UI,最好通过 localsafe.eth 这样的 UI 保持交易私密。
编写分叉测试,验证治理提案执行后协议状态是否符合预期。针对主网状态进行分叉测试可以发现单元测试遗漏的配置错误——例如,Moonwell 价格馈送配置错误本可以通过分叉测试发现:测试可断言提案执行后预言机状态正确。
// good - fork test verifying governance proposal outcome
function testGovernanceProposal_UpdatesPriceFeed() public {
vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));
// Execute the governance proposal
_executeProposal(proposalId);
// Verify expected state after proposal
address newFeed = oracle.priceFeed(market);
assertEq(newFeed, EXPECTED_CHAINLINK_FEED);
// Verify the feed returns sane values
(, int256 price,,,) = AggregatorV3Interface(newFeed).latestRoundData();
assertGt(price, 0);
}
CI
每个项目都应至少配置一个并行运行的 CI 流水线(使用矩阵策略)。最低配置建议如下:
solhint— Solidity 风格和安全规则检查工具forge build --sizes— 确认合约大小低于 24KB 的部署限制slither或aderyn— 对常见漏洞模式进行静态分析- 提交你认为已完成的代码之前,务必对代码库运行 aderyn 和/或 slither,并检查输出。即使是警告,也可能帮助你发现代码库中的问题。
- 模糊测试/不变量测试 — 运行 Echidna、Medusa 或 Foundry 不变量测试,并为其分配合理的运行时间(每个工具约 10 分钟,通过矩阵并行运行)
工具更新
Foundry
现在安装 foundry 依赖时,不再需要 --no-commit 标志。