Aave v3 不变量测试套件的内部文档

  • aave-dao
  • 发布于 2025-02-13 21:51
  • 阅读 13

本文档是 Aave v3 不变量测试套件的内部文档,介绍了如何运行测试套件、属性格式、如何添加对新函数的支持、如何从 Foundry 迁移测试、如何迁移 Certora 属性以及如何调试broken属性。该套件使用 Echidna 工具来检查 Aave v3 协议的不变量和后置条件,并通过 property mode 和 assertion mode 两种不同的模式执行。

Aave v3 不变量测试套件内部文档

目录

  1. 运行套件

    • 前置条件
    • 启动套件
    • 配置
  2. 属性格式

    • 不变量
    • 后置条件
      • 全局后置条件 (GPOST)
      • Handler 特定后置条件 (HSPOST)
  3. Handlers: 添加对新函数的支持

    • 概述
    • 添加新函数
    • 测试新函数
  4. 从 Foundry 迁移测试

    • 概述
    • 迁移步骤
  5. 迁移 Certora 属性

    • 概述
    • 属性映射
    • 迁移指南
  6. 调试损坏的属性

    • 日志 & 输出
    • Crytic to Foundry 测试助手
    • 在 Foundry 包装器中复现 Echidna 错误的步骤

运行套件

  • 前置条件:

    • 确保所有协议依赖项已安装:
    cp .env.example .env
    
    forge install
    • 确保已安装最新版本的 Echidna。 如果未安装,可以按照此处的指南进行安装。

    <br />

  • 启动套件: 该套件能够检查 Aave v3 协议的不变量和后置条件。 为此,它使用两种不同的模式,即属性模式和断言模式。

    • 属性模式: 检查协议不变量。 运行:
      make echidna
    • 断言模式: 检查协议后置条件。 运行:
    make echidna-assert
    • 额外: 运行套件而不检查属性,仅增加语料库大小和覆盖率。 运行:
      make echidna-explore

    <br />

  • 配置: 套件配置可以在 echidna_config.yaml 文件中找到。 此文件包含 Echidna 测试工具的配置,以下是配置中最重要的参数:

    • seqLen: 定义每个测试序列中的调用次数。
    • maxDepth: 设置要执行的测试序列的总数。
    • coverage: 启用覆盖率跟踪,存储在 corpusDir 指定的目录中。
    • corpusDir: 用于保存覆盖率数据的目录。 在此套件中,覆盖率保存在 tests/invariants/_corpus/echidna/default/_data/corpus 中。
    • deployContracts: 预先部署所需的库,以确保与 Echidna 和底层 hevm 的兼容性,特别是对于依赖于许多库的 Aave v3。 库使用带有 --compile-libraries 标志的 cryticArgs 参数链接。
    • workers: 设置 Echidna 的并行线程数,理想情况下接近机器的 CPU 线程数以获得最佳性能。 <br />

属性格式

正如公共文档中所述,此套件框架围绕两种类型的属性展开,即不变量后置条件。 以下部分将详细解释每种类型的属性以及如何实现它们。

不变量

  • 定义: 不变量是必须在系统的所有状态中都为真的属性。 当该工具在属性模式下运行时会检查这些属性,使 echidna 调用所有以 echidna_ 开头的公共函数,并确保其中的断言不会失败。 这些检查发生在测试序列中每次调用之间。

  • 示例: BASE_INVARIANT_A

    • 规范: debtToken totalSupply 应该等于所有用户余额(用户债务)的总和。
    • 实现: BaseInvariants::assert_BASE_INVARIANT_A
    function assert_BASE_INVARIANT_A(IERC20 debtToken) internal {
      uint256 sumOfUserBalances;
      for (uint256 i; i &lt; NUMBER_OF_ACTORS; i++) {
        sumOfUserBalances += debtToken.balanceOf(actorAddresses[i]);
      }
      assertEq(debtToken.totalSupply(), sumOfUserBalances, BASE_INVARIANT_A);
    }

    不变量的每个实现都必须在其包装器 echidna_** 函数中调用 Invariants.sol 文件,如以下代码片段所示:

    function echidna_BASE_INVARIANT_A() public returns (bool) {
      for (uint256 i; i &lt; debtTokens.length; ++i) {
        /// &lt;--- 循环遍历所有债务代币,
        /// 由于套件设置使用三个储备金,因此该循环将运行三次。
        assert_BASE_INVARIANT_A(IERC20(debtTokens[i]));
      }
      return true;
    }
  • 实施指南:

    • 定义协议的核心属性(例如,余额约束、流动性检查、内部会计准确性)。
    • 保持断言清晰明了。 如果循环遍历用户、资产或储备金,请力求简单 - 避免过多或复杂的逻辑以保持效率和可读性(检查上面循环遍历套件参与者的示例)。
    • 通过确保不变量彼此之间没有过度重叠来最大程度地减少冗余。 但是,一定程度的重叠可能有利于覆盖更多场景,因此请考虑进行战略性重叠以最大程度地提高检查覆盖率。

后置条件

  • 定义: 后置条件是必须在测试序列中特定操作或交互之后成立的属性。 它们有助于确保序列中的每个动作都产生有效的协议状态,从而专注于有针对性的结果而不是整个系统范围内的条件。 与在所有状态中一致检查的不变量不同,后置条件在指定点强制执行。 这些点包括每个 handler 调用的结束处(对于 handler 特定的后置条件)或 _after hook 的结束处(对于全局后置条件),从而验证特定动作的预期结果。

    正如公共文档所述,这些检查是在断言模式下执行的,其中 echidna 在检测到来自失败的 assert 语句的 Panic(1) 错误后会报告失败的警报。

  • 类别: 这些后置条件可以分为两类:全局后置条件和 handler 特定后置条件。 全局后置条件 (GPOST) 在每个测试序列结束时使用 _after hook 检查,而 handler 特定的后置条件 (HSPOST) 在特定 handler 调用的结束时检查。

  • 示例 GPOST: LENDING_GPOST_C

    • 规范: 如果储备金的 totalSupply 增加,则新的 totalSupply 必须小于或等于供应上限。
    • 实现: DefaultBeforeAfterHooks::assert_LENDING_GPOST_C
    function assert_LENDING_GPOST_C() internal {
      if (targetAsset == address(0)) return;
      uint256 totalSupplyUpdatedTreasury = _getRealTotalSupply(
        targetAsset,
        defaultVarsBefore.scaledTotalSupply,
        defaultVarsAfter.accruedToTreasury
      );
      if (totalSupplyUpdatedTreasury &lt; defaultVarsAfter.totalSupply) {
        if (
          defaultVarsAfter.supplyCap != 0 &&
          msg.sig != IPoolHandler.mintToTreasury.selector
        )
          assertLe(
            defaultVarsAfter.totalSupply,
            defaultVarsAfter.supplyCap,
            LENDING_GPOST_C
          );
      }
    }

    每个全局后置条件都必须在 _checkPostConditions 函数中 _after hook 的末尾调用,如以下代码片段所示:

    function _checkPostConditions() internal {
      // Implement post conditions here
      ...
    
      // LENDING
      assert_LENDING_GPOST_C(); /// &lt;--- 全局后置条件执行
    
      ...
    }
  • 示例 HSPOST: E_MODE_HSPOST_G

    • 规范: 在切换或离开 emode 后,用户的健康因子必须 >= 1。
    • 实现: 在 setUserEMode handler的末尾,检查后置条件。
    function setUserEMode(uint8 i) external setup {
      bool success;
      bytes memory returnData;
    
      ... /// &lt;--- 变量缓存、参与者调用等,为简洁起见进行了总结
    
      if (success) {
        _after();
    
        // POST-CONDITIONS
        if (eModeCategory != previousUserEModeCategory) {
          ...
          assertGe(_getUserHealthFactor(address(actor)), 1, E_MODE_HSPOST_G); /// &lt;--- 后置条件
        }
      }
    }
  • 实施指南:

    • 确定每个动作应产生的关键结果或状态更改(例如,余额更新、抵押品调整、利息累积)。
    • 旨在通过全局后置条件来补充不变量,确保仍然覆盖了无法实现为不变量的属性。 <br />

Handlers: 添加对新函数的支持

  • 概述: 正如公共文档所述,handlers 充当工具和协议之间的一种中间件层。 这就是为什么将新功能添加到协议或升级协议时,必须添加或更新新的 handler 函数以支持新功能。 以下部分将详细说明如何在 handlers 中添加对新函数的支持。

  • 添加新函数: 让我们以 Aave v3.2 升级为例,其中引入了 eMode 功能的更精细版本。 新功能允许将资产列在多个 eMode 中,从而删除了 setAssetEModeCategory 函数,转而使用两个更精细的函数 setAssetCollateralInEModesetAssetBorrowableInEMode。 以下步骤展示了通过在 handlers 中添加对这些新函数支持的过程指南:

    • 识别 Handler: 确定哪个 handler 合约将负责新函数。 在本例中,PoolPermissionedHandler handler 负责与协议的所有 permissioned 交互,因此应在此处添加对新函数的支持。

    • 识别参数: 确定动作需要哪些参数,哪些可以随机化,哪些应该被限制,哪些应该从有限的集合(如 helper 存储数组)中获取。

    对于 setAssetCollateralInEModesetAssetBorrowableInEMode 函数,参数如下:

    • asset: 要在 eMode 中设置的资产的地址。
    • eModeCategory: 要将资产设置到的 eMode 类别。
    • allowed: 一个布尔值,用于设置是否允许资产进入 eMode。

    对于 asset 参数,可以使用 _getRandomBaseAsset 函数从 helper 存储数组 baseAssets 中选择一个随机基础资产,对于 eModeCategory 参数,可以使用 _getRandomEModeCategory 函数从 helper 存储数组 ghost_categoryIds 中选择一个随机 eMode 类别。 最后,对于 allowed 参数,可以使用随机布尔参数。

    • 确定该动作是 permissioned 还是 permissionless: 如果该动作是 permissioned,则不需要使用参与者作为代理,因为套件设置为 poolAdmin,因此直接调用 handler 就足够了。 如果该动作是 permissionless,则必须使用参与者代理调用以及 setup 参与者选择修饰符。 以下代码是如何实现这两个函数的方法:
    function setAssetCollateralInEMode(
      bool allowed,
      uint8 i,
      uint8 j
    ) external {
      address asset = _getRandomBaseAsset(i); /// &lt;--- 随机基础资产选择
    
      uint8 categoryId = _getRandomEModeCategory(j); /// &lt;--- 随机 eCategory 选择
    
      // 直接调用 handler,因为套件具有 poolAdmin 角色
      contracts.poolConfiguratorProxy.setAssetCollateralInEMode(
        asset,
        categoryId,
        allowed
      );
    }

    permissionless 动作的外观如下:

    • 动作需要 eModeCategory 参数,因此可以使用 _getRandomEModeCategory 函数从 helper 存储数组 ghost_categoryIds 中选择一个随机 eMode 类别。
    • 函数应由参与者调用,因此使用 setup 修饰符和对协议的代理调用。
    • 函数应在 _before_after hooks 之间调用,以确保缓存值以正确检查后置条件。
    function setUserEMode(uint8 i) external setup {
      bool success; /// &lt;--- 变量以存储调用的成功和返回数据
      bytes memory returnData;
    
      uint8 eModeCategory = _getRandomEModeCategory(i); /// &lt;--- 随机 eMode 类别选择
    
      uint256 previousUserEModeCategory = pool.getUserEMode(address(actor));
    
      address target = address(pool);
    
      address[] memory assetsBorrowing = _getUserBorrowingAssets(
        address(actor)
      ); /// &lt;--- 缓存用于后置条件检查的所需变量
    
      _before(); /// &lt;--- Before hook
      (success, returnData) = actor.proxy( /// &lt;--- 对协议的代理调用
        target,
        abi.encodeWithSelector(IPool.setUserEMode.selector, eModeCategory)
      );
    
      if (success) {
        _after(); /// &lt;--- 成功后的 After hook
    
        // POST-CONDITIONS
        if (eModeCategory != previousUserEModeCategory) {
          /// &lt;--- 后置条件检查
          assertAssetsBorrowableInEmode(
            assetsBorrowing,
            eModeCategory,
            E_MODE_HSPOST_H
          );
          assertGe(_getUserHealthFactor(address(actor)), 1, E_MODE_HSPOST_G);
        }
      }
    }
    • 更新后置条件: 如果新函数引入了对协议状态的更改,请更新后置条件以反映这些更改。 例如,如果使用新的 eMode 更新,则用户在切换或离开 emode 后的健康因子必须 >= 1。 应如上面的示例所示实现后置条件 E_MODE_HSPOST_G。 <br />
  • 测试: 添加新的 handler 函数并更新后置条件后,运行该工具以确保新逻辑被覆盖并正常工作。 你可以在 echidna/default/_data/corpus 目录中检查 html 覆盖率报告。 <br />

从 Foundry 迁移测试

  • 概述: Foundry 单元和 staless 测试断言与 handler 特定后置条件密切相关,因为两者都执行确定的动作或一组动作,并检查在其执行后保持的关系和属性。 可以将这些测试和断言的大部分迁移到套件中(当前的一些后置条件已经从中获得灵感),从而确保协议仍在测试相同的属性以及变量之间的关系。
  • 分步迁移:

    1. 识别两个环境中要迁移的动作。 例如,borrow 函数。

    2. 将 Foundry 断言转换为后置条件甚至不变量。 例如,在测试 test_variable_borrow 中,这个常见的断言 assertEq(balanceAfter, balanceBefore + borrowAmount); 可以转换为以下后置条件: BORROWING_HSPOST_I: 在成功借款后,参与者资产余额应增加借款金额

      这可以在 `borrow` handler 函数的末尾实现,如下所示:
      
      ```solidity
      function borrow(uint256 amount, uint8 i, uint8 j) external setup {
          bool success;
          bytes memory returnData;
      
          ...
          uint256 actorAssetBalanceBefore = IERC20(asset).balanceOf(address(actor));
      
          _before();
          (success, returnData) = actor.proxy(
          address(pool),
          abi.encodeWithSelector(
              IPool.borrow.selector,
              asset,
              amount,
              DataTypes.InterestRateMode.VARIABLE,
              0,
              onBehalfOf
          )
          );
          ...
      
          if (success) {
          _after();
          // POST-CONDITIONS
          assertEq( /// &lt;--- 从 Foundry 迁移的后置条件 BORROWING_HSPOST_I
              IERC20(asset).balanceOf(address(actor)),
              actorAssetBalanceBefore + amount,
              BORROWING_HSPOST_I
          );
          ...
          }
      }
      ```
    3. 使用断言模式重新运行测试,以确保新属性在更广泛的状态下成立。 <br />

迁移 Certora 属性

  • 概述: Certora 的框架与套件的方法密切相关,因此许多 Certora 属性可以直接迁移到套件中。 可能需要调整格式和结构,但核心逻辑应该可以很好地转换。 最重要的是,由于该套件基于参与者并使用有限的参与者集,因此可以轻松实现涉及计算用户值总和的属性,例如 BASE_INVARIANT_A
  • 属性类型: 以下是将 Certora 属性类型映射到 Enigma Suite 属性类型,高亮显示了每个属性在套件中如何转换:
Certora 属性 Enigma Suite 属性
invariant 函数 不变量
rule 函数 HSPOST 后置条件
参数化 rule 函数 GPOST 后置条件
  • 迁移指南:
    • 不变量: 检查协议范围条件的 Certora invariant 函数通常可以实现为套件的不变量检查。
    • 后置条件:
    • Certora rule 函数通常与套件的 Handler 特定后置条件 (HSPOST) 对齐,该后置条件在单个 handler 调用后检查。
    • Certora 中的参数化 rule 函数(为所有协议动作实现规则)转换为套件中的 全局后置条件 (GPOST),允许在执行后进行协议范围的检查。

仔细调整每个属性,特别是那些涉及参数化或依赖于特定协议状态的属性。 虽然 Certora 和套件之间的属性框架在概念上相似,但 形式验证 (Certora) 和 覆盖率引导模糊测试 (Enigma Suite) 之间的引擎、执行过程和环境差异很大。 这种差异可能会影响某些假设。 <br />

调试损坏的属性

  • 日志 & 输出: 在运行时,Echidna 会显示一个终端 UI,该 UI 报告属性的状态(通过或失败)以及相关的调用堆栈。 当某个属性失败时,某些线程会启动一个“收缩”过程来简化调用堆栈,从而使调试更容易。 可以在 echidna_config.yaml 文件中使用 shrinkLimit 参数调整收缩 effort。 在完成失败属性的收缩后,可以使用 Ctrl+C 停止套件。 语料库和覆盖率数据会自动保存,并且仪表板的信息会输出到命令行,从而允许复制最小化的调用堆栈,以便使用 Foundry 包装器测试进行进一步调试。

  • Crytic to foundry test helper: CryticToFoundry 文件充当调用堆栈复现器,可以轻松调试 echidna 输出中的调用堆栈。 以下是如何将 helper 用于失败属性的示例:

    function test_borrow_assertion() public {
    _setUpActorAndDelay(USER1, 453881);
    this.approveDelegation(
      1482526189130252178123437018605205213532554266044322803735452163998541884248,
      226,
      251
    );
    _setUpActorAndDelay(USER1, 42941);
    this.supply(1000000000000000001, 33, 89);
    _setUpActorAndDelay(USER1, 67960);
    this.setEModeCategory(118, 189, 2300, 30039);
    _setUpActorAndDelay(USER2, 287316);
    this.setUserEMode(145);
    _setUpActorAndDelay(USER2, 438639);
    this.borrow(2848252610, 0, 2);
    }
  • 在 Foundry 包装器中复现 Echidna 错误的步骤:

    • 从 Echidna 输出中复制调用堆栈。
    • 启动 echidna_parser.py 工具。
    • 将调用堆栈粘贴到工具中。
    • 按 Enter 生成 Foundry 包装器测试。
    • 复制生成的测试并将其粘贴到 CryticToFoundry 文件中。
    • 运行测试并使用 foundry 详细输出 -vvvv 调试失败的属性。
  • 原文链接: github.com/aave-dao/aave...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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