Solidity 编码规范

  • RareSkills
  • 更新于 2024-09-03 17:49
  • 阅读 363

我们应当遵循的那些 Solidity 编码规范

本文的目的是记录在代码审查或审计中出现的与官方 Solidity 风格指南 的常见偏差,而不是重新讨论该指南。这里的一些项目不在风格指南中,但却是 Solidity 开发者常见的风格错误。

前两行

1. 包含 SPDX-License-Identifier

当然,你的代码在没有它的情况下也能编译,但你会收到警告,所以请消除这个警告。

2. 固定 Solidity pragma,除非编写库

你可能见过如下的 pragma:

pragma solidity ^0.8.0;

pragma solidity 0.8.26;

你应该使用哪个,何时使用?如果你是编译和部署合约的人,你知道你正在使用的 Solidity 版本,因此为了清晰起见,你应该将 Solidity 版本固定为你正在使用的编译器。

另一方面,如果你正在为其他人扩展创建库,例如 OpenZeppelin 和 Solady 所做的,你不应该固定 pragma,因为你不知道最终用户将使用哪个编译器版本。

导入(Imports)

3. 在导入语句中显式设置库版本

而不是这样做:

import "@openzepplin/contracts/token/ERC20/ERC20.sol";

这样做:

import "@openzeppelin/contracts@4.9.3/token/ERC20/ERC20.sol";

你可以通过点击 GitHub 左侧的分支下拉菜单并点击标签,然后选择最新版本来获取最新版本。使用最新的干净(非 rc,即非发布候选)版本。

solidity library version

如果你不对导入进行版本控制,而底层库更新了,你的代码可能会无法编译或表现出意外行为。

4. 使用命名导入而不是导入整个命名空间

而不是这样做

import "@openzeppelin/contracts@4.9.3/token/ERC20/ERC20.sol";

这样做

import {ERC20} from "@openzeppelin/contracts@4.9.3/token/ERC20/ERC20.sol";

如果导入文件中定义了多个合约或库,你将污染命名空间。如果编译器优化器没有删除它,这将导致死代码(你不应该依赖这一点)。

5. 删除未使用的导入

如果你使用 智能合约安全工具 像 Slither,这将被自动捕获。但一定要删除这些。不要害怕删除代码。

合约级别

6. 应用合约级别的 natspec

natspec(自然语言规范)的目的是提供易于人类阅读的内联文档。

下面是合约的 natspec 示例。

/// @title Foo 协议的流动性代币
/// @author Foo Incorporated
/// @notice 非技术读者的说明
/// @dev 给理解 Solidity 的人的说明
contract LiquidityToken {

}

7. 按照风格指南布局合约结构

函数应首先按“外部性”排序,然后按“状态改变性”排序。

它们的顺序应如下:接收和回退函数(如果适用),外部函数,公共函数,内部函数和私有函数。

在这些组中,可支付函数放在顶部,然后是非可支付函数,然后是视图函数,然后是纯函数。

contract ProperLayout {

    // 类型声明,例如 using Address for address

    // 状态变量
    address internal owner;
    uint256 internal _stateVar;
    uint256 internal _starteVar2;

    // 定义事件
    event Foo();
    event Bar(address indexed sender);

    //  定义错误
    error NotOwner();
    error FooError();
    error BarError();

    // 修饰器
    modifier onlyOwner() {
        if (msg.sender != owner) {
            revert NotOwner();
        }
        _;
    }

    // 函数
    constructor() {

    }

    receive() external payable {

    }

    falback() external payable {

    }

    // 函数首先按以下顺序分组
    // - 外部
    // - 公共
    // - 内部
    // - 私有
    // 注意外部函数按它们可以修改或与状态交互的程度“降序”排列
    function foo() external payable {

    }

    function bar() external {

    }

    function baz() external view {

    }

    function qux() external pure {

    }

    // 公共函数
    function fred() public {

    }

    function bob() public view {

    }

    // 内部函数
    // 内部视图函数
    // 内部纯函数
    // 私有函数
    // 私有视图函数
    // 私有纯函数
}

常量

8. 用常量替换魔法数字

如果你看到数字 100 就静静地坐在代码中,那是什么?100 百分比?100 基点?

通常,数字应作为常量写在合约的顶部。

9. 如果数字用于测量以太或时间,请使用 Solidity 关键字

不写

uint256 secondsPerDay = 60 * 60 * 24;

这样做:

1 days;

而不是写

require(msg.value == 10**18 / 10, "must send 0.1 ether");

这样做

require(msg.value == 0.1 ether, "must send 0.1 ether");

10. 使用下划线使大数字更易读

不这样做

uint256 private constant BASIS_POINTS_DENOMINATOR = 10000

这样做

uint256 private constant BASIS_POINTS_DENOMINATOR = 10_000

函数

11. 从不会被重写的函数中删除 virtual 修饰符

virtual 修饰符意味着“可以被子合约重写”。但如果你知道你不会重写该函数(因为你是部署者),那么这个修饰符就是多余的。只需删除它。

12. 将函数修饰符按正确顺序放置:可见性、可变性、虚拟、重写自定义修饰符

以下是正确的:


// 可见性(可支付性),[virtual],[override],[自定义]

function foo() public payable onlyAdmin {

}

function bar() internal view virtual override onlyAdmin {

}

13. 正确使用 natspec

有时被称为“Solidity 注释风格”,它的正式名称是 natspec:

规则与合约 natspec 类似,除了我们还根据函数参数和返回值指定参数。

这可以是描述参数名称的好方法,而不使用冗长的参数变量。

/// @notice 存入 ERC20 代币
/// @dev 触发存款事件
/// @dev 如果代币未在白名单中,则会回滚
/// @dev 如果合约未获得 ERC20 的批准,则会回滚
/// @param token 要存入的 ERC20 代币的地址
/// @param amount 要存入的 ERC20 代币的数量
/// @returns 用户收到的流动性代币的数量
function deposit(address token, uint256 amount) public returns (uint256) {

}

// 如果合约继承了函数,你也可以继承它们的 natspec
/// @inheritdoc Lendable
function calculateAccumulatedInterest(address token, uint256 since) public override view returns (uint256 interest) {

}

对于开发者参数,最好通知它可以进行何种状态更改,例如触发事件、发送以太、自毁等。

notice 和 param natspec 会被 Etherscan 读取。

etherscan reading natspec

你可以在以下屏幕截图中查看 Etherscan 从哪里获得该信息,截图显示了 代码

solidity function with natspec

整洁性

14. 删除注释掉的代码

这应该是不言自明的。如果代码被注释掉,那就是杂乱。

15. 仔细考虑变量名称

命名是编写良好代码的更难方面之一,但这对可读性有很大帮助。

一些建议:

  • 避免使用“通用名词”,如“用户”,更精确的,例如“管理员”、“买家”、“卖家”。

  • “数据”这个词通常是模糊的指示。与其使用“userData”,不如用“userAccount”。

  • 不要用两个不同的名词来指代同一个现实世界实体。例如,如果“depositor”和“liquidityProvider”在现实世界中指的是同一个实体,就只使用一个术语,不要在代码中同时使用两个。

  • 在变量名称中包含单位。与其使用“interestRate”,不如 “interestRatesBasisPoints”或“feeInWei”。

  • 状态改变函数的名称中应包含动词。

  • 在区分内部变量和函数与覆盖状态变量的函数参数时,要一致使用下划线。如果在变量前加下划线表示“内部”,确保在其他上下文中不用于表示其他含义,例如,与状态变量同名的函数参数。

  • 使用“get”来查看数据,使用“set”来更改数据是一种广泛遵循的编程约定。考虑将其纳入。

  • 完成代码编写后,离开电脑,然后在 15 分钟后回来,问自己每个变量和函数名称是否尽可能精确。这种刻意的努力对你来说比任何检查表都更有帮助,因为你比任何人都更了解代码库的意图。

组织大型代码库的其他技巧

  • 如果你有很多存储变量,可以在一个合约中定义所有存储变量,然后从该合约继承以获得对这些存储变量的访问。

  • 如果你的函数需要大量参数,请使用结构体来传递信息。

  • 如果你需要很多导入,可以将所有文件和类型导入到一个 Solidity 文件中,然后导入该文件(你需要故意打破命名导入的规则)。

  • 使用库将同一类别的函数分组在一起,使文件更小。

组织大型代码库是一门艺术。学习它的最佳方法是研究大型成熟项目的代码库。

学习更多

此检查表用于我们的 solidity bootcamp 进行代码审查。

参加 OpenSpace 集训营 ,可以学习到更多高阶知识

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/