Stylus 合约库 v0.2.0 审计

本次是对 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.rskeccark.rsmerkle.rsbits.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标准的全面支持:

  • ERC-1155 现在包括可燃烧扩展、元数据 URI 管理和供应控制。
  • ERC-4626 vault 功能允许将 ERC-20 Token包装到产生收益的 vault 中。
  • ERC-20 的 Flash-minting 功能。
  • 一个 SafeERC20 包装器。
  • 一个 vesting wallet 实现。
  • 几个额外的实用程序。

由于其稳健的设计,合约实现密切遵循 Solidity 的 OpenZeppelin 合约库,从而最大限度地提高了兼容性和一致性。ERC-165 已在所有合约中完全实现,因此接口检测通过 supports_interface 可靠且一致。

还引入了一个新的过程宏,用于为 Trait 生成一个 interface_id 函数并处理这些 Trait 中的自定义选择器。总之,这些增强功能在保持清晰、标准化设计的同时,提高了库的表达能力和安全性。

安全模型和信任假设

  • 密码学库的 Poseidon2 实现 与参考实现的 测试向量 相匹配。
  • 假设 Stylus SDK 和 Stylus 合约库使用的所有第三方依赖项都是安全的,没有恶意代码(例如后门),并且行为符合文档记录。
  • Motsu 是 OpenZeppelin 用于 Stylus 合约的内部测试实用程序,其功能如预期,不包含严重错误,并为库的单元测试提供可靠的覆盖。

高危

shr_assignshl_assign 中的 Limb 移位溢出

shr_assignshl_assign 函数实现了 Uint<N> 类型的移位和赋值运算符( <<=>>= ),该类型表示具有 Nlimb 的多 limb 无符号整数。但是,当 limb_shift 值为零时,会发生移位溢出 [1] [2]。在调试模式下,这会导致运行时 panic,并显示错误消息 attempt to shift left with overflow。在发布模式下,移位值被屏蔽,产生不正确的结果。考虑修改代码以在溢出发生之前解决此问题。

更新: 已在 commit c38b883pull request #658 中解决。该团队使用 checked_shl 来缓解溢出情况。

中危

ct_rem 函数中潜在的下溢

ct_rem 函数计算除法运算的余数。但是,如果 self.ct_num_bits() 返回零,则 index 的计算可能会导致下溢。当被除数 (self) 为零时会出现此问题,因为 ct_num_bits 对于零值返回零。在无符号整数 (usize) 中从零减一会导致下溢,在发布模式下回绕到 usize::MAX,并导致此函数的结果不正确。建议在被除数为零时直接返回零。

更新: 已在 commit 36ab98epull 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 和实现之间自定义选择器的差异)。我们将与该团队合作,在未来的迭代中添加此功能。

低危

Merkle 树中自定义哈希的验证函数假定可交换哈希

函数 verify_with_builderverify_multi_proof_with_builder 用作具有自定义哈希函数的 Merkle 树的验证程序。但是,这些函数以可交换的方式重建 Merkle 根,这假定所有自定义哈希算法都采用可交换哈希。虽然可交换哈希是常见的做法,但不能保证所有自定义哈希过程都采用此假设。

考虑更新文档以指定,当使用自定义哈希算法时,Merkle 树哈希过程必须以可交换的方式构建。

更新: 已在 commit 6f75f80pull request #674 中解决。

carryborrow 参数的数据类型不一致

算术进位(加法)和借位(减法)参数在 adcsbb 函数中定义为 Limb 类型,但在 adc_assignsbb_assign 函数中定义为 bool 类型。这些参数目前预计仅保存值 01,这是利用它们的函数所要求的。但是,如果某个函数错误地传递了值大于 1 的进位或借位参数,则可能会出现意外行为。

例如,如果在 sbb 函数中 a = 0b = u64::MAXborrow = 2,则 溢出保护机制 将会失败。考虑专门采用 bool 类型作为进位和借位参数,并在必要时在计算过程中将其转换为 Limb

更新: 已在 commit cde91fcpull request #675 中解决。

ct_num_bits 函数中的编码歧义

根据 ct_num_bits 函数的注释,它返回编码给定数字所需的最小位数。目前,它为数字 0 返回 0。但是,在实际编码场景中,至少需要 一位 才能表示任何数字,包括 0。为数字 0 返回 0 位可能会在使用至少一位来表示有效数字的系统中引入歧义或错误。考虑修改该函数以确保它为所有输入返回至少一位。

更新: 已在 commit e36c292pull request #681 中解决。

from_str_hex 函数中缺少溢出检查

from_str_hex 函数在将十六进制字符串解析为 Uint<LIMBS> 类型时,不包含溢出检查。如果输入字符串表示的数字超过 Uint<LIMBS> 类型的容量,则此省略可能导致意外的索引越界 panic。相比之下,from_str_radix 函数在 ct_add 函数中包含一个 溢出检查,确保显式检测到溢出情况,并使用清晰的错误消息触发 panic。

考虑在 from_str_hex 函数中实现溢出检查以增强其鲁棒性。

更新: 已在 commit a988e2bpull request #673 中解决。

Erc20Wrapper 中的底层 decimals 可能设置不正确

Erc20Wrapper 合约在构造期间 设置底层Token地址,但未从底层Token的 decimals 设置 underlying_decimals 存储字段。然后必须 手动设置 底层 decimals,如果 decimals 设置不正确,可能会导致Token功能出现严重错误。考虑在构造函数中使用底层Token的 decimals 分配 underlying_decimals 字段,或者包含检查以确保分配的值相等。

更新: 已在 commit add8075pull request #699 中解决。

IErc4626 中的状态可变性可以被限制

即使 IErc4626 Trait 当前使用 &mut self 标记了几个方法,但它们只需要读取合约存储,例如 total_assetsconvert_to_sharesconvert_to_assets 等。

通过在概念上只读的方法上要求 &mut self,该 Trait 允许实现引入意外或恶意的状态更改。这是因为开发人员可以覆盖“只读”函数并写入存储。为了保持预期的不变量并防止滥用,考虑将任何不修改状态的 IErc4626 方法的签名更改为采用 &self 而不是 &mut self

更新: 已在 commit 33b15edpull request #698 中解决。

具有 interface_idTrait 中的自定义选择器允许无法访问的模式

#[interface_id] 属性允许通过 #[selector] 属性将自定义选择器添加到 Trait 中的函数签名中。这反映了 Stylus SDK 中 #[public] 实现块中可用函数的功能。在 #[public] 块中包含具有相同选择器的函数的代码会产生无法访问的模式错误;但是,此强制在具有 #[interface_id]Trait 中不存在。因此,可以定义永远无法实现的具有 #[interface_id] 属性的 Trait。为了维护预期的编程不变量并减少开发人员错误,请考虑禁止创建具有重复函数选择器的 Trait

更新: 已在 commit b68f7aepull request #702 中解决。

interface_id 宏生成的代码中缺少 Trait 组件

#[interface_id]adc 函数包含一个函数级别的属性,#[allow(clippy::cast_possible_truncation)]。然而,在文件顶部的文件级别属性已经将此设置应用于整个文件,从而导致函数级别的属性变得多余。建议从 adc 函数中删除函数级别的属性,以增强代码的清晰度和可维护性。

更新: 已在提交 19974a9pull #706 中解决。

某些方法中缺少 \#[inline(always)] 属性

一些 Uint 方法,包括 ct_ltct_is_zeroct_eq,缺少 #[inline(always)] 属性,这与类似的方法(如 ct_gect_gtct_le)形成对比。这些方法很可能用于性能至关重要的上下文中。应用 #[inline(always)] 属性将确保一致的内联行为,最大限度地减少函数调用开销,并提高性能。考虑将 #[inline(always)] 属性添加到这些方法中。

更新: 已在提交 80402e2pull #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),这是正确的。注释不准确,因为它未能反映实现中正确的操作顺序。考虑修改注释。

更新: 已在提交 12adbbcpull #711 中解决。

源代码归属问题

该库的某些组件,例如包含来自 algebra/ec 的代码的曲线模块,以及使用来自 algebra/ff 的代码的字段模块,包含来自外部代码库的部分代码,但没有正确的归属。建议在代码注释中包含对原始代码库的引用,以确保正确的归属。

更新: 已在提交 eaa0ba2pull #712 中解决。

文档增强

在代码库中发现了两个增强文档的机会:

  • absorb 函数 强制 将模式设置为 Absorbing。但是,squeeze 函数允许模式保持 Absorbing,而没有进行相应的检查以确保模式为 Squeezing。此行为是正确的,因为该函数应保持在 Squeezing 模式,直到重新初始化。

为了提高清晰度,请考虑向 absorb 函数 添加注释:“从 Absorbing 到 Squeezing 模式的转换是单向的。” 此注释强调一旦进入 Squeezing 模式,就不可能返回到 Absorbing 模式,从而证明了 if 条件 的合理性。同样,请考虑向 squeeze 函数 添加注释:“当从 Absorbing 模式调用时,此函数会触发排列并转换为 Squeezing 模式。”

  • Poseidon2 实现构成了一个底层库的一部分,为构建完整的哈希函数提供了基础组件。因此,诸如填充、域分隔和 absorb/squeeze 转换之类的高级功能由库用户负责。

为了阐明这一点,请考虑使用如下语句修改文档:“此接口不实现填充或域分隔。用户负责填充输入、预置域分隔标记和管理 absorb/squeeze 转换。”

考虑实施建议的文档增强,以提高清晰度和可用性。

更新: 已在提交 eb72ebcpull #710 中解决。

与 Rust 原生整数类型相比,移位溢出的行为不一致

正如 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_assignshl_assign 函数的实现会为溢出移位返回 Zero。此行为与 Rust 中本机整数移位操作的默认操作不一致。考虑使该实现与 Rust 的本机行为保持一致,以确保一致性和可预测性。

更新: 已在提交 c38b883pull #658 中解决。移位操作将为移位溢出而恢复。

误导性文档

#[interface_id] 过程宏被描述为添加一个关联的常量[ 1, 2],但实际上是作为一个函数添加的。考虑更正此注释以反映准确的行为。

更新: 在提交 184bf10pull request #705 中部分解决,并在提交 b68f7aepull request #702 中部分解决。

拼写错误

在整个代码库中,发现了以下拼写错误:

考虑更正这些错误以及代码库中的任何其他错误,以提高整体清晰度。

更新: 已在提交 917b9e2pull 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。