ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WTF-Solidity 研读:OpenZeppelin 2017 年安全审计报告深度解析(Crowdsale 卡死资金与 Multisig 递归漏洞)

WTF-Solidity 研读:OpenZeppelin 2017 年安全审计报告深度解析(Crowdsale 卡死资金与 Multisig 递归漏洞) WTF-Solidity 研读OpenZeppelin 2017 年安全审计报告深度解析Crowdsale 卡死资金与 Multisig 递归漏洞【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity本文围绕 WTF-Solidity 仓库内 lib/openzeppelin-contracts/audits/2017-03.md 这份历史安全审计报告展开逐项还原审计方对早期 Zeppelin即今天的 OpenZeppelin Contracts合约库的评审结论包括两个严重漏洞、多个中等问题与大量逐行代码点评并结合仓库当前携带的 OpenZeppelin Contracts v5.6.0 源码说明这些审计意见在后来的版本中是如何被回应和演进的。读完本文你将掌握一套可复用的智能合约安全评审视角错误处理一致性、资金通路、重入与状态更新顺序、构造函数参数校验、approve 竞态等并能在当前仓库源码中一一找到对应的现代解法。一、审计背景与范围这份报告由 New Alchemy 的 Dennis Peterson 与 Peter Vessenes 于 2017 年 3 月撰写起因是 Zeppelin 团队邀请他们对自家 OpenZeppelin 合约库当时的托管仓库名为 zeppelin-solidity做一次第三方安全审计。审计目标是这套合约作为可直接安全部署的通用积木被大量水平参差的开发者直接使用因此合约必须能够开箱即用地安全运行。审计范围覆盖当时仓库contracts目录下的全部合约评审基线是 git commit9c5975a706b076b7000e8179f8101e0c61024c87。报告还附有一段 2021-07-19 的说明文中出现的 Zeppelin、OpenZeppelin、OpenZeppelin Contracts 等称谓此后多次改名本次审计结论适用于如今由 OpenZeppelin Contracts Community 维护的 OpenZeppelin Contracts。报告同时声明审计不担保代码的实用性、安全性、商业模式合规性仅作讨论用途——这也是后来所有专业审计报告沿用至今的标准免责范式。二、总体结论质量尚可但不宜直接上链审计执行摘要给出的总体判断是代码库整体质量相当不错——干净、模块化、通篇遵循最佳实践但它仍处于快速演进状态需要补充每份文件关于预期行为与未来计划的文档也需要由比 OpenZeppelin 自家团队更不客气的人写出更全面、更激进的测试。审计方最终发现2 个严重错误Critical和 1 个中等问题Moderate并明确表示在该 commit 修复前不建议任何人在公开环境部署这套代码。仓库当时已经带有 Truffle 单元测试审计方认为这是此类合约的必需品与最佳实践但建议继续加厚测试矩阵。报告对项目的宏观评价是非常有价值的项目创建一个易于扩展的框架有助于整体提高链上代码的平均质量引导开发者把改动收敛在特定区块而不是从零写一套未经审计的合约。同时反复强调一条铁律只要开发者动过 OpenZeppelin 合约改动后的代码就脱离了已审计状态任何处理资金、信息或其他有价值资产的代码都不应以未审计状态部署上链。Solidity 版本与语言特性建议当时库中大部分代码使用 Solidity 0.4.11但Ownership目录下部分文件仍标记为 0.4.0审计方建议统一升级。报告顺带预告了 Solidity 0.4.10 将带来的三个对合约安全至关重要的特性assert(condition)条件为假时抛出异常revert()回滚但不会耗尽剩余 gas相比直接 throw 更省 gas、行为更可控address.transfer(value)行为类似send但自动传播异常并支持.gas()指定 gas 上限。这些特性后来正是现代 Solidity 错误处理体系的基石也是后续所有统一错误处理风格讨论的技术前提。三、核心方法论之争throw 还是 return false报告用一整节讨论了 Solidity 的两种错误处理范式throw含后来的revert彻底清空调用栈直到上一个外部调用状态完全回滚逻辑简单工程师无需跟踪多层返回码返回false允许函数在失败后继续执行但需要调用方逐层检查返回值容易退化成状态跟踪混乱的有限状态机从而滋生 bug。审计方个人偏好throw因为它更简单、工程师要记的东西更少。但 2017 年的 OpenZeppelin 代码库里两种风格并存SimpleToken转账失败时 throw完整版 ERC20 却返回false有的 modifier 直接 throw有的则用条件包住函数体条件不满足时等效于让函数返回 false。报告建议要么全库统一风格要么明确写出什么场景用哪种方式的设计准则并承认某些场景无法二选一——比如 SafeMath 几乎必须 throw而 ERC20 标准规定了返回布尔值。报告特别点赞了一个把两种技巧组合得很巧妙的案例Multisig 第 65 行详见后文MultisigWallet 逐行点评。四、两个严重漏洞Critical4.1 Crowdsale 合约中的资金永久卡死CrowdsaleToken.sol是一个在收到 ETH 时按固定价格铸造代币的 StandardToken但它完全没有提供把募集到的 ETH 提走的函数。审计方措辞强烈没有任何场景应该有人原样部署这个合约无论是测试还是上线。结论是强烈建议新增一个标准的withdraw函数。这是所有融资类合约的经典必修课只要合约能收钱就必须设计显式、受控的提款通路否则资金会永久冻结在合约地址中。后续主流 Crowdsale/众筹合约无一例外都补齐了代币合约与募资合约分离 募资合约可提款的结构。4.2 MultisigWallet 的递归调用与每日限额绕过MultisigWallet.sol第 45 行在execute中检查转账金额是否低于每日限额daily limit。该函数只能由 Owner 调用。审计方提出一个攻击面推演如果多签钱包的所有者批准了一笔对resetSpentToday重置当日已花费额度的调用会怎样只要能构造一条调用链让 Owner 确认resetSpentToday之后再通过execute在递归调用中反复提款合约就可能被抽干甚至不需要递归在confirm与execute之间交替进行多次普通调用即可达到同样效果。审计方仍在推敲Shareable.sol的确认协议但看不出这种攻击不可能发生事实上它看起来是可能的。另一个令人不安的角度是共享所有者可以事后撤销revoke自己的确认即便打了几个简单的补丁这个灵活性依然可疑。报告把该 bug 拆成四个需要分别处理的原因resetSpentToday与confirm组合后既不限制可调用的日期也不限制可调用的次数一旦某次调用被确认并执行它看起来可以被重复执行confirmandCheck似乎没有判断目标函数是否已经被调用过即便加了判断revoke也需要更新逻辑处理函数调用完成之后再收到撤销请求的情况。结论很直接在修复这些问题之前不要使用 MultisigWallet。有趣的是这份审计指出的确认-执行-重复执行-撤销问题本质上就是后来多签钱包包括 WTF-Solidity 仓库自带的 50_MultisigWallet/MultisigWallet.sol 教学合约在实现时必须处理的状态机核心难点提案必须幂等、确认必须与具体提案绑定、已执行的提案不可重放。五、中等问题Moderate to Minor5.1 PullPayment被动收款模式的缺憾PullPayment.sol在当时是用户主动来取款的经典实现审计方认为它还需要打磨没有取消付款的机制考虑收款人丢钱包、给了一个作恶地址、或需要一个超过send默认 gas 的地址等场景都应支持取消asyncSend没有溢出检查建议在离数据操作最近的一层做上溢/下溢检查asyncSend允许排队待发的金额超过合约实际余额这大概率不是好主意即便有意为之也应换一个名字若允许就必须处理多个并发withdrawPayments调用之间的竞态缺少当前有多少笔待付款的查询能力这暗示需要一次小规模重写。此外报告也客观肯定了 PullPayment 在防重入方面的优点以太币发送发生在函数末尾checks-effects-interactions 的雏形且用的是.send()而非.call.value()。报告讨论了.call.value()的优劣如果你能确保所有状态更新都发生在发送之前.call.value()是更好的选择因为接收方 fallback 昂贵时.send会失败但对于要内嵌进其他合约的工具型合约用.send更稳妥折中方案是额外提供一个仅 Owner 可用.call.value发送以太的函数。第 14 行未使用 safeAdd 的问题再次被点名表面看付款金额只能增加实际上付款方可以通过溢出把付款额压低到任意值也可以累加一个未溢出的大额使付款总额超过合约余额导致后续 withdraw 必然失败。报告给出的可执行建议是跟踪所有未提取 asyncSend 的总和拒绝任何会超过剩余余额的新增付款。5.2 Shareable共享所有者确认协议审计方明确表示Shareable.sol还没有成熟到可以上线缺少函数且按现有写法可能遭受重排攻击reordering attack——矿工或与合约参与者赛跑的一方把自己的信息插入列表或映射确认与撤销逻辑必须以共享所有者做出极其恶劣的行为为前提重新审视构造函数对required参数没有任何健全性检查如_required len(_owners)就未校验万一_required接近MAX就麻烦了。六、逐行点评Line by Line Comments报告对当时合约逐文件给出了细粒度点评这些内容既是历史档案也是今天写合约时可以对照自查的检查清单。按目录分类整理如下。Lifecycle 目录Killable允许 Owner 调用selfdestruct并把资金转给 Owner本身没有问题。但报告提醒selfdestruct通常不该被使用——开发者往往想读取旧合约的数据却不理解selfdestruct会关闭对合约的访问。建议补充文档并把kill改名为completelyDestroy这类名字kill可以仅表示把钱转给 Owner。同时注意一个可 kill 的函数意味着 Owner 可以无视其他业务逻辑直接拿走资金这在某些场景是期望的在某些场景则相反。Migrations审计方推测该合约的目标是支持并记录向新合约地址的迁移但看不懂代码是如何实现这一目标的希望与 OpenZeppelin 团队当面复核。Pausable审计方喜欢这些暂停机制但提醒暂停给了 Owner 相当大的作恶griefing空间而这可能并不被使用该框架的参与者所察觉。建议在 TokenContract 中增加更安全的 pause/resume 示例逻辑特别是引入时间锁timelock到期后任何人都能解除暂停。另一个技术要点是当时 Pausable 的 modifier 使用if(bool){_;}模式——这对失败时返回 false 的函数没问题但对预期 throw 的函数可能有问题与统一 throw 或 return(false)的讨论呼应。Ownership 目录Ownable第 19 行的 modifier 不满足条件时直接 throw与 Pausable 等用if(bool){_;}的继承式 modifier 风格不一致。Claimable继承自 Ownable由现任 Owner 设置一个pendingOwner候选人需要主动认领所有权。DelayedClaimable既然 Claimable 已经继承 Ownable为何还要直接继承 Ownable双重继承徒增困惑。Contactable允许 Owner 设置一段公开的合约信息字符串无问题。Shareable前文已述缺_required len(_owners)校验、owners/_owners/owner命名混乱不推荐仅靠下划线区分变量名、注释声称有六类事件实际上只有两类、ownerIndex为何用地址哈希成uint作键建议直接用地址以加强类型、i) ... owners[2 i]让读者做算术、缺少propose新增操作函数、只有revoke没有propose、提防重排攻击若propose允许用户自选 bytes 提案内容坏事TM就会发生。Multisig只是一个接口。注意它允许更换 owner 地址但不允许改变 owner 的数量这限制了扩展性但也简化了实现。Payment 目录PullPayment 已在第五节详述要点重述防重入安全send 在最后 用.send.call.value()的取舍应实现cancel第 14 行缺 safeAdd溢出可压低付款额建议跟踪未提取总额并限制新增。Tokens 目录ERC20标准接口。报告记录了当时 Edcon 大会上披露的标准级安全洞approve不防竞态只是简单覆盖旧值。攻击者可以先获得一笔授权然后等 Owner 再次调用approve的瞬间抢先把旧限额花掉再叠加新限额——如果成功能花掉两笔限额之和。两种修法(1) 把旧限额作为参数传进来若已有人花费则更新失败(2) 把 value 参数当作增量而非替换值。在完全遵守当前 ERC20 标准的前提下无法修复但可以加一个secureApprove函数。影响有限——毕竟只能被你自己授权过的地址攻击用户侧缓解手段是先归零限额、确认到账后再设新限额。这条建议直接催生了后来 ERC-2612 permit 等方案。ERC20Basic更简单的接口去掉了 Approve。注意它偏离 ERC20 的另一处transfer 失败时 throw 而不是返回 false。BasicToken使用SafeSub与SafeMath所以 transfer 失败时 throw 而非返回 false符合 ERC20Basic 但不完全符合 ERC20 标准。StandardToken完整 ERC20 实现。transfer()和transferFrom()走 SafeMath失败会 throw 而非返回 false不是安全问题但偏离标准。SimpleTokenStandardToken 的示例实例。注意 decimals 为 18、总供应量只有 10,000换算成名义币值其实连 1 个整币都不到10,000 / 10^18。CrowdsaleToken收到 ETH 时按固定价格铸币的 StandardToken。没有提款函数资金会被困在合约里严重问题一。作为众筹示例它应当 Ownable 并允许 Owner 提走 ETH替代方案是提供一个仅可由独立 Crowdsale 合约调用的mint()函数这样可以在不改代币本身的前提下加入任意业务规则——这正是后来主流架构。VestedToken第 23、27 行transfer()与transferFrom()带canTransfermodifier余额不足时 throw但transfer()却返回布尔值——失败处理方式不一致可能坑到调用它的其他合约transferableTokens()依赖 safeSub余额不足同样会 throw。第 64 行的delete并无必要因为下一行本来就会覆盖该值。Root level 目录Bounty赏金合约通过让每个研究员各自部署一份独立合约来规避竞态若某研究员攻破了与自己对标的合约其他研究员不能立即领奖必须在自己合约里复现攻击。但开发者可以篡改意图——让deployContract()永远返回同一地址这会把researchers映射中该合约绑定的研究员地址覆盖掉可以通过禁止改写researchers来防御。DayLimitlimitedDailymodifier 调underLimit它既检查当日支出是否低于限额又把入参金额累加进spentToday。如果所有函数失败都 throw这没问题但 OpenZeppelin 并非全部如此存在返回 false 的函数和if(bool){_;}包裹的 modifier此时_value已被累加以太却可能因其他前置条件不满足而没有真正发送不过这在当时的 multisig 中不是问题。第 4、11 行的注释声称 DayLimit 是 multiowned 且 import 了 Shareable但 DayLimit 其实并不继承 Shareable——意图或许是让子合约继承Multisig 正是如此此时应删掉 import 并改掉注释。第 46 行用了手动溢出检查而不是 safeAdd既然调用它的函数反正会 throw用 safeAdd 并无坏处。LimitBalance无问题。MultisigWallet第 28、76、80 行的kill、setDailyLimit、resetSpentToday都要多签批准且 Shareable 会记录这些操作的哈希但建议它们各自再发出独立事件便于阅读。第 45 行的underLimit调用会先扣减每日限额然后 throw 或返回 0所以不存在限额被扣了但操作没走通的危险。第 65 行被盛赞为优雅设计onlyManyOwners会记录用户确认只有确认数足够时才执行函数体send 失败则整体 throw 并回滚确认确认数不足返回 false全部成功返回 true仅在指定交易意外失败时 throw。第 68 行 throw 是对的但注意该函数既可能返回 false 也可能 throw。第 92 行把clearPending()拆在 Shareable 与 MultisigWallet 两处略奇怪但这允许继承 Shareable 的合约对 pending 事务使用自定义结构体。SafeMathEdcon 演讲中的一个洞见——Solidity 的溢出行为当时属于未文档化行为依赖它的源码理论上可能因未来编译器修订而失效但编译产物没问题且即便编译器真这样修订也会有大量警告。这正是把溢出检查隔离在 SafeMath 里的理由。除这个小顾虑外SafeMath 本身没问题。七、从审计到现代当前仓库 v5.6.0 中的回应WTF-Solidity 仓库携带的 lib/openzeppelin-contracts 已是 OpenZeppelin Contracts v5.6.0见 package.json。把 2017 年的每一条审计意见对照今天的源码可以看到一次完整的安全观进化也是本文最值得收藏的对照表2017 审计意见当前仓库 v5.x 的回应证据位置throw 与 return false 风格不统一全面转向 revert 自定义错误custom error函数失败不再返回 falseOwnable.sol 的OwnableUnauthorizedAccount/OwnableInvalidOwnerERC20.sol 文件头注释明确写着functions revert instead returningfalseon failurePausable 用if(bool){_;}Owner 作恶空间大改用whenNotPaused/whenPausedmodifier 内部直接revert EnforcedPause()/ExpectedPause()从机制上消除静默失败Pausable.solSafeMath 需隔离溢出检查v0.8 起编译器内建 checked arithmeticSafeMath 整体退役数学工具重组为Math/SafeCast/SignedMathutils/mathERC20 approve 竞态无法在标准内修复推出 ERC-2612permit签名授权无需持有 ETH 发交易nonce 防重放另有draft-ERC20TemporaryApproval探索临时授权ERC20Permit.sol重入/递归调用风险Multisig 事件提供nonReentrantmodifier用存储槽状态机NOT_ENTERED/ENTERED拦截嵌套调用并有 transient storage 变体ReentrancyGuard.sol、ReentrancyGuardTransient.solPullPayment 缺取消、竞态、计数现代取款类逻辑收敛为锁定/计划释放的专用模块仓库中的 VestingWallet.sol 即资金入合约、受益人按计划自行提取的成熟实现Killable/selfdestruct 的歧义自毁类功能在现代设计中大幅退场升级与销毁语义被拆分为更精细的机制如 proxy 体系避免一条函数拿走全部的粗粒度设计值得注意的是当年的MultisigWallet、Shareable、CrowdsaleToken、PullPayment等文件如今已不在 v5.x 的 contracts 目录中说明它们要么被重写要么被判定为不值得保留的模式。这本身就是审计价值的终极体现——审计不仅修 bug还会淘汰反模式。八、从这份报告提炼的安全评审清单把整份报告压缩成一份可操作的检查单供你在审自己的合约或阅读 WTF-Solidity 教程代码时逐项对照资金通路完整性能收钱的合约必须有受控的提款函数Crowdsale 教训任何能收不能取的状态都是严重缺陷。错误处理一致性全库统一 throw/revert 或统一返回码或显式写出设计准则注意 modifier 用if(bool){_;}还是直接 revert 的差异。状态更新顺序外部调用放在所有状态变更之后PullPayment 的 send 在最后即是最早的 checks-effects-interactions 实践。重入与递归确认-执行-撤销类协议要防范同一调用被重复执行限额被重置后再提款必要时加非重入锁。溢出/下溢在数据操作最底层做检查2017 年靠 SafeMath今天靠编译器 checked arithmetic。构造函数与参数健全性required、owner 数量、地址合法性等必须在构造时校验Shareable 教训。映射与列表的重排攻击任何往公共数据结构里插入自己的数据的入口都要按最坏意图推演propose/revoke 教训。approve 类竞态授权接口要防旧额度新额度叠加花费或用 permit/nonce 类方案。测试强度单元测试是硬性要求且要由不客气的人写——边界条件、恶意调用方、gas 限制都要覆盖。WTF-Solidity 仓库本身就是这套检查单的活教材如 S01_ReentrancyAttack/ReentrancyAttack.sol 演示重入攻击与ReentrancyGuard防御、19_Fallback/Fallback.sol 与 20_SendETH/SendETH.sol 讲解 send/transfer/call 的区别正是审计讨论.sendvs.call.value()的现代延伸、50_MultisigWallet/MultisigWallet.sol 展示多签确认状态机22_Call 与 23_Delegatecall 则深入低层调用原语。把 2017 年审计报告与这些教程对照阅读能同时获得历史漏洞形态与现代防御写法两个视角。九、结语2017 年的这份审计报告是 OpenZeppelin 合约库最早期的公开安全评审档案之一。它的价值远超两个 bug 的修复记录它示范了一套完整的合约评审方法论——先看宏观设计是否鼓励安全扩展再统一错误处理语义再逐个模块推演资金流、竞态与恶意参与者行为最后落到逐行代码。十余年后回看报告中每一个不推荐直接部署的结论都在 OpenZeppelin Contracts 后来的版本演进中得到了实质回应统一 revert、内建算术检查、permit 签名授权、非重入锁、更细粒度的权限与资金模块。对今天的 Solidity 开发者而言这份档案既是理解现代安全原语为什么长成这样的最佳入口也是一份可以直接照抄进自己代码评审流程的检查清单。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表