news 2026/9/15 14:50:37

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 逐行点评")。

四、两个严重漏洞(Critical)

4.1 Crowdsale 合约中的资金永久卡死

CrowdsaleToken.sol是一个在收到 ETH 时按固定价格铸造代币的 StandardToken,但它完全没有提供把募集到的 ETH 提走的函数。审计方措辞强烈:"没有任何场景应该有人原样部署这个合约,无论是测试还是上线。"结论是强烈建议新增一个标准的withdraw函数。

这是所有融资类合约的经典必修课:只要合约能收钱,就必须设计显式、受控的提款通路,否则资金会永久冻结在合约地址中。后续主流 Crowdsale/众筹合约无一例外都补齐了"代币合约与募资合约分离 + 募资合约可提款"的结构。

4.2 MultisigWallet 的递归调用与每日限额绕过

MultisigWallet.sol第 45 行在execute中检查转账金额是否低于每日限额(daily limit)。该函数只能由 Owner 调用。审计方提出一个攻击面推演:

  1. 如果多签钱包的所有者批准了一笔对resetSpentToday(重置当日已花费额度)的调用,会怎样?
  2. 只要能构造一条调用链,让 Owner 确认resetSpentToday之后,再通过execute在递归调用中反复提款,合约就可能被抽干;
  3. 甚至不需要递归:在confirmexecute之间交替进行多次普通调用即可达到同样效果。

审计方仍在推敲Shareable.sol的确认协议,但"看不出这种攻击不可能发生,事实上它看起来是可能的"。另一个令人不安的角度是:共享所有者可以事后撤销(revoke)自己的确认,即便打了几个简单的补丁,这个灵活性依然可疑。

报告把该 bug 拆成四个需要分别处理的原因:

  1. resetSpentTodayconfirm组合后,既不限制可调用的日期,也不限制可调用的次数;
  2. 一旦某次调用被确认并执行,它看起来可以被重复执行;
  3. confirmandCheck似乎没有判断"目标函数是否已经被调用过";
  4. 即便加了判断,revoke也需要更新逻辑,处理"函数调用完成之后再收到撤销请求"的情况。

结论很直接:在修复这些问题之前,不要使用 MultisigWallet。有趣的是,这份审计指出的"确认-执行-重复执行-撤销"问题,本质上就是后来多签钱包(包括 WTF-Solidity 仓库自带的 50_MultisigWallet/MultisigWallet.sol 教学合约)在实现时必须处理的状态机核心难点:提案必须幂等、确认必须与具体提案绑定、已执行的提案不可重放。

五、中等问题(Moderate to Minor)

5.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:使用SafeSubSafeMath,所以 transfer 失败时 throw 而非返回 false,符合 ERC20Basic 但不完全符合 ERC20 标准。

StandardToken:完整 ERC20 实现。transfer()transferFrom()走 SafeMath,失败会 throw 而非返回 false,不是安全问题但偏离标准。

SimpleToken:StandardToken 的示例实例。注意 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 行的killsetDailyLimitresetSpentToday都要多签批准,且 Shareable 会记录这些操作的哈希,但建议它们各自再发出独立事件便于阅读。第 45 行的underLimit调用会先扣减每日限额,然后 throw 或返回 0,所以不存在"限额被扣了但操作没走通"的危险。第 65 行被盛赞为优雅设计:onlyManyOwners会记录用户确认,只有确认数足够时才执行函数体;send 失败则整体 throw 并回滚确认;确认数不足返回 false,全部成功返回 true,仅在指定交易意外失败时 throw。第 68 行 throw 是对的,但注意该函数既可能返回 false 也可能 throw。第 92 行把clearPending()拆在 Shareable 与 MultisigWallet 两处略奇怪,但这允许继承 Shareable 的合约对 pending 事务使用自定义结构体。

SafeMath:Edcon 演讲中的一个洞见——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/OwnableInvalidOwner;ERC20.sol 文件头注释明确写着"functions revert instead returningfalseon failure"
Pausable 用if(bool){_;},Owner 作恶空间大改用whenNotPaused/whenPausedmodifier 内部直接revert EnforcedPause()/ExpectedPause(),从机制上消除"静默失败"Pausable.sol
SafeMath 需隔离溢出检查v0.8 起编译器内建 checked arithmetic,SafeMath 整体退役;数学工具重组为Math/SafeCast/SignedMathutils/math
ERC20 approve 竞态无法在标准内修复推出 ERC-2612permit(签名授权,无需持有 ETH 发交易,nonce 防重放),另有draft-ERC20TemporaryApproval探索临时授权ERC20Permit.sol
重入/递归调用风险(Multisig 事件)提供nonReentrantmodifier,用存储槽状态机(NOT_ENTERED/ENTERED)拦截嵌套调用,并有 transient storage 变体ReentrancyGuard.sol、ReentrancyGuardTransient.sol
PullPayment 缺取消、竞态、计数现代取款类逻辑收敛为锁定/计划释放的专用模块,仓库中的 VestingWallet.sol 即"资金入合约、受益人按计划自行提取"的成熟实现
Killable/selfdestruct 的歧义自毁类功能在现代设计中大幅退场,升级与销毁语义被拆分为更精细的机制(如 proxy 体系),避免"一条函数拿走全部"的粗粒度设计

值得注意的是:当年的MultisigWalletShareableCrowdsaleTokenPullPayment等文件如今已不在 v5.x 的 contracts 目录中,说明它们要么被重写,要么被判定为不值得保留的模式。这本身就是审计价值的终极体现——审计不仅修 bug,还会淘汰反模式

八、从这份报告提炼的安全评审清单

把整份报告压缩成一份可操作的检查单,供你在审自己的合约或阅读 WTF-Solidity 教程代码时逐项对照:

  1. 资金通路完整性:能收钱的合约必须有受控的提款函数(Crowdsale 教训);任何"能收不能取"的状态都是严重缺陷。
  2. 错误处理一致性:全库统一 throw/revert 或统一返回码,或显式写出设计准则;注意 modifier 用if(bool){_;}还是直接 revert 的差异。
  3. 状态更新顺序:外部调用放在所有状态变更之后(PullPayment 的 send 在最后即是最早的 checks-effects-interactions 实践)。
  4. 重入与递归:确认-执行-撤销类协议要防范"同一调用被重复执行""限额被重置后再提款";必要时加非重入锁。
  5. 溢出/下溢:在数据操作最底层做检查(2017 年靠 SafeMath,今天靠编译器 checked arithmetic)。
  6. 构造函数与参数健全性required、owner 数量、地址合法性等必须在构造时校验(Shareable 教训)。
  7. 映射与列表的重排攻击:任何"往公共数据结构里插入自己的数据"的入口都要按最坏意图推演(propose/revoke 教训)。
  8. approve 类竞态:授权接口要防"旧额度+新额度叠加花费",或用 permit/nonce 类方案。
  9. 测试强度:单元测试是硬性要求,且要"由不客气的人"写——边界条件、恶意调用方、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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 14:49:45

博客网站需要的功能最佳实践:拒绝拖沓,3天搞定核心体验

博客网站需要的功能最佳实践:拒绝拖沓,3天搞定核心体验 改个需求建站公司拖一周,这种绝望感相信很多做过独立博客或企业站的朋友都体会过。你明明只想要个简单的暗色模式切换,对方却回复“需要重新评估UI规范”,结果一周过去,连个按钮颜色都没定下来。这时候你就该反思了,是不是在前期定义【博客网站需要的功能】…

作者头像 李华
网站建设 2026/9/15 14:49:22

PID图纸识别软件选型指南:五大维度避开认知陷阱

1. 为什么选个P&ID识别软件&#xff0c;比想象中难得多做流程工业数字化这些年&#xff0c;我接触过不少准备上马图纸识别项目的团队。大家最初的诉求往往很朴素&#xff1a;把积压的纸质版或扫描版P&ID&#xff08;管道及仪表流程图&#xff09;变成可编辑、可检索的电…

作者头像 李华
网站建设 2026/9/15 14:48:17

声学回声消除深度学习基线:频谱掩膜与工程化最小闭环

简介&#xff1a;一份基于深度学习的声学回声消除基线代码包&#xff0c;面向语音通信、视频会议、语音识别等场景的算法工程师与研究人员&#xff0c;用于快速搭建并理解深度神经网络回声消除基线系统&#xff0c;解决远场拾音中的回声干扰问题。压缩包共31个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/15 14:46:35

Buf 完整指南:如何把 Protobuf 工程从手写脚本带到一站式工具链

Buf 完整指南&#xff1a;如何把 Protobuf 工程从手写脚本带到一站式工具链 【免费下载链接】buf The best way of working with Protocol Buffers. 项目地址: https://gitcode.com/GitHub_Trending/bu/buf Buf 是 Protocol Buffers 的现代化工程工具链&#xff1a;它把…

作者头像 李华