本文是对 Mantle V2 智能合约代码进行审计的报告,审计发现了 1 个严重问题和多个中低风险问题;Mantle V2 是一个以太坊二层扩容方案,将 L2 上的原生货币从 ETH 更改为 MNT,代码库与 Optimism 代码库有很多相似之处。审计报告对合约, 特权角色,安全模型,风险问题都做了详细分析。
TypeL2 RollupTimelineFrom 2024-01-31To 2024-03-01LanguagesSolidityTotal Issues29 (12 resolved, 1 partially resolved)Critical Severity Issues1 (1 resolved)High Severity Issues0 (0 resolved)Medium Severity Issues3 (3 resolved)Low Severity Issues7 (1 resolved)Notes & Additional Information18 (7 resolved, 1 partially resolved)
我们审计了 mantlenetworkio/mantle-v2 仓库,提交哈希为 e29d360。
以下文件在审计范围内:
packages/contracts-bedrock/contracts
├── L1
│ ├── L1CrossDomainMessenger.sol
│ ├── L1ERC721Bridge.sol
│ ├── L1StandardBridge.sol
│ ├── L2OutputOracle.sol
│ ├── OptimismPortal.sol
│ ├── ResourceMetering.sol
│ └── SystemConfig.sol
└── L2
│ ├── BaseFeeVault.sol
│ ├── BVM_ETH.sol
│ ├── CrossDomainOwnable.sol
│ ├── CrossDomainOwnable2.sol
│ ├── CrossDomainOwnable3.sol
│ ├── GasPriceOracle.sol
│ ├── L1Block.sol
│ ├── L1FeeVault.sol
│ ├── L2CrossDomainMessenger.sol
│ ├── L2ERC721Bridge.sol
│ ├── L2StandardBridge.sol
│ ├── L2ToL1MessagePasser.sol
│ └── SequencerFeeVault.sol
此外,我们还对下列合约进行了有限的审查,因为它们是上述范围内合约的依赖项。因此,提出了一些低级别和备注级别的问题。然而,并未对这些文件进行完整的审计。
├── universal
│ ├── ERC721Bridge.sol
│ ├── StandardBridge.sol
│ ├── CrossDomainMessenger.sol
│ ├── OptimismMintableERC20.sol
│ ├── Semver.sol
│ └── FeeVault.sol
最后,以下文件仅根据与基础提交 bb0ff70 的差异进行了审计:
├── deployment
│ ├── PortalSender.sol
│ ├── SystemDictator.sol
├── legacy
│ └── LegacyERC20MNT.sol
├── libraries
│ ├── Burn.sol
│ ├── Encoding.sol
│ ├── Hashing.sol
│ ├── Predeploys.sol
│ ├── Types.sol
Mantle V2 是一个以太坊二层(L2)扩展解决方案,它使用欺诈证明而不是有效性证明来保障安全性。该协议旨在提供低交易费用和高吞吐量,同时保持完整的 EVM 兼容性。Mantle V2 构建在以太坊之上,使用 OP Stack,因此与 Optimism 有许多相似之处。就差异而言,最重要的是 L2 中使用的原生货币从 ETH 更改为 Mantle Token (MNT)。MNT 是以太坊主网上的 ERC-20 代币,因此必须对代码进行调整。在深入了解所有具体更改的细节之前,值得总结一下 Mantle v2 堆栈是什么以及它是如何工作的。该系统基于两个主要的合约目录,即 L1
和 L2
目录。
顾名思义,L1
目录将包含管理以下内容的合约:
ResourceMetering
合约,用于根据 EIP-1559 处理 gas 单位测量,L2OutputOracle
合约,它托管 L2 区块的最终状态根,以及 SystemConfig
合约,它仅用于检索不同参数的系统配置。另一方面,L2
目录将包含管理以下内容的合约:
GasPriceOracle
,其中 MNT 和 ETH 之间的价格关系由 tokenRatio
参数管理,并且由于存储在 L1Block
合约中的信息,可以检索 L1 区块信息,以及旨在管理跨域所有权交互的可拥有合约。L1
和 L2
的三个主要部分如下:
在 L1 上,ERC-20 资产、MNT 代币和 ETH 通过 L1StandardBridge
合约进行管理,而 ERC-721 资产通过 L1ERC721Bridge
合约进行管理。在 L2 上,可以找到等效的合约,即 L2StandardBridge
和 L2ERC721Bridge
合约。
两个桥都有共同的函数来初始化到另一域的资产转移,例如 bridgeMNT
、bridgeERC20
、bridgeETH
或 bridgeERC721
函数,以及最终确定来自另一域的桥接,例如 finalizeBridgeERC20
、finalizeBridgeMNT
、finalizeBridgeETH
或 finalizeBridgeERC721
函数。ERC-20 和 ERC-721 资产锁定在桥合约中,而 MNT 和 ETH 在桥接到另一域时转移到 L1CrossDomainMessenger
/ L2CrossDomainMessenger
合约,或者在最终确定到当前域的桥接时从它们转移。
L1CrossDomainMessenger
和 L2CrossDomainMessenger
合约仅仅是桥和 OptimismPortal
/ L2toL1MessagePasser
之间的中介。消息传递者不区分资产,他们唯一做的是传递任意的 编码消息 以及 MNT 或 ETH。当桥接到另一域时,会触发 sendMessage
函数,而当最终确定来自另一域的桥接时,会调用 relayMessage
函数。任何 sendMessage
调用的目标都是 OptimismPortal
/ L2toL1MessagePasser
,而 relayMessage
执行的目标通常是桥合约。
OptimismPortal
(portal) 合约是从 L1 向 L2 存入资产时 L1 执行的最终点,而 L2toL1MessagePasser
合约是从 L2 向 L1 提款的最终点。这些合约发出事件,其参数是被转移资产的编码消息。两者之间一个值得注意的区别是,OptimismPortal
托管了用于证明和最终确定从 L2 到 L1 的任何提款交易的机制,而 L2toL1MessagePasser
仅仅传递从 L2 到 L1 的消息。提款交易的验证是 L2 状态根被使用以确保提款交易已在 L2 上有效触发的地方。OptimismPortal
和 L2toL1MessagePasser
合约是有效持有 MNT 和 ETH 的合约。
背后的原因是,人们可以跳过从桥到消息传递者的整个流程并直接针对它们,从而节省 gas。但是,这是一个不太用户友好的流程,并且容易出错。此外,用户也只能跳过桥并通过消息传递者发送任意数据。消息传递者会将执行传递到门户或 L2 上的消息传递器。Mantle v2 引入的主要区别是将 L2 上的原生货币从 ETH 更改为 MNT。这意味着 ETH 在 L2 上转换为 ERC-20 资产,特别是 OptimismMintableERC20
代币的一个实例。这带有一些细微之处:
当从 L2 桥接到 L1 时:
由于 MNT 到 ETH 的汇率会波动,为了正确计算交易费用,GasPriceOracle
合约中引入了 tokenRatio
值。tokenRatio
表示与 ETH 相比 MNT 的价值,这使得能够正确计算费用值。此代币比率在客户端级别进行管理,并且旨在由受信任的运营商在每次价格关系发生变化时进行调整。
Mantle v2 引入了许多其他功能,这些功能在协议级别实现,并且被认为超出了当前智能合约列表的范围。我们建议查看以下官方页面,以进行更深入的新功能分析。
系统中存在多个特权参与者:
L2OutputOracle
中,只有 CHALLENGER
地址可以还原已经推送的状态根,而只有 PROPOSER
地址可以添加新根。OptimismPortal
中,GUARDIAN
地址可以暂停/取消暂停
提款验证和最终确定。SystemConfig
合约的 owner
可以更改不安全的区块签名者、整个配置以及 batcherHash
, overhead
, scalar
, gasLimit
和 baseFee
参数。GasPriceOracle
合约的 owner
可以更改 operator 地址,而 operator
可以更改代币比率。没有健全性检查可以确保 operator 设置的代币比率是正确的。DEPOSITOR_ACCOUNT
地址可以在 L1Block
合约中设置 L1 区块值。在任何时候,都假定所有这些特殊参与者都已正确配置,并且他们的任何私钥都没有泄露。
跨链消息传递和建立在其上的资产桥接是每个 L2 解决方案不可或缺的一部分。但是,这些跨链通信的正确性取决于底层节点和客户端操作。因此,我们假设跨链通信发出的数据已从一层正确中继到另一层。鉴于区分 MNT 和 ETH 的复杂性增加(在 L1 和 L2 上),大量逻辑在协议级别处理。与原始 Optimism 代码相比,这使得系统合约和客户端具有更大的依赖关系。更准确地说,假设存款交易将完全按照请求执行。有关由于某些不正确的节点中继或客户端执行导致资金可能卡住的反例,请参见附录。
此外,建立 MNT 到 ETH 价格关系的代币比率被假定为具有抗操纵性,如官方文档中所述。
BVM_ETH
和 MNT
存放在消息传递器中的资金可能被盗在 L2CrossDomainMessenger
合约中,relayMessage
函数 将对 _target
执行任意的外部调用。与此同时,该函数应在外部调用中失败,并且具有处理这种情况的逻辑。如果外部调用失败,则 failedMessages[versionedHash]
映射将被设置为 true
,此时,任何人都可以重试事务的执行。
当从 L1 向 L2 转移 ETH 时,如果用户通过了 L1StandardBridge
逻辑,则将铸造 BVM_ETH
并将其转移到 L2 上的 L2CrossDomainMessenger
,然后触发 relayMessage
执行以将这些 BVM_ETH
移动到其最终目的地。同样,如果用户在从 L2 向 L1 转移 MNT 时通过了 L2StandardBridge
[合约](https://github.com/mantlenetworkio/mantle-v2/blob/e29d360904db5e5ec81888885f7b7250- 访问冷存储,需要消耗 2600 单位的 gas。
另外,请注意 hasMinGas
函数的第二个参数是以下两个变量的总和:
RELAY_RESERVED_GAS
,设置为 40000 单位的 gas。这是对外部调用后继续执行 relayMessage
所需 gas 量的估计。这与 Optimism 代码相比没有变化。RELAY_GAS_CHECK_BUFFER
,设置为 5000 单位的 gas,表示在 hasMinGas
函数和外部调用之间应该使用的量。这与 Optimism 代码相比也没有变化。hasMinGas
函数包含以下公式:
[ gasLeft - (40000 + _reservedGas) ] * 63/64 >= _minGas
这里,_reservedGas
是 45000 单位的 gas,其中只有 5000 单位被估计为外部调用之前的缓冲区。考虑到所有这些,在 gas 估计和外部调用之间,总共有 5000 的缓冲区加上 40000 的剩余部分,移除了 36600 单位 gas 的最坏情况,总共有 8400 单位 gas(并且不是 文档中提到的 5700)。在外部调用之后,另外 40000 单位的 gas 被预留下来以完成正常执行。
L1CrossDomainMessenger
覆盖在代码中添加了一些额外的指令:一个对 MNT token 合约的 approve
调用,以防被转发的消息包含 MNT token 的转移,以及第二个approve
,用于将 allowance 设置回 0,这在外部调用后重复进行。第二个 approval 是否需要取决于是否存在外部调用的目标未消耗给定 approvals 的情况。
一个 ERC-20 token 的 approval 可以跨越几千到 30000 或 40000 单位的 gas,远远超过了提供的几千单位的缓冲区。一些OptmismMintableERC20
token 的实例对于每个 approve
调用甚至可能消耗超过 40000 单位的 gas。approve
调用的 gas 消耗绝对取决于值是从零设置为正值还是反过来,或者从非零设置为非零值。请注意,对于 L2CrossDomainMessenger
的 relayMessage
函数,可以提出类似的论点。
鉴于以上情况,请考虑重新评估 RELAY_GAS_CHECK_BUFFER
和 RELAY_RESERVED_GAS
的值,并确定是否需要第二个 approval,以避免由于原始 Optimism 代码中包含的额外指令而导致意外的 gas 失败,这些代码对这些默认估计值没有任何更改。此外,考虑到从 Optimism 代码添加的逻辑,应该调整 gas 缓冲区以确保添加足够的开销,以使交易不会失败。值得注意的是,可以设计为失败的 relayMessage
调用可以阻止存款和取款的最终确定,从而为 DoS 攻击打开大门。
更新: 已在 pull request #114 的提交 67f0904 和提交 92ebaf9 中解决。
finalizeWithdrawalTransaction
中可能失败OptimismPortal
合约的 finalizeWithdrawalTransaction
函数 已从原始 Optimism 代码中略作更改,以适应桥接 MNT 所需的更改,与其他资产的桥接分开。与合约中的许多其他函数一样,该函数应该在外部调用失败且 tx.origin
是 ESTIMATION_ADDRESS
时回滚。相反,如果外部调用没有失败,即使 tx.origin
是 ESTIMATION_ADDRESS
,调用也不应该回滚。
由于引入的更改,原始 Optimism 行为不再保留。现在,即使外部调用没有失败并且 tx.origin
是 ESTIMATION_ADDRESS
,如果唯一被桥接的资产是 ETH,finalizeWithdrawalTransaction
执行仍然可能失败。这是因为 MNT 转移是否成功的布尔标志默认为 false
,导致调用回滚。但是,如果没有 MNT 被转移,这不应该发生。正确的修复方法是将 l1mntSuccess
布尔值 默认设置为 true
,以便保持相同的 Optimism 行为,但这也会使布尔值无用,如 issue N01 中所述。
考虑使 gas 估计行为与从 Optimism 继承并在代码库的其他部分保持不变的行为一致。这样做时,请考虑提到关于正在使用的无用布尔变量的问题。
更新: 已在 pull request #105 的提交 6022c06 中解决。
L2OutputOracle
合约的 proposeL2Output
函数 被定义为 payable
,但它不处理任何 msg.value
。虽然该函数被限制为只能由 PROPOSER
调用,但如果传递任何 msg.value
,资金可能会卡在合约中,因为没有办法将它们取出。
考虑 proposeL2Output
是否必须是 payable
并记录原因。或者,考虑删除 payable
属性。
更新: 已在 pull request #138 的提交 af0d029 中解决。
在整个代码库中,在一些情况下,由于外部调用失败,资产可能会被锁定在合约中。以下是两个例子:
当从 L2 桥接 ETH 到 L1 时,OptimismPortal
的 finalizeWithdrawalTransaction
函数 被调用。这将尝试调用L1StandardBridge
的 finalizeBridgeETH
函数,该函数执行一个外部调用到 ETH 的接收者。但是,如果这样的调用失败,返回的 success
布尔值将为 false,并且 finalizeWithdrawalTransaction
将专门在 tx.origin
是 ESTIMATION_ADDRESS
时回滚,但如果调用者是尝试最终确定桥接回 L1 的普通用户,则不会回滚。此外,finalizedWithdrawals
映射将设置为此特定提款为 true
,从而阻止任何将来重放交易并使其工作的尝试。结果是 ETH 将卡在 OptimismPortal
中。
当从 L2 桥接 ERC-721 token 到 L1 时,将调用相同的 finalizeWithdrawalTransaction
,但这次将在 finalizeERC721Bridge
函数中调用 L1ERC721Bridge
。此函数将有效地执行从 L1ERC721Bridge
到 ERC-721 token 接收者的 safeTransferFrom
。safeTransferFrom
的使用意味着也触发一个 _checkOnERC721Received
Hook,如果接收者是一个未实现正确接口来接收 token 的合约,则会回滚。如果调用回滚,则会发生与之前相同的情况,因为 finalizeWithdrawalTransaciton
不会回滚,并且提款将被标记为已完成。这里的结果是 ERC-721 token 将卡在桥中。
考虑在合约的文档字符串中记录此类行为,或者采取补救措施。
请注意,ETH 卡在 OptimismPortal
中的情况是从 Optimism 合约继承的,并且 Optimism 已经采取了一种立场,他们将此责任委托给应该了解风险的用户。引用他们的一个问题:
OptimismPortal 的一个怪癖是它没有交易重放。如果交易失败,它将直接失败,并且与之关联的所有 ETH 将保留在 OptimismPortal 合约中。用户已被警告过这一点并了解风险,因此 Optimism 不对用户错误承担任何责任。
更新: 已确认,未解决。Mantle 团队表示:
不修复;我们已经在 CrossDomainMessenger 合约级别实施了交易重放。
在代码库中,一些合约旨在可升级,因此可能定义了一个 __gap
变量。可升级合约旨在通过代理调用,因此它们通常具有一个 initialize
函数,该函数通过代理调用。此函数设置代理存储槽的初始变量值。为了防止有人直接初始化实现合约,在某些情况下,在构造函数内部调用 initialize
函数。但是,这会消耗不必要的 gas 并且不是最佳的。所有可初始化合约都扩展了 OpenZeppelin Initializable
合约,该合约定义了一个内部函数,称为 _disableInitializers
,它执行相同的禁用实现初始化的任务,但不会通过将变量设置为不必要的值来浪费 gas。
考虑调用 _disableInitializers
函数而不是在构造函数中调用 initialize
函数。
此外,某些合约似乎有错放和错误使用的 __gap
变量,如 ERC721Bridge 和 L1ERC721Bridge 合约的情况。前者有两个不占用任何槽位的 immutable 变量,并且 __gap
变量被设置为具有 49 个槽位的大小,尽管规范值为 50,而后者有一个被 deposits 映射占用的槽位. 这可能会产生误导,因为它可能解释了 __gap
变量的 49 个槽位大小而不是 50 个槽位大小。但是,contract L1ERC721Bridge is ERC721Bridge, Semver
定义中存储布局的工作方式是首先定义 ERC721Bridge
槽位,然后是 Semver
槽位,最后是 L1ERC721Bridge
作为最后一个。
考虑审查代码库并始终使用可升级性标准模式,其中 __gap
变量的大小反映了当前上下文合约占用的存储槽位数量。
更新: 已确认,未解决。Mantle 团队表示:
不修复。它不会影响主要逻辑。
每次将 tokenRatio
变量更新为新值时,都会在 GasPriceOracle
合约中发出 TokenRatioUpdated
事件。其参数是先前和新设置的值。但是,该事件错误地两次发出新的 token ratio,而不是发出先前的 token ratio,然后是新的 token ratio。
考虑将 previousTokenRatio
变量分配给当前的 token ratio,而不是使用的输入参数。
更新: 已在 pull request #138 的提交 572600a 中解决。
在整个代码库中,有几个部分具有不完整的文档字符串:
在 CrossDomainMessenger
合约的 relayMessage 函数中,没有记录 _mntValue
参数。
CrossDomainOwnable3
合约的 OwnershipTransferred 事件没有记录 previousOwner
、newOwner
和 isLocal
参数是什么。
GasPriceOracle
合约中的 TokenRatioUpdated、OwnershipTransferred
和 OperatorUpdated
事件没有记录它们的参数是什么。
在 GasPriceOracle
合约的 transferOwnership 函数中,没有记录 _owner
参数。
在 L1StandardBridge
合约的 depositMNT 函数中,没有记录 _amount
参数。
在 L1StandardBridge
合约的 depositMNTTo 函数中,没有记录 _amount
参数。
在 L2StandardBridge
合约的 bridgeETH 函数中,没有记录 _value
参数。
在 OptimismMintableERC20
合约的 l1Token 函数中,并非所有返回值都被记录。在同一合约的 l2Bridge、remoteToken 和 bridge 函数中也发生了同样的情况。
在 OptimismPortal
合约的 WithdrawalProven 事件中,未记录 from
和 to
参数。
在 OptimismPortal
合约的 [initialize](https://github.com/mantlenetworkio/mantle-v2/blob/e29d360904db5e5ec81888885f7b7250f8255895/packages/contracts-bedrock/contracts/L1/OptimismPortal使用 abi.encodeWithSignature
或 abi.encodeWithSelector
为底层调用生成 calldata 并非一种罕见的操作。然而,第一种方法并不具备防止手误的安全保障,而第二种方法则不具备类型安全保障。这两种方法的结果都容易出错,因此应被认为是不安全的。在 Encoding.sol
中,有几处不安全的 ABI 编码:
在第 92 行。
在第 124 行。
考虑将所有不安全的 ABI 编码替换为 abi.encodeCall
,后者会检查所提供的值是否实际匹配被调用函数期望的类型,并且还可以避免因手误引起的错误。
更新: 已确认,未解决。Mantle 团队表示:
没有必要修复此问题。
在整个 代码库 中,有几个部分没有文档字符串。例如:
BVM_ETH
合约的 mint
函数没有文档。GasPriceOracle
合约的变量、事件 和 修饰符 没有文档。L1CrossDomainMessenger
合约的 L1_MNT_ADDRESS 变量缺少文档。同样的变量在 L1StandardBridge
中缺少 文档字符串,在 L2StandardBridge
合约中也缺少 文档字符串。L2StandardBridge
合约的 bridgeMNTTo
函数没有文档。OptimismPortal
合约的 depositTransaction
函数 中,可以同时桥接 ETH 和 MNT。如果是这种情况,用户不应使用通过 L2CrossDomainMessenger
和 L2StandardBridge
进行桥接的正常流程,因为这仅支持一次桥接一种资产。考虑警告用户注意这一点。考虑彻底记录作为任何合约公共 API 一部分的所有函数(及其参数)。实现敏感功能的函数,即使不是 public
,也应清楚地记录。编写文档字符串时,请考虑遵循 以太坊自然规范格式 (NatSpec)。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
在整个代码库中,存在定义了布尔值但逻辑上没有用的情况:
L1StandardBridge
合约的 _initiateBridgeMNT
函数中 approve
调用的 success
值。Mantle token 的 approve
函数 要么 revert,要么返回 true,没有任何情况下其结果值为 false。OptimismPortal
合约的 finalizeWithdrawalTransaction
函数的 l1mntSuccess
变量 要么为 true,要么 transfer
调用 reverted。它 永远不会为 false。L2CrossDomainMessenger
合约的 relayMessage
函数的 ethSuccess
变量 要么为 true,要么 approve
函数 reverted。每当评估其值时,它永远不会为 false。考虑重构代码以避免使用不必要的布尔值。这样做时,应注意保持相同的执行流程,尤其是在评估当前不必要的布尔值的地方。
更新: 已在 pull request #128 的 commit d8efd33 中解决。
在整个代码库中发现了几个不正确或具有误导性的文档字符串的实例:
BVM_ETH.sol
:
BVM_ETH
定义上方的注释已过时,可能会产生误导LegacyERC20MNT.sol
:
Burn.sol
:
Types
:
mntTxValue
和 ethTxValue
共享相同的文档,但它们在 UserDepositTransaction
中服务于不同的目的。WithdrawalTransaction
的 struct 文档中,可以删除不存在的字段 value
的文档字符串。此外,mntValue
和 ethValue
字段都没有文档。考虑更新具有误导性的文档字符串实例,以提高清晰度和可读性。
更新: 已在 pull request #129 的 commit fd4fc03 中部分解决。BVM_ETH.sol
中过时的注释仍然存在。
SequencerFeeVault
合约的 RECIPIENT
变量声明为 public
。但是,它还有一个特定的 getter 定义。
考虑删除重复的 getter,只保留一个实例来检索 RECIPIENT
变量的值。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
public
函数可以声明为 external
在整个代码库中,有多个 实例 定义了 public
函数的合约。但是,这些函数可以定义为 external
。
为了节省 gas 并提高代码清晰度,请考虑审查代码库并将所有未在代码本身中调用的函数标记为 external
。
更新: 已在 pull request #130 的 commit c6f2d81 中解决。
ERC721Bridge
合约有一个特定的 require
语句,以确保调用者是 EOA 而不是合约。但是,其他合约有一个特定的 修饰符 称为 onlyEOA
,用于相同的目的。
考虑在整个代码库中一致地使用 onlyEOA
修饰符,以提高代码的可读性和质量。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
在代码库中,有一些文档字符串包含拼写错误的实例:
OptimismPortal
合约的 第 32 行,“whcih”应为“which”。OptimismPortal
合约的 第 72 行,第一个文档字符串行缺少“If the value”,因此在逻辑上与第二个文档字符串行没有联系。考虑审查整个代码库并解决拼写错误,以提高代码质量和可读性。
更新: 已在 pull request #131 的 commit 53fe7ce 中解决。
根据 Solidity 风格指南建议,私有或内部变量标识符应以 _
作为前缀。在整个代码库中,有多个 实例 的变量命名不符合这些准则。
考虑审查代码库并修复任何不规则的变量命名实例,并采用所有的 Solidity 风格指南,以提高整体代码质量和可读性。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
在 L1CrossDomainMessenger
中,正在使用 魔法常量。在链接的实例中,检查可以从 < 2
更改为 <= MESSAGE_VERSION
。
考虑始终定义具有显式名称的常量,以提高代码库的可读性和可理解性。
更新: 已在 pull request #132 的 commit 75e7984 中解决。但是,在 L2CrossDomainMessenger
上也发生了同样的情况,但尚未在那里修复。
在 CrossDomainOwnable 和 GasPriceOracle 合约中,所有权以单步转移。这可能会带来风险,因为设置不正确的地址意味着合约的所有权将永久丢失,没有恢复的方法。
考虑使用两步所有权转移流程,例如 OpenZeppelin 的 Ownable2Step。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
在整个 代码库 中,一些事件没有将其参数索引:
FeeVault
合约的 Withdrawal
事件OptimismPortal
合约的 Paused
和 Unpaused
事件考虑索引事件参数,以提高链下服务搜索和过滤特定事件的能力。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
在智能合约中提供特定的安全联系人(例如电子邮件或 ENS 名称)可以大大简化个人在代码中发现漏洞时进行沟通的过程。这种做法是有益的,因为它允许代码所有者规定漏洞披露的沟通渠道,从而消除了因缺乏如何披露的知识而导致沟通不畅或无法报告的风险。此外,如果合约包含第三方库并且在这些库中出现错误,则这些库的维护人员可以更轻松地与有关问题的相关人员联系并提供缓解说明。
在整个 代码库 中,有很多合约没有安全联系人的实例。
考虑在合约定义上方添加包含安全联系人的 NatSpec 注释。建议使用 @custom:security-contact
约定,因为它已被 OpenZeppelin Wizard 和 ethereum-lists 采用。
更新: 已确认,未解决。Mantle 团队表示:
没有必要修复此问题。
在 LegacyERC20MNT
合约中,address(_who)
类型转换是不必要的。
为了提高代码库的整体清晰度、意图和可读性,请考虑删除不必要的类型转换。
更新: 已在 pull request #133 的 commit 9032ff2 中解决。
Hashing
库合约的 hashDepositTransaction
函数在代码库中从未使用过。此外,以下代码最终也保持未使用状态,因为它目前仅辅助 hashDepositTransaction
函数:
Encoding
库的函数 encodeDepositTransaction
Hashing
库的函数 hashDepositSource
Types
库的 struct UserDepositTransaction
为了提高代码库的整体清晰度、意图和可读性,请考虑删除任何未使用的代码。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
Predeploys
库中地址的常量值没有按递增顺序排列,这在需要添加新地址时容易出错。
考虑按递增顺序排列所有地址。
更新: 已确认,未解决。Mantle 团队表示:
无需修复。
在 SystemDictator
合约中,正在调用 step3
函数,以从 AddressManager
合约中移除已弃用的地址。但是,BVM_CanonicalTransactionChain
地址被移除了两次,第一次是在第 293 行,第二次是在第 300 行。
考虑只移除一次 BVM_CanonicalTransactionChain
的已弃用地址。
更新: 已在 pull request #135 的 commit 4ed9335 中解决。
在整个 代码库 中,[Predeploys
库](https://github.com/mantlenetworkio/mantle-v2/blob/e29d360904db5e5ec- 将ETH从L1桥接到L2的正常用户执行流程需要用户首先在L1StandardBridge
上触发_initiateBridgeETH
函数。这将调用L1CrossDomainMessenger
合约的sendMessage
函数,然后该函数将调用OptimismPortal
的depositTransaction
函数。执行此操作时,事件中发出的from
参数是L1CrossDomainMessenger
的别名地址,而to
是L2CrossDomainMessenger
。
OptimismPortal
,传递正确的参数来执行完全相同的操作。但是,这可以通过从外部拥有的帐户直接触发depositTransaction
或通过用户控制的中间合约来完成。如果是后者,则此时from
参数将是用户控制的合约的别名地址。现在假设我们处于最新的场景中,并且步骤5
未能执行。在协议级别,将恢复步骤3
中拍摄的快照。在这种假设情况下,from
会铸造一些WETH,但是这些WETH不会转移到L2CrossDomainMessenger
。现在,由于这种模式,用户可以使用这次ethValue == msg.value == 0
和ethTxValue
与以前相同的值,通过其控制的合约再次触发交易。这次,WETH不会被铸造,但是与之前相同的金额将从它们卡住的地方(用户控制的地址的别名地址,没有私钥来解锁资金)转移到原始接收者L2CrossDomainMessenger
,从而有效地解锁资金。
所有这些似乎解决了重要的issue。但是,它也引入了一种极端情况:如果从L1CrossDomainMessenger
执行depositTransaction
,并且relayMessage
执行失败,则资金将卡在L1CrossDomainMessener
的别名地址中,因为它无法再次使用不同的ethValue
和ethTxValue
值调用depositTransaction
。但是,这种情况极不可能发生,并且只能在少数极端情况下发生。如果发生以下情况,relayMessage
函数可能会revert:
1) 没有足够的gas来正确完成执行的任何步骤(甚至在外部调用之前或在最低gas检查之前)。2) 使用的消息的版本 >= 2。3) 当msg.value != _mntValue
。4) 如果gasleft() - RELAY_RESERVED_GAS下溢。
即使不应发生上述任何情况,但将来开发引入错误的可能可能会打破此假设。本文的目的是展示节点和客户端的正确性假设被打破的影响。因此,我们强烈建议彻底测试端到端的跨链功能。另一方面,如果发生上述任何一种情况,资金将被卡住,而恢复资金的唯一方法是升级合约或在协议级别修复issue。两种可能的解决方案可能是:
L1CrossDomainMessenger
合约中引入一个特殊的受限访问函数,以使用自定义参数调用depositTransaction
。这样,可以使用msg.value == 0
和ethTxValue != 0
复制调用并解锁资金。depositTransaction
,其中msg.sender == L1CrossDomainMessenger & tx.origin != L1CrossDomainMessenger
。这将达到相同的结果。如果最终未应用任何补救措施,请考虑记录这种情况,描述涉及的潜在风险。
- 原文链接: blog.openzeppelin.com/ma...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!