这是一篇关于 Dojo 引擎代码仓库的安全审计报告,主要评估了 Dojo 框架智能合约的安全性。审计发现了一个严重和高危漏洞,以及一些代码质量问题,包括权限管理不一致和潜在的资源覆盖风险。报告还提出了代码简化、命名改进和修复拼写错误的建议,以提高代码库的可读性和可维护性。
类型游戏时间线从 2024-09-02 到 2024-09-12 语言 Cairo 总问题 11 个 (9 个已解决) 严重程度问题 1 个 (1 个已解决) 高严重程度问题 1 个 (1 个已解决) 中等严重程度问题 1 个 (1 个已解决) 低严重程度问题 4 个 (4 个已解决) 注释 & 附加信息 4 个 (2 个已解决)
我们审查了 dojoengine/dojo 仓库,提交版本为 e1a349b。
审查范围包括以下文件:
crates/dojo-core/src
├── contract
│ ├── base_contract.cairo
│ ├── contract.cairo
│ └── upgradeable.cairo
├── lib.cairo
├── model
│ ├── introspect.cairo
│ ├── layout.cairo
│ ├── metadata.cairo
│ └── model.cairo
├── storage
│ ├── database.cairo
│ ├── layout.cairo
│ ├── packing.cairo
│ └── storage.cairo
├── utils
│ └── utils.cairo
└── world
├── config.cairo
├── errors.cairo
├── update.cairo
└── world_contract.cairo
Dojo 抽象了链上开发的复杂性,使开发者能够专注于应用程序的其他方面。这通过一个工具链来实现,用于构建、迁移、部署、证明和结算可扩展的应用程序状态,这些状态被称为 "worlds",在生产环境中。Dojo 的一个重要特征是,它提供了一种创建可证明的游戏状态的方法,这些状态依赖于通过零知识 (ZK) 证明的链下转换。这有助于解决链上游戏的一个问题,即状态和逻辑位于公共网络上,所有状态转换都由节点提供商验证,这意味着有效的转换受到网络成本和计算限制的约束。另一个重要特性是 Dojo 的一组 macros,这些 宏 在编译时会转换为全面的查询。该框架提供了一个对合约存储架构的抽象,以动态创建任意复杂的表,以及一个根据任意业务逻辑读取和写入记录的接口。本次审查重点关注相关的智能合约。
Dojo 由三个部分组成:
#[dojo::model] 定义的 world 的结构化数据。本质上,这些是 Cairo 结构体,自动实现 Introspect trait,该 trait 概述了模型的数据结构,以便可以轻松跟踪更改。#[dojo::contract] 定义的用于更新游戏状态并与 world 合约交互的函数。它们保存游戏逻辑,并且能够更改 world 状态,前提是它们具有来自相关 model 所有者的明确许可。权限在合约级别定义,这意味着同一合约内的所有函数将继承相同的权限。基本的理解是,任何复杂的数据结构都可以递归地描述为子集合(即,数组、结构、元组或枚举)或固定大小数据的集合。固定大小的数据也分为最多 256 个字的块。通过这种方式,可以使用树结构来描述表记录,其中树中的子节点表示集合中的单个元素(其本身可能是一个集合)。叶子是固定大小的数据块,并保存在从树中相应路径派生的伪随机存储位置。这使得可以读取或写入任意复杂集合的任何成员,而不会干扰其他成员。任何人都可以使用 Model 合约定义表的数据结构,并且 Dojo 框架会处理相应的到存储位置的映射。
World 数据库旨在开放和可扩展,具有最少的限制。总体原则是,具有对存储区域的写入访问权限的地址(详见下文)可以任意修改存储。数据库不强制执行表结构、输入验证规则、反应式 Hook 或任何类型的数据一致性检查。唯一的规则是不同的存储区域是隔离的,因此它们不会相互干扰。
这意味着用户应该信任所有具有写入访问权限的地址只会发布有效数据,并将遵守预期的表结构,以避免损坏存储区域。在实践中,应用程序可以通过授予对强制执行应用程序级别逻辑和一致性要求的 Systems(智能合约)的访问权限来实现此目的。
系统定义了以下资源:
World 合约是所有其他资源的顶级容器。Namespace 分区。Model 智能合约表示。Contract 智能合约实现。每个资源都可以有任意数量的 owner 和 writer 地址,它们遵循相同的层次结构。这意味着 world 所有者隐式拥有所有其他资源,并且命名空间所有者或 写入者 隐式地对命名空间内的模型和合约具有相同的角色。
owner 地址的通用权力包括:
owner 和 writer 角色的能力。metadata_uri 的能力,该 metadata_uri 可用于提供与资源关联的附加信息。具体的角色和权力分配如下:
World 的地址将成为其第一个所有者。Namespace 并成为其所有者。World 合约实现。还有一种机制,world 所有者可以使用原始 world 创建者 配置的机制来更新任意存储位置。期望是 L3 架构可以使用它来直接证明和更新 world 状态,类似于 L2 ZK rollup 如何将定期状态更新发布到 L1。
deploy_contract 函数在合约选择的选择器下注册系统。 这意味着攻击者可以部署恶意合约,以便覆盖来自任何命名空间的现有资源记录。
当注册或升级模型时,将验证选择器是否未在另一个资源下注册。 但是,deploy_contract函数缺少此检查。 有几个潜在的后果,但最值得注意的是,攻击者可以成为WORLD资源的所有者,从而允许他们更新任何模型。
考虑确保选择器在注册新合约之前未使用。
更新: 已在 pull request #2 中解决。
创建新命名空间时,其标识符是其名称的哈希值。 但是,当注册或更新模型时,命名空间及其哈希值由模型合约提供,并且未验证彼此是否一致。 同样,当部署新合约时,命名空间及其哈希值由合约提供,并且未检查其一致性。 由于哈希值用于访问控制,而名称用于相应的事件中,因此两者之间的任何差异都会产生ModelRegistered,ModelUpgraded或ContractDeployed事件的不正确记录。
此外,在创建或升级模型时,选择器由新模型提供,并且本质上不依赖于命名空间。 这意味着,如果 调用者 具有适当的权限,则ModelUpgraded事件可以更改模型的 name 和 namespace,这将创建一个无意义的记录。 尽管这些示例不会直接损坏数据库,但该系统的设计原则之一是Torii 索引器应该能够从事件中重建并提供状态。 因此,任何不一致都会误导索引器的用户。
此设计的另一个后果是,行为良好的模型和合约会从其其他属性派生其选择器(如 metadata model 所示)。 这意味着它们是在编译时确定的,并且可以抵抗更改。 但是,攻击者可以抢占模型和合约部署,以便声明这些选择器,从而阻止(或至少使复杂化)有效的部署。 这可能通过 抢先交易 攻击实时发生,或者只需查看或预测来自开源代码的选择器。 健全的命名空间机制应防止这种可能性。
为此,请考虑强制执行所有提供的命名空间及其哈希值之间的一致性,确保选择器是从相关名称和命名空间派生的。 哈希值可以在首次派生时缓存,以避免多次重新计算。 值得注意的是,模型每次更新时都会提供其命名空间哈希值,这意味着它可以任意更改其命名空间以进行访问控制。 类似的观察结果适用于合约和模型标签,这些标签不必与其名称或命名空间匹配,并且可以随着时间的推移而更改。 这些不会影响事件记录,但可能在对命名空间一致性要求的任何潜在更新中都需要考虑。
更新: 已在 pull request #2 中解决。
该代码库对大多数访问控制使用权限层次结构。 特别是,WORLD所有者间接拥有所有资源,而命名空间所有者间接拥有该命名空间内的所有模型。 但是,此模型存在一些偏差:
考虑通过修复此模型的偏差来严格遵守分层权限。
更新: 已在 pull request #4 中解决。
升级合约时,如果选择器与已知合约不对应,则会panic,出现invalid_resource_selector 错误。 但是,代码已经确定该选择器是已知的。 这可能会使用户感到困惑,因为执行失败的原因是invalid_resource_selector 错误。
考虑在此恐慌情况下使用resource_conflict错误,从而更清楚地说明程序失败的原因。
更新: 已在 pull request #5 中解决。
与大多数存储布局功能相反,write_enum_layout 读取一个值,而没有首先确保它在跨度内。
考虑在write_enum_layout 函数中添加验证,以确保正在读取的值在跨度内。 这将有助于提高代码的清晰度和可读性。
更新: 已在 pull request #6 中解决。
带符号整数 的 Introspect 实现都将布局描述为需要 251 位。 所有模型都会自动实现 Introspect trait,该 trait 概述了模型的数据结构。 如果用户定义了一些内置 Cairo 类型,他们只需要派生 Introspect,但是如果该类型是不受支持的类型,则他们必须手动实现 Introspect trait。 每个内置 Cairo 类型都描述了布局,并具有与数据类型对应的特定位大小。 但是,对于带符号整数来说,情况并非如此,带符号整数再次总是需要 251 位。
考虑使用带符号整数的数据大小来定义布局。
更新: 已解决。 不是一个问题。
在 Cairo 中,所有带符号整数都使用完整的 felt252 大小表示。
在整个代码库中,发现了多个具有误导性的注释实例:
set_differ_program_hash 函数注释 描述了一个不存在的 config_hash 参数。deploy_contract 函数注释 描述了一个不存在的 init_calldata 参数。set 函数注释 缺少 offset 参数。revoke_writer 函数注释 多次错误地提到“model”(而不是“resource”)。考虑更新注释以使其与代码实现保持一致。 这将有助于提高代码库的可读性和可维护性。
更新: 已在 pull request #7 中解决。
在整个代码库中,发现了多个代码简化的机会:
y。 相反,可以使用x。MIN_BYTE_ARRAY_SIZE常量冗余地转换为u32类型。calculate_packed_size 函数假定至少有一个元素的大小不为零。 即使这在代码库中是正确的,为了逻辑推理,它也应该从 0 开始,或者应该断言此假设。if 块总是立即退出该函数,因此此else块是不必要的。考虑实施上述代码简化建议,以提高代码库的清晰度和可维护性。
更新: 确认,将解决。
在整个代码库中,发现了多个改进函数和变量命名的机会:
fpow 函数使用平方和乘法算法的版本。 因此,double变量应称为square。base_address 的类型为 StorageAddress(不是 StorageBaseAddress),因此将其称为 base_address 可能会引起混淆。fill_with_zeroes假定它将传递一个空数组。 相反,它应称为append_zeroes以表示其实际行为。考虑实施上述重命名建议,以提高代码的清晰度。
更新: 确认,将解决。
在整个代码库中,发现了多个拼写错误的实例:
packingfelt 缺少空格。deploy_and_get_metadata和get_differ_program_hash函数以及panic_with_byte_array恐慌错误。所有这些都应该是“its”。assert_caller_namespace_write_access函数,以下注释“如果 调用者 没有命名空间的 写入者 角色,则会发生恐慌。” 应该为:“如果 调用者 没有命名空间的 写入者 角色,则会发生恐慌。” 在assert_caller_model_write_access中发现了类似的错误。any_none函数的此注释缺少一个字。考虑更正上述拼写错误,以提高代码库的可读性。
更新: 已在 pull request #8 中解决。
直接使用值而不先将其存储在常量中(即“神奇数字”)可能会导致未来 开发人员 和代码库的读者感到困惑。 在整个代码库中,发现了多个无法解释的神奇数字的实例。
database.cairo文件中,0 用于多次引用地址域(1,2,3,4)。layout.cairo中,文字251已被多次用来指代PACKING_MAX_BITS常量。为了提高代码库的可读性,请考虑将固定值存储在常量中,然后在代码中使用这些常量。
更新: 已在 pull request #9 中解决。
Dojo 是一个用于实现灵活且用户友好的区块链数据库的框架,该数据库随后可用于创建可证明的游戏和
- 原文链接: openzeppelin.com/news/do...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!