本文是FHEVM(基于全同态加密的EVM)安全开发指南,通过案例分析,深入探讨了FHEVM智能合约开发中常见的安全漏洞,并提供了相应的缓解措施和开发检查清单,旨在帮助开发者构建更安全的FHEVM应用,重点关注加密算术的风险、访问控制、异步回调、静默失败、信息泄露等问题,并强调最小化权限、充分验证和防止重放攻击的重要性。
FHEVM 安全开发者指南
目录
简介 - FHEVM 的保密性 访问控制列表 (ACL) 的工作原理 安全深度分析 1 - 算术和访问控制 漏洞 A - 未经检查的算术溢出导致收费不足 漏洞 B - 由于缺少授权和过度宽泛的权限导致的信息泄露 安全深度分析 2 - 防止异步解密回调中的重放 漏洞 - 无状态回调重放 安全深度分析 3 - 静默失败和区块重组 漏洞 A - 静默失败的竞标赢得拍卖 漏洞 B - 竞速链最终确定性的信息披露 进一步的考虑 瞬时许可和账户抽象 第三方调用者的外部加密值 任意外部调用 结论 - 从保密性到完整性
- 2025 年 12 月 18 日

Sebastian Fabry

Sebastian Fabry
区块链安全研究员
Zama 的 FHEVM 将全同态加密 (FHE) 集成到与 EVM 兼容的链中,因此智能合约可以计算加密值(例如,euint64),同时保持函数参数和状态的端到端保密性。链下协处理器处理繁重的 FHE 数学运算,而链上合约协调状态更新、访问控制以及(必要时)异步解密回调。
本指南使用一个说明性的案例研究:一个保密的 Wrapped Ether 代币 (cWETH),它调用一个单独的 FeeHandler 来计算转移费用,以及一个使用 cWETH 进行竞标的拍卖合约。我们将使用这些示例演示多个陷阱。但是,请注意,为了简洁起见,代码片段是不完整的。
你将在最后找到一份实用的清单,以便在审核你的保密合约之前对其进行审查。
在 FHEVM 中,每个密文都由一个 bytes32 Handle标识,并带有自己的访问控制,该访问控制由一个集中的 ACL 合约管理。生成或有权访问密文Handle的合约可以授予另一个地址使用它的能力,可以通过 FHE.allow 持久授权,也可以通过 FHE.allowTransient 仅为当前交易授权。当一个函数接收到一个Handle时,它可以使用 FHE.isSenderAllowed 验证调用者是否实际被授权对该密文执行操作。这些权限决定了谁可以对一个值执行加密操作,并且是防止滥用或间接披露的基础。在实践中,开发者应该默认只授予手头操作所需的最小权限,并将授权检查放在密文被使用的附近。
场景:在下面的代码中,cWETH 合约通过将加密的 amount 传递给 FeeHandler 来实现其内部转移例程,FeeHandler 返回加密的费用Handle。为了使交互跨调用工作,cWETH 使用持久许可授予 FeeHandler 对 amount 密文的权限,然后使用该费用在保持保密性的同时更新余额。在 FeeHandler 内部,费用计算为超过 amount 的基点,并且生成的加密 fee 可供调用者使用,因此可以被代币逻辑使用。
Copy
加密整数的算术运算有意不进行检查,以避免通过还原泄漏信息,因此运算会在溢出和下溢时回绕。如果提供了足够大的 amount,则费用计算期间的中间乘法可能会回绕为一个很小的值,并产生可忽略不计或为零的费用。
缓解措施: 使用 FHE 比较检测加密下的溢出,然后使用 FHE.select 继续使用有效的回退值。这是一种有意的静默失败模式,用于保密:该函数在不泄露条件的情况下继续执行,同时强制执行安全的结果。在这个费用示例中,当检测到溢出时,选择应该钳制为最大费用,确保转移不会收费不足。
Copy
授予 FeeHandler 对 amount 的持久访问权限使其可以跨调用计算费用,但是在不验证调用者是否被授权使用该密文Handle的情况下,攻击者可以通过中间合约路由调用。这个攻击者合约可以使用复制的Handle调用 calculateFee(amount),FeeHandler 接受它,计算 fee,并且(按照设计)授予访问权限给 msg.sender(攻击者合约)。该合约现在保留了 fee 的 ACL 权限,并且可以转发它们(例如,通过授予访问权限给攻击者的地址或使其可以公开解密)。因此,即使 fee 本身最初没有携带对攻击者的许可,该助手也会变成一个披露 oracle。
缓解措施: 在任何计算之前,验证调用者对提供的密文Handle的权限,并将许可范围限定为尽可能的一次性交互。保持持久授权的稀少性和严格的范围,以便密文Handle不会传播到不受信任的上下文中。
Copy
场景: 一些操作需要密文Handle的解密值。此解密由链下中继器异步完成。在下面的代码中,cWETH 合约实现了两个函数(withdraw 和 withdrawCallback) 来解密 cWETH 并将其解包为 ETH,而 _withdrawRequests 映射用作两个步骤之间的链接。此映射以请求提款的用户地址为键。这意味着必须先完成两步提款才能启动另一个提款。
在第一次调用 withdraw 的交易中,请求的金额被销毁,并且销毁的金额在映射中被跟踪。合约还标记要公开解密的销毁金额,这实际上是在调用 ACL。
在第二个交易中,中继器调用 withdrawCallback,提供提款人的地址、明文销毁金额和正确性证明。合约从映射中查找密文Handle,以查看是否有提款挂起,并使用此值来验证证明。通过将锁定的 ETH 释放给接收者来完成操作。请注意,由于保密代币中的小数位数较少,因此该金额按 _rate 缩放,以便更好地适应余额的 euint64 类型。
Copy
虽然回调验证了中继器的证明,但首次成功履行后使 _withdrawRequests 条目保持有效的设计容易受到重放攻击。攻击者可以为任何金额发起提款,观察中继器在调用回调时的 calldata,并使用此复制的 calldata 自己重放第二个交易。通过重复此攻击,只要映射保持不变,攻击者就可以耗尽锁定的 ETH。
缓解措施: 确保解密回调既验证中继器的响应,又强制执行异步链接 (_withdrawRequests) 的一次性使用。在与外部地址交互之前,使记录的映射条目失效,并且保持回调的逻辑最小化,以限制外部价值转移或调用,除非它们是明确预期的并且经过审计。这会将解密流程变成一个可消耗一次的管道,并关闭基于重放的耗尽。
Copy
异步链接必须仔细考虑。异步调用的跟踪不应在用户之间冲突,并且应在请求之间处理。例如,密文Handle不是一个安全的标识符。相反,应用清理策略,例如通过引入一个专门的异步请求计数器,或者验证,如上面的“WithdrawalAlreadyPending”检查所示。
场景: 在此示例中,SealedTreasureAuction 合约实现了一个保密的密封投标拍卖,参与者以 cWETH 进行投标。投标作为 externalEuint64 值提交,并使用来自 cWETH 的保密转移提取。合约跟踪加密状态中的当前领导者(euint64 _highestBid 和 eaddress _highestBidder),并且仅在拍卖结束后显示获胜者。然后,获胜者可以声明访问存储为加密坐标的保密宝藏位置(纬度和经度)。
以下代码片段重点介绍了 cWETH 内部 _update 函数中的另一个 FHE 实现细节。由于所有余额和金额都是加密的,因此 EVM 无法基于中间 FHE 结果进行分支或还原。链下协处理器了解 balance >= amount 是否成立,但链上执行必须线性进行,而不能以明文形式看到该位。合约总是继续并进行零转移无操作,而不是在余额不足时还原,从而使更高级别的逻辑可以检查加密下的有效 transferred 值。我们将此模式称为静默失败。
Copy
如果余额不足,合约不会还原,并且内部 _update 逻辑会静默转移并返回零金额。拍卖忽略有效的 transferred 金额,并将请求的 amount 与 highestBid 进行比较,因此任何竞标者都可以提交任意大的加密投标,触发零值转移,并且仍然成为 highestBidder,而无需转移任何 cWETH 代币。
缓解措施: 当与可能静默钳制或清零值的保密代币集成时,拍卖必须将其获胜者选择基于有效收到的 (transferred) 金额,即减去费用,并且拒绝未转移价值的投标。在这种情况下,合约应该计算 transferred > 0 的保护,并在更新 _highestBid 和 _highestBidder 之前将其折叠到加密比较中。
Copy
在此拍卖中,出售的价值是宝藏的坐标,并且一旦获胜者可以解密它们,披露它们就是不可逆转的。如果 discloseWinner 在 auctionEnd 过去后立即将 _highestBid 和 _highestBidder 公开解密,则用户可以了解宝藏的位置,然后如果区块重组将其获胜投标替换为后来的投标,则追溯性地输掉拍卖。链上状态将显示不同的获胜者,但原始竞标者已经免费知道坐标。
缓解措施: 当信息是产品时,合约应该将确定获胜者与授予解密访问权限分开,并将披露与最终确定性延迟对齐。第一次调用通过在拍卖结束后记录当前区块时间来安排披露。第二次调用,强制执行保证拍卖结束时区块的最终确定性的延迟,然后授予解密权限和宝藏访问权限。
Copy
深入分析侧重于特定的合约级别错误,但是有一些更广泛的 FHEVM 陷阱很容易被忽略,因为它们位于合约“之间”:在瞬时存储中,在链下如何生成加密输入中,以及任意外部调用如何与 ACL 交互。这些本身不是完整的错误,但是它们很容易出错,并且可能会静默地破坏保密性。
瞬时许可和Handle(例如通过 allowTransient、fromExternal 或中间加密值)旨在是短期的。单个调用授予访问权限,使用它,然后该访问权限在交易结束时有效地消失。通过账户抽象,多个用户操作共享相同的交易,因此共享相同的瞬时存储。如果 Alice 的操作授予合约对Handle x 的瞬时访问权限,并且 Mallory 的操作在同一交易中运行,则 Mallory 可以重用 x 并从 Alice 的瞬时许可中受益。这可以被滥用为原本独立的用户操作之间的数据泄漏通道。
有一种可用的对策可以擦除受影响的瞬时存储,即 FHE.cleanTransientStorage() 函数。但是,在实践中,期望每个 FHEVM 合约在每个调用路径的末尾都调用此函数是不现实的。更好的模式是让 AA 钱包通过从 ZamaEthereumConfig 查询 confidentialProtocolId() 来检测 FHEVM 合约,然后使用其自己的“清理”用户操作跟进每个 FHE 敏感的用户操作。这样,每个用户操作都可以获得一个新鲜的瞬时上下文,而无需将该责任强加给每个协议。
外部加密值,即与 FHE.fromExternal 一起使用的 (externalEuint64 value, bytes proof) 元组,是为特定的合约地址和特定的调用者地址 (msg.sender) 在链下创建的。在典型的 cWETH 流程中,用户使用自己作为调用者加密 cWETH 的金额,然后直接调用 cWETH.confidentialTransferFrom。在这种情况下,该元组有效地绑定到该用户。另一个帐户无法重放它,因为当 msg.sender 与用户输入的上下文不匹配时,FHE.fromExternal 将失败。
当为一个第三方调用者而不是用户加密一个值时,问题就出现了,例如,一个时间锁合约,它稍后将代表用户调用 cWETH.confidentialTransferFrom。然后,该元组对任何可以用此 (value, proof) 进行此时间锁调用 cWETH 的人有效,而不仅仅是对原始用户有效。一个可以触发时间锁再次调用 cWETH.confidentialTransferFrom 的攻击者,可以通过观察他们自己的余额变化,从 calldata 中复制该元组并了解用户的机密金额。
有三种可能的缓解措施。一种是简单地避免为第三方调用者加密值。另一种是让中间合约(例如,时间锁)使用 FHE.fromExternal 验证该元组,从而验证用户上下文,然后使用密文Handle进行进一步的调用。最后,时间锁可以加密地将该元组绑定到预期的用户上下文,以便其他帐户无法重用它。但是,请注意 proof 组件是可变的(例如,通过重新排序链下签名),因此仅仅散列 (value, proof) 字节不是检测重用的可靠方法。因此,不可重放性必须来自该元组如何绑定到身份和上下文,而不是来自其原始字节表示。
任意外部调用总是危险的,但是在 FHEVM 上下文中,它们可以完全绕过保密性。任何从特权上下文中运行的 execute(address target, bytes data) 类型函数都可以用来调用 ACL 合约并授予对加密状态的访问权限,从而有效地将该执行者变成一个通用的数据 oracle。当一个合约持有或被授予机密数据时,暴露任意外部调用是一个直接的保密性风险。此类合约应避免不受约束的调用,并且应牢记 ACL 进行仔细审计,而不仅仅是传统的 EVM 安全假设。
本文介绍了 FHEVM 的每个密文 ACL 如何支持保密性,同时将大部分安全负担转移到完整性和状态控制。在费用计算案例研究中,我们看到未经检查的加密算术如何在没有仔细的保护模式的情况下静默地收费不足,以及在助手程序中忽略授权(尤其是在与持久许可配对时)如何将良性集成变成数据泄漏通道。在异步解密流程中,我们强调了通过在任何外部交互之前消耗记录来将中继器回调视为一次性事件的重要性。密封投标拍卖示例表明,静默失败如何破坏围绕托管的经济假设,以及当你出售的资产是信息时,链最终确定性为何重要。最后,我们探讨了围绕账户抽象下的瞬时许可、第三方调用者的外部加密值以及对 ACL 合约的任意外部调用的三个交叉考虑因素。在所有示例中,共同的主题是最小化权限,在使用点进行验证,检查算术,显式处理回退情况,并防止可重放性。
这些保护措施并非免费:加密保护模式、显式授权检查和更严格的控制流程会增加 gas。是否启用每个检查应遵循你的威胁模型,并根据具体情况进行权衡。
对于每个加密算术步骤,分析回绕风险并应用加密保护和安全的回退值选择。
当一个合约接收到一个密文Handle时,验证调用者是否被授权使用适当的授权检查对其进行操作。
仅授予必要的最小权限。对于助手调用,请首选瞬时的、交易范围的许可。如果持久许可是不可避免的,请约束 密文Handle的流动位置并在每个使用者中强制执行检查。确保必须有权访问Handle的帐户被明确授予访问权限。
确保在任何外部调用或价值转移之前,异步交易记录无效。
验证回调中的中继器证明,并尽可能缩小回调范围,以减少重入和重放风险。
对于任何可能静默钳制或清零效果的 FHEVM 调用,在更新依赖于它的状态之前检查有效结果,或设计恢复路径。
如果信息的披露是被出售的产品,并且区块重组是一个问题,请使用两步披露。首先,在安排披露时记录当前区块高度。其次,仅在链的最终性/确认阈值已通过后(不在同一交易中)才授予 ACL 访问权限。
在账户抽象的上下文中,当多个用户操作在一个交易中运行时,请确保在操作之间擦除瞬时存储。
当为第三方合约调用者上下文外部加密输入时,请确保它防止其他未经授权的用户重放此输入值证明元组。
确保任意外部调用受到适当的访问控制,以防止通过 ACL 泄漏数据。
请注意,委托调用中的被调用者可以访问和解密调用者的密文Handle。
有用的链接
确保阅读 Zama 文档中的 Solidity 指南,以获得额外的见解。一些示例的灵感来自它们。
如果你正在寻找机密合约蓝图,请查看基于 FHEVM 的 OpenZeppelin 机密合约库。
要详细了解底层,请考虑 Zama 的 FHEVM 库。
准备好保护你的代码了吗?
platform.twitter.com
此内容被阻止。请联系网站所有者以解决此问题。
ERR_BLOCKED_BY_CSP
此内容被阻止。请联系网站所有者以解决此问题。
- 原文链接: openzeppelin.com/news/a-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!