本次审计分析了 Mantle V2 中 op-geth 的代码,发现了几个严重的安全问题。
TypeLayer 2Timeline 从 2024-02-09 到 2024-02-29 语言 Go 总问题 18 (7 解决, 1 部分 解决) 关键严重性问题 3 (3 解决) 高严重性问题 0 (0 解决) 中严重性问题 0 (0 解决) 低严重性问题 5 (2 解决, 1 部分 解决) 备注及附加信息 10 (2 解决)
我们审计了 mantlenetworkio/op-geth 仓库的头部提交 4d05b2c。所有文件都与基础提交 0a77db9 进行了差异审计。任何不在此差异中的代码都 没有 被审计。因此,我们不保证未更改代码的正确性。
在范围内的文件包括:
op-geth
├── accounts/abi/bind/backends/simulated.go
├── beacon/engine
│ ├── gen_blockparams.go
│ └── types.go
├── cmd
│ ├── evm/internal/t8ntool/execution.go
│ └── utils/flags.go
├── consensus/misc/eip1559.go
├── core
│ ├── genesis.go
│ ├── mantle_upgrade.go
│ ├── state_prefetcher.go
│ ├── state_processor.go
│ ├── state_transition.go
│ ├── txpool/txpool.go
│ ├── types
│ │ ├── deposit_tx.go
│ │ ├── gen_receipt_json.go
│ │ ├── meta_transaction.go
│ │ ├── rollup_l1_cost.go
│ │ ├── transaction.go
│ │ └── transaction_marshalling.go
│ └── vm
│ ├── interpreter.go
│ ├── jump_table.go
│ └── runtime.go
├── eth
│ ├── api_backend.go
│ ├── backend.go
│ ├── catalyst/api.go
│ ├── ethconfig/config.go
│ ├── state_accessor.go
│ └── tracers
│ ├── api.go
│ └── internal/tracetest/calltrace_test.go
├── graphql
│ └── graphql.go
├── internal
│ └── ethapi
│ ├── api.go
│ └── transaction_args.go
├── les/state_accessor.go
├── light/txpool.go
├── miner
│ ├── miner.go
│ ├── payload_building.go
│ └── worker.go
├── params
│ ├── config.go
│ └── protocol_params.go
└── rpc/http.go
Mantle V2 是一种以太坊的二层 (L2) 扩展解决方案,它使用欺诈证明而不是有效性证明来保证安全性。该协议旨在提供低交易费用和高吞吐量,同时保持完全的 EVM 兼容性。Mantle V2 构建在以太坊之上,使用 OP Stack,因此与 Optimism 共享许多相似之处。这次审计特别关注它的 L2 执行客户端,该客户端是从 Optimism 的 op-geth 仓库分叉而来的。尽管进行了更改以根据 Mantle 的需求定制客户端,但其名称仍然保持为 op-geth。因此,在本报告中,“op-geth”这一术语将指 Mantle 的 op-geth 版本,而不是 Optimism 的版本。
Mantle V1 到目前为止一直在 Optimism 的 OVM 上运行。因此,采用 OP Stack Bedrock 版本是一次巨大的转变。其他变更包括 EIP-1559 支持、冗余组件的移除、稳定的区块时间和区块状态标记。这设置了审计的起始点(基础提交)。虽然上述更改反映了 Mantle 与 OP Stack Bedrock 的兼容性,但还引入了其他更改,将 Mantle 区别开来。这些区别包括使用 Mantle DA 作为数据可用性层,使用 MNT 作为原生 L2 代币而不是 ETH,原生元交易和费用优化策略。本报告特别关注最后三项区别,这是在头部提交中引入的更改。
由于 MNT 是 L2 上的原生代币,Gas 的工作方式可能与 L1 上略有不同。对于所有 L2,用户必须支付两个成本:L2 执行成本和 L1 数据可用性成本。因此,向用户收取的 Gas 费用必须涵盖这两者。然而,后者的成本以 ETH 计,因此由于 MNT 是 L2 的原生代币,用户必须以 MNT 支付他们的 Gas 费用。Mantle 的解决方案是使用 tokenRatio
来将 ETH Gas 的单位按比例缩放为 MNT Gas 的单位。这个 tokenRatio
被定义为 ETH 价格除以 MNT 价格。因此,根据这两个资产的价格,用户为其交易成功必须支付的 L2 Gas 数量会有所不同。
这与 L1 的行为形成对比,在 L1 上,在相同状态下执行的相同交易应花费相同数量的 Gas。因此,有可能用户在 L2 上以某个 Gas 限制执行交易,并且根据 tokenRatio
,该交易可能成功或因耗尽 Gas 而回滚。为了尝试防止这种情况的发生,Mantle 提供了一种方式,让用户进行 Gas 估计,并在其计算中返回一个 20% 的缓冲区以留出更多的波动空间。尽管如此,如果两种资产中的某一个价格发生剧烈变化,则可能会发生这种情况。
op-geth 执行客户端依赖于适当的配置正确设置。其中一些参数被硬编码到客户端代码中,而其他的则需要传入和读取。提供这些参数的一方是一个受信实体。此外,正确的配置还包括对系统所依赖的 L1 合约进行良好的访问控制。客户端还依赖于来自 op-node 的信息。例如,存款交易(来自 L1)首先由 op-node 处理,然后再发送到 op-geth。假设 op-node 正在正确处理这些存款交易。目前,Mantle 是一个中心化系统,因此继承了作为一个系统的所有潜在风险。此外,op-geth 依赖于一些特定的 L2 智能合约,获取重要信息,例如 tokenRatio
、L1 basefee
、费用计算的其他参数,甚至是 L2 ETH 的 ERC-20 合约。op-geth 依赖于这些智能合约按预期工作。
在桥接方面,Mantle 依赖于 L1 和 L2 桥接合约的实现正确性。op-geth 期待的需要与智能合约的行为保持一致。例如,如果用户通过 L1 信使智能合约桥接到 L2 账户,且 L2 交易回滚,则资金可能会被卡住。然而,目前不太可能发生这种情况,因为在 L1 上的值已被正确硬编码,不允许发生这种情况。L1 智能合约与 op-geth 之间的这种相互作用对于系统正常运作至关重要。任何 L2 的安全性都依赖于底层 L1 区块链的安全性。由于 Mantle 是一种乐观 Rollup,系统的安全性取决于以一种非激励恶意行为的方式发送和执行欺诈证明的能力。虽然 Mantle 力求尽可能兼容 EVM,但与以太坊之间存在一些差异,可以在 他们的文档 中查看。
BVM_ETH
代币以耗尽协议从 L1 到 L2 存入 MNT 和 ETH 的过程始于 OptimismPortal
合约的 depositTransaction
函数。虽然该合约是由 L1CrossDomainMessenger
合约使用,但用户也可以直接调用它。它允许任何人指定要在 L2 上铸造和/或转移 MNT 和/或 ETH 的值。铸造的 MNT 和铸造的 ETH 的值是通过 从用户中提取 MNT 代币 和 msg.value
决定的。然而,MNT 和 ETH 的交易值仅从用户输入转发到 TransactionDeposited
事件。节点监听此事件, 解析它,并将其包含在一个区块中,以便在客户端中执行。
当客户端处理存款交易时,会检查 L2 用户是否 有足够的可用余额 用于给定的 MNT 转移值。如果没有,执行将回滚。然而,这一检查从未针对 ETH 交易值执行。实际上, ETH 余额转移 是通过读取合约存储槽中的 from
和 to
余额来执行的, 应用差值 的值,并为这些槽设置新状态。在不进行溢出/下溢检查的情况下发生这一切。 此外,用于设置状态的 common.BigToHash
函数将负的 big.Int
值转换为正的十六进制表示。
这意味着,拥有 L2 上零 ETH 余额的攻击者可以发起存款交易,将 100 ETH 转移到他们的第二个控控地址内。然后,在 transferBVMETH
函数中,他们的 from
余额被计算为 -100 ETH,但在状态中写为 +100 ETH,而他们的第二个账户也获得另一个 +100 ETH,总计在 L2 上获利 200 ETH,而没有任何 L1 投资(除了 Gas)。这个 ETH 值可以简单地提取到 L1,以耗尽所有锁定的 ETH。
考虑在直接操控资产状态时应用更强的余额和溢出检查。
更新: 在 pull request #42 中解决,提交号 0cf00ba。
transaction.go
文件的 Cost
函数 supposedly 确定了用户被收取的 L2 交易成本。这被计算为 GasPrice * GasLimit
的最大 Gas 成本,加上交易 Value
,并可选地加上 blob 交易成本 BlobGas * BlobGasFeeCap
(虽尚未支持)。当函数被修补以添加可选的 blob 成本时,团队因删除 Value
成本添加而引入了复制粘贴错误。因此,尽管返回值被解释为包括 Value
,但实际上并不包括。这对交易池的交易验证产生了不良影响 [ 1, 2] ,在此检查用户的余额是否覆盖交易成本。
然而,由于成本未包括交易值,用户无法承担的交易值,仍会作为有效交易添加到池中。然后,在 state_transition.go
中处理交易以购买 Gas 时,将再次执行 余额检查,此次将使用 Value
,导致交易回滚而不向用户收费。这导致 DoS 攻击向量,攻击者可以通过网络发送大量有利可图的交易,节点将优先处理这些交易,但它们从未被执行且不产生费用,同时也阻止其他交易被添加到交易池,有效导致区块链停止工作。
考虑将交易 Value
添加到交易成本中。此外,考虑是否可以用一到两个方法来更好地管理成本计算的代码冗余。
更新: 在 pull request #41 中解决,提交号 44c9a41。
core/txpool/txpool.go
中的 validateMetaTxList
函数 的目的是检查赞助者是否有足够的余额来支付其赞助的交易的 Gas 费用。这由 validateTx
函数 、 promoteExecutables
函数 和 demoteUnexecutables
函数 使用,以确定哪些交易是有效的。
validateMetaTxList
函数遍历列表中的所有交易,并检查赞助者是否 有足够的资金单独支持每笔交易。然而,在某些情况下,赞助者可能有足够的余额单独支持每笔交易,但所有交易加起来却不够。节点将所有这些交易视为有效,并将其保留在其 txpool
中。直到节点构建块并开始处理这些交易时,才会意识到这些交易中的一些可能会失败。因此,恶意攻击者可以通过提交许多赞助交易向节点发起 DoS 攻击,以赞助尽可能多的其余额,而节点将所有交易视为有效,因此将其存储在其 txpool
中,实际上只有其中一个是有效的。这可能导致节点停滞于处理其他用户的交易,实际上使 L2 停止运作。
考虑更新 validateMetaTxList
函数,以检查赞助者的余额是否能支付 sponsorCostSum
,即所有赞助金额的总和。
更新: 在 pull request #43 中解决,提交号 2619376。
在 light/txpool.go
的 validateTx
函数 中,如果赞助者没有足够的资金 支付 sponsorAmount
,返回的错误是 core.ErrInsufficientFunds
。但是,为了清晰和 与 txpool/txpool.go
一致,考虑返回 types.ErrSponsorBalanceNotEnough
错误。
更新: 在 pull request #50 中解决,提交号 65ab3a1。
tokenRatio
tokenRatio
的值 存储在 L2 的 GasPriceOracle.sol
合约中 作为 uint256
。这个 tokenRatio
旨在表示 ETH 价格除以 MNT 价格的商值,并用于计算 L2 的 Gas。目前,当更新此值时,所有小数都会被截断。依据当前 ETH 和 MNT 的市场价格,截断只会导致 Gas 计算中的轻微误差。然而,如果 MNT 的值相对 ETH 价格增长,则可能导致误差持续增长。
因此,考虑将 tokenRatio
值放大,并在 op-geth 使用该值时再缩放至其适当规模,以提高精度。
更新: 已确认,将解决。
轻客户端实现的 txpool
验证 新增的交易,就像核心实现一样。这包括检查用户和元交易赞助者是否有足够的余额来覆盖费用。
然而,用户余额仅以 L2 费用进行检查 [ 1, 2],而未考虑 L1 费用,尽管必须考虑将汇总交易费用计算到 L1。这意味着低估的成本可能会导致交易在被发送到完整节点后因资金不足而回滚。
尽管轻节点尚未投入使用,建议修正验证以考虑 L1 成本,以备网络与轻节点去中心化时做好准备。
更新: 已确认,将解决。Mantle 团队已表示:
轻节点尚未使用,我们稍后会修复。
在 state_transition.go
文件的 buyGas
函数中,mgval
的值意图要复制到 balanceCheck
。然而,复制的不是值,而是指向该值的指针。因此,通过此赋值 在这行代码中,变量共享同一内存。幸运的是,balanceCheck
变量随后因 new(big.Int).SetUint64()
被赋予另一个指针。
然而,如果在将来的修订中此代码更改为 balanceCheck.SetUint64()
,则不会为此值分配新内存,意味着会覆盖 mgval
。在此代码上下文中,这意味着交易 值还被额外计算 作为 Gas 成本,导致 值的双重支出 或为 交易赞助者的额外支出。
考虑完全避免共享指针,始终通过 new(big.Int).SetUint64()
分配新值。虽然在给定函数中这不是问题,但在未来如不仔细处理,最好降低风险。
更新: 在 pull request #52 中解决,提交号 75b60cb。
在整个代码库中,有误导性文档的实例:
transaction.go
的 第 80 行,注释目前写为“这是由 DynamicFeeTx、LegacyTx 和 AccessListTx 实现的”,但应更新为包括 BlobTx
和 DepositTx
类型。transaction.go
的 第 377 行,注释目前写为“gas gasPrice + value”,但应写为“(gas gasPrice) + (blobGas * blobGasPrice) + value”。state_transition.go
的 第 655 行,注释目前写为“为剩余 Gas 返回 ETH”,但应写为“MNT”。此外,开发者文档中也存在误导性文档:
BaseFee
升级,BaseFee
是由一个配置合约设置(或保留为最后的 BaseFee
)。考虑修复报告中的文档实例,以改善代码库的整体可读性。
_更新: 在 pull request #41 和 pull request #52 中部分解决,提交号 44c9a41。尽管在 transaction.go
的第 377 行和 state_transition.go
的第 655 行的注释已解决,但在 transaction.go
的第 80 行的注释以及开发者文档仍未更改。_
代码冗余可能导致代码膨胀,并在将来更新代码时增加错误风险。在整个代码库中,有几处代码冗余的实例:
newPUSH0InstructionSet
函数 的代码与 newShanghaiInstructionSet
函数 的相同。DoEstimateGas
函数中的这段代码 用于估计元交易的 Gas 限制是冗余的,因为有 calculateGasWithAllowance
函数。meta_transaction.go
的第 87 行,len(MetaTxPrefix)
与现有常量 MetaTxPrefixLength
相同。rollup_l1_cost.go
的第 73 至 76 行,合约状态直接获取,而不是使用 DeriveL1GasInfo
函数。worker.go
中,header.BaseFee
被 重复覆盖两次 是冗余的。考虑消除代码冗余的任何实例,或明确记录这些冗余的必要性。
更新: 已确认,将解决。
Mantle 代码库当前使用 github.com/ethereum/go-ethereum
作为其模块名,如 go.mod 文件 中所示。使用这个不属于它的 GitHub 仓库的名称,阻止了其他人的导入 Mantle 的代码,并使测试变得更加困难。
考虑使用 Mantle 自己的路径作为其模块名。
更新: 已确认,将解决。Mantle 团队已表示:
没有问题。在 mantle-v2 中,我们将替换
github.com/ethereum/go-ethereum v1.11.6
=>github.com/mantlenetworkio/op-geth...
等等。
在 txJSON
结构体中,Data
字段被 标记 并 引用 为“输入”。在 go-ethereum
参考实现中,此字段也称为 Input
。
为了防止参数混淆,并缩小与 go-ethereum
存储库之间的差距,考虑将 Data
字段重命名为 Input
。
更新: 已确认,将解决。Mantle 团队已表示:
目前不会修复,我们将稍后升级到最新的
go-ethereum
。
元交易的有效性可能受 区块编号 限制。当这个 区块编号被超越 时,交易将不被接受为有效。然而,从用户体验的角度来看,让赞助者设置区块编号而不是时间戳是不太直观的。
为了增强用户体验,考虑将单位从区块编号更改为区块时间戳。
更新: 已确认,尚未解决。Mantle 团队已表示:
Mantle-v2 的区块高度和区块时间一一对应,这只是出于用户体验原因。如果我们修改它,将引入重大修改。目前不会修复。
随着代码库的增长和成熟,未使用的变量可能导致代码膨胀、命名冲突和阅读代码时的困惑。在整个代码库中,有未使用变量的实例:
OptimismL1FeeRecipient
变量 未被使用,因为 L1 费用已包含在 前往 OptimismBaseFeeRecipient
的费用中。OverrideMantleBaseFee
变量 未使用,并且根据注释,应该在分叉后移除。OverrideShanghai
变量 未使用,并且根据注释,应该在分叉后移除。考虑删除这些未使用的变量,以改善代码清晰度。
更新: 已确认,将解决。
最佳实践是显式使用键创建结构体。这有助于改善代码库的可读性和可维护性。使用不明确的结构声明会降低代码的可读性,并且更容易出错。一个不明确结构声明的示例是 backend.go
中的 EthAPIBackend
。
考虑使用键值语法以达到明确的目的。
更新: 在 pull request #55 中解决,提交号 79bab65。
在以太坊中,calldata
中的零字节和非零字节被征收不同的税。因此,当交易在 气体估算(没有余额检查)运行模式 中购买 Gas 时,会计算 RLP 编码交易的零字节和非零字节,以近似 L1 费用成本。对于这些特定的运行模式,固定 80 的启发式值会加到 Ones
(非零字节)数量之上。这是为了覆盖在估算时未知的交易字段,但在执行过程中将携带非零字节数据。此外,这些字节计数在 DataGas
函数 中用于依据每个零字节和非零字节应用不同的Gas费用。然而,在 Regolith 更新之前,Ones
还添加了一个魔法值 68。
考虑在文件顶部使用一个 const
值和一些上下文信息,而不是魔法数字。这将确保随着协议演进,对该值的维护。同时,考虑将 RollupGasData
结构体 的 Ones
字段重命名为 NonZero
,以更好地反映其含义。
更新: 已确认,将解决。
在开发过程中,良好描述的 TODO 注释将使跟踪和解决问题的过程更容易。没有这些信息,这些注释可能会变老,重要的系统安全信息可能在发布到生产时被遗忘。这些注释应在项目的问题备份中跟踪,并在系统部署之前解决。
考虑删除所有待办事项注释,并在问题备份中跟踪它们。或者,考虑将每个内联待办事项注释链接到相应的问题备份条目。
更新: 已确认,将解决。
SponsorPercent
的双重检查元交易是由 tx.origin
之外的另一方同意代表用户赞助 Gas 费用或其百分比的交易。赞助者可以赞助的最大比例上限应为 100%。在 meta_transaction.go
中,这个上限在 DecodeMetaTxParams
函数 的 第 100 至 102 行 中检查。
在 DecodeAndVerifyMetaTxParams
函数中, 调用了此 DecodeMetaTxParams
函数,然后 再次检查 100% 的上限。考虑删除此第二个检查,因为它已经执行过。
更新: 在 pull request #57 中解决,提交号 ff8f850。
deposit_tx.go
文件 实现了 DepositTx
类型。其他类型的交易实现位于 tx_access_list.go
、tx_blob.go
、tx_dynamic_fee.go
和 tx_legacy.go
文件中。
考虑将 deposit_tx.go
文件重命名为 tx_deposit.go
,以保持一致性。
更新: 已确认,尚未解决。Mantle 团队已表示:
没有问题。这个文件与
depositTx
数据结构相关。
Mantle 的 op-geth 升级到版本 2 引入了新功能到汇总中。这包括原生元交易、MNT 作为原生代币和 Gas 费优化。审计发现了三个关键严重性问题,以及其他几个低严重性问题。我们强烈建议 Mantle 在上线之前实施更广泛的 QA 和测试,以防止潜在的未发现漏洞被利用。这在与节点 DOS 攻击和存款交易相关的领域尤其重要。我们非常感激与 Mantle 团队的合作,他们在审计期间提供了很大的支持,并及时回答了问题。
- 原文链接: blog.openzeppelin.com/ma...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!