来自模糊测试前线的教训

  • Recon
  • 发布于 2024-07-26 15:48
  • 阅读 30

本文详细探讨了在EigenLayer生态系统中对Renzo协议进行模糊测试的过程和挑战,包括覆盖率的提升、动态部署,以及处理各种外部事件(如用户资金惩罚和LST的折扣与重基机制)。通过定义和实现一系列功能,作者分享了在模糊测试过程中学到的关键经验,对安全性评估具有重要意义。

Jul 24, 2024

1

任何模糊测试 Engagement 通常由两个部分组成:实现覆盖和实际模糊测试你对系统定义的属性。在几周的时间里,我努力实现前者,建立了一个专为 Renzo 协议创建的 fuzzing scaffolding,并学到了关于模糊测试的有价值的经验教训,我将在这里分享。

注意:这个 Engagement 不是 Renzo 付款或认可的,旨在作为实用的研究实验,以开发在 EigenLayer 生态系统中模糊测试项目的技术。

实现覆盖

我负责的主要目标是为 Renzo 搭建初始的测试框架,并确保所有被模糊测试器(在此案例中为 Medusa)针对的函数都达到了覆盖率。尽管一开始看似简单,加上了 Recon 的 scaffolding 工具使其更容易,但这证明比我最初想到的要复杂得多,要求不断迭代,并思考当模糊测试器执行调用时实际发生的事情。

EigenLayer 集成

Renzo 基于 EigenLayer 构建,以简化允许质押者将自己的质押资金质押到 EigenLayer 中,并将其质押分配到不同的 AVS(附加验证服务)。

EigenLayer 是一个以太坊协议,允许重新质押,质押者可以通过 EigenLayer 质押本地 ETH 或 LST(流动质押代币),并向建立在 EigenLayer 之上的其他协议(AVS)扩展加密经济安全服务,从中获得额外的质押奖励。

由于 Renzo 与 EigenLayer 的功能紧密结合,我首先需要一种方法来部署 EigenLayer 系统,以便其可以集成到 Renzo 的模糊测试套件中,更真实地模拟实际系统的行为,而不是通过简单地模拟 EigenLayer 系统而可能引入不同的行为。因为 EigenLayer 系统相对紧凑,这幸运地并没有像通常那样复杂。

幸运的是,EigenLayer 测试套件已经包含了其测试设置中用于 Foundry 的完整系统部署,然而不幸的是,我直到自己根据部署脚本构建了自己的系统后才注意到这一点。然而,这种做已经完成的事情并不是完全不好的,因为我所创建的 EigenLayerSystem 部署合约最终提供了在后续需要的额外功能。

在为 Renzo 使用 Recon 搭建模糊测试框架后,我创建的 EigenLayer setup 可以轻松地作为子模块添加到项目中,并通过从 EigenLayerSystem 合约继承,调用 deployEigenLayerLocal 函数将 EigenLayer 系统部署到这个框架中,并以公共状态变量的形式公开所有核心合约,从而便于与其他协议的集成:

添加 eigenlayer-fuzzing 子模块简化了任何与 EigenLayer 集成的协议的模糊测试设置。

我现在准备开始在 Renzo 系统的主要合约 RestakeManager 上获取一些覆盖率,或者我这么认为……

简单的夹紧

在按照 Chimera 框架设置 Recon 的框架之后,我立即有了一个看起来像这样的 TargetFunctions 合约:

这个没有夹紧的初始目标函数合约需要很长时间才能实现覆盖,即使没有定义任何属性。

但在进行任何较长时间的运行后,覆盖率总是会在某些点卡住。

经过对覆盖率报告的调查和使用单元测试从 Medusa 语料库执行调用序列后,问题的根源显而易见:从根本上讲,由于模糊测试器的搜索空间太大,某些代码路径总是会回退,因此需要适当缩小搜索空间(小心以避免漏掉可能的有效状态)。

或者,模糊测试器可以被简单地放任几个小时,直到发现不会回退的代码路径,但我们在 Recon 的一般观点是,模糊测试器应该可以被整个协议团队或外部审计员使用,因此设置一个需要几天或几周的模糊测试设置,即使没有定义属性,对频繁运行也不是很方便,结果会随着代码库的更改而发现更少的潜在漏洞。

在这种情况下,解决方案是通过有限的夹紧来缩小搜索空间,即减少模糊测试器执行的输入值集。例如,地址总共有 2^160 种可能,但只有少量子集(在实现的 actor 设置中使用上述两种)能够在 Renzo 系统中执行任何操作,因为它们需要持有 ETH 或不同的流动质押代币,其余的将导致模糊测试器在因无趣原因如余额不足而回退的调用上进行无数次运行。

因此,决定简单地夹紧这些演员地址以及传递到函数调用中的代币地址,这样只有在系统中实际使用的演员和代币才会用于调用执行或作为输入传递到系统中的函数。

应用于目标合约的夹紧示例。 实现一个 _getRandomX 函数简化夹紧,使其在目标函数中保持一致,并且在这种情况下将搜索空间减少到只有感兴趣的地址。

动态部署

在这项工作的过程中,接下来的领悟是,Renzo 系统实际上能够处理比我在 RenzoSetup 合约中创建的初始设置更多的抵押代币组合(以及对应的 EigenLayer 策略)和 OperatorDelegators。

在我的设置中,系统部署了 Renzo 表示他们将支持的两种流动质押代币(来自于他们的 code4rena 竞赛页面):stETH 和 wbETH,这两个代币分别被设置在两个 OperatorDelegator 合约的实例中(基本上是 EigenLayer Operator 的外壳),每个都有不同的分配(operatorDelegator1 - 70%,operatorDelegator2 - 30%)。

然而,Renzo 最终会添加对更多 LST 的支持(如他们文档中所示),最终将可能有多种 LST 和 OperatorDelegator 的组合,每个组合有自己的不同分配,如下图所示:

这与 Recon 与 Centrifuge 的 Engagement 中讨论的见解类似 此处

所以考虑到 Renzo 可能会在启动后增加对更多 LST 的支持,鉴于 EigenLayer 目前已经支持额外的 11 种在 Renzo 所使用的 LST 之上,我引入了能够让模糊测试器动态部署和切换系统中新的代币和策略的能力,设置在 restakeManager_deployTokenStratOperatorDelegator 函数中。

这最终证明是一个在复杂化上超过所需的教训,实际上并没有增加任何新的有趣系统变动,而是可以通过模糊测试不同的分配值来实现,因此后来被撤回。然而,不同的 OperatorDelegator 及其不同的分配的心理模型是获得对 Renzo 系统更深入理解的关键解锁。

外部性

在通过上述方法实现对目标合约的覆盖后,重点转向如何在 Renzo 所参与的更大系统中模拟潜在的不利事件,以确定这些事件是否会破坏 Renzo 所定义的任何不变性。

我在这里称这些为外部性,因为与经济中的外部性类似,所探讨的事件类型可能给一方带来利益,而另一方则承担损失,即可能导致用户资金损失,牺牲另一方的利益。

原生惩罚

原生惩罚是以太坊权益证明安全机制的一个重要部分,在此机制中,执行不当、试图错误修改全局状态根的验证者节点会从其锁定以成为验证者的部分质押中被削减。

在系统中对这一点进行测试非常重要,因为通过 Renzo 质押本地 ETH 的用户正在将他们的资金委托给一个 OperatorDelegator,该委托者代表他们通过一个 EigenPod 在 EigenLayer 中质押。如果这个 OperatorDelegator 因不当行为而在惩罚事件中受到惩罚,将会对 Renzo 系统的会计产生副作用。

通过 deployEigenLayerLocal 函数访问整个 EigenLayer 系统,使得在 EigenLayerSystem 合约中实现一个 slashNative 函数非常简单,该函数通过减少正在使用的 EthPOSDeposit 合约的余额来模拟一个惩罚事件,并通过调用 EigenPodManager 考虑到 EigenPod 中余额的变化:

这可以通过集成协议的模糊测试目标函数之一调用,在这种情况下是来自 Renzo 的 RestakeManagerTargets 合约中的 restakeManager_slash_native

AVS 惩罚

EigenLayer 的核心功能是允许将质押委托给提供服务的 Operators 构建在 EigenLayer 之上的 AVS。然而,考虑到这些 Operators 是以去信任的方式为系统提供重要服务,EigenLayer 实现了 AVS 惩罚,允许对显示出恶意行为的 Operator 进行质押惩罚。

在许多方面,这与原生 ETH 惩罚相似,但由于用户可以将流动质押代币(LSTs)委托给 Operators,因此当发生 AVS 惩罚事件时,这些也需要被考虑,以确保妥善惩罚恶意的 Operator。

由于 EigenLayer 系统的 Slasher 合约尚未最终确定,因此用于 Renzo 模糊测试库的实现基于对其可能行为的解释,参考 ISlasher 接口。基本上,slashAVS 函数的工作类似于上面描述的 slashNative 函数,燃烧由 Operator 持有的代币,并修改给定 Operator 的会计记录以反映这一点:

slashNative 类似,这也可以通过集成协议中的模糊测试目标函数之一调用,在这种情况下为 restakeManager_slash_native

折扣与重基:同一枚硬币的两面

LSTs 旨在保持与其质押 ETH 背书的挂钩,但它们实现这一目标的机制可能对与它们集成的系统产生影响。在 Renzo 的情况下,用户可以通过 LSTs 向系统存入资金,并收到 ezETH(本地 Renzo 代币)作为质押的凭证。为了确定用户应获得多少 ezETH 作为存入 LST 的对应,Renzo 使用价格预言机以及当前的 totalSupply 的流通 ezETH。

在理想情况下,这将允许正常的汇率计算,然而,历史上 LSTs 曾显示出受到脱钩事件的影响(如 此处 所描述)。此外,每个 LST 代币实现了一种重基机制,以处理随着累计验证者的质押奖励而增加的代币价值。

为了更准确地模拟这些效果,以下描述的折扣和重基机制被实现到 Renzo 的模糊测试库中。

折扣

由于折扣意味着代币价格的降低,这只需要将 Renzo 系统中使用的预言机返回的价格减少,以模拟这一点。鉴于测试套件是为本地测试设置的,模仿预言机可以直接被模糊测试器调用,以在以下目标函数中模拟这种事件:

决定不引入新价格的夹紧,以允许更大幅度模拟在不利市场条件下可能出现的显著价格波动的边缘案例场景。

重基

Renzo 最初打算支持的代币(stETH 和 wbETH)使用不同的机制来模拟由于重基事件造成的价格上涨,其中一种通过股份的累积使用利息,而另一种则使用修改的汇率。在简要探索创建一个支持这些机制的共享接口后,我意识到,由于它们实际上都不影响系统中的代币总供应量,唯一与之相关的又是 LST 与 ezETH 之间的汇率。

这意味着,在 Renzo 系统中,重基事件可以通过相对于 ezETH 的 LST 价格上涨来进行核算,这在以下函数中实现:

结论

模糊测试证明自己是揭示可能导致难以被人工审查者推理的漏洞边缘案例的非常有用的工具。然而,为了让模糊测试器有效地探索这些边缘案例,往往需要在模糊测试器设置和推理外部事件方面付出大量的手动工作,这些事件可能引入副作用,并确定使用模糊测试器模拟这些事件的最佳方式。

幸运的是,一旦在基础协议中定义了这些功能,例如 slashNativeslashAVS,它们可以轻松集成到其他地方,因此只需创建一次。

对于其他特定协议的边缘事件,如 LST 折扣和重基,尽管它们的实现可能是特定协议所独有的,但它们的理念通常可以在使用相同机制的其他协议中重复使用(如这里示例中重基实现所示,受到 与 eBTC 的 Engagement 的启发)。


订阅 Recon

10 个月前启动

Recon 帮助你构建和运行不变性测试

  • 原文链接: getrecon.substack.com/p/...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Recon
Recon
江湖只有他的大名,没有他的介绍。