本次是对 OpenZeppelin 的 rust-contracts-stylus 代码库进行审计的结果总结。审计范围包括密码学库和合约库,共发现 22 个问题,包括1个高危、2个中危、9个低危问题。密码学库包含算术、曲线、域操作以及Poseidon2、Pedersen哈希算法的实现;合约库实现了ERC-20,ERC-1155等token 标准,以及访问控制,金融等相关合约。
TypeLibraryTimeline 从 2025-04-22 到 2025-05-14 (密码学库) 和 2025-05-26 到 2025-06-06 (合约库)
语言:Rust
问题总数:22 (20 个已解决)
严重问题数:0 (0 个已解决)
高危问题数:1 (1 个已解决)
中危问题数:2 (1 个已解决)
低危问题数:9 (9 个已解决)
注意事项 & 补充信息:10 (9 个已解决)
客户报告的问题数:0 (0 个已解决)
我们审计了 OpenZeppelin/rust-contracts-stylus 仓库,分为两个部分。
第一部分是密码学库。我们审计了 a67ab70 commit 对应的库,时间从 2025-04-22 到 2025-05-09。然后,我们审计了引入 pedersen
目录的 2350766 commit,时间从 2025-05-12 到 2025-05-14。
lib/crypto/src
├── arithmetic
│ ├── limb.rs
│ ├── mod.rs
│ └── uint.rs
├── bits.rs
├── const_helpers.rs
├── curve
│ ├── helpers.rs
│ ├── mod.rs
│ └── sw
│ ├── affine.rs
│ ├── mod.rs
│ └── projective.rs
├── field
│ ├── fp.rs
│ ├── group.rs
│ ├── instance.rs
│ ├── mod.rs
│ └── prime.rs
├── hash.rs
├── keccak.rs
├── lib.rs
├── merkle.rs
├── pedersen
│ ├── instance
│ │ ├── mod.rs
│ │ └── starknet.rs
│ ├── mod.rs
│ └── params.rs
├── poseidon2
│ ├── instance
│ │ ├── babybear.rs
│ │ ├── bls12.rs
│ │ ├── bn256.rs
│ │ ├── goldilocks.rs
│ │ ├── mod.rs
│ │ ├── pallas.rs
│ │ └── vesta.rs
│ ├── mod.rs
│ └── params.rs
└── test_helpers.rs
第二部分是合约库,我们审计了 5ed33dd commit 对应的库。以下文件在审计范围内:
contracts/src
├── access
│ ├── control.rs
│ ├── mod.rs
│ ├── ownable.rs
│ └── ownable_two_step.rs
├── finance
│ ├── mod.rs
│ └── vesting_wallet.rs
├── lib.rs
├── token
│ ├── common
│ │ ├── erc2981.rs
│ │ └── mod.rs
│ ├── erc1155
│ │ ├── extensions
│ │ │ ├── burnable.rs
│ │ │ ├── metadata_uri.rs
│ │ │ ├── mod.rs
│ │ │ ├── supply.rs
│ │ │ └── uri_storage.rs
│ │ ├── mod.rs
│ │ └── receiver.rs
│ ├── erc20
│ │ ├── extensions
│ │ │ ├── burnable.rs
│ │ │ ├── capped.rs
│ │ │ ├── erc4626.rs
│ │ │ ├── flash_mint.rs
│ │ │ ├── metadata.rs
│ │ │ ├── mod.rs
│ │ │ ├── permit.rs
│ │ │ └── wrapper.rs
│ │ ├── interface.rs
│ │ ├── mod.rs
│ │ └── utils
│ │ ├── mod.rs
│ │ └── safe_erc20.rs
│ ├── erc721
│ │ ├── extensions
│ │ │ ├── burnable.rs
│ │ │ ├── consecutive.rs
│ │ │ ├── enumerable.rs
│ │ │ ├── metadata.rs
│ │ │ ├── mod.rs
│ │ │ ├── uri_storage.rs
│ │ │ └── wrapper.rs
│ │ ├── interface.rs
│ │ ├── mod.rs
│ │ └── receiver.rs
│ └── mod.rs
└── utils
├── cryptography
│ ├── ecdsa.rs
│ ├── eip712.rs
│ └── mod.rs
├── introspection
│ ├── erc165.rs
│ └── mod.rs
├── math
│ ├── alloy.rs
│ ├── mod.rs
│ └── storage
│ ├── checked.rs
│ ├── mod.rs
│ └── unchecked.rs
├── metadata.rs
├── mod.rs
├── nonces.rs
├── pausable.rs
└── structs
├── bitmap.rs
├── checkpoints
│ ├── generic_size.rs
│ └── mod.rs
└── mod.rs
contracts-proc/src
├── interface_id.rs
└── lib.rs
密码学库由六个主要区域组成:arithmetic 文件夹、curve 文件夹、field 文件夹、poseidon2 文件夹、pedersen 文件夹以及根文件夹中的其他模块,如 hash.rs
、keccark.rs
、merkle.rs
、bits.rs
等。
hash
模块实现了一个通用的哈希函数,与核心库中的哈希函数相比,它支持更大、更安全的输出,核心库仅支持 u64
输出。merkle
模块提供用于验证 Merkle 证明的函数。
arithmetic 模块通过在构成大数的底层 64 位块 ("limbs") 上提供安全、高效和可移植的算术运算,来支持大整数运算。
curve 模块定义了实现和使用椭圆曲线所需的核心 Trait 和帮助程序,支持仿射和投影表示,高效的群运算和批量字段反转。它用作椭圆曲线上更高级别密码运算的基础。
field 模块为有限域算术奠定了基础,为安全、高效和通用的字段运算提供了 Trait、实现和实用程序,这对于密码学和代数计算至关重要。
Poseidon2 模块提供了 Poseidon2 哈希函数的快速且灵活的实现。Pedersen 模块提供了与 Starknet 兼容的 Pedersen 哈希函数的快速实现。它们适用于现代密码学应用,尤其是在零知识和区块链环境中。
Stylus 智能合约库已得到广泛更新,以符合最新的 Stylus SDK 约定并提供更广泛的Token实用程序。
构造函数现在使用 #[constructor]
属性显式定义并以原子方式部署,确保合约初始化不会被跳过或篡改。错误处理已使用关联类型进行了标准化,并且所有事件现在都派生自 Debug
,以便更轻松地进行链上日志检查和链下调试。继承模式已转变为基于 SDK 的 Trait 的方法,使用 #[implements(...)]
宏进行基于 Trait 的继承。
在功能方面,该库引入了对多种Token标准的全面支持:
SafeERC20
包装器。由于其稳健的设计,合约实现密切遵循 Solidity 的 OpenZeppelin 合约库,从而最大限度地提高了兼容性和一致性。ERC-165 已在所有合约中完全实现,因此接口检测通过 supports_interface
可靠且一致。
还引入了一个新的过程宏,用于为 Trait 生成一个 interface_id
函数并处理这些 Trait 中的自定义选择器。总之,这些增强功能在保持清晰、标准化设计的同时,提高了库的表达能力和安全性。
shr_assign
和 shl_assign
中的 Limb 移位溢出shr_assign
和 shl_assign
函数实现了 Uint<N>
类型的移位和赋值运算符( <<=
和 >>=
),该类型表示具有 N
个 limb 的多 limb 无符号整数。但是,当 limb_shift
值为零时,会发生移位溢出 [1] [2]。在调试模式下,这会导致运行时 panic,并显示错误消息 attempt to shift left with overflow
。在发布模式下,移位值被屏蔽,产生不正确的结果。考虑修改代码以在溢出发生之前解决此问题。
更新: 已在 commit c38b883 的 pull request #658 中解决。该团队使用 checked_shl
来缓解溢出情况。
ct_rem
函数中潜在的下溢ct_rem
函数计算除法运算的余数。但是,如果 self.ct_num_bits()
返回零,则 index
的计算可能会导致下溢。当被除数 (self
) 为零时会出现此问题,因为 ct_num_bits
对于零值返回零。在无符号整数 (usize
) 中从零减一会导致下溢,在发布模式下回绕到 usize::MAX
,并导致此函数的结果不正确。建议在被除数为零时直接返回零。
更新: 已在 commit 36ab98e 的 pull request #667 中解决。
interface_id
属性的 Trait 中的自定义选择器在实现中未被检查#[interface_id]
属性被实现为一个 程序宏,可以将其添加到 Trait 中。这个宏重新生成 Trait 源代码,并添加一个 interface_id
函数来生成它的 ERC-165 标识符。
该 宏会消耗 Trait 中的任何 #[selector]
属性,允许它们存在于 Trait 中。在 Stylus SDK 中,#[selector]
属性可能只存在于 #[public]
块中,并且不为 Trait 中的宏提供逻辑。但是,没有机制来强制 Trait 实现与具有自定义选择器的 Trait 的接口相匹配。
因此,开发人员必须记住在 Trait 实现中添加任何自定义选择器,以正确支持接口标识符,并且没有检查来确保这一点。因此,特定实现的 ABI 可能与其 Trait 不匹配,从而违反 Rust 中的假定不变量,并可能导致合约集成失败。
考虑重构 interface_id
宏的实现,以验证自定义选择器与各个 Trait 实现的兼容性。
更新: 已确认,待解决。Stylus 合约团队表示:
我们已建议 Stylus SDK 团队包括
#[interface_id]
功能,以及检索每个方法的选择器并自动实现每个路由器功能的选项,以这种方式构建继承(这样做将允许在编译时检查 Trait 和实现之间自定义选择器的差异)。我们将与该团队合作,在未来的迭代中添加此功能。
函数 verify_with_builder 和 verify_multi_proof_with_builder 用作具有自定义哈希函数的 Merkle 树的验证程序。但是,这些函数以可交换的方式重建 Merkle 根,这假定所有自定义哈希算法都采用可交换哈希。虽然可交换哈希是常见的做法,但不能保证所有自定义哈希过程都采用此假设。
考虑更新文档以指定,当使用自定义哈希算法时,Merkle 树哈希过程必须以可交换的方式构建。
更新: 已在 commit 6f75f80 的 pull request #674 中解决。
carry
和 borrow
参数的数据类型不一致算术进位(加法)和借位(减法)参数在 adc
和 sbb
函数中定义为 Limb
类型,但在 adc_assign
和 sbb_assign
函数中定义为 bool
类型。这些参数目前预计仅保存值 0
或 1
,这是利用它们的函数所要求的。但是,如果某个函数错误地传递了值大于 1 的进位或借位参数,则可能会出现意外行为。
例如,如果在 sbb
函数中 a = 0
、b = u64::MAX
和 borrow = 2
,则 溢出保护机制 将会失败。考虑专门采用 bool
类型作为进位和借位参数,并在必要时在计算过程中将其转换为 Limb
。
更新: 已在 commit cde91fc 的 pull request #675 中解决。
ct_num_bits
函数中的编码歧义根据 ct_num_bits
函数的注释,它返回编码给定数字所需的最小位数。目前,它为数字 0
返回 0
。但是,在实际编码场景中,至少需要 一位 才能表示任何数字,包括 0
。为数字 0
返回 0
位可能会在使用至少一位来表示有效数字的系统中引入歧义或错误。考虑修改该函数以确保它为所有输入返回至少一位。
更新: 已在 commit e36c292 的 pull request #681 中解决。
from_str_hex
函数中缺少溢出检查from_str_hex
函数在将十六进制字符串解析为 Uint<LIMBS>
类型时,不包含溢出检查。如果输入字符串表示的数字超过 Uint<LIMBS>
类型的容量,则此省略可能导致意外的索引越界 panic。相比之下,from_str_radix
函数在 ct_add
函数中包含一个 溢出检查,确保显式检测到溢出情况,并使用清晰的错误消息触发 panic。
考虑在 from_str_hex
函数中实现溢出检查以增强其鲁棒性。
更新: 已在 commit a988e2b 的 pull request #673 中解决。
Erc20Wrapper
合约在构造期间 设置底层Token地址,但未从底层Token的 decimals 设置 underlying_decimals
存储字段。然后必须 手动设置 底层 decimals,如果 decimals 设置不正确,可能会导致Token功能出现严重错误。考虑在构造函数中使用底层Token的 decimals
分配 underlying_decimals
字段,或者包含检查以确保分配的值相等。
更新: 已在 commit add8075 的 pull request #699 中解决。
即使 IErc4626
Trait 当前使用 &mut self
标记了几个方法,但它们只需要读取合约存储,例如 total_assets
、convert_to_shares
、convert_to_assets
等。
通过在概念上只读的方法上要求 &mut self
,该 Trait 允许实现引入意外或恶意的状态更改。这是因为开发人员可以覆盖“只读”函数并写入存储。为了保持预期的不变量并防止滥用,考虑将任何不修改状态的 IErc4626
方法的签名更改为采用 &self
而不是 &mut self
。
更新: 已在 commit 33b15ed 的 pull request #698 中解决。
interface_id
的 Trait 中的自定义选择器允许无法访问的模式#[interface_id]
属性允许通过 #[selector]
属性将自定义选择器添加到 Trait 中的函数签名中。这反映了 Stylus SDK 中 #[public]
实现块中可用函数的功能。在 #[public]
块中包含具有相同选择器的函数的代码会产生无法访问的模式错误;但是,此强制在具有 #[interface_id]
的 Trait 中不存在。因此,可以定义永远无法实现的具有 #[interface_id]
属性的 Trait。为了维护预期的编程不变量并减少开发人员错误,请考虑禁止创建具有重复函数选择器的 Trait。
更新: 已在 commit b68f7ae 的 pull request #702 中解决。
interface_id
宏生成的代码中缺少 Trait 组件#[interface_id]
宏adc
函数包含一个函数级别的属性,#[allow(clippy::cast_possible_truncation)]
。然而,在文件顶部的文件级别属性已经将此设置应用于整个文件,从而导致函数级别的属性变得多余。建议从 adc
函数中删除函数级别的属性,以增强代码的清晰度和可维护性。
更新: 已在提交 19974a9 的 pull #706 中解决。
\#[inline(always)]
属性一些 Uint
方法,包括 ct_lt
,ct_is_zero
和 ct_eq
,缺少 #[inline(always)]
属性,这与类似的方法(如 ct_ge
,ct_gt
和 ct_le
)形成对比。这些方法很可能用于性能至关重要的上下文中。应用 #[inline(always)]
属性将确保一致的内联行为,最大限度地减少函数调用开销,并提高性能。考虑将 #[inline(always)]
属性添加到这些方法中。
更新: 已在提交 80402e2 的 pull #707 中解决。
PartialEq
实现中的错误注释Projective
点的 PartialEq
实现的注释声明,当 (X * Z^2) = (X' * Z'^2)
且 (Y * Z^3) = (Y' * Z'^3)
时,点 (X, Y, Z)
和 (X', Y', Z')
相等。但是,该实现验证的是 (X * Z'^2) = (X' * Z^2)
且 (Y * Z'^3) = (Y' * Z^3)
,这是正确的。注释不准确,因为它未能反映实现中正确的操作顺序。考虑修改注释。
更新: 已在提交 12adbbc 的 pull #711 中解决。
该库的某些组件,例如包含来自 algebra/ec 的代码的曲线模块,以及使用来自 algebra/ff 的代码的字段模块,包含来自外部代码库的部分代码,但没有正确的归属。建议在代码注释中包含对原始代码库的引用,以确保正确的归属。
更新: 已在提交 eaa0ba2 的 pull #712 中解决。
在代码库中发现了两个增强文档的机会:
Absorbing
。但是,squeeze 函数允许模式保持 Absorbing
,而没有进行相应的检查以确保模式为 Squeezing
。此行为是正确的,因为该函数应保持在 Squeezing
模式,直到重新初始化。为了提高清晰度,请考虑向 absorb 函数 添加注释:“从 Absorbing 到 Squeezing 模式的转换是单向的。” 此注释强调一旦进入 Squeezing
模式,就不可能返回到 Absorbing
模式,从而证明了 if 条件 的合理性。同样,请考虑向 squeeze 函数 添加注释:“当从 Absorbing 模式调用时,此函数会触发排列并转换为 Squeezing 模式。”
为了阐明这一点,请考虑使用如下语句修改文档:“此接口不实现填充或域分隔。用户负责填充输入、预置域分隔标记和管理 absorb/squeeze 转换。”
考虑实施建议的文档增强,以提高清晰度和可用性。
更新: 已在提交 eb72ebc 的 pull #710 中解决。
正如 Rust RFC 560 中所指定的,对于宽度为 N
的类型(例如,u32
,其中 N = 32
),如果右侧操作数(移位量)大于或等于 N
,则会将其屏蔽为 shift_amount % N
。这确保了有效的移位量保持在有效范围内。例如:
移位 1u32 << 32
等效于 1u32 << 0
(因为 32 % 32 = 0
),从而生成 1
。
移位 1u32 << 33
等效于 1u32 << 1
(因为 33 % 32 = 1
),从而生成 2
。
但是,shr_assign
和 shl_assign
函数的实现会为溢出移位返回 Zero
。此行为与 Rust 中本机整数移位操作的默认操作不一致。考虑使该实现与 Rust 的本机行为保持一致,以确保一致性和可预测性。
更新: 已在提交 c38b883 的 pull #658 中解决。移位操作将为移位溢出而恢复。
#[interface_id]
过程宏被描述为添加一个关联的常量[ 1, 2],但实际上是作为一个函数添加的。考虑更正此注释以反映准确的行为。
更新: 在提交 184bf10 的 pull request #705 中部分解决,并在提交 b68f7ae 的 pull request #702 中部分解决。
在整个代码库中,发现了以下拼写错误:
变量 “ underline_token
” 应重命名为 “underlying_token
”。
测试 “ prevents_non_onwers_from_transferring
” 应重命名为 “prevents_non_owners_from_transferring”。
在 contracts/src/access/control.rs
的 第 111 行 中,注释应为“The caller of a function...”
考虑更正这些错误以及代码库中的任何其他错误,以提高整体清晰度。
更新: 已在提交 917b9e2 的 pull request #709 中解决。
代码库的某些部分缺乏足够的单元测试。例如,crypto arithmetic limb.rs 模块没有测试其所有功能。考虑增加测试覆盖率,以确保在各种极端情况下的正确性。
更新: 已确认,将解决。该团队表示:
我们将在里程碑 3 期间改进加密和合约部分的测试覆盖率。
检查 Poseidon2 S-box 的可逆性:在 new()
函数中添加检查 gcd(P::D, F::MODULUS - 1) == 1
,以确保 S-box 映射 f(x)=x^D
的可逆性。
在 Poseidon2 中强制执行偶数个完整轮次和最小轮次数:根据 Poseidon2 论文,完整轮次的总数必须为偶数。当前的实现假设满足此条件。考虑添加一项检查以确保强制执行此要求。此外,必须完成最小轮次数才能达到所需的安全参数。
Stylus 库的合约已升级,以利用最新的 Stylus SDK,现在提供更丰富、更用户友好的功能集。值得注意的是,它为 ERC-20 代币添加了一个完全集成的 ERC-4626 金库 - 这是 DeFi 应用程序的基本构建块 - 并通过可燃烧扩展、元数据 URI 管理和供应控制扩展了 ERC-1155 支持,从而解决了不断增长的 NFT 用例。这些增强功能极大地拓宽了该库的适用性,并展示了清晰、组织良好的代码结构。
密码库在六个模块中提供了一套全面的密码实用程序,专为安全高效的区块链应用程序而设计。它具有用于生成更大尺寸输出的通用哈希模块、用于高效 Merkle 证明验证的 merkle 模块以及用于安全大整数运算的算术模块。此外,曲线模块支持使用仿射和投影表示形式的椭圆曲线运算,而字段模块支持有限域算术计算。Poseidon2 和 Pedersen 模块提供快速的、针对区块链优化的哈希函数,专为零知识证明而定制。
此次审计发现了一个高危问题和两个中危问题,以及合约和密码库范围内的几个低危发现和一般说明。为了保持强大的安全性和功能完整性,强烈建议在引入主要的库更新时定期进行审计。持续的开发和迭代将进一步加强该库的实用性以及对 Arbitrum Stylus 生态系统的贡献。
- 原文链接: blog.openzeppelin.com/st...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!