Nomad的Replica合约存在一个实现缺陷,导致无法正确验证消息的身份。
一个实现上的 bug 导致 Replica
合约 无法正确验证消息。这个问题允许伪造任何消息,只要它还没有被处理。因此,依赖 Replica
来验证入站消息的合约遭受了安全故障。这种身份验证失败导致欺诈性消息被传递到 Nomad BridgeRouter
合约。
Nomad 在 Merkle 树(称为“消息树”)中提交跨链消息。这棵树的根通过乐观机制传播到远程链。
Replica
合约使用 mapping(bytes32 => uint256)
跟踪来自其他链的根。这会将根映射到它们变为有效的时间戳。消息可能不会在根的乐观计时器经过之前被处理。当读取映射时,如果条目尚未设置,则会读取默认值(也称为 0 值)。uint256
的默认值为 0
。因此,任何未被证明的根在此映射中都将具有 0
的时间戳。
在根传播到另一个链后,消息包含在树中通过 Merkle 证明来证明。消息在其下被证明的根存储在 Replica
合约的 mapping(bytes32 => bytes32)
中。这会将消息的哈希值映射到其下被证明的根。在这种情况下,bytes32
的默认值为 bytes32(0)
。因此,任何未被证明的消息在此映射中都将具有 bytes32(0)
的根。
当消息被提交到 process
函数时,协议从映射中读取根,并检查 acceptableRoot
函数是否返回 true
。此函数旨在仅当有效根的乐观超时时间已结束时才返回 true
。此函数检查根的特殊旧值(来自较旧的系统版本),以及尚未被证明的根(在根映射中具有 0
时间戳)。它通过针对块时间戳检查来确保根的计时器已过 return block.timestamp >= _time;
。
function acceptableRoot(bytes32 _root) public view returns (bool) {
// this is backwards-compatibility for messages proven/processed
// under previous versions
// 这是对在先前版本下证明/处理的消息的后向兼容性
if (_root == LEGACY_STATUS_PROVEN) return true;
if (_root == LEGACY_STATUS_PROCESSED) return false;
uint256 _time = confirmAt[_root];
if (_time == 0) {
return false;
}
return block.timestamp >= _time;
当 Replica
在其关联的 Home
合约之后部署时,Replica
合约会使用特定状态进行初始化。这确保了新部署不必重放来自远程 Home
的所有过去更新才能处理消息。部署者可以传递一个 _committedRoot
,消息树的历史记录开始接收更新。
在初始化器期间,confirmAt[_committedRoot]
设置为 1
。这确保了包含在其初始根中的消息可以被处理。这允许新初始化的 Replica
接收早于其部署的消息。
但是,如果在 Replica
与其对应的 Home
合约同时部署的情况下(就像最初部署时一样),Home
Merkle 树不包含任何消息。在 Nomad 实现中,没有叶子的 Merkle 树的根为 bytes32(0)
。因此,与对应的 `Home` 合约同时部署的 Replica
将使用 bytes32(0)
的根进行初始化,并且 confirmAt[bytes32(0)]
将设置为 1
。
function initialize(
uint32 _remoteDomain,
address _updater,
bytes32 _committedRoot,
uint256 _optimisticSeconds
) public initializer {
__NomadBase_initialize(_updater);
// set storage variables
// 设置存储变量
entered = 1;
remoteDomain = _remoteDomain;
committedRoot = _committedRoot;
// pre-approve the committed root.
// 预先批准提交的根。
confirmAt[_committedRoot] = 1;
_setOptimisticTimeout(_optimisticSeconds);
}
这是 acceptableRoot
的相关行。
uint256 _time = confirmAt[_root];
if (_time == 0) {
return false;
}
return block.timestamp >= _time;
uint256 _time = confirmAt[_root];
首先,将 confirmAt[_root]
加载到一个名为 _time
的变量中。对于未知消息(包括伪造消息),此 _root
等于 bytes32(0)
。在任何以 _committedRoot
设置为 bytes32(0)
初始化的 Replica
上,confirmAt[bytes32(0)]
的值在初始化时设置为等于 1
。
if (_time == 0) {
return false;
}
因为 _time
是 1,所以我们跳过这个块。
return block.timestamp >= _time;
_time
为 1
,因此任何有效的块时间戳都将大于或等于 1
。因此,acceptableRoot(bytes32(0));
始终为这些 Replica
合约返回 true
。
这允许未经证明的消息通过 process
函数中的以下检查。反过来,这允许消息无需先被证明即可被处理。
function process(bytes memory _message) public returns (bool _success) {
// ...
require(acceptableRoot(messages[_messageHash]), "!proven");
// ...
}
相关代码于 2022 年 6 月 21 日在智能合约升级中引入。
以前,process
函数中的 require
语句编写如下:
function process(bytes memory _message) public returns (bool _success) {
// ...
require(messages[_messageHash] == MessageStatus.Proven, "!proven");
// ...
}
之前的 require
语句要求 messages
映射中有一个非 0 值。这只能通过先前对 prove
的调用来设置。更改后,可以从映射中加载 0
值,并将其传递给 acceptableRoot
。
此更改是消息处理语义的更大行为更改的一部分。具体来说,旧的 require
语句会永久阻止在无效证明的情况下处理有效消息。较新的 require 语句可防止欺诈更新程序通过提供无效证明来阻止有效消息的传递。为了实现这一点,我们引入了上述“可接受的根”部分中描述的消息到根的映射。
Replica
合约升级的审计细节是什么?此 Nomad 系统升级已于 2022 年 5 月和 6 月初由 Quantstamp 审核。6 月 9 日收到了最终报告。此更改是在审核期间的5 月 26 日进行的,作为与审核相关的清理和增强(特别是与 QSP-2 和 QSP-34 相关),并且包含在为修复后重新审核提供的提交哈希中。
Nomad 还通过 ImmuneFi bounty 激励公众审查代码,该奖励自 6 月 9 日起生效。此问题未通过奖励系统报告。
生产环境已于 2022 年 6 月 21 日在以下交易中升级:
Nomad Watchers 观察并响应 Updater 密钥的泄露,但尚未观察智能合约 bug 引起的可疑活动。由于漏洞存在于智能合约的 process
函数中,因此消息不需要欺诈性的 Updater 签名,因此不会触发 Watchers。因此,没有 Watchers 采取行动。
副本已从 Nomad Bridge 和 Nomad Governance 合约中取消注册。这可以防止 Bridge 处理任何消息。这减轻了流程函数中漏洞的影响。新的桥交易仍然可以启动,但是无法处理。 NomadBridge GUI 已被禁用。我们建议用户在部署完整修复程序并执行系统重启之前,不要尝试桥接。
- 原文链接: medium.com/nomad-xyz-blo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!