首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
70 阅读
3
How to receive a network packet in Linux
63 阅读
4
Maps and Memory Leaks in Go
54 阅读
5
C++ redis connection pool
52 阅读
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
登录
Search
标签搜索
web3
solidity
web3 security
c++
uniswapV3
redis
evm
uniswap
性能测试
k8s
wireguard
CNI
http
tailscale
nginx
linux
设计模式
Jericho
累计撰写
51
篇文章
累计收到
13
条评论
首页
栏目
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
页面
搜索到
2
篇与
的结果
2025-10-28
Openzeppelin-Access Control
一、Access Control访问控制——即“谁被允许做这件事”——在智能合约的世界中至关重要。您的合约的访问控制可能决定谁能铸造代币、对提案进行投票、冻结转账以及其他许多事情。因此,理解如何实现它是至关重要的,以免其他人窃取您的整个系统。1.Ownership and Ownable 所有权和 Ownable最常见和最基本的访问控制形式是所有权的概念:有一个账户是合约的 owner ,可以在其上执行管理任务。对于只有一个管理用户的合约来说,这种方法是完全可以接受的。OpenZeppelin Contracts 提供了 Ownable 来实现您合约中的所有权。// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract MyContract is Ownable { constructor(address initialOwner) Ownable(initialOwner) {} function normalThing() public { // anyone can call this normalThing() } function specialThing() public onlyOwner { // only the owner can call specialThing()! } }部署时, Ownable 合约的 owner 被设置为提供的 initialOwner 参数。Ownable 还允许你:将 transferOwnership 从所有者账户转移到新的账户,以及renounceOwnership让所有者放弃这个管理权限,在集中式管理阶段结束后,这是一个常见模式。完全移除所有者意味着受 onlyOwner 保护的管理任务将不再可调用!Ownable 是一种简单而有效的访问控制实现方式,但您应注意将所有权转移到一个无法再与此合约交互的错误账户所带来的风险。解决这个问题的一个替代方案是使用 Ownable2Step ;一种 Ownable 变体,要求新所有者通过调用 acceptOwnership 明确接受所有权转移。请注意,合约也可以是另一个合约的所有者!这为使用例如 Gnosis Safe、Aragon DAO 或您自己创建的完全自定义合约打开了大门。通过这种方式,您可以使用组合性为您的合约添加额外的访问控制复杂层。您不必使用单个常规以太坊账户(外部所有账户,或 EOA)作为所有者,而是可以使用由您的项目领导者运行的 2-of-3 多重签名,例如。该领域著名的项目,如 MakerDAO,使用类似此系统的方案。2.Role-Based Access Control 基于角色的访问控制虽然所有权的简单性对于简单系统或快速原型设计可能很有用,但通常需要不同级别的授权。您可能希望一个账户有权限从系统中禁止用户,但不创建新代币。基于角色的访问控制(RBAC)在这方面提供了灵活性。本质上,我们将定义多个角色,每个角色允许执行不同的操作集。例如,一个账户可能有“moderator”、“minter”或“admin”角色,然后您将检查这些角色,而不是简单地使用 onlyOwner 。这个检查可以通过 onlyRole 修饰符来强制执行。另外,您将能够定义规则,说明如何授予账户角色、撤销角色等。大多数软件使用基于角色的访问控制系统:一些用户是普通用户,一些可能是supervisors或者managers,而少数通常具有管理员权限。3.Using AccessControl 使用 `AccessControlOpenZeppelin Contracts 提供 AccessControl 用于实现基于角色的访问控制。其使用方法非常简单:对于您想要定义的每个角色,您将创建一个新的角色标识符,该标识符用于授予、撤销和检查一个账户是否具有该角色。这里有一个使用 AccessControl 在 ERC-20 代币中定义“铸造者”角色的简单示例,该角色允许拥有该角色的账户创建新的代币:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessControlERC20MintBase is ERC20, AccessControl { // Create a new role identifier for the minter role bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); error CallerNotMinter(address caller); constructor(address minter) ERC20("MyToken", "TKN") { // Grant the minter role to a specified account _grantRole(MINTER_ROLE, minter); } function mint(address to, uint256 amount) public { // Check that the calling account has the minter role if (!hasRole(MINTER_ROLE, msg.sender)) { revert CallerNotMinter(msg.sender); } _mint(to, amount); } }在使用 AccessControl 在您的系统上之前,请确保您完全理解其工作原理,或者从本指南中复制粘贴示例。虽然清晰明确,但这并不是我们无法通过 Ownable 实现的事情。事实上, AccessControl 的优势在于需要细粒度权限的场景,这可以通过定义多个角色来实现。让我们通过定义一个“销毁者”角色来增强我们的 ERC-20 代币示例,该角色允许账户销毁代币,并使用 onlyRole 修饰符:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessControlERC20Mint is ERC20, AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); constructor(address minter, address burner) ERC20("MyToken", "TKN") { _grantRole(MINTER_ROLE, minter); _grantRole(BURNER_ROLE, burner); } function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { _mint(to, amount); } function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { _burn(from, amount); } }太干净了!通过这种方式分离关注点,可以实现比简单的所有权方法访问控制更细粒度的权限级别。限制系统每个组件能够做什么被称为最小权限原则,这是一种良好的安全实践。请注意,每个账户仍然可以有多个角色,如果需要的话。4.Granting and Revoking Roles 授权和撤销角色上述的 ERC-20 代币示例使用了 _grantRole ,这是一个在程序化分配角色(如在构造期间)时非常有用的 internal 函数。但如果我们后来想将'minter'角色授予其他账户,该怎么办?默认情况下,具有角色的账户不能将其授予或撤销给其他账户:拥有角色仅意味着让 hasRole 检查通过。若要动态地授予和撤销角色,您需要角色的管理员协助。每个角色都有一个关联的 admin 角色,该角色拥有调用 grantRole 和 revokeRole 函数的权限。如果调用账户具有相应的 admin 角色,则可以通过这些函数来授予或撤销角色。为了便于管理,多个角色可以拥有相同的 admin 角色。一个角色的 admin 甚至可以是该角色本身,这将导致具有该角色的账户也能够授予和撤销该角色。这种机制可用于创建类似于组织结构的复杂权限结构,但它也提供了一种管理简单应用的方法。 AccessControl 包含一个特殊角色,称为 DEFAULT_ADMIN_ROLE ,它作为所有角色的默认管理员角色。具有此角色的账户将能够管理任何其他角色,除非使用 _setRoleAdmin 选择一个新的管理员角色。由于它默认是所有角色的管理员,实际上它也是自己的管理员,这个角色具有显著的风险。为了降低这种风险,我们提供了 AccessControlDefaultAdminRules ,它是 AccessControl 的一个推荐扩展,为这个角色添加了一系列强制安全措施:管理员被限制在一个账户上,并且需要经过一个带有步骤间延迟的两步转账程序。让我们看看 ERC-20 代币示例,这次利用默认的管理员角色:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessControlERC20MintMissing is ERC20, AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); constructor() ERC20("MyToken", "TKN") { // Grant the contract deployer the default admin role: it will be able // to grant and revoke any roles _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { _mint(to, amount); } function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { _burn(from, amount); } }请注意,与前面的示例不同,没有账户被授予“铸造者”或“销毁者”角色。然而,因为这些角色的管理员角色是默认管理员角色,并且该角色已授予 msg.sender ,该账户可以调用 grantRole 来授予铸造或销毁权限,并调用 revokeRole 来移除这些权限。动态角色分配通常是一个理想的特性,例如在信任度可能随时间变化的参与者的系统中。它还可以用于支持 KYC 等用例,其中角色持有者列表可能事先未知,或者包含在单个交易中可能成本过高。5.Querying Privileged Accounts 查询特权账户由于账户可能会动态地授予和撤销角色,因此无法总是确定哪些账户持有特定角色。这很重要,因为它允许证明系统的一些特定属性,例如管理账户是多签或 DAO,或者某个角色已被从所有用户中移除,从而有效地禁用任何相关功能。在底层, AccessControl 使用了 EnumerableSet ,这是 Solidity 的 mapping 类型的更强大的变体,它允许进行键枚举。可以使用 getRoleMemberCount 来获取具有特定角色的账户数量,然后调用 getRoleMember 来获取这些账户的地址。const minterCount = await myToken.getRoleMemberCount(MINTER_ROLE); const members = []; for (let i = 0; i < minterCount; ++i) { members.push(await myToken.getRoleMember(MINTER_ROLE, i)); }6.Delayed operation 延迟操作访问控制对于防止未经授权访问关键功能至关重要。这些功能可用于铸造代币、冻结转账或执行完全改变智能合约逻辑的升级。虽然 Ownable 和 AccessControl 可以防止未经授权的访问,但它们并未解决不良管理员攻击其自身系统损害用户利益的问题。这就是 TimelockController 所要解决的问题。TimelockController 是一个由提议者(proposers)和执行者(executors)管理的代理。当设置为智能合约的所有者/管理员/控制器时,它确保提议者所下达的任何维护操作都会受到延迟。这种延迟通过给予用户时间来审查维护操作,并在他们认为这样做符合自身利益时退出系统,从而保护智能合约的用户。7.Using TimelockController 使用 `TimelockController默认情况下,部署 TimelockController 的地址将获得对时间锁的管理权限。此角色赋予分配提议者、执行者和其他管理者的权利。配置 TimelockController 的第一步是为至少一个提议者和一个执行者分配角色。这些角色可以在构造时分配,或稍后由具有管理员角色的任何人分配。这些角色不是排他的,这意味着一个账户可以同时拥有这些角色。角色通过 AccessControl 接口进行管理,每个角色的 bytes32 值可以通过 ADMIN_ROLE 、 PROPOSER_ROLE 和 EXECUTOR_ROLE 常量访问。AccessControl 之上还有一个附加功能:将执行者角色赋予 address(0) ,在时间锁过期后,任何人都可以执行提案。虽然这个功能很有用,但应该谨慎使用。此时,由于已经分配了提议者和执行者,时间锁可以执行操作。一个可选的后续步骤是部署者放弃其管理权限,并将时间锁自我管理。如果部署者决定这样做,所有进一步的维护工作,包括指派新的提议者/调度者或更改时间锁持续时间,都必须遵循时间锁工作流程。这将时间锁的治理与连接到时间锁的合约的治理联系起来,并对时间锁维护操作实施延迟。如果部署者放弃管理权以支持时间锁本身,指派新的提议者或执行者将需要时间锁操作。这意味着如果负责这两个角色中的任何一个的账户变得不可用,那么整个合约(以及它控制的任何合约)将无限期锁定。在提议者和执行者角色分配完毕,并且时间锁负责其自身管理的情况下,您现在可以将任何合约的所有权/控制权转移给时间锁。推荐配置是将这两个角色授予一个安全的治理合约,例如 DAO 或多签合约,并额外将执行者角色授予由负责协助维护操作的人员持有的几个 EOA。这些钱包不能接管时间锁的控制,但可以帮助使工作流程更加顺畅。8.Minimum delay 最小延迟由 TimelockController 执行的操作不受固定延迟的限制,而是受最小延迟的限制。某些重大更新可能需要更长的延迟。例如,如果几天的时间就足以让用户审计一次铸造操作,那么在安排智能合约升级时,使用几周甚至几个月的延迟是合理的。最小延迟(可通过 getMinDelay 方法访问)可以通过调用 updateDelay 函数进行更新。请记住,只有时间锁本身才能访问此函数,这意味着此维护操作必须通过时间锁本身进行。9.Access Management 访问管理对于合约系统,使用 AccessManager 实例可以实现更好的角色集成管理。与分别管理每个合约的权限不同,AccessManager 将所有权限存储在一个合约中,使您的协议更容易进行审计和维护。尽管 AccessControl 比 Ownable 为您的合约添加权限提供了更动态的解决方案,但在集成新的合约实例后,去中心化协议往往会变得更加复杂,并且需要您在每个合约中分别跟踪权限。这增加了权限管理和监控的复杂性。生产系统中的权限管理协议通常需要更集成的替代方案,以应对通过多个 AccessControl 实例产生的碎片化权限。AccessManager 的设计围绕角色和目标功能的概念展开:角色以多对多的方式授予账户(地址),以实现灵活性。这意味着每个用户可以有一个或多个角色,多个用户可以拥有相同的角色。对受限目标函数的访问仅限于一个角色。目标函数由一个合约(称为目标)上的一个函数选择器定义。要授权一个调用,调用者必须拥有分配给当前目标函数(合约地址 + 函数选择器)的角色。10.Using AccessManager 使用 `AccessManagerOpenZeppelin Contracts 提供 AccessManager 用于跨任意数量合约管理角色。 AccessManager 本身是一个可以即部署即用的合约。它在构造函数中设置了一个初始管理员,该管理员将被允许执行管理操作。为了限制对合约某些函数的访问,您应该继承与管理者一起提供的 AccessManaged 合约。这提供了 restricted 修饰符,可用于保护任何对外部可见的函数。请注意,您必须在构造函数中指定 AccessManager 实例的地址( initialAuthority ),以便 restricted 修饰符知道使用哪个管理者来检查权限。这里是一个简单的 ERC-20 代币示例,它定义了一个 mint 函数,该函数受到 AccessManager 的限制:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessManagedERC20Mint is ERC20, AccessManaged { constructor(address manager) ERC20("MyToken", "TKN") AccessManaged(manager) {} // Minting is restricted according to the manager rules for this function. // The function is identified by its selector: 0x40c10f19. // Calculated with bytes4(keccak256('mint(address,uint256)')) function mint(address to, uint256 amount) public restricted { _mint(to, amount); } }在使用 AccessManager 之前或从本指南中复制粘贴示例之前,请确保您完全理解 AccessManager 的工作原理。一旦托管合约部署完成,它就处于管理者的控制之下。初始管理员可以将铸造者角色分配给一个地址,并允许该角色调用 mint 函数。例如,以下是用 Ethers.js 编写的 JavaScript 代码演示了这一点:const MINTER = 42n; // Roles are uint64 (0 is reserved for the ADMIN_ROLE) await manager.grantRole(MINTER, user, 0); await manager.setTargetFunctionRole( target, ['0x40c10f19'], // bytes4(keccak256('mint(address,uint256)')) MINTER );尽管每个角色都有其自身的功能权限列表,但每个角色成员( address )都有一个执行延迟,这将决定账户在执行需要其角色的功能时需要等待多长时间。延迟操作必须在 AccessManager 中先调用 schedule 函数,然后才能执行,无论是调用目标函数还是使用 AccessManager 的 execute 函数。此外,角色可以设置授权延迟,以防止立即添加成员。AccessManager 管理员可以按以下方式设置此授权延迟:const HOUR = 60 * 60; const GRANT_DELAY = 24 * HOUR; const EXECUTION_DELAY = 5 * HOUR; const ACCOUNT = "0x..."; await manager.connect(initialAdmin).setGrantDelay(MINTER, GRANT_DELAY); await manager.connect(initialAdmin).grantRole(MINTER, ACCOUNT, EXECUTION_DELAY);请注意,角色不定义名称。与 AccessControl 情况相反,角色被识别为数值,而不是在合约中以 bytes32 值硬编码。仍然可以使用 labelRole 函数通过角色标签来允许工具发现(例如用于角色探索)。await manager.labelRole(MINTER, "MINTER");由于 AccessManaged 的管理员可以修改所有权限,建议仅将一个管理员地址安全地保存在多重签名或治理层下。为此,初始管理员可以设置所有必需的权限、目标和函数,分配新的管理员,并最终放弃其管理员角色。为了改进事件响应协调,管理员模式允许完全关闭目标合约。当合约关闭时,所有对目标合约中受限目标函数的调用都将回滚。关闭和开启合约不会更改任何设置,无论是权限还是延迟。特别是,调用特定目标函数所需的角色不会被修改。这种模式适用于需要暂时关闭合约以评估紧急情况并重新配置权限的事件响应操作。const target = await myToken.getAddress();await manager.setTargetClosed(target, true);await manager.setTargetClosed(target, false);即使一个 AccessManager 为目标函数定义了权限,如果管理的合约实例没有使用 restricted 修饰符,或者其管理者不同,这些权限也不会被应用。11.Role Admins and Guardians 角色管理员和守护者AccessControl 合约的一个重要方面是角色成员不会授予或撤销角色。相反,它依赖于角色管理员的概念来授予和撤销。对于 AccessManager ,同样适用此规则,只有该角色的管理员能够调用 grant 和 revoke 函数。请注意,调用这些函数将受到执行角色管理员执行延迟的影响。此外, AccessManager 还为每个角色存储一个守护者作为额外的保护。这个守护者有能力取消任何角色成员已安排的、有执行延迟的操作。考虑一个角色将默认将其初始管理员和守护者设置为 ADMIN_ROLE ( 0 )。对 ADMIN_ROLE 的成员要小心,因为它作为每个角色的默认管理员和监护人。行为不当的监护人可以随意取消操作,影响 AccessManager 的运行。12.Manager configuration 管理器配置AccessManager 提供了一个内置界面,用于配置权限设置,其 ADMIN_ROLE 成员可以访问。此配置界面包括以下功能:使用 labelRole 函数为角色添加标签。为角色分配管理员和监护人,使用 setRoleAdmin 和 setRoleGuardian 。通过 setGrantDelay 设置每个角色的授权延迟。作为管理员,某些操作将需要延迟。类似于每个成员的执行延迟,一些管理员操作需要等待执行,并应遵循 schedule 和 execute 工作流程。更具体地说,这些延迟函数是用于配置特定目标合约设置的函数。这些函数应用的延迟可以通过管理员使用 setTargetAdminDelay 调整。延迟的管理员操作包括:更新 AccessManaged 合约的授权方使用 updateAuthority 。通过 setTargetClosed 关闭或打开目标。通过 setTargetFunctionRole 更改权限,以决定角色是否可以调用目标函数。13.Using with Ownable 与 Ownable 一起使用已经从 Ownable 继承的合约可以通过将所有权转移给管理员迁移到 AccessManager。之后,所有带有 onlyOwner 修饰符的函数调用都应该通过管理员的自 execute 函数进行,即使调用者不需要延迟。await ownable.connect(owner).transferOwnership(accessManager);14.Using with AccessControl 与 AccessControl 一起使用For systems already using AccessControl, the DEFAULT_ADMIN_ROLE can be granted to the AccessManager after revoking every other role. Subsequent calls should be made through the manager’s execute method, similar to the Ownable case.对于已经使用 AccessControl 的系统,在撤销所有其他角色后,可以将 DEFAULT_ADMIN_ROLE 授予 AccessManager 。后续调用应该通过管理员的自 execute 方法进行,类似于 Ownable 的情况。await accessControl.connect(admin).revokeRole(MINTER_ROLE, account);await accessControl.connect(admin).grantRole(DEFAULT_ADMIN_ROLE, accessManager);await accessControl.connect(admin).renounceRole(DEFAULT_ADMIN_ROLE, admin);
2025年10月28日
1 阅读
0 评论
1 点赞
2025-05-22
Solidity Style Guide
规范文档https://docs.soliditylang.org/en/v0.8.17/style-guide.html1.缩进每个缩进使用4个空格 应该避免使用空格和tab混合缩进2.间隔对于顶级声明来说应该使用两个空行Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract A { // ... } contract B { // ... } contract C { // ... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract A { // ... } contract B { // ... } contract C { // ... }合约中的函数间隔使用一个空行 抽象合约中的函数之间不使用空行Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; abstract contract A { function spam() public virtual pure; function ham() public virtual pure; } contract B is A { function spam() public pure override { // ... } function ham() public pure override { // ... } }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; abstract contract A { function spam() virtual pure public; function ham() public virtual pure; } contract B is A { function spam() public pure override { // ... } function ham() public pure override { // ... } }3.每行最大的长度推荐最大的长度为120个字符对于换行应该遵循的规则:第一个参数不应追加到左括号之后。只使用一个缩进。每个参数应独占一行。结束符); 应单独放在最后一行。对于函数调用的示例Yes:thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 );No:thisFunctionCallIsReallyLong(longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong(longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3);赋值语句示例Yes:thisIsALongNestedMapping[being][set][toSomeValue] = someFunction( argument1, argument2, argument3, argument4 );No:thisIsALongNestedMapping[being][set][toSomeValue] = someFunction(argument1, argument2, argument3, argument4);事件定义和事件触发示例Yes:event LongAndLotsOfArgs( address sender, address recipient, uint256 publicKey, uint256 amount, bytes32[] options ); LongAndLotsOfArgs( sender, recipient, publicKey, amount, options );No:event LongAndLotsOfArgs(address sender, address recipient, uint256 publicKey, uint256 amount, bytes32[] options); LongAndLotsOfArgs(sender, recipient, publicKey, amount, options);4.源代码文件编码最好使用 UTF-8 或 ASCII 编码5.Importsimports语句应该始终放在文件的顶部Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; import "./Owned.sol"; contract A { // ... } contract B is Owned { // ... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract A { // ... } import "./Owned.sol"; contract B is Owned { // ... }6.函数的放置顺序函数从前往后放置顺序constructorreceive function (if exists)fallback function (if exists)externalpublicinternalprivate对于同一优先级的函数, view and pure放在后面Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract A { constructor() { // ... } receive() external payable { // ... } fallback() external { // ... } // External functions // ... // External functions that are view // ... // External functions that are pure // ... // Public functions // ... // Internal functions // ... // Private functions // ... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract A { // External functions // ... fallback() external { // ... } receive() external payable { // ... } // Private functions // ... // Public functions // ... constructor() { // ... } // Internal functions // ... }7.表达式中的空格应该避免使用多余的空格 括号、方括号或花括号内,单行函数声明除外。Yes:spam(ham[1], Coin({name: "ham"}));No:spam( ham[ 1 ], Coin( { name: "ham" } ) );单行函数声明除外:function singleLine() public { spam(); }逗号和分号之前去除多余的空格Yes:function spam(uint i, Coin coin) public;No:function spam(uint i , Coin coin) public ;赋值和运算符周围的超过一个空格的也需要去除Yes:x = 1; y = 2; longVariable = 3;No:x = 1; y = 2; longVariable = 3;不要在receive和fallback函数名的地方增加多余的空格Yes:receive() external payable { ... } fallback() external { ... }No:receive () external payable { ... } fallback () external { ... }8.控制结构合约、库、函数和结构体主体应该和左括号在一行Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract Coin { struct Bank { address owner; uint balance; } }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract Coin { struct Bank { address owner; uint balance; } }if、while 和 for 关键词与左括号之间应该有一个空格,右括号和左花括号之间也应该有一个空格Yes:if (...) { ... } for (...) { ... }No:if (...) { ... } while(...){ } for (...) { ...;}控制结构中如果包含的语句只在一行时可以省略括号Yes:if (x < 10) x += 1;No:if (x < 10) someArray.push(Coin({ name: 'spam', value: 42 }));对于包含 else 或 else if 子句的 if 块,else 应该与 if 的右括号放在同一行Yes:if (x < 3) { x += 1; } else if (x > 7) { x -= 1; } else { x = 5; } if (x < 3) x += 1; else x -= 1;No:if (x < 3) { x += 1; } else { x -= 1; }9.函数定义对于比较短的函数定义,建议将函数体的左括号与函数定义保持在同一行。右括号的缩进级别应与函数定义相同。左括号前应有一个空格。Yes:function increment(uint x) public pure returns (uint) { return x + 1; } function increment(uint x) public pure onlyOwner returns (uint) { return x + 1; }No:function increment(uint x) public pure returns (uint) { return x + 1; } function increment(uint x) public pure returns (uint){ return x + 1; } function increment(uint x) public pure returns (uint) { return x + 1; } function increment(uint x) public pure returns (uint) { return x + 1;}函数的修饰符应该是按照如下顺序:VisibilityMutabilityVirtualOverride自定义 修饰符Yes:function balance(uint from) public view override returns (uint) { return balanceOf[from]; } function shutdown() public onlyOwner { selfdestruct(owner); }No:function balance(uint from) public override view returns (uint) { return balanceOf[from]; } function shutdown() onlyOwner public { selfdestruct(owner); }对于较长的函数定义,建议将每个参数单独放在一行,并与函数内语句保持相同的缩进级别。右括号和右括号也应单独放在一行,并与函数定义保持相同的缩进级别。Yes:function thisFunctionHasLotsOfArguments( address a, address b, address c, address d, address e, address f ) public { doSomething(); }No:function thisFunctionHasLotsOfArguments(address a, address b, address c, address d, address e, address f) public { doSomething(); } function thisFunctionHasLotsOfArguments(address a, address b, address c, address d, address e, address f) public { doSomething(); } function thisFunctionHasLotsOfArguments( address a, address b, address c, address d, address e, address f) public { doSomething(); }如果函数有修饰符 修饰符放在一列Yes:function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); } function thisFunctionNameIsReallyLong( address x, address y, address z ) public onlyOwner priced returns (address) { doSomething(); }No:function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); } function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); } function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); }多行输出参数和返回语句应遵循“最大行长度”部分中建议的换行长行的相同样式Yes:function thisFunctionNameIsReallyLong( address a, address b, address c ) public returns ( address someAddressName, uint256 LongArgument, uint256 Argument ) { doSomething() return ( veryLongReturnArg1, veryLongReturnArg2, veryLongReturnArg3 ); }No:function thisFunctionNameIsReallyLong( address a, address b, address c ) public returns (address someAddressName, uint256 LongArgument, uint256 Argument) { doSomething() return (veryLongReturnArg1, veryLongReturnArg1, veryLongReturnArg1); }对于继承的合约上的base需要参数的构造函数,如果函数很长或难以阅读,建议将base构造函数以与修饰符相同的方式放到多行。Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // Base contracts just to make this compile contract B { constructor(uint) { } } contract C { constructor(uint, uint) { } } contract D { constructor(uint) { } } contract A is B, C, D { uint x; constructor(uint param1, uint param2, uint param3, uint param4, uint param5) B(param1) C(param2, param3) D(param4) { // do something with param5 x = param5; } }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // Base contracts just to make this compile contract B { constructor(uint) { } } contract C { constructor(uint, uint) { } } contract D { constructor(uint) { } } contract A is B, C, D { uint x; constructor(uint param1, uint param2, uint param3, uint param4, uint param5) B(param1) C(param2, param3) D(param4) { x = param5; } } contract X is B, C, D { uint x; constructor(uint param1, uint param2, uint param3, uint param4, uint param5) B(param1) C(param2, param3) D(param4) { x = param5; } }对于简单的函数定义使用一行function shortFunction() public { doSomething(); }10.Mappings在变量声明中,不要用空格分隔关键字 mapping 和其类型。不要用空格分隔任何嵌套的 mapping 关键字和其类型。Yes:mapping(uint => uint) map; mapping(address => bool) registeredAddresses; mapping(uint => mapping(bool => Data[])) public data; mapping(uint => mapping(uint => s)) data;No:mapping (uint => uint) map; mapping( address => bool ) registeredAddresses; mapping (uint => mapping (bool => Data[])) public data; mapping(uint => mapping (uint => s)) data;11.变量定义定义数组的时候不要将类型和[]分开Yes:uint[] x;No:uint [] x;12.Other Recommendationsstring类型应该使用双引号而不是单引号Yes:str = "foo"; str = "Hamlet says, 'To be or not to be...'";No:str = 'bar'; str = '"Be yourself; everyone else is already taken." -Oscar Wilde';运算符周围应该有一个空格Yes:x = 3; x = 100 / 10; x += 3 + 4; x |= y && z;No:x=3; x = 100/10; x += 3+4; x |= y&&z;高优先级的运算符周围可以省略空格Yes:x = 2**3 + 5; x = 2*y + 3*z; x = (a+b) * (a-b);No:x = 2** 3 + 5; x = y+z; x +=1;13.布局顺序合约中的元素布局顺序Pragma statementsImport statementsInterfacesLibrariesContracts在合约中库、接口顺序Type declarationsState variablesEventsModifiersFunctions14.命名规范命名风格为了避免困惑,以下的将指代不同命名风格b (single lowercase letter)B (single uppercase letter)lowercaseUPPERCASEUPPER_CASE_WITH_UNDERSCORESCapitalizedWords (or CapWords)mixedCase (differs from CapitalizedWords by initial lowercase character!)Note在 CapWords 中使用首字母缩写时,请将首字母缩写的所有字母大写。因此,HTTPServerError 优于 HttpServerError。在 mixedCase 中使用首字母缩写时,请将首字母缩写的所有字母大写,但如果它是名称的开头,则保留第一个字母小写。因此,xmlHTTPRequest 优于 XMLHTTPRequest。避免使用l - Lowercase letter elO - Uppercase letter ohI - Uppercase letter eye切勿将这些字符用作单字母变量名。它们通常与数字 1 和 0 难以区分。合约和库名合约和库的名字应该使用类似 CapWords的风格. Examples: SimpleToken, SmartBank, CertificateHashRepository, Player, Congress, Owned.合约和库的名字应该匹配他们的文件名.如果一个合约文件包含了多个合约或者库,那么这个文件名应该以核心的合约命名。如果可以避免尽量不要这样。Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // Owned.sol contract Owned { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner); _; } function transferOwnership(address newOwner) public onlyOwner { owner = newOwner; } }Congress.sol:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; import "./Owned.sol"; contract Congress is Owned, TokenRecipient { //... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // owned.sol contract owned { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner); _; } function transferOwnership(address newOwner) public onlyOwner { owner = newOwner; } }Congress.sol:// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.7.0; import "./owned.sol"; contract Congress is owned, tokenRecipient { //... }结构体名字结构体名字应该使用 CapWords 风格. Examples: MyCoin, Position, PositionXY.事件名字事件名字应该使用CapWords 风格. Examples: Deposit, Transfer, Approval, BeforeTransfer, AfterTransfer.函数名字函数名字应该使用混合大小写. Examples: getBalance, transfer, verifyOwner, addMember, changeOwner.函数参数名函数参数应该使用缓和大小写.Examples: initialSupply, account, recipientAddress, senderAddress, newOwner.当编写对自定义结构进行操作的库函数时,该结构应该是第一个参数,并且应始终命名为“self”本地状态变量名使用大小写混合Examples: totalSupply, remainingSupply, balancesOf, creatorAddress, isPreSale, tokenExchangeRate.常量常量应该使用大写字母并且下划线分割 Examples: MAX_BLOCKS, TOKEN_NAME, TOKEN_TICKER, CONTRACT_VERSION.修饰符名使用大小写混合 Examples: onlyBy, onlyAfter, onlyDuringThePreSale.枚举类型名名字命名应该是使用CapWords风格. Examples: TokenGroup, Frame, HashStyle, CharacterLocation.15.NatSpec建议使用 NatSpec 对所有公共接口(ABI 中的所有内容)进行完整注释。// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; /// @author The Solidity Team /// @title A simple storage example contract SimpleStorage { uint storedData; /// Store `x`. /// @param x the new value to store /// @dev stores the number in the state variable `storedData` function set(uint x) public { storedData = x; } /// Return the stored value. /// @dev retrieves the value of the state variable `storedData` /// @return the stored value function get() public view returns (uint) { return storedData; } }Tag Context@titleA title that should describe the contract/interfacecontract, library, interface@authorThe name of the authorcontract, library, interface@noticeExplain to an end user what this doescontract, library, interface, function, public state variable, event@devExplain to a developer any extra detailscontract, library, interface, function, state variable, event@paramDocuments a parameter just like in Doxygen (must be followed by parameter name)function, event@returnDocuments the return variables of a contract’s functionfunction, public state variable@inheritdocCopies all missing tags from the base function (must be followed by the contract name)function, public state variable@custom:...Custom tag, semantics is application-definedeverywhere
2025年05月22日
4 阅读
0 评论
1 点赞