Uniswap Labs 精简法庭审计

本文是对Uniswap的Tribunal项目的代码审计报告,该项目是一个用于在PGA链上结算跨链交换的框架。审计发现了23个问题,包括高、中、低风险等级的问题,并提出了修复建议。大多数问题在部署前已得到解决,但测试覆盖率不足,建议增加测试。

概要 类型: DeFi 时间线: 从 2025-08-11 → 到 2025-09-05

语言: Solidity

发现

总问题数:23(已解决 20 个)

严重:0(已解决 0 个)· 高:2(已解决 2 个)· 中:2(已解决 2 个)· 低:7(已解决 6 个)

备注 & 附加信息

提出 12 条备注(已解决 10 条)

范围

OpenZeppelin 审计了 Uniswap/Tribunal 仓库,提交哈希为 675141f

以下文件在审计范围内:

 src
├── BlockNumberish.sol
├── ERC7683Tribunal.sol
├── Tribunal.sol
├── interfaces
│   ├── IArbSys.sol
│   ├── IDestinationSettler.sol
│   ├── IRecipientCallback.sol
│   ├── ITribunal.sol
│   └── ITribunalCallback.sol
├── lib
│   ├── DomainLib.sol
│   └── PriceCurveLib.sol
└── types
    ├── TribunalStructs.sol
    └── TribunalTypeHashes.sol

系统概述

Tribunal 是一个用于结算 PGA(优先级 Gas 拍卖)链上的跨链互换的框架。它确保资金严格按照发起人的授权转移,并且在发生争议时,由单一一方可以完成结算。Tribunal 与 The Compact 协议集成,用于存款和索赔,并提供两个集成Hook,_processDirective_quoteDirective,使桥或消息传递层能够在目标链上实现交付和定价。

在 The Compact 协议中,Tribunal 充当仲裁者,直接与协议本身和分配者互动。它被设计为一个高度可定制的框架。作为本次审计范围的一部分,还包括 ERC7683Tribunal 合约,以确保与 ERC7683Allocator 合约的兼容性,从而实现 ERC-7683 标准。

高层生命周期

  1. 准备: 发起人在源链上存入并注册一个 Compact,指定一个调整者、一个跨链执行和一个源链回退操作,该操作将触发目标链 compact 的存入和注册。
  2. 激活阶段: 调整者共同签署一个 Adjustment,以显示和激活一个 Fill(通常首先是跨链。源链回退可以稍后激活)。
  3. 完成: 执行者执行实时的 Fill,或者发起人取消。桥接的资金(如果有)会根据记录的处理方式路由到执行者/发起人或在目标链上注册。

执行路径

  • 同链: Tribunal 通过 The Compact 索赔并支付给接收者。
  • 跨链: Tribunal 记录一个处理方式,并调用 _processDirective,以便集成者传递消息/价值。settleOrRegister 标准化桥接资金的到达方式并解决竞争(提前取消 vs 先前执行)。
  • 回调(可选): 接收者回调可以链接操作(例如,在目标链上自动注册)。

安全模型和信任假设

在审计期间,我们做出了以下信任假设:

  • 桥/消息层信任: 跨链的安全性和活跃性取决于 _processDirective 的继承实现和底层传输。不正确的实现、延迟交付或分叉/重组处理错误可能导致资金卡住或延迟。
  • 调整者正确性: 调整者通过 fillIndex 选择活动的 fill,设置目标区块和任何补充价格曲线,并且可以选择限制执行者和区块窗口。恶意或错误的调整者可能会拒绝服务或在配置的范围内扭曲定价。
  • 可信的回调: 接收者和发送者回调执行外部代码。虽然回调受到重入保护和返回值检查(对于接收者回调)的保护,但它们仍然可能回退并阻止 fill。因此,假设它们是可信的。
  • Token 行为: 明确支持转移费 token,但给予接收者的金额会少于请求的金额。集成和 UI 必须清楚地提及这一点。

高危

settleOrRegister 函数在使用非原生 Token batchDeposit* 时存在 DoS 漏洞

在批量存款期间,TheCompact 强制执行 对提供的存款结构的特定要求。例如,如果存款中的第一个 token 是非原生的,则相关的原生金额必须为零。

settleOrRegister 中,合约的余额 被转发到 batchDeposit* 函数,而没有验证第一个 token 是否为非原生的。如果攻击者向 Tribunal 合约发送哪怕 1 wei 的原生 token,则允许对所有涉及非原生 token 的调用进行 DoS 攻击。由于 Tribunal 有一个空的 receive 函数,因此没有任何限制可以阻止攻击者增加合约余额并利用此条件。

考虑仅将请求的金额转发到 Compact 的 batchDeposit* 函数,并且当存款涉及非原生 token 时完全省略原生 token。

更新: 已在提交 719ac2a1667290 中解决。

applySupplementalPriceCurve 中的不正确逻辑

applySupplementalPriceCurve 函数使用 errorBuffer 来聚合来自 sharesScalingDirection 的错误,并且如果 errorBuffer 不等于 0,则最终回退。问题在于,如果缩放方向相同,sharesScalingDirection 返回 true;如果不同,则返回 false。这意味着该逻辑会聚合正确的执行,并将它们视为 errorBuffer 中的错误。

考虑在将 sharesScalingDirection 的结果添加到 errorBuffer 之前,检查它是否返回 false

更新: 已在提交 36c31ca 中解决。

中危

_fill 中验证窗口检查反转,导致窗口内的 fill 回退

在该协议中,调整者可以选择约束谁可以执行 fill 以及在指定的 targetBlock 之后可以执行多久。这些约束条件编码在 Adjustment.validityConditions 中:低 160 位保存一个可选的独占执行者地址(零表示没有限制),高位保存一个可选的区块窗口长度。当设置了非零窗口时,fill 应该允许从 targetBlocktargetBlock + validBlockWindow(包括)。另外,下限强制执行(fillBlock >= targetBlock)已经在 deriveAmounts 中通过 InvalidTargetBlock 检查 处理

Tribunal 合约中的当前实现 反转了窗口检查,当 fill 发生在有效窗口内时回退,仅允许在窗口过后执行。根据预期的语义,比较应该强制执行上限(即,仅当 fill 发生在 targetBlock + validBlockWindow 之后时才回退)。

考虑翻转比较以强制执行上限,并保持执行者限制不变。

更新: 已在提交 ccc9cb61667290 中解决。

通过接收者回调重入

在处理完 fill 之后,fill 函数通过 performRecipientCallback 执行对接收者的回调。然后,任何剩余的 ETH 都将返回给执行者。这会产生重入风险,因为接收者可能会利用回调通过执行 settleOrRegister 函数来窃取合约中持有的资金。

考虑将 nonReentrant 修饰符添加到 settleOrRegister 函数。

更新: 已在提交 6ede7aa 中解决。

低危

_deriveMandateHash 中潜在的 Panic

_deriveMandateHash 函数 验证 fillIndex 参数,以防止访问 fillHashes 数组之外的元素。访问超出有效范围的索引会导致运行时 panic。当前的验证允许 fillIndex 等于数组长度,这不是一个有效的索引,并且在访问时可能导致 panic。

考虑调整验证,以确保索引严格小于数组长度,从而防止任何越界访问。

更新: 已在提交 49516d9 中解决。

_quote 忽略补充价格曲线

_fill 函数 应用 adjustment.supplementalPriceCurve,而 _quote 函数 不应用。这导致根据操作是报价还是执行,会使用不同的有效价格曲线。

考虑使 _quote 的行为与 _fill 对齐,以便两者一致地应用 adjustment.supplementalPriceCurve

更新: 已在提交 91336cf 中解决。

InvalidTargetBlock 错误中参数顺序无效

deriveAmounts 函数 中,有一个检查确保 targetBlock 不在未来。如果此条件失败,该函数将回退并显示 InvalidTargetBlock 错误。此错误定义为以 (blockNumber, targetBlockNumber) 的顺序获取参数,但当前实现以相反的顺序提供它们。

考虑更正 InvalidTargetBlock 错误中的参数顺序,以匹配其定义的签名。

更新: 已在提交 f73c604 中解决。

从 Calldata 解码时未屏蔽 adjuster 的高位

_parseCalldata 函数 中,adjuster 使用 LibBytes.loadCalldata 加载,然后在 assembly 中分配,而没有屏蔽高 12 个字节。虽然正确编码的 calldata 将具有清零的高位,但如果其他 assembly 代码稍后依赖于严格的 160 位表示(例如,在相等性检查、哈希或存储打包中),则格式错误的输入可能会导致意外行为。

考虑在解码期间显式地将该值屏蔽为 160 位。

更新: 已在提交 d5e148f 中解决。

浮动 Pragma

Pragma 指令应该被固定,以清楚地标识合约将使用哪个 Solidity 版本进行编译。

在整个代码库中,发现了多个浮动 pragma 指令的实例:

考虑使用固定的 pragma 指令。

更新: 已确认,未解决。团队表示:

由于 Tribunal 实际上是一个旨在被其他合约继承的框架,因此我们认为浮动 pragma 实际上是合适的!

不完整的文档字符串

IRecipientCallback.sol 中,tribunalCallback 函数的文档不完整。具体来说,chainId 参数未被描述,并且并非所有返回值都记录在案。

考虑透彻地记录所有函数/事件(及其参数或返回值),这些函数/事件是合约公共 API 的一部分。在编写文档字符串时,请考虑遵循 Ethereum Natural Specification Format (NatSpec)。

更新: 已在提交 5d24d78 中解决。

缺失的文档字符串

在整个代码库中,发现了多个缺失文档字符串的实例:

考虑透彻地记录所有函数(及其参数),这些函数是任何合约公共 API 的一部分。实现敏感功能的函数,即使不是公共的,也应清楚地记录在案。在编写文档字符串时,请考虑遵循 Ethereum Natural Specification Format (NatSpec)。

更新: 已在提交 61f0ef0 中解决。

备注 & 附加信息

BlockNumberish 中昂贵的函数分派

BlockNumberish 合约 中,_getBlockNumberish 函数根据链 ID 动态分配给两个实现之一。当使用 via-IR pipeline 编译时,此设置导致通过 switch 语句进行函数分派。因此,与直接条件检查相比,每次调用 _getBlockNumberish 都会产生额外的开销。由于每次调用必须首先解析函数指针,然后执行所选的实现,而不是执行单个分支并返回,因此这种分派机制不必要地增加了 Gas 成本。

考虑将该逻辑合并到单个 _getBlockNumberish 函数中,该函数在运行时评估链 ID,并直接返回适当的区块编号,从而无需函数指针分派。

更新: 已在提交 471a538 中解决。

settleOrRegister 中的无法访问的代码

settleOrRegister 函数中,当 mandateHash 为零时,执行 batchDeposit 函数,并且该函数在 224 行 立即返回。在该函数的后面,当 mandateHash 为空时,有 另一个代码块 执行相同的操作。由于之前的返回语句阻止到达它,因此永远不会执行第二个代码块。

考虑删除无法访问的代码块,或者重新构建逻辑,以便在需要时可以执行第二个块。

更新: 已在提交 542858c 中解决。

未验证瞬态存储支持

在 Compact 中,一个专门的 Tstorish 合约 通过在不支持瞬态存储时回退到常规存储写入来确保兼容性。但是,在 Tribunal 中,不存在等效的回退机制。相反,瞬态存储直接用于 nonReentrant 修饰符 中,而没有检查是否支持该功能。

考虑实现类似于 Compact 的 Tstorish 合约的回退机制。

更新: 已确认,未解决。团队表示:

不必要的复杂化,因为 Tribunal 不像 The Compact 那样有相同的“需要在每个可想象的 EVM 链上的相同部署地址上支持”的依赖性。

PriceCurveLib.getCalculatedValues 条件中存在冗余的索引保护

PriceCurveLib 中的 getCalculatedValues 函数 迭代价格曲线段,并使用 hasPassedZeroDuration 标志来跟踪何时遇到零持续时间元素。当到达下一个非零段时,代码有条件地从先前的(零持续时间)缩放因子进行插值,使用一个复合条件:hasPassedZeroDuration && i > 0 && getBlockDuration(PriceCurveElement.wrap(parameters[i - 1])) == 0

i > 0 检查是冗余的。如果 hasPassedZeroDurationtrue,则意味着已经在先前的迭代中处理了一个零持续时间元素并且执行了循环 continue,因此最早的后续非零迭代必须具有 i >= 1。因此,在现有的控制流下,parameters[i - 1] 访问已经是安全的,并且额外的 i > 0 保护不会增加安全性。

考虑从条件中删除 i > 0,并依靠 hasPassedZeroDuration 来暗示存在先前的元素。

更新: 已在提交 950d014 中解决。

require 语句中的自定义错误

自从Solidity 版本 0.8.26 以来,可以在 require 语句中直接使用自定义错误。此功能最初仅限于 IR pipeline,但从 版本 0.8.27 开始,在旧的 pipeline 中也可用。

代码库当前使用多个 if 检查,后跟带有自定义错误的 revert 语句。虽然在功能上是正确的,但这种方法比必要的更冗长,并且与使用带有自定义错误的 require 相比,可能会增加 Gas 成本。

考虑用直接包含自定义错误的 require 语句替换 if-revert 模式。这将简化代码,并且在保留预期错误处理的同时,可能会减少 Gas 使用量。

更新: 已确认,未解决。团队表示:

我们认为 if 语句通常更明确。

Magic Numbers

在整个代码库中,发现了多个具有无法解释含义的字面量值的实例:

在整个代码库中,发现了多个合约没有安全联系人的实例:

考虑在每个合约定义上方添加包含安全联系人的 NatSpec 注释。建议使用 @custom:security-contact 约定,因为它已被 OpenZeppelin Wizardethereum-lists 采用。

Update:** 在 commit 49851cf 中已解决。

不安全的类型转换

PriceCurveLib 库的 applySupplementalPriceCurve 函数中,combinedScalingFactor 被不安全地转换为 uint240

为了防止意外行为,请考虑在转换之前确保 combinedScalingFactor 的值在 uint240 类型的范围内。

Update:** 在 commit a64d286 中已解决。

具有 internal 可见性的未使用函数

ERC7683Tribunal.sol 中,getFillerData 函数 未被使用。

考虑将 getFillerData 函数设置为 externalpublic,因为它提供了编码填充数据的逻辑。或者,如果该函数不打算在外部使用,请考虑完全删除它。这样做有助于提高代码库的整体清晰度和可维护性。

Update:** 在 commit bcdead3 中已解决。

未使用的引用

ERC7683Tribunal.sol 中,发现了多个未使用的引用的实例:

考虑删除未使用的引用,以提高代码库的整体清晰度和可读性。

Update:** 在 commit 42e78fb 中已解决。

不正确的文档

maximumClaimAmounts 的 NatSpec 注释指出,“每个承诺的最小索赔金额”,这与清楚地传达最大边界的变量名称直接冲突。

为了提高代码库的清晰度和可维护性,请考虑更新文档。

Update:** 在 commit 20fb2a7 中已解决。

结论

Tribunal是一个为PGA(优先级 Gas 拍卖)链上的跨链交换设计的结算框架。它强制所有转账都符合发起人的授权,并与The Compact集成以管理存款和索赔。该框架还提供了集成点,允许外部桥或消息传递层处理目标链上的交付和定价。

在审计过程中,发现了几个高、中、低和注意级别的问题。所有高危和中危问题,以及大多数低危和注意级别的发现,都在部署之前得到了解决。当前的测试覆盖率对于生产级协议来说是有限的,并且将大大受益于全面的单元测试、集成测试和端到端测试。此外,加入模糊测试可以增强代码库的成熟度。

感谢Uniswap Labs团队在整个审查过程中提供的支持,并及时回复了审计团队提出的所有问题。

准备好保护你的代码了吗?

请求审计 →

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

0 条评论

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