solskill
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSolidity Development Standards
Solidity 开发标准
Instructions for how to write solidity code, from the Cyfrin security team.
以下是来自Cyfrin安全团队的Solidity代码编写指南。
Philosophy
理念
- Everything will be attacked - Assume that any code you write will be attacked and write it defensively.
- 所有代码都会遭到攻击 — 假设你编写的任何代码都会被攻击,因此要采用防御性编程。
Code Quality and Style
代码质量与风格
- Absolute and named imports only — no relative () paths
..
solidity
// good
import {MyContract} from "contracts/MyContract.sol";
// bad
import "../MyContract.sol";- Prefer over
revert, with custom errors that are prefix'd with the contract name and 2 underscores.require
solidity
error ContractName__MyError();
// Good
myBool = true;
if (myBool) {
revert ContractName__MyError();
}
// bad
require(myBool, "MyError");- In tests, prefer stateless fuzz tests over unit tests
solidity
// good - using foundry's built in stateless fuzzer
function testMyTest(uint256 randomNumber) { }
// bad
function testMyTest() {
uint256 randomNumber = 0;
}Additionally, write invariant (stateful) fuzz tests for core protocol properties. Use invariant-driven development: identify O(1) properties that must always hold and encode them directly into core functions (FREI-PI pattern). Use a multi-fuzzing setup like Chimera to run the same invariant suite across Foundry, Echidna, and Medusa — different fuzzers find different bugs.
- Functions should be grouped according to their visibility and ordered:
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)- Headers should look like this:
solidity
/*//////////////////////////////////////////////////////////////
INTERNAL STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////*/- Layout of file
Pragma statements
Import statements
Events
Errors
Interfaces
Libraries
ContractsLayout of contract:
Type declarations
State variables
Events
Errors
Modifiers
Functions- Use the branching tree technique when creating tests Credit for this to Paul R Berg
- Target a function
- Create a file
.tree - Consider all possible execution paths
- Consider what contract state leads to what path
- Consider what function params lead to what paths
- Define "given state is x" nodes
- Define "when parameter is x" node
- Define final "it should" tests
Example:
├── 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 STREAMINGExample:
solidity
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);
}-
Prefer strict pragma versions for contracts, and floating pragma versions for tests, libraries, abstract contracts, interfaces, and scripts.
-
Add a security contact to the natspec at the top of your contracts
solidity
/**
* @custom:security-contact mycontact@example.com
* @custom:security-contact see https://mysite.com/ipfs-hash
*/ -
Remind people to get an audit if they are deploying to mainnet, or trying to deploy to mainnet
-
NEVER. EVER. NEVER. Have private keys be in plain text. The only exception to this rule is when using a default key from something like anvil, and it must be marked as such.
-
Whenever a smart contract is deployed that is ownable or has admin properties (like,), the admin must be a multisig from the very first deployment — never use the deployer EOA as admin (testnet is the only acceptable exception). See Trail of Bits: Maturing Your Smart Contracts Beyond Private Key Risk — "Layer 1" (single EOA) governance is no longer acceptable for DeFi.
onlyOwner -
Don't initialize variables to default values
solidity
// good
uint256 x;
bool y;
// bad
uint256 x = 0;
bool y = false;- Prefer using named return variables if this can omit declaring local variables
solidity
// 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;
}-
Preferinstead of
calldatafor read-only function inputsmemory -
Don't cachearray length
calldata
solidity
// 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) { }-
Reading from storage is expensive — prevent identical storage reads by caching unchanging storage slots and passing/using cached values
-
Revert as quickly as possible; perform input checks before checks which require storage reads or external calls
-
Useinstead of
msg.senderinsideownerfunctionsonlyOwner -
Useinstead of Solidity
SafeTransferLib::safeTransferETHto send ETHcall() -
Modify input variables instead of declaring an additional local variable when an input variable's value doesn't need to be preserved
-
Usemodifier before other modifiers
nonReentrant -
Usefor faster
ReentrancyGuardTransientmodifiersnonReentrant -
Preferinstead of
Ownable2StepOwnable -
Don't copy an entire struct from storage to memory if only a few slots are required
-
Remove unnecessary "context" structs and/or remove unnecessary variables from context structs
-
When declaring storage and structs, align the order of declarations to pack variables into the minimum number of storage slots. If variables are frequently read or written together, pack them in the same slot if possible
-
For non-upgradeable contracts, declare variables asif they are only set once in the constructor
immutable -
Enable the optimizer in
foundry.toml -
If modifiers perform identical storage reads as the function body, refactor modifiers to internal functions to prevent identical storage reads
-
Use Foundry's encrypted secure private key storage instead of plaintext environment variables
-
Upgrades: When upgrading smart contracts, do not change the order or type of existing variables, and do not remove them. This can lead to storage collisions. Also write tests for any upgrades.
- 仅使用绝对导入和命名导入 — 禁止使用相对()路径
..
solidity
// good
import {MyContract} from "contracts/MyContract.sol";
// bad
import "../MyContract.sol";- 优先使用而非
revert,并使用以合约名称加双下划线为前缀的自定义错误。require
solidity
error ContractName__MyError();
// Good
myBool = true;
if (myBool) {
revert ContractName__MyError();
}
// bad
require(myBool, "MyError");- 在测试中,优先使用无状态模糊测试而非单元测试
solidity
// 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)
用户可见的状态变更函数
(external或public,非view或pure)
用户可见的只读函数
(external或public,view或pure)
内部状态变更函数
(internal或private,非view或pure)
内部只读函数
(internal或private,view或pure)- 标题应采用如下格式:
solidity
/*//////////////////////////////////////////////////////////////
INTERNAL STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////*/- 文件布局
Pragma语句
导入语句
事件
错误
接口
库
合约合约布局:
类型声明
状态变量
事件
错误
修饰器
函数- 创建测试时使用分支树技术 此方法归功于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示例:
solidity
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版本。
-
在合约顶部的natspec中添加安全联系方式
solidity
/**
* @custom:security-contact mycontact@example.com
* @custom:security-contact see https://mysite.com/ipfs-hash
*/ -
提醒用户如果要部署到主网,务必进行审计。
-
绝对不要将私钥以明文形式存储。唯一的例外是使用anvil等工具的默认密钥,且必须明确标记。
-
当部署具有所有权或管理员属性(如)的智能合约时,从首次部署开始,管理员必须是多签钱包(multisig) — 绝对不要使用部署者EOA作为管理员(测试网是唯一可接受的例外)。详见Trail of Bits:让你的智能合约摆脱私钥风险 — 对于DeFi而言,"第一层"(单一EOA)治理已不再被接受。
onlyOwner -
不要将变量初始化为默认值
solidity
// good
uint256 x;
bool y;
// bad
uint256 x = 0;
bool y = false;- 如果可以省略声明局部变量,优先使用命名返回变量
solidity
// 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;
}-
对于只读函数输入,优先使用而非
calldatamemory -
不要缓存数组的长度
calldata
solidity
// 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.senderowner -
使用而非Solidity的
SafeTransferLib::safeTransferETH来发送ETHcall() -
当输入变量的值无需保留时,修改输入变量而非声明额外的局部变量
-
在其他修饰器之前使用修饰器
nonReentrant -
使用来实现更快的
ReentrancyGuardTransient修饰器nonReentrant -
优先使用而非
Ownable2StepOwnable -
如果只需要少数几个槽位,不要将整个结构体从存储复制到内存
-
移除不必要的"上下文"结构体,或从上下文结构体中移除不必要的变量
-
声明存储变量和结构体时,调整声明顺序以将变量打包到最少的存储槽中。如果变量经常被一起读取或写入,尽可能将它们打包到同一个槽中
-
对于不可升级的合约,如果变量仅在构造函数中设置一次,将其声明为
immutable -
在中启用优化器
foundry.toml -
如果修饰器与函数体执行相同的存储读取,将修饰器重构为内部函数以避免重复的存储读取
-
使用Foundry的加密安全私钥存储,而非明文环境变量
-
升级:升级智能合约时,不要更改现有变量的顺序或类型,也不要删除它们。这可能导致存储冲突。同时为任何升级编写测试用例。
Deployment
部署
Use Foundry scripts () for both production deployments and test setup. This ensures the same deployment logic runs in development and on mainnet, making deployments more auditable and reducing the gap between test and production environments. Avoid custom test-only setup code that diverges from real deployment paths. Ideally, your deploy scripts are audited as well.
forge scriptExample: use a shared base script that both tests and production inherit from, like this BaseTest using scripts pattern.
使用Foundry脚本()进行生产部署和测试环境搭建。这确保开发环境和主网使用相同的部署逻辑,使部署更易于审计,并缩小测试与生产环境之间的差距。避免使用与实际部署路径不同的自定义测试专用搭建代码。理想情况下,你的部署脚本也应经过审计。
forge script示例:使用一个共享的基础脚本,供测试和生产环境继承,例如这个使用脚本模式的BaseTest。
Governance
治理
Use safe-utils or equivalent tooling for governance proposals. This makes multisig interactions testable, auditable, and reproducible through Foundry scripts rather than manual UI clicks. If you must use a UI, it's preferred to keep your transactions private, using a UI like localsafe.eth.
Write fork tests that verify expected protocol state after governance proposals execute. Fork testing against mainnet state catches misconfigurations that unit tests miss — for example, the Moonwell price feed misconfiguration would have been caught by a fork test asserting correct oracle state post-proposal.
solidity
// 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);
}使用safe-utils或类似工具处理治理提案。这使得多签交互可通过Foundry脚本进行测试、审计和重现,而非手动点击UI。如果必须使用UI,建议使用localsafe.eth等UI以保持交易隐私。
编写分叉测试,验证治理提案执行后的预期协议状态。针对主网状态的分叉测试能发现单元测试遗漏的配置错误 — 例如,Moonwell价格馈送配置错误可通过断言提案后预言机状态正确的分叉测试发现。
solidity
// 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
Every project should have a minimum CI pipeline running in parallel (use a matrix strategy). Suggested minimum:
- — Solidity linter for style and security rules
solhint - — verify contract sizes are under the 24KB deployment limit
forge build --sizes - or
slither— static analysis for common vulnerability patternsaderyn- Before committing code that you think is done, be sure to run aderyn and/or slither on the codebase and inspect the output. Even warnings may lead you to find issues in the codebase.
- Fuzz/invariant testing — run Echidna, Medusa, or Foundry invariant tests with a reasonable time budget (~10 min per tool, in parallel via matrix)
每个项目都应至少有一个并行运行的CI流水线(使用矩阵策略)。建议的最低配置:
- — 用于风格和安全规则的Solidity代码检查工具
solhint - — 验证合约大小是否在24KB的部署限制内
forge build --sizes - 或
slither— 用于常见漏洞模式的静态分析工具aderyn- 在提交你认为已完成的代码之前,务必在代码库上运行aderyn和/或slither并检查输出。即使是警告也可能帮助你发现代码库中的问题。
- 模糊/不变式测试 — 运行Echidna、Medusa或Foundry不变式测试,并分配合理的时间预算(每个工具约10分钟,通过矩阵并行运行)