solidity

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Solidity 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

代码质量与风格

  1. Absolute and named imports only — no relative (
    ..
    ) paths
solidity
// good
import {MyContract} from "contracts/MyContract.sol";

// bad
import "../MyContract.sol";
  1. Prefer
    revert
    over
    require
    , with custom errors that are prefix'd with the contract name and 2 underscores.
solidity
error ContractName__MyError();

// Good
myBool = true;
if (myBool) {
    revert ContractName__MyError();
}

// bad
require(myBool, "MyError");
  1. 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.
  1. 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)
  1. Headers should look like this:
solidity
    /*//////////////////////////////////////////////////////////////
                      INTERNAL STATE-CHANGING FUNCTIONS
    //////////////////////////////////////////////////////////////*/
  1. Layout of file
Pragma statements
Import statements
Events
Errors
Interfaces
Libraries
Contracts
Layout of contract:
Type declarations
State variables
Events
Errors
Modifiers
Functions
  1. Use the branching tree technique when creating tests Credit for this to Paul R Berg
  • Target a function
  • Create a
    .tree
    file
  • 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 STREAMING
Example:
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);
}
  1. Prefer strict pragma versions for contracts, and floating pragma versions for tests, libraries, abstract contracts, interfaces, and scripts.
  2. 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
  */  
  1. Remind people to get an audit if they are deploying to mainnet, or trying to deploy to mainnet
  2. 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.
    • This includes in your deploy scripts. We should always use
      forge script <path> --account $ACCOUNT --sender $SENDER
      for our deploy scripts, and never use
      vm.envUnit()
      in our scripts.
    • For hardhat, you want to use hardhat encrypted keystores
  3. Whenever a smart contract is deployed that is ownable or has admin properties (like,
    onlyOwner
    ), 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.
  4. Don't initialize variables to default values
solidity
// good
uint256 x;
bool y;

// bad
uint256 x = 0;
bool y = false;
  1. 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;
}
  1. Prefer
    calldata
    instead of
    memory
    for read-only function inputs
  2. Don't cache
    calldata
    array length
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) { }
  1. Reading from storage is expensive — prevent identical storage reads by caching unchanging storage slots and passing/using cached values
  2. Revert as quickly as possible; perform input checks before checks which require storage reads or external calls
  3. Use
    msg.sender
    instead of
    owner
    inside
    onlyOwner
    functions
  4. Use
    SafeTransferLib::safeTransferETH
    instead of Solidity
    call()
    to send ETH
  5. Modify input variables instead of declaring an additional local variable when an input variable's value doesn't need to be preserved
  6. Use
    nonReentrant
    modifier before other modifiers
  7. Use
    ReentrancyGuardTransient
    for faster
    nonReentrant
    modifiers
  8. Prefer
    Ownable2Step
    instead of
    Ownable
  9. Don't copy an entire struct from storage to memory if only a few slots are required
  10. Remove unnecessary "context" structs and/or remove unnecessary variables from context structs
  11. 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
  12. For non-upgradeable contracts, declare variables as
    immutable
    if they are only set once in the constructor
  13. Enable the optimizer in
    foundry.toml
  14. If modifiers perform identical storage reads as the function body, refactor modifiers to internal functions to prevent identical storage reads
  15. Use Foundry's encrypted secure private key storage instead of plaintext environment variables
  16. 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.
  1. 仅使用绝对路径和命名导入 — 禁止使用相对路径(
    ..
solidity
// good
import {MyContract} from "contracts/MyContract.sol";

// bad
import "../MyContract.sol";
  1. 优先使用
    revert
    而非
    require
    ,并使用以合约名称加两个下划线为前缀的自定义错误。
solidity
error ContractName__MyError();

// Good
myBool = true;
if (myBool) {
    revert ContractName__MyError();
}

// bad
require(myBool, "MyError");
  1. 测试中优先使用无状态模糊测试而非单元测试
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上运行同一套不变量测试套件——不同的模糊测试工具能发现不同的漏洞。
  1. 函数应根据可见性分组并按以下顺序排列:
构造函数
receive函数(若存在)
fallback函数(若存在)
面向用户的状态变更函数
    (external或public,非view或pure)
面向用户的只读函数
    (external或public,view或pure)
内部状态变更函数
    (internal或private,非view或pure)
内部只读函数
    (internal或private,view或pure)
  1. 代码块标题应采用如下格式:
solidity
    /*//////////////////////////////////////////////////////////////
                      INTERNAL STATE-CHANGING FUNCTIONS
    //////////////////////////////////////////////////////////////*/
  1. 文件结构
编译指令(Pragma)
导入语句
事件定义
错误定义
接口
合约
合约内部结构:
类型声明
状态变量
事件
错误
修饰器
函数
  1. 创建测试时使用分支树技术 该方法由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);
}
  1. 合约优先使用严格版本的编译指令,测试、库、抽象合约、接口和脚本可使用浮动版本的编译指令。
  2. 在合约顶部的natspec注释中添加安全联系方式
solidity
/**
  * @custom:security-contact mycontact@example.com
  * @custom:security-contact see https://mysite.com/ipfs-hash
  */  
  1. 提醒用户,若要部署到主网或尝试部署到主网,必须进行审计。
  2. 绝对、绝对、绝对不要将私钥以明文形式存储。唯一的例外是使用anvil等工具的默认密钥,且必须明确标记。
    • 这也适用于部署脚本。我们应始终使用
      forge script <path> --account $ACCOUNT --sender $SENDER
      执行部署脚本,绝对不要在脚本中使用
      vm.envUnit()
    • 对于Hardhat,应使用Hardhat的加密密钥库
  3. 当部署具有所有权或管理员属性(如
    onlyOwner
    )的智能合约时,从首次部署开始,管理员必须是多签钱包(multisig)——绝对不要使用部署者EOA作为管理员(测试网是唯一可接受的例外)。详见Trail of Bits:让你的智能合约摆脱私钥风险 —— DeFi领域不再接受“Layer 1”(单一EOA)治理模式。
  4. 不要将变量初始化为默认值
solidity
// good
uint256 x;
bool y;

// bad
uint256 x = 0;
bool y = false;
  1. 如果可以省略局部变量声明,优先使用命名返回变量
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;
}
  1. 对于只读函数的输入参数,优先使用
    calldata
    而非
    memory
  2. 不要缓存
    calldata
    数组的长度
solidity
// good — calldata长度读取成本低
for (uint256 i; i < items.length; ++i) { }

// bad — 无需缓存calldata的长度
uint256 len = items.length;
for (uint256 i; i < len; ++i) { }
  1. 读取存储的成本很高——通过缓存不变的存储槽并传递/使用缓存值,避免重复读取相同的存储内容
  2. 尽早触发回滚;先执行输入检查,再执行需要读取存储或外部调用的检查
  3. onlyOwner
    函数中使用
    msg.sender
    而非
    owner
  4. 发送ETH时,优先使用
    SafeTransferLib::safeTransferETH
    而非Solidity原生的
    call()
  5. 当输入变量的值无需保留时,直接修改输入变量,而非声明额外的局部变量
  6. nonReentrant
    修饰器应放在其他修饰器之前
  7. 使用
    ReentrancyGuardTransient
    实现更快的
    nonReentrant
    修饰器
  8. 优先使用
    Ownable2Step
    而非
    Ownable
  9. 如果只需要结构体中的少数字段,不要将整个结构体从存储复制到内存
  10. 移除不必要的“上下文”结构体,或从上下文结构体中移除不必要的变量
  11. 声明存储变量和结构体时,调整声明顺序以将变量打包到最少的存储槽中。如果变量经常被一起读取或写入,尽可能将它们打包到同一个存储槽中
  12. 对于不可升级的合约,如果变量仅在构造函数中设置一次,将其声明为
    immutable
  13. foundry.toml
    中启用优化器
  14. 如果修饰器和函数体执行相同的存储读取操作,将修饰器重构为内部函数,避免重复读取存储
  15. 使用Foundry的加密安全私钥存储,而非明文环境变量
  16. 合约升级:升级智能合约时,不要更改现有变量的顺序或类型,也不要删除它们。这可能导致存储冲突。同时要为任何升级编写测试。

Deployment

部署

Use Foundry scripts (
forge script
) 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.
Example: 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:
  • solhint
    — Solidity linter for style and security rules
  • forge build --sizes
    — verify contract sizes are under the 24KB deployment limit
  • slither
    or
    aderyn
    — static analysis for common vulnerability patterns
    • 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流水线(使用矩阵策略)。建议的最低配置:
  • solhint
    — 用于风格与安全规则检查的Solidity代码检查工具
  • forge build --sizes
    — 验证合约大小不超过24KB的部署限制
  • slither
    aderyn
    — 用于检测常见漏洞模式的静态分析工具
    • 在提交你认为已完成的代码前,务必在代码库上运行aderyn和/或slither并检查输出结果。即使是警告也可能帮你发现代码库中的问题。
  • 模糊/不变量测试 — 运行Echidna、Medusa或Foundry不变量测试,并设置合理的时间预算(每个工具约10分钟,通过矩阵并行运行)

Tool updates

工具更新

Foundry

Foundry

To install foundry dependencies, you don't need the
--no-commit
flag anymore.
现在安装Foundry依赖时,不再需要
--no-commit
参数。