Solana Fusion 兑换审计

OpenZeppelin 对 1inch Solana Fusion 协议进行了审计,该协议通过荷兰拍卖机制促进代币交换,同时抵御抢先交易。审计范围包括订单生命周期管理、荷兰拍卖实施、费用结构以及白名单机制。审计发现了一些中低风险问题,并提出了改进代码可维护性、文档清晰度以及安全实践的建议,最终 1inch 团队响应迅速并积极配合解决了大部分问题。

目录

概要

TypeDefiTimelineFrom 2025-03-31To 2025-04-08LanguagesRustTotal Issues11 (6 resolved, 1 partially resolved)Critical Severity Issues0 (0 resolved)High Severity Issues0 (0 resolved)Medium Severity Issues1 (0 resolved)Low Severity Issues2 (0 resolved, 1 partially resolved)Notes & Additional Information8 (6 resolved)

范围

OpenZeppelin 审计了 1inch/solana-fusion-protocol 仓库,提交哈希为 commit 8743d38

审计范围包括以下文件:

 programs
├── fusion-swap
│   └── src
│       ├── auction.rs
│       ├── error.rs
│       └── lib.rs
└── whitelist
    └── src
        ├── error.rs
        └── lib.rs

系统概述

1inch Fusion 是一个去中心化交易所,它通过利用荷兰式拍卖机制,在促进代币交换的同时,还能抵抗抢先交易。该系统没有公开的开放订单簿,而是采用基于托管的方法,用户(makers)创建特定的交换订单。这些订单定义了要出售的源资产和数量,以及他们愿意收到的目标资产的最低数量。

该系统采用随时间衰减的汇率,在开始时对 maker 更有利,然后在订单的持续时间内逐渐降低到他们可接受的最低汇率。一个白名单参与者网络(resolverstakers)监控这些订单,并在汇率根据他们的策略变得有利可图时竞争完成它们。这种设计旨在保护用户免受 MEV 活动(如公共 AMM 池中常见的夹层攻击)的侵害。

该系统包括两个主要的 Solana 程序:

  1. fusion_swap ( 5uzpYuGqBaetRMXPDtGWGN9W4mdmgBzpGHcQACrZ1npi): 处理订单创建、托管管理、订单执行和取消的核心逻辑。
  2. whitelist ( DyXFcRxGWFoMz1j76SeMXHjQqZKudLXeJY3h1K7BNJiQ): 管理授权地址(resolvers)的集合,这些地址被允许在 fusion_swap 程序中执行或取消过期的订单。

订单生命周期

交换订单的生命周期涉及几个可能的路径。

  1. 创建: 用户(maker)通过调用 create 指令来启动该流程。他们提供 OrderConfig,其中包括:

    • 源代币和数量(src_amount
    • 目标代币和最低可接受数量(min_dst_amount
    • 估计的目标数量(estimated_dst_amount),用于盈余费用计算
    • 订单到期时间戳(expiration_time
    • 荷兰式拍卖的详细信息(dutch_auction_data)。
    • 费用配置(fee),包括潜在的协议费用、集成商费用和取消费用
    • 指示源/目标资产是否为原生 SOL 的标志(src_asset_is_nativedst_asset_is_native

maker 签署交易,并且他们指定的 SPL 代币或 SOL 中的 src_amount 被发送到专用的托管代币账户(关联代币账户 - ATA)。如果是 SOL,它将被包装成 wSOL。ATA 由一个程序派生地址(PDA)拥有,该地址对于订单详细信息(escrow 账户)是唯一的,PDA 作为其授权。各种检查确保订单的有效性(例如,非零金额、有效到期时间、一致的费用配置)。

  1. 执行: 一个加入白名单的 taker(resolver)找到一个合适的订单并调用 fill 指令。

    • Taker 指定订单详细信息(OrderConfig)以及他们希望购买的源代币 amount(允许部分执行)。
    • 系统验证订单尚未过期,并且 taker 已加入白名单(拥有来自 whitelist 程序的有效 ResolverAccess 账户)。
    • 它计算 taker 必须支付的所需 dst_amount,并在使用 calculate_rate_bump 时根据当前时间计入荷兰式拍卖汇率调整。
    • 指定的源代币 amountescrow_src_ata 转移到 taker_src_ata
    • 计算出的 dst_amount 由 taker 支付。然后将此金额分配如下:

      • 集成商费用将转到 integrator_dst_acc(如果适用)。
      • 协议费用(包括任何盈余费用)将转到 protocol_dst_acc(如果适用)。
      • 剩余金额将转到 maker_receiver(或他们的 maker_dst_ata)。
    • 如果执行完成整个 src_amount,则 escrow_src_ata 将关闭,并且其 lamport 余额(租金)将返回给 maker。
  2. Maker 取消: 最初的 maker 可以通过调用 cancel 指令来决定取消其订单。
    • Maker 提供从订单参数派生的唯一 order_hash
    • 该指令验证 maker 是签名者。
    • escrow_src_ata 中剩余的任何源代币都将转回 maker_src_ata
    • escrow_src_ata 关闭,将其 lamport 余额(租金)返回给 maker。
  3. Resolver 取消: 如果订单过期,它将有资格由白名单 Resolver 通过 cancel_by_resolver 指令取消。此机制激励清理过期订单。
    • 白名单 resolver 调用该指令,提供 OrderConfig 和他们愿意接受的 reward_limit
    • 系统验证订单已过期,并且调用者已加入白名单。
    • 系统检查是否允许 Resolver 取消。
    • 如果源资产不是原生 SOL,则剩余代币将转回 maker_src_ata
    • cancellation_premium 根据自到期后经过的时间 ( calculate_premium) 计算,以 order.fee.max_cancellation_premium 为上限。
    • escrow_src_ata 关闭。至关重要的是,它的整个 lamport 余额(租金 + 如果源是包装的 SOL,则包括任何本金)将转移到 resolver
    • 然后,resolver 立即将这些收到的 lamport 的一部分转回给 maker。退还给 maker 的金额是收到的总额减去计算出的 cancellation_premium(进一步以 Resolver 提供的 reward_limit 为上限)。Resolver 保留溢价作为奖励。

荷兰式拍卖实施

荷兰式拍卖会随着时间的推移修改汇率,从而使 Taker 的订单越来越便宜。它通过 OrderConfig 中的 AuctionData 结构和 calculate_rate_bump 函数来实现。

  • 配置: AuctionData 包含:

    • start_time:拍卖开始的 Unix 时间戳。
    • duration:拍卖期的总长度。
    • initial_rate_bump:应用于目标金额的起始调整(以基点为单位,BASE_1E5)。正向加成意味着 Taker 最初支付的_多于_从 min_dst_amount 得出的基本汇率。
    • points_and_time_deltas:一个定义曲线上点的向量。每个点都指定一个 rate_bumptime_delta。这允许定义自定义衰减曲线。
  • 计算: calculate_rate_bump 函数根据当前 timestamp 确定适用的加成:

    • start_time 之前,使用 initial_rate_bump
    • start_time + duration 之后,加成值为 0(这意味着汇率恢复为 min_dst_amount 所暗示的汇率)。
    • 在拍卖期间,该函数会迭代 points_and_time_deltas。它找到与当前 timestamp 对应的曲线段,并在该段的起点和终点的 rate_bump 值之间执行线性插值,以找到当前加成。

费用

该协议包含多种类型的费用,这些费用在 OrderConfig.feeFeeConfig 结构)中配置:

  1. 协议费用:fill 期间,由 Taker 支付的总 dst_amount 的百分比(protocol_fee,相对于 BASE_1E5 的基点)。如果提供,此费用将定向到 protocol_dst_acc
  2. 集成商费用:fill 期间,由 Taker 支付的总 dst_amount 的百分比(integrator_fee,相对于 BASE_1E5 的基点)。如果提供,此费用将定向到 integrator_dst_acc,从而允许 UI 提供商或集成商赚取收入。
  3. 盈余费用(正滑点): 如果 Maker 收到的实际金额(在从 dst_amount 中扣除协议费用和集成商费用后)超过订单中提供的 estimated_dst_amount,则此正差额(盈余)的百分比(surplus_percentage,相对于 BASE_1E2 = 100% 的基点)将作为额外费用收取。此盈余费用将添加到协议费用金额中。
  4. 取消溢价: 通过 max_cancellation_premium(绝对 lamport 金额)配置。当 Resolver 使用 cancel_by_resolver 取消过期订单时,他们将根据自到期后经过的时间来赚取溢价,该溢价以此值为上限。此费用实际上由 Maker 从托管 ATA 中持有的资金(特别是其 lamport 余额)支付。

白名单

whitelist 程序充当 fusion_swap 程序中特定操作的访问控制层。

  • 目的: 它将充当 taker(调用 fill)和取消过期订单(调用 cancel_by_resolver)的能力限制为仅授权地址。 这实现了“专业做市商网络”的概念。
  • 机制:
    • 该程序维护一个中央 WhitelistState 账户(由 WHITELIST_STATE_SEED 播种的 PDA),该账户存储 owner 公钥。
    • owner 有权管理白名单。
    • 要将用户(resolver)列入白名单,Owner 调用 register,提供用户的公钥。这将创建一个空的 ResolverAccess 账户(由 RESOLVER_ACCESS_SEED 和用户的 Key 播种的 PDA)。此账户的存在表明该用户已加入白名单。
    • 要删除用户,Owner 调用 deregister,这将关闭用户的 ResolverAccess 账户。
    • fusion_swap 中的 fillcancel_by_resolver 指令包括约束,以验证交易签名者(takerresolver)是否具有使用 whitelist 程序的 ID 和正确的种子派生的有效 ResolverAccess 账户。

安全模型和信任假设

此协议中的用户和参与者在以下安全假设和已知风险下运行:

  • 白名单依赖性: 执行过程的安全性和活跃性完全取决于白名单 Resolver。 用户信任 whitelist 程序的 Owner:

    • 仅将合格且非恶意的 Resolver 列入白名单。
    • 维护足够数量的活跃 Resolver,以确保订单能够执行。
    • 不要随意删除 Resolver 或将所有权转移给不受信任的方。
  • Resolver 行为: 用户信任列入白名单的 Resolver 将在经济上理性地行事,并在有利可图时执行订单。 即使汇率变得有利,也不能保证订单会执行。
  • 链下依赖性: 订单发现和潜在的提交可能依赖于链下基础设施(例如,API、前端,例如 1inch 的前端)。 用户和 Resolver 信任此基础设施可用、准确且具有抗审查性。 此层的停机或操纵可能会阻止订单创建或执行。 中心化后端对于订单流至关重要。 如果它变得不可用或行为不正确,则有效的订单可能无法转发给 Taker,从而导致协议可用性降低。

相反,如果后端无法正确过滤无效订单,它们仍然可能被传递,从而破坏链下匹配过程的完整性。 后端引入了一层信任,这与去中心化系统的最小化信任精神相矛盾。 用户和 Taker 必须假设后端不会审查或选择性地延迟订单。 尽管链上程序使用 PDA 检查来确保执行时订单的完整性,但它无法阻止后端影响哪些订单被查看或优先处理。

  • 到期处理: 过期订单依赖于 Resolver 调用 cancel_by_resolver 进行清理,并通过取消溢价来激励。 如果没有 Resolver 取消,资金(尤其是原生 SOL)将保留在托管 ATA 中,直到 Maker 手动取消或最终由 Resolver 取消。 用户信任这种激励机制是足够的。

特权角色

以下角色在系统中拥有特殊能力:

  1. 白名单 Owner:
    • 描述:whitelist 程序的 WhitelistState 账户中指定为 Owner 的单个地址。
    • 能力:
      • 添加 Resolver ( register)
      • 删除 Resolver ( deregister)
      • 转移白名单的所有权 ( transfer_ownership)
  2. Resolver / Taker:
    • 描述: 已通过 register 函数由白名单 Owner 列入白名单的地址,从而创建相应的 ResolverAccess PDA。
    • 能力(在 fusion_swap 中):
      • 执行活动订单 ( fill)
      • 取消过期订单以获取溢价 ( cancel_by_resolver)

中等风险

缺乏事件触发

两个程序中的任何指令在执行时都不会触发事件。 这种疏忽阻碍了透明度和外部可观察性。 如果没有触发的事件,仪表板、索引器和其他智能合约等链下使用者必须依赖自定义交易解析逻辑来推断状态变化,例如订单创建、执行或取消。 这增加了实施复杂性,并对内部指令格式造成了脆弱的依赖性。

尽管交易数据在链上公开可用,但缺乏标准化的事件触发大大降低了监控协议活动的简易性。 这种设计选择可能会影响开发者和用户体验,因为触发事件的协议允许更易于访问和标准化地跟踪有意义的链上事件。

考虑在发生敏感更改后触发事件,以方便跟踪并通知正在跟踪程序活动的链下客户端。

更新: 已确认,未解决。 1inch 团队表示:

添加事件意味着增加交易的 CU,我们目前无法证明这一点是合理的。

低风险

缺乏外部文档

solana-fusion-protocol 仓库缺少基本的项目信息,包括 README.md 文件、项目描述以及任何形式的文档目录或使用指南。 这大大降低了代码库的可访问性和可维护性,特别是对于试图理解或与协议集成的新的贡献者、审计员和外部开发人员。 缺少 README.md 还意味着既没有关于如何设置、测试或部署项目的明确指导,也没有关于其目的、架构或依赖项的任何详细信息。

包含带有设置说明、使用示例和协议概述的基本 README.md 是开源和生产代码库中广泛接受的最佳实践。 这确保了项目可以可靠地使用和审查,并且还有助于建立代码的可信度和可用性。

考虑至少将以下内容添加到文档中:

  • 协议的简短说明
  • 设置和构建说明
  • 如何运行测试
  • 合约架构和模块描述

更新: 已在 pull request #72 中部分解决。 1inch 团队表示:

已记录。 我们添加了白皮书,并且我们将在未来的版本中添加基本的 README.md。

缺少文档字符串

fusion_swap 程序的重要部分缺少必要的文档字符串,从而降低了清晰度并增加了误用的风险:

  • 程序模块 ( #[program]): 缺少顶层文档字符串,用于解释合约的用途和功能。
  • 指令处理程序 ( createfillcancelcancel_by_resolver): 没有关于目的、参数、预期前提条件、副作用或错误情况的文档。
  • OrderConfig 结构: 缺少结构及其字段(idsrc_amountmin_dst_amountexpiration_time 等)的文档字符串,这些字段对于订单逻辑至关重要。
  • UniTransferParams 枚举: 没有枚举或其变体(NativeTransferTokenTransfer)的解释,这些变体抽象了代币转账。
  • 辅助函数: order_hashget_fee_amountsuni_transfer 缺少描述逻辑、参数和预期行为的文档字符串。

考虑将简洁的文档添加到上述区域。 这样做将大大提高用户和审计员的可维护性、可读性和安全性。

更新: 已确认,将解决。 1inch 团队表示:

已记录。 我们将在未来的版本中添加必要的文档。

注释 & 补充信息

transfer_ownership 执行即时所有权转移,没有安全措施

whitelist 程序中的 transfer_ownership 函数在被调用后立即将所有权分配给 _new_owner 地址。 这种方法会带来风险,因为由于人为错误,可能会将不正确或意外的地址设置为新的所有者。

如果没有确认机制(例如,两步所有权转移,例如 proposeOwneracceptOwnership),则没有机会从错误配置中恢复。如果所有权意外分配给不需要的合约地址、销毁地址或不受预期接收者控制的地址,则合约可能会变得不可逆转地无法访问或管理不善。

为了缓解这种情况,请考虑实施两步转移模式,其中新所有者必须在更改最终确定之前明确接受所有权。 或者,如果系统设计中存在确保安全使用当前一步转移机制的保证,则应明确记录这些保证以证明该方法的合理性。

更新: 已确认,未解决。 1inch 团队表示:

在不太可能发生的情况下,如果对白名单合约的控制权丢失,重新部署到新的地址就足够了,而不会显着中断协议功能。 因此,我们认为没有强烈需要额外的检查。

白名单程序中“所有者”的模糊使用可能会导致混淆

whitelist 程序的上下文中,术语“所有者”用于指代控制白名单状态的参与者。 但是,此术语在 Solana 生态系统中可能会产生误导,因为在此区块链网络中,账户的 owner 是有权修改账户数据的程序 ID。 这与可能控制程序中行为或状态的任何权限或管理 Key 不同。

对于熟悉以太坊的开发人员来说,这种歧义尤其成问题,因为在以太坊中,“所有者”通常表示一种特权用户角色,而不是程序所有权。

为了提高清晰度并与 Solana 约定保持一致,请考虑使用更精确的术语,例如 authorityadmincontroller

更新: 已在 pull request #84 中解决。

已使用参数 _new_owner 上具有误导性的下划线前缀

transfer_ownership 函数采用 _new_owner 作为参数,该参数在函数体中使用。 但是,下划线前缀通常表示参数是有意未使用的。 这会产生 _new_owner 未被使用的误导性印象,可能会使读者或维护者感到困惑。

为了提高代码清晰度并遵守常规命名实践,请考虑从 _new_owner 中删除下划线。

更新: 已在 pull request #83 中解决。

Pubkey 值上多余的 .key() 调用

transfer_ownership 函数中,whitelist_state.owner = _new_owner.key(); 的赋值是多余的,因为 _new_owner 已经是 Pubkey.key() 方法通常在 AccountInfo 对象上使用以检索 Pubkey。 但是,在这种情况下,在 Pubkey 上调用 .key() 只是返回自身,从而增加了不必要的冗长,并可能使读者感到困惑。

考虑将 whitelist_state.owner = _new_owner.key(); 赋值替换为 whitelist_state.owner = _new_owner;,以使代码更简洁和惯用。

更新: 已在 pull request #82 中解决。

程序使用过时的 Anchor 依赖项

当前,程序依赖于 anchor-langanchor-spl crate 的过时版本。 自发布以来,有一个新版本包含错误修复、改进的开发人员人体工程学以及可能增强代码库的安全性和可维护性的新功能。

使用过时的依赖项可能会使程序容易受到较新版本中已解决的已知漏洞或错误的影响。 它还可能阻碍最佳实践的采用,并降低与生态系统中其他最新工具的兼容性。

考虑升级到 anchor-langanchor-spl crate 的最新版本,确保较新版本中引入的更改与当前代码库兼容。

更新: 已在 pull request #80 中解决。

Anchor.toml 中的 toolchain 部分为空

Anchor.toml 文件缺少指定的 toolchain 版本。 忽略此设置可能会导致不同开发人员/审计员使用的 Anchor CLI 版本与项目期望的版本之间不一致。 版本不匹配可能会引入细微的错误、编译错误或意外行为,尤其是在较新版本中引入了重大更改的情况下。

定义工具链版本有助于确保构建的可重现性和跨团队和 CI 系统的一致开发环境。 它还提高了审计员在已知 Anchor 版本下查看代码的清晰度。

为了缓解这种情况,请在 Anchor.tomltoolchain 部分中指定预期的 Anchor CLI 版本。

更新: 已在 pull request #81 中解决。

程序文件包含过多代码行

fusion_swap 程序当前在单个大型文件中实现所有指令。 这种单一结构对可读性、协作和未来的可扩展性产生负面影响。

较小的、特定于指令的模块更容易理解和导航。 当每个指令(例如,createcancel)及其关联的组件(例如其 Accounts 结构)位于单独的文件中时,开发人员可以更轻松地理解和维护代码库。 此模块化还降低了合并冲突的可能性,尤其是在多个团队成员同时处理不同的指令时。 此外,随着程序的大小或复杂性增加,当前的平面结构可能会变得越来越难以管理,从而使调试或重构更容易出错。

考虑通过将每个指令放入单独的模块或文件中来拆分 fusion_swap 程序。 这将有助于提高代码库的清晰度、促进团队协作并提高长期可维护性。

更新: 已确认,未解决。 1inch 团队表示:

已记录 - 我们已决定暂时不进行更改。

不正确的文档字符串

在整个代码库中,发现了多个包含技术上不正确或具有误导性信息的文档字符串的实例:

  1. /// Account to store order conditions(存在于 fusion-swap/src/lib.rs 中的 415501577638 行)。 此描述不准确地表明托管账户存储订单条件。 实际上,它是用作托管源代币账户的权限的 PDA。 它的地址是从订单详细信息(order_hash)派生的,但它不直接存储订单配置。 更清晰的描述是:

/// PDA derived from order details, acting as the authority for the escrow ATA

  1. /// size(timestamp) + size(rate_bump) < 64(存在于 auction.rs 上的 3750 行)。 此语句在事实上是不正确的。 timestamp 是一个 u64(64 位),而 rate_bump 虽然最初是一个 u16 值,但为了算术目的而被转换为 u64。 即使考虑到原始类型,组合的位大小也是 64 + 16 = 80,这不小于 64。 目的是证明 time_difference * rate_bump 乘法不会溢出 u64 容器。 实际上,这些值是受约束的:
    • 时间差是从 u16(最大值 65535)值派生的。
    • rate_bump 值也源自 u16 值。

因此,最大乘法结果约为 65535 * 65535 ≈ 2^32,这可以安全地容纳在 u64 容器中。 虽然逻辑是合理的,但基于位大小的理由是有缺陷的,应该加以澄清。

清晰准确的文档字符串对于正确性、可维护性和可审计性至关重要。 因此,请考虑解决上述不正确/具有误导性的文档字符串的实例。

更新: 已在 pull request #85 中解决。

结论

1inch Fusion 是 Solana 区块链上的一个去中心化交易所,它通过利用荷兰式拍卖机制来促进抗抢先交易的代币交换。 用户不是使用公共订单簿,而是创建具有定义的最低回报的托管交换订单。 汇率随时间推移而下降(基于荷兰式拍卖模型),从对 maker 有利开始,一直下降到白名单参与者接受交易为止。 此设置有助于防止 MEV 攻击。该实现体现了对 Solana 开发的扎实理解,具有对极端情况的强大处理、全面的测试套件以及周到的设计选择。 虽然没有发现严重漏洞,但发现了一些小问题,并提供了可行的建议,以提高代码可维护性、遵守最佳实践、文档和整体清晰度。 感谢 1inch 团队在整个过程中反应迅速且具有协作精神。

  • 原文链接: blog.openzeppelin.com/fu...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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