101项智能合约安全检查清单

智能合约安全检查清单

> * 原文:https://secureum.substack.com/p/smart-contract-security-101-secureum > * 译文出自:[登链翻译计划](https://github.com/lbc-team/Pioneer) > * 译者:[翻译小组](https://learnblockchain.cn/people/412) > * 校对:[Tiny 熊](https://learnblockchain.cn/people/15) > * 本文永久链接:[learnblockchain.cn/article…](https://learnblockchain.cn/article/2185) "[清单宣言.如何把事情做对](http://atulgawande.com/book/the-checklist-manifesto/) "是[Atul Gawande](http://atulgawande.com/about/)的一本书,他是著名的外科医生、作家和公共卫生领袖。[马尔科姆-格拉德威尔](https://en.wikipedia.org/wiki/Malcolm_Gladwell)在对这本书的评论中写道: > *Gawande首先区分了无知的错误(因为我们知道的不够多而犯的错误)和不称职的错误(因为我们没有正确利用我们所知道的东西而犯的错误)。他写道,现代世界的失败其实就是其中的第二种错误,他通过一系列医学的例子,告诉我们外科医生的日常工作是如何变得如此复杂,以至于出现这样或那样的错误几乎是不可避免的:对于一个原本称职的医生来说,错过一个步骤,或者忘记问一个关键问题,或者在当下的压力下,没有为每一个可能出现的情况做好计划,实在是太容易了。然后,Gawande拜访了飞行员和建造摩天大楼的人,并带回了一个解决方案。专家们需要检查清单--引导他们完成任何复杂程序中的关键步骤。在本书的最后一节,Gawande展示了他的研究团队是如何采用这一理念,开发出一份安全的手术清单,并将其应用于世界各地,并取得了惊人的成功*。 鉴于快速发展的以太坊基础设施(新平台、新语言、新工具和新协议)的复杂程度令人瞠目结舌,以及部署管理数百万美元的智能合约所带来的风险,我认为智能合约开发者/审核员的工作有点类似于上文提到的外科医生/飞行员/摩天大楼建筑师的工作。 虽然可能还没有生命危险(目前),但智能合约有很多的事情要做对,很容易漏掉一些检查,做出不正确的假设或没有考虑到潜在的情况。其结果是利用智能合约漏洞把资金耗尽,从而降低人们对这个未来无信任的去中心化基础设施的信任。因此,智能合约专家也需要检查清单。 本篇文章从不同来源整理了101个智能合约安全陷阱和最佳实践的清单。这份清单并非详尽无遗,它包括来自广泛参考来源的建议,如[Slither的 detector 文档](https://github.com/crytic/slither/wiki/Detector-Documentation)、[智能合约弱点分类注册表](https://swcregistry.io/)、[Solidity的已知漏洞列表](https://docs.soliditylang.org/en/v0.8.1/bugs.html)、[Sigma Prime](https://github.com/sigp/solidity-security-blog)、ConsenSys的[已知攻击](https://consensys.github.io/smart-contract-best-practices/known_attacks/)&[最佳实践](https://consensys.net/blog/blockchain-development/solidity-best-practices-for-smart-contract-security/)、[DASP](https://dasp.co/)以及来自[OpenZeppelin](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable)和[Trail of Bits](https://github.com/crytic/slither/wiki/Upgradeability-Checks)的可升级性检查。 该检查清单简要地描述了陷阱/建议,而没有详细解释其原理,并假设读者已经知道或者可参考列出的来源。我相信这份方便的 "安全智能合约检查清单 "将有助于开发者/审核员在以太坊上使用Solidity构建更安全和稳健的智能合约。 ## 智能合约安全检查清单 1. **Solidity版本**。使用非常老的Solidity版本,无法从错误修复和较新的安全检查中获益。使用最新版本可能会使合约容易受到未发现的编译器错误的影响。请考虑使用这些版本之一:*0.5.11-0.5.13、0.5.15-0.5.17、0.6.8或0.6.10-0.6.11.*(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity))。 2. **未锁定版本**。合约应使用与其测试过的编译器版本/标志去部署。锁定 pragma(例如在`pragma solidity 0.5.10`中不使用 *^* ) 可以确保合约不会意外地被部署到一个有未修正错误的旧编译器版本。(见[这里](https://swcregistry.io/docs/SWC-103)) 3. **同时使用多个Solidity pragma**。最好在所有合约中使用一个Solidity编译器版本,而不是使用有不同错误和安全检查的不同版本。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#different-pragma-directives-are-used)) 4. **不正确的访问控制**。执行关键逻辑的合约函数应通过地址检查(如所有者、控制器等)进行适当的访问控制,通常是在[修改器](https://learnblockchain.cn/docs/solidity/contracts.html#modifier)中进行的。缺少的检查将允许攻击者控制关键逻辑。(见[这里](https://docs.openzeppelin.com/contracts/3.x/api/access)和[这里](https://dasp.co/#item-2)) 5. **不受保护的提款函数**。调用未受保护的(*external*/*public*)函数向用户控制的地址发送以太币/代币,可能允许用户提取未经授权的资金。(见[这里](https://swcregistry.io/docs/SWC-105)) 6. **无保护地调用自毁**。用户/攻击者可能会误杀/故意销毁合约。必须控制对此类函数的访问。(见[此处](https://swcregistry.io/docs/SWC-106)) 7. **修改器的副作用:**修改器应该只进行状态检查,而不应该更改状态,以及违反[检查-修改-交互(check-effects-interactions)](https://learnblockchain.cn/docs/solidity/security-considerations.html#checks-effects-interactions)模式的外部调用。这些副作用可能会被开发者/审核员忽略,因为修改器的代码通常远离函数实现。(参见[这里](https://consensys.net/blog/blockchain-development/solidity-best-practices-for-smart-contract-security/)) 8. **不正确的修改器**。如果一个修改器没有执行*_*或*revert*,使用该修改器的函数将返回默认值,导致意外行为。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-modifier)) 9. **构造函数名称:**在*solc 0.4.22*之前,构造函数名称必须与包含它的合约同名。错误的命名不会使它成为一个构造函数,这对安全有影响。*solc 0.4.22*引入了*constructor*关键字。在*solc 0.5.0*之前,合约可以同时拥有旧式和新式的构造函数名,如果两者都存在,则第一个定义的构造函数名优先于第二个,这也导致了安全问题。*Solc 0.5.0*强制使用*constructor*关键字。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#multiple-constructor-schemes)和[这里](https://swcregistry.io/docs/SWC-118)) 10. **无效构造函数**:对基类合约构造函数的调用如果没有实现,会导致假设错误。检查构造函数是否实现,如果没有实现则删除调用。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#void-constructor)) 11. **隐式构造函数callValue检查**。合约没有定义构造函数,但有一个定义了构造函数的基类合约,在没有明确可支付构造函数的情况下,不要revert 返回非零的callValue的调用。这是由于*v0.4.5*中引入的一个编译器错误,在*v0.6.8*中得到了修复。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 12. **受控的委托调用:** *delegatecall()*或*callcode()*到用户控制的地址,将允许在调用者的状态下执行恶意合约。需确保此类调用的目标地址可信。(见[这里](https://swcregistry.io/docs/SWC-112)) 13. **重入漏洞**。不受信任的外部合约调用可能会回调自己的合约,导致意想不到的结果,如多次提款或失序事件。使用check-effects-interactions模式或进行重入保护。(见[此处](https://swcregistry.io/docs/SWC-107)) 14. **ERC777 回调和重入:**ERC777代币允许通过在代币转让期间调用的钩子进行任意回调。如果不使用重入防护,恶意合约地址可能会导致此类回调的重入攻击。(见[这里](https://quantstamp.com/blog/how-the-dforce-hacker-used-reentrancy-to-steal-25-million)) 15. **避免用 transfer() / send() 来作为缓解重入攻击的措施**。虽然*transfer()*和*send()*被推荐为防止重入攻击的最佳安全做法,因为它们只使用 2300 Gas ,但操作码的 Gas 重新定价可能会破坏已部署的合约。推荐使用*call()*代替,没有硬编码的 Gas 限制,以及使用”检查-效果-交互“模式或重入防护来保护重入攻击。(见[这里](https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/)和[这里](https://swcregistry.io/docs/SWC-134)) 16. **私有链上数据**。标明变量为*private*并不意味着不能在链上读取这些变量。私有数据不应该不加密地存储在合约代码或状态中,而应该加密或链外存储。(见[这里](https://swcregistry.io/docs/SWC-136)) 17. **弱PRNG**。依靠*block.timestamp*、*now*或*blockhash*的PRNG在一定程度上会受到矿工的影响,应避免使用。(见[这里](https://swcregistry.io/docs/SWC-120)) 18. **块值作为时间代理**: *block.timestamp*和*block.number*不是很好的时间代理,因为存在同步、矿工操纵和改变块时间的问题。(见[这里](https://swcregistry.io/docs/SWC-116)) 19. **整数上溢/下溢**。若不使用OpenZeppelin的SafeMath(或类似的库)检查溢出/下溢,如果用户/攻击者能够控制这种算术运算的整数操作数,可能会导致漏洞或意外行为。*Solc v0.8.0*为所有算术运算引入了默认的溢出/底溢检查。(见[这里](https://swcregistry.io/docs/SWC-101)和[这里](https://blog.soliditylang.org/2020/10/28/solidity-0.8.x-preview/)) 20. **先除后乘:**先乘后除一般比较好,以避免精度损失,因为Solidity整数除法可能会截断。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#divide-before-multiply)) 21. **交易顺序依赖:**可以通过监控mempool对特定的以太坊交易强制执行竞赛条件。例如,经典的ERC20 *approve()*变更可以使用这种方法进行前置。不要对交易顺序依赖性进行假设。(见[这里](https://swcregistry.io/docs/SWC-114)) 22. **ERC20 approve()竞赛条件:**使用OpenZepppelin的*SafeERC20*实现中的*safeIncreaseAllowance()*和*safeDecreaseAllowance()*来防止竞赛条件操纵授权金额。(见[这里](https://swcregistry.io/docs/SWC-114)) 23. **签名的可塑性**。*ecrecover*函数容易受到签名可塑性的影响,可能导致重放攻击。考虑使用OpenZeppelin的[ECDSA库](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol)。(参见[这里](https://swcregistry.io/docs/SWC-117)、[这里](https://swcregistry.io/docs/SWC-121)和[这里](https://medium.com/cryptronics/signature-replay-vulnerabilities-in-smart-contracts-3b6f7596df57)) 24. **ERC20 transfer()不返回boolean:** 用*solc > 0.4.22*编译的合约与这样的函数交互将回退。使用OpenZeppelin的SafeERC20封装器。(参见 [这里](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc20-interface) 和 [这里](https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca)) 25. **ERC721 ownerOf()不正确的返回值 :** 用*solc > 0.4.22*编译的合约与ERC721 *ownerOf()*交互,如果返回一个*bool*而不是*address*类型,将会回退。使用OpenZeppelin的ERC721合约。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc721-interface)) 26. **意外的以太币和this.balance**。合约可以通过*payable*函数、*selfdestruct()、coinbase*交易或创建前的预发送接收以太币。因此,依赖于*this.balance*的合约逻辑可以被操纵。(参见[这里](https://github.com/sigp/solidity-security-blog#3-unexpected-ether-1)和[这里](https://swcregistry.io/docs/SWC-132)) 27. **fallback与receive()**。检查是否考虑了*fallback*/*receive*函数的所有预防措施, 他们与与可见性、状态可变性和以太坊转账有微妙关系。(见 [这里](https://docs.soliditylang.org/en/latest/contracts.html#fallback-function) 和 [这里](https://docs.soliditylang.org/en/latest/contracts.html#receive-ether-function)) 28. **危险的==:**对代币/Ether使用严格等于可能会意外/恶意地导致意外行为。根据合约逻辑,考虑使用*>=*或*<=*代替*==*来处理此类变量。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities)) 29. **锁定以太币**。通过*payable*函数接受以太币但没有提款机制的合约将锁定以太币。需去掉*payable*属性或增加提款功能。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#contracts-that-lock-ether)) 30. **危险地使用tx.origin**。使用*tx.origin*进行授权可能会被MITM恶意合约滥用。请使用*msg.sender*代替。(见[此处](https://swcregistry.io/docs/SWC-115)) 31. **合约检查:**检查是否从外部自有账户(EOA)或合约账户调用,通常使用*extcodesize*检查。但在部署期间,合约还没有源代码时,可能会被合约规避。检查是否*tx.origin == msg.sender*是另一种选择。两者都有需要考虑的影响。(见 [这里](https://consensys.net/blog/blockchain-development/solidity-best-practices-for-smart-contract-security/)) 32. **删除在一个结构体的映射**。删除包含映射的结构体不会删除映射的内容,这可能会导致意想不到的后果。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#deletion-on-mapping-containing-a-structure)) 33. **恒真或恒假**:恒真(总是真)或恒假(总是假)可能有潜在的逻辑缺陷或多余的检查,例如,*x >= 0*,如果*x*是*uint*,则总是真。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#tautology-or-contradiction)) 34. **布尔常数**。在代码中使用布尔常数(*true/false*)(如判断条件),则表明逻辑有缺陷。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#misuse-of-a-boolean-constant)) 35. **布尔相等**。布尔变量可以直接在条件中检查,而不需要使用*true*/*false*的相等运算符。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#boolean-equality)) 36. **状态修改函数**。在*solc >=0.5.0*中,修改状态(在汇编中或其他)但被标记为*constant*/*pure*/*view*的函数会因为使用*STATICCALL*操作码而在*solc >=0.5.0*中回退(在之前的版本中工作)。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#constant-functions-using-assembly-code)) 37. **低级调用的返回值**。确保检查低级调用(*call*/*callcode*/*delegatecall*/*send*/等)的返回值,以避免意外失败。(见[这里](https://swcregistry.io/docs/SWC-104)) 38. **低级调用的账户存在性检查**。即使被调用的账户不存在,低级调用*call*/*delegatecall*/*staticcall*也返回true(根据EVM设计)。如果需要的话,必须在调用前检查账户是否存在。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls)) 39. **危险的覆盖:**局部变量、状态变量、函数、修改器或事件的名称会覆盖内置的Solidity符号,如*now*或其他来自当前作用域的声明,这些都会产生误导,并可能导致意外的使用和行为。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#builtin-symbol-shadowing)) 40. **危险的状态变量覆盖**。在派生合约中覆盖状态变量可能会对关键变量造成危险,如合约所有人owner(例如,基础合约中的修改器检查该变量,但变量被错误地覆盖了),以及合约错误地使用基础合约变量及覆盖变量。因此不要覆盖状态变量。(见[这里](https://swcregistry.io/docs/SWC-119)) 41. **本地变量的声明前使用**。在声明之前使用一个变量(无论是后来声明的还是在另一个作用域中声明的)会在*solc < 0.5.0*中导致意外行为,但*solc >= 0.5.0*实现了C99风格的作用域规则,其中变量只能在声明之后使用,并且只能在相同或嵌套的作用域中使用。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#pre-declaration-usage-of-local-variables)) 42. **循环内的高成本操作**。循环内的状态变量更新(使用SSTOREs)等操作会耗费大量 Gas ,成本很高,并可能导致Out-Of-Gas错误。最好使用局部变量进行优化。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#costly-operations-inside-a-loop)) 43. **循环内的调用:**循环内对外部合约的调用是危险的(特别是当循环索引可以由用户控制时),因为如果其中一个调用还原或执行耗尽 Gas ,可能导致DoS。避免在循环内调用,检查循环索引不能被用户控制或被约束。(参见[这里](https://swcregistry.io/docs/SWC-113)) 44. **块 Gas Limit 的DoS**。当执行 Gas 成本超过块 Gas 限制时,例如在未知大小的数组上循环等编程模式可能导致DoS。(见[此处](https://swcregistry.io/docs/SWC-128)) 45. **事件缺失**。应发出关键状态变化的事件(如所有者和其他关键参数),以便在链外跟踪这一事件。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-events-access-control)) 46. **未索引的事件参数**。某些事件的参数应被索引(如ERC20转移/批准事件),以便将其纳入区块的bloom过滤器,以便更快地访问。如果不这样做,可能会混淆寻找此类索引事件的链外工具。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#unindexed-erc20-event-oarameters)) 47. **库中事件签名不正确**。库中事件中使用的合约类型导致事件签名哈希值不正确。在哈希签名中没有使用 "address "类型,而是使用了实际的合约名称,导致日志中出现错误的哈希值。这是由于*v0.5.0*中引入的一个编译器错误,在*v0.5.8*中得到了修正(见[此处](https://docs.soliditylang.org/en/v0.8.1/bugs.html))。 48. **危险的单元表达式**。诸如*x =+ 1*这样的单元表达式很可能是程序员真正想使用*x += 1*的错误表达。单元 *+*运算符在*solc v0.5.0*中已被废弃(参见[这里](https://swcregistry.io/docs/SWC-129))。 49. **缺少零地址验证**。地址类型参数的设置者应包括零地址检查,否则合约函数可能无法访问或代币可能永远烧毁。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-zero-address-validation)) 50. **关键地址变更**。更改合约中的关键地址应分两步进行,第一步交易(从旧地址/当前地址)登记新地址(即授予所有权),第二步交易(从新地址)用新地址取代旧地址(即要求所有权)。这样就有机会从第一步错误使用的错误地址中恢复过来。否则,合约函数可能会变得无法访问。(见[此处](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/1488)和[此处](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2369)) 51. **assert()状态改变**。根据最佳做法,*assert()*语句中的不变性检查不应修改状态。应使用*require()*进行此类检查。(见[这里](https://swcregistry.io/docs/SWC-110)) 52. **require() vs assert():** *require()*应该用于检查输入和返回值的错误条件,而*assert()*应该用于不变性检查。在*solc 0.4.10*和*0.8.0*之间,*require()*使用*REVERT* (*0xfd*)操作码,在失败时退还剩余 Gas ,而*assert()*使用INVALID (*0xfe*)操作码,消耗了所有提供的 Gas 。(见[这里](https://docs.soliditylang.org/en/v0.8.1/control-structures.html#error-handling-assert-require-revert-and-exceptions)) 53. **过时的关键字**。使用过时的函数/运算符,如*block.blockhash()*为*blockhash()*,msg.gas为*gasleft(),throw*为*revert()*,*sha3()*为*keccak256()*,*callcode()*为*delegatecall(),suicide()*为*selfdestruct(),constant为*view*或*var应为*准确的类型名*,应避免使用这些过时的函数/操作,以防止在新的编译器版本中出现意外错误。(见[这里](https://swcregistry.io/docs/SWC-111)) 54. **函数默认可见性**: 在*solc < 0.5.0*中,没有指定可见性类型指定符时,则函数默认为*public*。这可能导致一个漏洞,恶意用户可能会进行未经授权的状态更改。*solc >= 0.5.0* 需要显式函数可见性指定符。(参见[这里](https://swcregistry.io/docs/SWC-100)) 55. **继承顺序不正确**。从具有相同函数的多个合约继承的合约应规定正确的继承顺序,即从一般到具体,以避免继承错误的函数实现。(见[此处](https://swcregistry.io/docs/SWC-125)) 56. **继承缺失**。一个合约可能看起来(根据名称或实现的函数)继承自另一个接口或抽象合约,但实际上并没有这样做。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-inheritance)) 57. **Gas 不足**。交易中继者需要得到信任,才能为交易成功提供足够的 Gas 。(见[此处](https://swcregistry.io/docs/SWC-126)) 58. **修改引用类型参数**。作为参数传递给函数的结构体/数组/映射可以是由数据位置指定的值类型(memory)或引用类型(storage)(在*solc 0.5.0*之前是可选的)。确保在函数参数中正确使用memory和storage,并显式表达所有数据位置。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#modifying-storage-array-by-value)) 59. **用函数类型变量进行任意跳转:**在汇编操作中应谨慎处理和避免函数类型变量,以防止跳转到任意代码位置。(见[这里](https://swcregistry.io/docs/SWC-127)) 60. **有多个可变长度参数的散列碰撞**。在某些情况下,使用*abi.encodePacked()*与多个可变长度参数一起使用可能会导致哈希碰撞。不要允许用户访问*abi.encodePacked()*中使用的参数。使用固定长度的数组或使用*abi.encode()*。(见[这里](https://swcregistry.io/docs/SWC-133)和[这里](https://docs.soliditylang.org/en/v0.5.3/abi-spec.html#non-standard-packed-mode)) 61. **脏的高阶位带来的可塑性风险**。没有占满32个字节的类型可能包含 "脏高阶位",这不会影响对类型的操作,但对*msg.data*会产生不同的结果。(见[这里](https://docs.soliditylang.org/en/v0.8.1/security-considerations.html#minor-details)) 62. **汇编中移位不正确**。Solidity汇编中的移位运算符(*shl(x, y)*, *shr(x, y)*, *sar(x, y)*)在*y*上应用*x*位的移位运算,而不是相反,这可能会引起混淆。检查移位操作中的值是否颠倒。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-shift-in-assembly)) 63. **汇编的使用**。使用EVM 汇编容易出错,应避免使用或仔细检查其正确性。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#assembly-usage)) 64. **右向左覆盖控制字符(U+202E)**。恶意行为者可以使用 "右向左覆盖 "unicode字符来强制渲染RTL文本,并使用户混淆合约的真实意图。U+202E 字符不应该出现在智能合约的源代码中。(见[这里](https://swcregistry.io/docs/SWC-130)) 65. **常量**。不变的状态变量应声明为常量,以节省 Gas 。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant)) 66. **类似的变量名称**。名称相似的变量可能会相互混淆,因此应避免使用。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#variable-names-too-similar)) 67. **未初始化的状态/局部变量**。未初始化的状态/局部变量被编译器分配为零值,可能会造成意想不到的结果,例如将token转移到零地址。应明确初始化所有状态/局部变量。(参见 [这里](https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-state-variables) 和 [这里](https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-local-variables)) 68. **未初始化的存储指针:**未初始化的本地存储变量可能指向合约中意想不到的存储位置,从而导致漏洞。*Solc 0.5.0*及以上版本不允许这种指针。(见[此处](https://swcregistry.io/docs/SWC-109)) 69. **在构造函数中调用未初始化的函数指针:**由于编译器错误,在用*solc*版本*0.4.5-0.4.25*和*0.5.0-0.5.7*编译的合约的构造函数中调用未初始化的函数指针会导致意外行为。(见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-function-pointers-in-constructors)) 70. **很长的数字常量**。应仔细检查有许多数字的数字常量,因为它们容易出错。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#too-many-digits)) 71. **超出范围的枚举:** *Solc < 0.4.5* 对超出范围的枚举产生了意外的行为*.* 检查枚举转换或使用更新的编译器。(参见[这里](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-enum-conversion)) 72. **未调用的public函数**。从未在合约内调用的*public*函数应宣布为*external*,以节省gas。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external)) 73. **死代码/无法到达的代码**。死代码可能表明程序员出错、逻辑缺失或潜在的优化机会,需要标记出来予以删除或适当处理。(见[此处](https://en.wikipedia.org/wiki/Dead_code)) 74. **未使用的返回值**。函数调用中未使用的返回值表明程序员错误,可能会产生意外行为。(见[此处](https://github.com/crytic/slither/wiki/Detector-Documentation#unused-return)) 75. **未使用的变量**。未使用的状态/局部变量可能表明程序员出错、逻辑缺失或潜在的优化机会,需要标记出来予以删除或适当处理。(见[此处](https://swcregistry.io/docs/SWC-131)) 76. **多余的语句**。没有效果但不产生代码的语句可能表明程序员出错或逻辑缺失,需要标明删除或适当处理。(见[此处](https://swcregistry.io/docs/SWC-135)) 77. **使用 ABIEncoderV2 带符号整数的存储数组**。将带符号整数数组分配给不同类型的存储数组可能导致数组中的数据损坏。这是由于*v0.4.7*中引入的一个编译器错误,并在*v0.5.10*中得到修复。(见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 78. **动态构造函数参数被ABIEncoderV2**。当使用ABIEncoderV2时,包含动态大小数组的结构体或数组的合约构造函数会回退或解码为无效数据。这是由于在*v0.4.16*中引入的编译器错误,在*v0.5.9*中得到了修正。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 79. **带有ABIEncoderV2**的多槽元素的存储数组。当在外部函数调用中或在*abi.encode()*中直接编码时,包含结构体或其他静态大小数组的存储数组无法正确读取。这是由于*v0.4.16*中引入的一个编译器错误,并在*v0.5.10*中得到了修复。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 80. **使用ABIEncoderV2**读取含有静态大小和动态编码成员的Calldata结构。读取包含动态编码但静态大小的成员的Calldata结构可能会导致错误的值。这是由于*v0.5.6*中引入的一个编译器错误,并在*v0.5.11*中得到了修复。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 81. **使用ABIEncoderV2的打包存储:**如果使用ABIEncoderV2直接从存储中编码,类型短于32字节的存储结构体和数组可能导致数据损坏。这是由于*v0.5.0*中引入的一个编译器错误,并在*v0.5.7*中得到了修复。(见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 82. **使用Yul优化器和ABIEncoderV2**的加载不正确。Yul优化器错误地将*MLOAD*和*SLOAD*调用替换为先前已写入加载位置的值。只有当ABIEncoderV2被激活,并且除了编译器设置中的常规优化器外,还手动激活了实验性的Yul优化器,才会发生这种情况。这是由于*v0.5.14*中引入的一个编译器错误,并在*v0.5.15*中进行了修复。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 83. **使用ABIEncoderV2动态编码基类型的数组切片**。访问基类型为动态编码的数组(如多维数组)的数组片断可能导致读取无效数据。这是由于*v0.6.0*中引入的编译器错误,在*v0.6.8*中进行了修正。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 84. **在使用ABIEncoderV2时,格式化过程中缺少转义:**当ABIEncoderV2被启用时,直接传递给外部或编码函数调用的包含双反斜杠字符的字符串常量会导致使用不同的字符串。这是由于在*v0.5.14*中引入的一个编译器错误,并在*v0.6.8*中进行了修复。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 85. **双移位溢出**。大常数的双位移位,其总和超过256位,会导致意外的数值。总移位大小为 2\*256 或更多的嵌套逻辑移位操作会被错误地优化。这只适用于由属于编译时常量表达式的位数进行的移位操作。这是由于*v0.5.5*中引入的编译器错误,并在*v0.5.6*中进行了修正。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 86. **不正确的字节指令优化**:优化器错误地处理了第二个参数为31的字节操作码或求值为31的常量表达式。这可能导致意外的值。当在*bytesNN*类型上执行编译时常量值(不是索引)为31的索引访问时,或在内联汇编中使用字节操作码时,会发生这种情况。这是由于*v0.5.5*中引入的编译器错误,并在*v0.5.7*中得到了修复。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 87. **用Yul优化器移除必要的赋值** 。当使用*for*循环、*continue*和*break*语句时,Yul优化器可以删除*for*循环内声明的变量的必要赋值。这是由于*v0.5.8*/*v0.6.0*中引入的一个编译器错误,并在*v0.5.16*/*v0.6.1*中得到了修复。(见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 88. **私有方法被覆盖**。虽然基础合约的私有方法不可见,也不能直接从派生合约中调用,但仍然可以声明一个同名同类型的函数,从而改变基础合约函数的行为。这是由于*v0.3.0*中引入的一个编译器错误,并在*v0.5.17*中进行了修正。(见[此处](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 89. **元组赋值多栈槽组件**。元组赋值的组件占用多个堆栈槽,例如嵌套的元组、外部函数指针或动态大小的calldata数组的引用,可能导致无效值。这是由于*v0.1.6*中引入的一个编译器错误,并在*v0.6.6*中得到了修正。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 90. **动态数组清理**。当分配一个动态大小的数组,其类型大小最多为16个字节,导致分配的数组收缩时,被删除的插槽的某些部分没有清零。这是编译器错误,在*v0.7.3*中修复。(见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 91. **空字节数组复制**。将空字节数组(或字符串)从内存或Calldata复制到存储中,如果随后增加目标数组的长度而不存储新数据,可能导致数据损坏。这是一个编译器错误,在*v0.7.4*中修复。(见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 92. **内存数组创建溢出**。创建非常大的内存数组可能导致内存区域重叠,从而导致内存损坏。这是由于*v0.2.0*中引入的一个编译器错误,在*v0.6.5*中得到了修复。(参见[这里](https://solidity.ethereum.org/2020/04/06/memory-creation-overflow-bug/)) 93. **Calldata using for**。调用带有calldata参数的内部库函数,如果通过 "*using for "*调用,可能导致读取无效数据。这是由于*v0.6.9*中引入的一个编译器错误,在*v0.6.10*中得到了修复。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 94. **自由函数的重新定义**。当在一个源代码单元中定义了两个或更多具有相同名称和参数类型的自由函数(合约之外的函数)时,或当一个导入的自由函数别名覆盖了另一个名称不同但参数类型相同的自由函数时,编译器没有标记错误。这是由于*v0.7.1*中引入的一个编译器错误,并在*v0.7.2*中进行了修正。(参见[这里](https://docs.soliditylang.org/en/v0.8.1/bugs.html)) 95. **基于代理的可升级合约中未加保护的初始化器**。基于代理的可升级合约需要使用*public*初始化函数,而不是用明确调用一次的构造函数。防止多次调用这种初始化函数(例如通过OpenZeppelin的*Initializable*库中的*initializer*修改器)是必须的。(参见[这里](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializers)和[这里](https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializer-is-not-called)) 96. **初始化基于代理的可升级合约中的状态变量**。这应该在初始化函数中进行,而不是作为状态变量声明的一部分,在这种情况下,状态变量不会被设置。(见[此处](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#avoid-initial-values-in-field-declarations)) 97. **导入基于代理的可升级合约**。从基于代理的可升级合约中导入的合约也应是可升级的,因为这些合约已被修改为使用初始化器而不是构造器。(见[此处](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#use-upgradeable-libraries)) 98. **避免在基于代理的可升级合约中使用selfdestruct或delegatecall**。这将导致逻辑合约被销毁,所有的合约实例最终将委托调用一个地址,而不需要任何代码。(见[这里](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#potentially-unsafe-operations)) 99. **基于代理的可升级合约中的状态变量**。这种合约中状态变量的声明顺序/布局和类型/可变性应在升级时准确地保留,以防止关键的存储布局不匹配错误。(见[此处](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts)) 100. **基于代理的可升级合约中代理/合约之间的函数ID碰撞**。恶意代理合约可能会利用函数ID碰撞来调用非预期的代理函数而不是合约函数。检查函数ID碰撞。(见[这里](https://github.com/crytic/slither/wiki/Upgradeability-Checks#functions-ids-collisions)和[这里](https://forum.openzeppelin.com/t/beware-of-the-proxy-learn-how-to-exploit-function-clashing/1070)) 101. **基于代理的可升级合约中代理/合约函数之间的覆盖**。代理合约中的覆盖函数防止逻辑合约中的函数被调用。(见[此处](https://github.com/crytic/slither/wiki/Upgradeability-Checks#functions-shadowing)) ## 总结 这篇文章从广泛引用的资料中整理了101个基本智能合约安全陷阱和最佳实践的清单。它涉及最常见的Solidity和EVM相关方面。根据共同的底层特征或影响,已将清单项目归纳在一起。将放在Github上,以便在社区参与和讨论的情况下对其进行修正、更新和改进。 这里排除了复杂的交互、经济角度和协议逻辑特定的漏洞。然而,鉴于这是细微的DeFi漏洞/漏洞发生的地方,而自动化工具还没有能力标记这些漏洞,因此,肯定有空间来制定另一个检查清单,专门扩展到访问控制、预言机、闪电贷、rebase/通缩代币、流行的代币标准、协议家族、抢跑/跟跑/夹心三明治、监护启动等。 智能合约开发者/审核员/协议的安全负担是巨大的。自动工具(如Slither)可以检查其中的许多陷阱,是开发/审计过程中的必备工具,但它们在某些情况下可能会出现错误提示,需要人为干预以获得更大的信心/覆盖率。检查清单有助于减少这种认知负荷,并可以帮助在以太坊上构建更安全和稳健的智能合约。 我希望你觉得这有些用处。感谢您的阅读,期待你的评论和反馈。 ------ 本翻译由 [Cell Network](https://www.cellnetwork.io/?utm_souce=learnblockchain) 赞助支持。

  • 原文:https://secureum.substack.com/p/smart-contract-security-101-secureum
  • 译文出自:登链翻译计划
  • 译者:翻译小组
  • 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

"清单宣言.如何把事情做对 "是Atul Gawande的一本书,他是著名的外科医生、作家和公共卫生领袖。马尔科姆-格拉德威尔在对这本书的评论中写道:

Gawande首先区分了无知的错误(因为我们知道的不够多而犯的错误)和不称职的错误(因为我们没有正确利用我们所知道的东西而犯的错误)。他写道,现代世界的失败其实就是其中的第二种错误,他通过一系列医学的例子,告诉我们外科医生的日常工作是如何变得如此复杂,以至于出现这样或那样的错误几乎是不可避免的:对于一个原本称职的医生来说,错过一个步骤,或者忘记问一个关键问题,或者在当下的压力下,没有为每一个可能出现的情况做好计划,实在是太容易了。然后,Gawande拜访了飞行员和建造摩天大楼的人,并带回了一个解决方案。专家们需要检查清单--引导他们完成任何复杂程序中的关键步骤。在本书的最后一节,Gawande展示了他的研究团队是如何采用这一理念,开发出一份安全的手术清单,并将其应用于世界各地,并取得了惊人的成功

鉴于快速发展的以太坊基础设施(新平台、新语言、新工具和新协议)的复杂程度令人瞠目结舌,以及部署管理数百万美元的智能合约所带来的风险,我认为智能合约开发者/审核员的工作有点类似于上文提到的外科医生/飞行员/摩天大楼建筑师的工作。

虽然可能还没有生命危险(目前),但智能合约有很多的事情要做对,很容易漏掉一些检查,做出不正确的假设或没有考虑到潜在的情况。其结果是利用智能合约漏洞把资金耗尽,从而降低人们对这个未来无信任的去中心化基础设施的信任。因此,智能合约专家也需要检查清单。

本篇文章从不同来源整理了101个智能合约安全陷阱和最佳实践的清单。这份清单并非详尽无遗,它包括来自广泛参考来源的建议,如Slither的 detector 文档、智能合约弱点分类注册表、Solidity的已知漏洞列表、Sigma Prime、ConsenSys的已知攻击&最佳实践、DASP以及来自OpenZeppelin和Trail of Bits的可升级性检查。

该检查清单简要地描述了陷阱/建议,而没有详细解释其原理,并假设读者已经知道或者可参考列出的来源。我相信这份方便的 "安全智能合约检查清单 "将有助于开发者/审核员在以太坊上使用Solidity构建更安全和稳健的智能合约。

智能合约安全检查清单

  1. Solidity版本。使用非常老的Solidity版本,无法从错误修复和较新的安全检查中获益。使用最新版本可能会使合约容易受到未发现的编译器错误的影响。请考虑使用这些版本之一:0.5.11-0.5.13、0.5.15-0.5.17、0.6.8或0.6.10-0.6.11.(见此处)。
  2. 未锁定版本。合约应使用与其测试过的编译器版本/标志去部署。锁定 pragma(例如在pragma solidity 0.5.10中不使用 ^ ) 可以确保合约不会意外地被部署到一个有未修正错误的旧编译器版本。(见这里)
  3. 同时使用多个Solidity pragma。最好在所有合约中使用一个Solidity编译器版本,而不是使用有不同错误和安全检查的不同版本。(见这里)
  4. 不正确的访问控制。执行关键逻辑的合约函数应通过地址检查(如所有者、控制器等)进行适当的访问控制,通常是在修改器中进行的。缺少的检查将允许攻击者控制关键逻辑。(见这里和这里)
  5. 不受保护的提款函数。调用未受保护的(external/public)函数向用户控制的地址发送以太币/代币,可能允许用户提取未经授权的资金。(见这里)
  6. 无保护地调用自毁。用户/攻击者可能会误杀/故意销毁合约。必须控制对此类函数的访问。(见此处)
  7. 修改器的副作用:修改器应该只进行状态检查,而不应该更改状态,以及违反检查-修改-交互(check-effects-interactions)模式的外部调用。这些副作用可能会被开发者/审核员忽略,因为修改器的代码通常远离函数实现。(参见这里)
  8. 不正确的修改器。如果一个修改器没有执行_revert,使用该修改器的函数将返回默认值,导致意外行为。(参见这里)
  9. 构造函数名称:solc 0.4.22之前,构造函数名称必须与包含它的合约同名。错误的命名不会使它成为一个构造函数,这对安全有影响。solc 0.4.22引入了constructor关键字。在solc 0.5.0之前,合约可以同时拥有旧式和新式的构造函数名,如果两者都存在,则第一个定义的构造函数名优先于第二个,这也导致了安全问题。Solc 0.5.0强制使用constructor关键字。(见这里和这里)
  10. 无效构造函数:对基类合约构造函数的调用如果没有实现,会导致假设错误。检查构造函数是否实现,如果没有实现则删除调用。(参见这里)
  11. 隐式构造函数callValue检查。合约没有定义构造函数,但有一个定义了构造函数的基类合约,在没有明确可支付构造函数的情况下,不要revert 返回非零的callValue的调用。这是由于v0.4.5中引入的一个编译器错误,在v0.6.8中得到了修复。(参见这里)
  12. 受控的委托调用: delegatecall()callcode()到用户控制的地址,将允许在调用者的状态下执行恶意合约。需确保此类调用的目标地址可信。(见这里)
  13. 重入漏洞。不受信任的外部合约调用可能会回调自己的合约,导致意想不到的结果,如多次提款或失序事件。使用check-effects-interactions模式或进行重入保护。(见此处)
  14. ERC777 回调和重入:ERC777代币允许通过在代币转让期间调用的钩子进行任意回调。如果不使用重入防护,恶意合约地址可能会导致此类回调的重入攻击。(见这里)
  15. 避免用 transfer() / send() 来作为缓解重入攻击的措施。虽然transfer()send()被推荐为防止重入攻击的最佳安全做法,因为它们只使用 2300 Gas ,但操作码的 Gas 重新定价可能会破坏已部署的合约。推荐使用call()代替,没有硬编码的 Gas 限制,以及使用”检查-效果-交互“模式或重入防护来保护重入攻击。(见这里和这里)
  16. 私有链上数据。标明变量为private并不意味着不能在链上读取这些变量。私有数据不应该不加密地存储在合约代码或状态中,而应该加密或链外存储。(见这里)
  17. 弱PRNG。依靠block.timestampnowblockhash的PRNG在一定程度上会受到矿工的影响,应避免使用。(见这里)
  18. 块值作为时间代理block.timestampblock.number不是很好的时间代理,因为存在同步、矿工操纵和改变块时间的问题。(见这里)
  19. 整数上溢/下溢。若不使用OpenZeppelin的SafeMath(或类似的库)检查溢出/下溢,如果用户/攻击者能够控制这种算术运算的整数操作数,可能会导致漏洞或意外行为。Solc v0.8.0为所有算术运算引入了默认的溢出/底溢检查。(见这里和这里)
  20. 先除后乘:先乘后除一般比较好,以避免精度损失,因为Solidity整数除法可能会截断。(见这里)
  21. 交易顺序依赖:可以通过监控mempool对特定的以太坊交易强制执行竞赛条件。例如,经典的ERC20 approve()变更可以使用这种方法进行前置。不要对交易顺序依赖性进行假设。(见这里)
  22. ERC20 approve()竞赛条件:使用OpenZepppelin的SafeERC20实现中的safeIncreaseAllowance()safeDecreaseAllowance()来防止竞赛条件操纵授权金额。(见这里)
  23. 签名的可塑性ecrecover函数容易受到签名可塑性的影响,可能导致重放攻击。考虑使用OpenZeppelin的ECDSA库。(参见这里、这里和这里)
  24. ERC20 transfer()不返回boolean:solc > 0.4.22编译的合约与这样的函数交互将回退。使用OpenZeppelin的SafeERC20封装器。(参见 这里 和 这里)
  25. ERC721 ownerOf()不正确的返回值 :solc > 0.4.22编译的合约与ERC721 ownerOf()交互,如果返回一个bool而不是address类型,将会回退。使用OpenZeppelin的ERC721合约。(参见这里)
  26. 意外的以太币和this.balance。合约可以通过payable函数、selfdestruct()、coinbase交易或创建前的预发送接收以太币。因此,依赖于this.balance的合约逻辑可以被操纵。(参见这里和这里)
  27. fallback与receive()。检查是否考虑了fallback/receive函数的所有预防措施, 他们与与可见性、状态可变性和以太坊转账有微妙关系。(见 这里 和 这里)
  28. 危险的==:对代币/Ether使用严格等于可能会意外/恶意地导致意外行为。根据合约逻辑,考虑使用>=<=代替==来处理此类变量。(参见这里)
  29. 锁定以太币。通过payable函数接受以太币但没有提款机制的合约将锁定以太币。需去掉payable属性或增加提款功能。(见这里)
  30. 危险地使用tx.origin。使用tx.origin进行授权可能会被MITM恶意合约滥用。请使用msg.sender代替。(见此处)
  31. 合约检查:检查是否从外部自有账户(EOA)或合约账户调用,通常使用extcodesize检查。但在部署期间,合约还没有源代码时,可能会被合约规避。检查是否tx.origin == msg.sender是另一种选择。两者都有需要考虑的影响。(见 这里)
  32. 删除在一个结构体的映射。删除包含映射的结构体不会删除映射的内容,这可能会导致意想不到的后果。(见此处)
  33. 恒真或恒假:恒真(总是真)或恒假(总是假)可能有潜在的逻辑缺陷或多余的检查,例如,x >= 0,如果xuint,则总是真。(见这里)
  34. 布尔常数。在代码中使用布尔常数(true/false)(如判断条件),则表明逻辑有缺陷。(见这里)
  35. 布尔相等。布尔变量可以直接在条件中检查,而不需要使用true/false的相等运算符。(见这里)
  36. 状态修改函数。在solc >=0.5.0中,修改状态(在汇编中或其他)但被标记为constant/pure/view的函数会因为使用STATICCALL操作码而在solc >=0.5.0中回退(在之前的版本中工作)。(见这里)
  37. 低级调用的返回值。确保检查低级调用(call/callcode/delegatecall/send/等)的返回值,以避免意外失败。(见这里)
  38. 低级调用的账户存在性检查。即使被调用的账户不存在,低级调用call/delegatecall/staticcall也返回true(根据EVM设计)。如果需要的话,必须在调用前检查账户是否存在。(见这里)
  39. 危险的覆盖:局部变量、状态变量、函数、修改器或事件的名称会覆盖内置的Solidity符号,如now或其他来自当前作用域的声明,这些都会产生误导,并可能导致意外的使用和行为。(参见这里)
  40. 危险的状态变量覆盖。在派生合约中覆盖状态变量可能会对关键变量造成危险,如合约所有人owner(例如,基础合约中的修改器检查该变量,但变量被错误地覆盖了),以及合约错误地使用基础合约变量及覆盖变量。因此不要覆盖状态变量。(见这里)
  41. 本地变量的声明前使用。在声明之前使用一个变量(无论是后来声明的还是在另一个作用域中声明的)会在solc < 0.5.0中导致意外行为,但solc >= 0.5.0实现了C99风格的作用域规则,其中变量只能在声明之后使用,并且只能在相同或嵌套的作用域中使用。(参见这里)
  42. 循环内的高成本操作。循环内的状态变量更新(使用SSTOREs)等操作会耗费大量 Gas ,成本很高,并可能导致Out-Of-Gas错误。最好使用局部变量进行优化。(见这里)
  43. 循环内的调用:循环内对外部合约的调用是危险的(特别是当循环索引可以由用户控制时),因为如果其中一个调用还原或执行耗尽 Gas ,可能导致DoS。避免在循环内调用,检查循环索引不能被用户控制或被约束。(参见这里)
  44. 块 Gas Limit 的DoS。当执行 Gas 成本超过块 Gas 限制时,例如在未知大小的数组上循环等编程模式可能导致DoS。(见此处)
  45. 事件缺失。应发出关键状态变化的事件(如所有者和其他关键参数),以便在链外跟踪这一事件。(见此处)
  46. 未索引的事件参数。某些事件的参数应被索引(如ERC20转移/批准事件),以便将其纳入区块的bloom过滤器,以便更快地访问。如果不这样做,可能会混淆寻找此类索引事件的链外工具。(见这里)
  47. 库中事件签名不正确。库中事件中使用的合约类型导致事件签名哈希值不正确。在哈希签名中没有使用 "address "类型,而是使用了实际的合约名称,导致日志中出现错误的哈希值。这是由于v0.5.0中引入的一个编译器错误,在v0.5.8中得到了修正(见此处)。
  48. 危险的单元表达式。诸如x =+ 1这样的单元表达式很可能是程序员真正想使用x += 1的错误表达。单元 +运算符在solc v0.5.0中已被废弃(参见这里)。
  49. 缺少零地址验证。地址类型参数的设置者应包括零地址检查,否则合约函数可能无法访问或代币可能永远烧毁。(见此处)
  50. 关键地址变更。更改合约中的关键地址应分两步进行,第一步交易(从旧地址/当前地址)登记新地址(即授予所有权),第二步交易(从新地址)用新地址取代旧地址(即要求所有权)。这样就有机会从第一步错误使用的错误地址中恢复过来。否则,合约函数可能会变得无法访问。(见此处和此处)
  51. assert()状态改变。根据最佳做法,assert()语句中的不变性检查不应修改状态。应使用require()进行此类检查。(见这里)
  52. require() vs assert(): require()应该用于检查输入和返回值的错误条件,而assert()应该用于不变性检查。在solc 0.4.100.8.0之间,require()使用REVERT (0xfd)操作码,在失败时退还剩余 Gas ,而assert()使用INVALID (0xfe)操作码,消耗了所有提供的 Gas 。(见这里)
  53. 过时的关键字。使用过时的函数/运算符,如block.blockhash()blockhash(),msg.gas为gasleft(),throwrevert()sha3()keccak256()callcode()delegatecall(),suicide()selfdestruct(),constant为viewvar应为准确的类型名,应避免使用这些过时的函数/操作,以防止在新的编译器版本中出现意外错误。(见这里)
  54. 函数默认可见性: 在solc < 0.5.0中,没有指定可见性类型指定符时,则函数默认为public。这可能导致一个漏洞,恶意用户可能会进行未经授权的状态更改。solc >= 0.5.0 需要显式函数可见性指定符。(参见这里)
  55. 继承顺序不正确。从具有相同函数的多个合约继承的合约应规定正确的继承顺序,即从一般到具体,以避免继承错误的函数实现。(见此处)
  56. 继承缺失。一个合约可能看起来(根据名称或实现的函数)继承自另一个接口或抽象合约,但实际上并没有这样做。(见此处)
  57. Gas 不足。交易中继者需要得到信任,才能为交易成功提供足够的 Gas 。(见此处)
  58. 修改引用类型参数。作为参数传递给函数的结构体/数组/映射可以是由数据位置指定的值类型(memory)或引用类型(storage)(在solc 0.5.0之前是可选的)。确保在函数参数中正确使用memory和storage,并显式表达所有数据位置。(参见这里)
  59. 用函数类型变量进行任意跳转:在汇编操作中应谨慎处理和避免函数类型变量,以防止跳转到任意代码位置。(见这里)
  60. 有多个可变长度参数的散列碰撞。在某些情况下,使用abi.encodePacked()与多个可变长度参数一起使用可能会导致哈希碰撞。不要允许用户访问abi.encodePacked()中使用的参数。使用固定长度的数组或使用abi.encode()。(见这里和这里)
  61. 脏的高阶位带来的可塑性风险。没有占满32个字节的类型可能包含 "脏高阶位",这不会影响对类型的操作,但对msg.data会产生不同的结果。(见这里)
  62. 汇编中移位不正确。Solidity汇编中的移位运算符(shl(x, y), shr(x, y), sar(x, y))在y上应用x位的移位运算,而不是相反,这可能会引起混淆。检查移位操作中的值是否颠倒。(参见这里)
  63. 汇编的使用。使用EVM 汇编容易出错,应避免使用或仔细检查其正确性。(见此处)
  64. 右向左覆盖控制字符(U+202E)。恶意行为者可以使用 "右向左覆盖 "unicode字符来强制渲染RTL文本,并使用户混淆合约的真实意图。U+202E 字符不应该出现在智能合约的源代码中。(见这里)
  65. 常量。不变的状态变量应声明为常量,以节省 Gas 。(见此处)
  66. 类似的变量名称。名称相似的变量可能会相互混淆,因此应避免使用。(见此处)
  67. 未初始化的状态/局部变量。未初始化的状态/局部变量被编译器分配为零值,可能会造成意想不到的结果,例如将token转移到零地址。应明确初始化所有状态/局部变量。(参见 这里 和 这里)
  68. 未初始化的存储指针:未初始化的本地存储变量可能指向合约中意想不到的存储位置,从而导致漏洞。Solc 0.5.0及以上版本不允许这种指针。(见此处)
  69. 在构造函数中调用未初始化的函数指针:由于编译器错误,在用solc版本0.4.5-0.4.250.5.0-0.5.7编译的合约的构造函数中调用未初始化的函数指针会导致意外行为。(见这里)
  70. 很长的数字常量。应仔细检查有许多数字的数字常量,因为它们容易出错。(见此处)
  71. 超出范围的枚举: Solc < 0.4.5 对超出范围的枚举产生了意外的行为. 检查枚举转换或使用更新的编译器。(参见这里)
  72. 未调用的public函数。从未在合约内调用的public函数应宣布为external,以节省gas。(见此处)
  73. 死代码/无法到达的代码。死代码可能表明程序员出错、逻辑缺失或潜在的优化机会,需要标记出来予以删除或适当处理。(见此处)
  74. 未使用的返回值。函数调用中未使用的返回值表明程序员错误,可能会产生意外行为。(见此处)
  75. 未使用的变量。未使用的状态/局部变量可能表明程序员出错、逻辑缺失或潜在的优化机会,需要标记出来予以删除或适当处理。(见此处)
  76. 多余的语句。没有效果但不产生代码的语句可能表明程序员出错或逻辑缺失,需要标明删除或适当处理。(见此处)
  77. 使用 ABIEncoderV2 带符号整数的存储数组。将带符号整数数组分配给不同类型的存储数组可能导致数组中的数据损坏。这是由于v0.4.7中引入的一个编译器错误,并在v0.5.10中得到修复。(见这里)
  78. 动态构造函数参数被ABIEncoderV2。当使用ABIEncoderV2时,包含动态大小数组的结构体或数组的合约构造函数会回退或解码为无效数据。这是由于在v0.4.16中引入的编译器错误,在v0.5.9中得到了修正。(参见这里)
  79. 带有ABIEncoderV2的多槽元素的存储数组。当在外部函数调用中或在abi.encode()中直接编码时,包含结构体或其他静态大小数组的存储数组无法正确读取。这是由于v0.4.16中引入的一个编译器错误,并在v0.5.10中得到了修复。(参见这里)
  80. 使用ABIEncoderV2读取含有静态大小和动态编码成员的Calldata结构。读取包含动态编码但静态大小的成员的Calldata结构可能会导致错误的值。这是由于v0.5.6中引入的一个编译器错误,并在v0.5.11中得到了修复。(参见这里)
  81. 使用ABIEncoderV2的打包存储:如果使用ABIEncoderV2直接从存储中编码,类型短于32字节的存储结构体和数组可能导致数据损坏。这是由于v0.5.0中引入的一个编译器错误,并在v0.5.7中得到了修复。(见这里)
  82. 使用Yul优化器和ABIEncoderV2的加载不正确。Yul优化器错误地将MLOADSLOAD调用替换为先前已写入加载位置的值。只有当ABIEncoderV2被激活,并且除了编译器设置中的常规优化器外,还手动激活了实验性的Yul优化器,才会发生这种情况。这是由于v0.5.14中引入的一个编译器错误,并在v0.5.15中进行了修复。(参见这里)
  83. 使用ABIEncoderV2动态编码基类型的数组切片。访问基类型为动态编码的数组(如多维数组)的数组片断可能导致读取无效数据。这是由于v0.6.0中引入的编译器错误,在v0.6.8中进行了修正。(参见这里)
  84. 在使用ABIEncoderV2时,格式化过程中缺少转义:当ABIEncoderV2被启用时,直接传递给外部或编码函数调用的包含双反斜杠字符的字符串常量会导致使用不同的字符串。这是由于在v0.5.14中引入的一个编译器错误,并在v0.6.8中进行了修复。(参见这里)
  85. 双移位溢出。大常数的双位移位,其总和超过256位,会导致意外的数值。总移位大小为 2*256 或更多的嵌套逻辑移位操作会被错误地优化。这只适用于由属于编译时常量表达式的位数进行的移位操作。这是由于v0.5.5中引入的编译器错误,并在v0.5.6中进行了修正。(参见这里)
  86. 不正确的字节指令优化:优化器错误地处理了第二个参数为31的字节操作码或求值为31的常量表达式。这可能导致意外的值。当在bytesNN类型上执行编译时常量值(不是索引)为31的索引访问时,或在内联汇编中使用字节操作码时,会发生这种情况。这是由于v0.5.5中引入的编译器错误,并在v0.5.7中得到了修复。(参见这里)
  87. 用Yul优化器移除必要的赋值 。当使用for循环、continuebreak语句时,Yul优化器可以删除for循环内声明的变量的必要赋值。这是由于v0.5.8/v0.6.0中引入的一个编译器错误,并在v0.5.16/v0.6.1中得到了修复。(见这里)
  88. 私有方法被覆盖。虽然基础合约的私有方法不可见,也不能直接从派生合约中调用,但仍然可以声明一个同名同类型的函数,从而改变基础合约函数的行为。这是由于v0.3.0中引入的一个编译器错误,并在v0.5.17中进行了修正。(见此处)
  89. 元组赋值多栈槽组件。元组赋值的组件占用多个堆栈槽,例如嵌套的元组、外部函数指针或动态大小的calldata数组的引用,可能导致无效值。这是由于v0.1.6中引入的一个编译器错误,并在v0.6.6中得到了修正。(参见这里)
  90. 动态数组清理。当分配一个动态大小的数组,其类型大小最多为16个字节,导致分配的数组收缩时,被删除的插槽的某些部分没有清零。这是编译器错误,在v0.7.3中修复。(见这里)
  91. 空字节数组复制。将空字节数组(或字符串)从内存或Calldata复制到存储中,如果随后增加目标数组的长度而不存储新数据,可能导致数据损坏。这是一个编译器错误,在v0.7.4中修复。(见这里)
  92. 内存数组创建溢出。创建非常大的内存数组可能导致内存区域重叠,从而导致内存损坏。这是由于v0.2.0中引入的一个编译器错误,在v0.6.5中得到了修复。(参见这里)
  93. Calldata using for。调用带有calldata参数的内部库函数,如果通过 "using for "调用,可能导致读取无效数据。这是由于v0.6.9中引入的一个编译器错误,在v0.6.10中得到了修复。(参见这里)
  94. 自由函数的重新定义。当在一个源代码单元中定义了两个或更多具有相同名称和参数类型的自由函数(合约之外的函数)时,或当一个导入的自由函数别名覆盖了另一个名称不同但参数类型相同的自由函数时,编译器没有标记错误。这是由于v0.7.1中引入的一个编译器错误,并在v0.7.2中进行了修正。(参见这里)
  95. 基于代理的可升级合约中未加保护的初始化器。基于代理的可升级合约需要使用public初始化函数,而不是用明确调用一次的构造函数。防止多次调用这种初始化函数(例如通过OpenZeppelin的Initializable库中的initializer修改器)是必须的。(参见这里和这里)
  96. 初始化基于代理的可升级合约中的状态变量。这应该在初始化函数中进行,而不是作为状态变量声明的一部分,在这种情况下,状态变量不会被设置。(见此处)
  97. 导入基于代理的可升级合约。从基于代理的可升级合约中导入的合约也应是可升级的,因为这些合约已被修改为使用初始化器而不是构造器。(见此处)
  98. 避免在基于代理的可升级合约中使用selfdestruct或delegatecall。这将导致逻辑合约被销毁,所有的合约实例最终将委托调用一个地址,而不需要任何代码。(见这里)
  99. 基于代理的可升级合约中的状态变量。这种合约中状态变量的声明顺序/布局和类型/可变性应在升级时准确地保留,以防止关键的存储布局不匹配错误。(见[此处](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts))
  100. 基于代理的可升级合约中代理/合约之间的函数ID碰撞。恶意代理合约可能会利用函数ID碰撞来调用非预期的代理函数而不是合约函数。检查函数ID碰撞。(见这里和这里)
  101. 基于代理的可升级合约中代理/合约函数之间的覆盖。代理合约中的覆盖函数防止逻辑合约中的函数被调用。(见此处)

总结

这篇文章从广泛引用的资料中整理了101个基本智能合约安全陷阱和最佳实践的清单。它涉及最常见的Solidity和EVM相关方面。根据共同的底层特征或影响,已将清单项目归纳在一起。将放在Github上,以便在社区参与和讨论的情况下对其进行修正、更新和改进。

这里排除了复杂的交互、经济角度和协议逻辑特定的漏洞。然而,鉴于这是细微的DeFi漏洞/漏洞发生的地方,而自动化工具还没有能力标记这些漏洞,因此,肯定有空间来制定另一个检查清单,专门扩展到访问控制、预言机、闪电贷、rebase/通缩代币、流行的代币标准、协议家族、抢跑/跟跑/夹心三明治、监护启动等。

智能合约开发者/审核员/协议的安全负担是巨大的。自动工具(如Slither)可以检查其中的许多陷阱,是开发/审计过程中的必备工具,但它们在某些情况下可能会出现错误提示,需要人为干预以获得更大的信心/覆盖率。检查清单有助于减少这种认知负荷,并可以帮助在以太坊上构建更安全和稳健的智能合约。

我希望你觉得这有些用处。感谢您的阅读,期待你的评论和反馈。

本翻译由 Cell Network 赞助支持。

  • 发表于 2021-02-25 14:26
  • 阅读 ( 3036 )
  • 学分 ( 217 )
  • 分类:智能合约

评论