mcpField
S
solidity

solidity

github.com/cyfrin开发144179 次安装19 周前更新
代码仓库

关于 solidity

Solidity Development Standards 是 Cyfrin 安全团队制定的 Solidity 编码、测试、安全、部署与治理规范,面向希望在开发和发布流程中采用一致实践的智能合约开发者。

主要功能

  1. 01防御式 Solidity 编码规范
  2. 02模糊测试与不变量测试指导
  3. 03安全密钥与管理员实践
  4. 04Foundry 部署和治理工作流
  5. 05CI lint 与静态分析检查

适用场景

  1. 01统一 Solidity 代码风格
  2. 02规划合约模糊与不变量测试
  3. 03准备安全的合约部署
  4. 04为智能合约项目设置 CI 检查

安装

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

$npx skills add https://github.com/cyfrin/solskill --skill solidity

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

详细介绍

Solidity 开发规范

来自 Cyfrin 安全团队的 Solidity 代码编写指南。

理念

  • 一切都可能遭到攻击——假设你编写的任何代码都会遭到攻击,并采取防御性编写方式。

代码质量与风格

  1. 仅使用绝对导入和命名导入——不要使用相对(..)路径
// good
import {MyContract} from "contracts/MyContract.sol";

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

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

// bad
require(myBool, "MyError");
  1. 在测试中,优先使用无状态模糊测试,而非单元测试
// 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. 函数应按可见性分组,并按以下顺序排列:
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. 文件头应如下所示:
    /*//////////////////////////////////////////////////////////////
                      INTERNAL STATE-CHANGING FUNCTIONS
    //////////////////////////////////////////////////////////////*/
  1. 文件布局
Pragma statements
Import statements
Events
Errors
Interfaces
Libraries
Contracts

合约布局:

Type declarations
State variables
Events
Errors
Modifiers
Functions
  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

示例:

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. 合约优先使用严格的 pragma 版本;测试、库、抽象合约、接口和脚本优先使用浮动 pragma 版本。最低版本使用 0.8.34 或更高版本——0.8.28 至 0.8.33 存在一个高危瞬态存储漏洞,在清除存储时,IR 流水线(--via-ir)可能输出 sstore 而非 tstore(反之亦然),从而将数据写入错误的存储域。

  2. 在合约顶部的 natspec 中添加安全联系信息

/**
  * @custom:security-contact [email protected]
  * @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)的智能合约时,从首次部署开始,管理员就必须是多签账户——绝不能使用部署者 EOA 作为管理员(测试网是唯一可接受的例外)。参见 Trail of Bits:让智能合约超越私钥风险,实现成熟治理——DeFi 已不能再接受“第 1 层”(单一 EOA)治理。

  4. 不要将变量初始化为默认值

// good
uint256 x;
bool y;

// bad
uint256 x = 0;
bool y = false;
  1. 如果使用命名返回变量可以省去局部变量声明,应优先使用命名返回变量
// 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 数组长度

// 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. 从存储读取数据开销很大——应缓存不变的存储槽,避免重复读取相同的存储数据,并传递或使用缓存值

  2. 尽可能快速地回滚;应先进行输入检查,再进行需要读取存储或调用外部函数的检查

  3. 在 onlyOwner 函数中使用 msg.sender,而非 owner

  4. 使用 SafeTransferLib::safeTransferETH 发送 ETH,避免使用 Solidity call()

  5. 如果不需要保留输入变量的值,请直接修改输入变量,而不是额外声明局部变量

  6. 将 nonReentrant 修饰符放在其他修饰符之前

  7. 使用 ReentrancyGuardTransient 来加快 nonReentrant 修饰符的执行速度。需要 pragma solidity ^0.8.34;——由于瞬态存储清除漏洞,请勿与 0.8.28–0.8.33 搭配使用。

  8. 优先使用 Ownable2Step,而不是 Ownable

  9. 如果只需要结构体中的少数几个槽位,不要将整个结构体从存储复制到内存

  10. 移除不必要的“上下文”结构体,和/或移除上下文结构体中不必要的变量

  11. 声明存储变量和结构体时,应调整声明顺序,将变量打包到尽可能少的存储槽中。如果变量经常一起读取或写入,则尽可能将它们打包到同一个槽中

  12. 对于不可升级的合约,如果变量只在构造函数中设置一次,则将其声明为 immutable

  13. 在 foundry.toml 中启用优化器

  14. 如果修饰符执行的存储读取与函数体中的读取相同,请将修饰符重构为内部函数,以避免重复读取相同的存储内容

  15. 使用 Foundry 的加密安全私钥存储,而不是明文环境变量

  16. 升级:升级智能合约时,不要更改现有变量的顺序或类型,也不要删除它们。这可能导致存储冲突。此外,还要为任何升级编写测试。

部署

生产环境部署和测试设置都使用 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 标志。

评论

支持 Markdown