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 更有利,然后在订单的持续时间内逐渐降低到他们可接受的最低汇率。一个白名单参与者网络(resolvers
或 takers
)监控这些订单,并在汇率根据他们的策略变得有利可图时竞争完成它们。这种设计旨在保护用户免受 MEV 活动(如公共 AMM 池中常见的夹层攻击)的侵害。
该系统包括两个主要的 Solana 程序:
fusion_swap
( 5uzpYuGqBaetRMXPDtGWGN9W4mdmgBzpGHcQACrZ1npi
): 处理订单创建、托管管理、订单执行和取消的核心逻辑。whitelist
( DyXFcRxGWFoMz1j76SeMXHjQqZKudLXeJY3h1K7BNJiQ
): 管理授权地址(resolvers)的集合,这些地址被允许在 fusion_swap
程序中执行或取消过期的订单。交换订单的生命周期涉及几个可能的路径。
创建: 用户(maker
)通过调用 create
指令来启动该流程。他们提供 OrderConfig
,其中包括:
src_amount
)min_dst_amount
)estimated_dst_amount
),用于盈余费用计算expiration_time
)dutch_auction_data
)。fee
),包括潜在的协议费用、集成商费用和取消费用src_asset_is_native
,dst_asset_is_native
)maker 签署交易,并且他们指定的 SPL 代币或 SOL 中的 src_amount
被发送到专用的托管代币账户(关联代币账户 - ATA)。如果是 SOL,它将被包装成 wSOL。ATA 由一个程序派生地址(PDA)拥有,该地址对于订单详细信息(escrow
账户)是唯一的,PDA 作为其授权。各种检查确保订单的有效性(例如,非零金额、有效到期时间、一致的费用配置)。
执行: 一个加入白名单的 taker
(resolver)找到一个合适的订单并调用 fill
指令。
OrderConfig
)以及他们希望购买的源代币 amount
(允许部分执行)。whitelist
程序的有效 ResolverAccess
账户)。dst_amount
,并在使用 calculate_rate_bump
时根据当前时间计入荷兰式拍卖汇率调整。amount
从 escrow_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。cancel
指令来决定取消其订单。
order_hash
。escrow_src_ata
中剩余的任何源代币都将转回 maker_src_ata
。escrow_src_ata
关闭,将其 lamport 余额(租金)返回给 maker。cancel_by_resolver
指令取消。此机制激励清理过期订单。
resolver
调用该指令,提供 OrderConfig
和他们愿意接受的 reward_limit
。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_bump
和 time_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.fee
(FeeConfig
结构)中配置:
fill
期间,由 Taker 支付的总 dst_amount
的百分比(protocol_fee
,相对于 BASE_1E5
的基点)。如果提供,此费用将定向到 protocol_dst_acc
。fill
期间,由 Taker 支付的总 dst_amount
的百分比(integrator_fee
,相对于 BASE_1E5
的基点)。如果提供,此费用将定向到 integrator_dst_acc
,从而允许 UI 提供商或集成商赚取收入。dst_amount
中扣除协议费用和集成商费用后)超过订单中提供的 estimated_dst_amount
,则此正差额(盈余)的百分比(surplus_percentage
,相对于 BASE_1E2
= 100% 的基点)将作为额外费用收取。此盈余费用将添加到协议费用金额中。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
有权管理白名单。register
,提供用户的公钥。这将创建一个空的 ResolverAccess
账户(由 RESOLVER_ACCESS_SEED
和用户的 Key 播种的 PDA)。此账户的存在表明该用户已加入白名单。deregister
,这将关闭用户的 ResolverAccess
账户。fusion_swap
中的 fill
和 cancel_by_resolver
指令包括约束,以验证交易签名者(taker
或 resolver
)是否具有使用 whitelist
程序的 ID 和正确的种子派生的有效 ResolverAccess
账户。此协议中的用户和参与者在以下安全假设和已知风险下运行:
白名单依赖性: 执行过程的安全性和活跃性完全取决于白名单 Resolver。 用户信任 whitelist
程序的 Owner:
相反,如果后端无法正确过滤无效订单,它们仍然可能被传递,从而破坏链下匹配过程的完整性。 后端引入了一层信任,这与去中心化系统的最小化信任精神相矛盾。 用户和 Taker 必须假设后端不会审查或选择性地延迟订单。 尽管链上程序使用 PDA 检查来确保执行时订单的完整性,但它无法阻止后端影响哪些订单被查看或优先处理。
cancel_by_resolver
进行清理,并通过取消溢价来激励。 如果没有 Resolver 取消,资金(尤其是原生 SOL)将保留在托管 ATA 中,直到 Maker 手动取消或最终由 Resolver 取消。 用户信任这种激励机制是足够的。以下角色在系统中拥有特殊能力:
whitelist
程序的 WhitelistState
账户中指定为 Owner 的单个地址。register
)deregister
)transfer_ownership
)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]
): 缺少顶层文档字符串,用于解释合约的用途和功能。create
、fill
、cancel
、cancel_by_resolver
): 没有关于目的、参数、预期前提条件、副作用或错误情况的文档。OrderConfig
结构: 缺少结构及其字段(id
、src_amount
、min_dst_amount
、expiration_time
等)的文档字符串,这些字段对于订单逻辑至关重要。UniTransferParams
枚举: 没有枚举或其变体(NativeTransfer
、TokenTransfer
)的解释,这些变体抽象了代币转账。order_hash
、get_fee_amounts
和 uni_transfer
缺少描述逻辑、参数和预期行为的文档字符串。考虑将简洁的文档添加到上述区域。 这样做将大大提高用户和审计员的可维护性、可读性和安全性。
更新: 已确认,将解决。 1inch 团队表示:
已记录。 我们将在未来的版本中添加必要的文档。
transfer_ownership
执行即时所有权转移,没有安全措施whitelist 程序中的 transfer_ownership
函数在被调用后立即将所有权分配给 _new_owner
地址。 这种方法会带来风险,因为由于人为错误,可能会将不正确或意外的地址设置为新的所有者。
如果没有确认机制(例如,两步所有权转移,例如 proposeOwner
和 acceptOwnership
),则没有机会从错误配置中恢复。如果所有权意外分配给不需要的合约地址、销毁地址或不受预期接收者控制的地址,则合约可能会变得不可逆转地无法访问或管理不善。
为了缓解这种情况,请考虑实施两步转移模式,其中新所有者必须在更改最终确定之前明确接受所有权。 或者,如果系统设计中存在确保安全使用当前一步转移机制的保证,则应明确记录这些保证以证明该方法的合理性。
更新: 已确认,未解决。 1inch 团队表示:
在不太可能发生的情况下,如果对白名单合约的控制权丢失,重新部署到新的地址就足够了,而不会显着中断协议功能。 因此,我们认为没有强烈需要额外的检查。
在 whitelist
程序的上下文中,术语“所有者”用于指代控制白名单状态的参与者。 但是,此术语在 Solana 生态系统中可能会产生误导,因为在此区块链网络中,账户的 owner
是有权修改账户数据的程序 ID。 这与可能控制程序中行为或状态的任何权限或管理 Key 不同。
对于熟悉以太坊的开发人员来说,这种歧义尤其成问题,因为在以太坊中,“所有者”通常表示一种特权用户角色,而不是程序所有权。
为了提高清晰度并与 Solana 约定保持一致,请考虑使用更精确的术语,例如 authority
、admin
或 controller
。
更新: 已在 pull request #84 中解决。
_new_owner
上具有误导性的下划线前缀transfer_ownership
函数采用 _new_owner
作为参数,该参数在函数体中使用。 但是,下划线前缀通常表示参数是有意未使用的。 这会产生 _new_owner
未被使用的误导性印象,可能会使读者或维护者感到困惑。
为了提高代码清晰度并遵守常规命名实践,请考虑从 _new_owner
中删除下划线。
更新: 已在 pull request #83 中解决。
在 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-lang
和 anchor-spl
crate 的过时版本。 自发布以来,有一个新版本包含错误修复、改进的开发人员人体工程学以及可能增强代码库的安全性和可维护性的新功能。
使用过时的依赖项可能会使程序容易受到较新版本中已解决的已知漏洞或错误的影响。 它还可能阻碍最佳实践的采用,并降低与生态系统中其他最新工具的兼容性。
考虑升级到 anchor-lang
和 anchor-spl
crate 的最新版本,确保较新版本中引入的更改与当前代码库兼容。
更新: 已在 pull request #80 中解决。
Anchor.toml
中的 toolchain
部分为空Anchor.toml
文件缺少指定的 toolchain
版本。 忽略此设置可能会导致不同开发人员/审计员使用的 Anchor CLI 版本与项目期望的版本之间不一致。 版本不匹配可能会引入细微的错误、编译错误或意外行为,尤其是在较新版本中引入了重大更改的情况下。
定义工具链版本有助于确保构建的可重现性和跨团队和 CI 系统的一致开发环境。 它还提高了审计员在已知 Anchor 版本下查看代码的清晰度。
为了缓解这种情况,请在 Anchor.toml
的 toolchain
部分中指定预期的 Anchor CLI 版本。
更新: 已在 pull request #81 中解决。
fusion_swap
程序当前在单个大型文件中实现所有指令。 这种单一结构对可读性、协作和未来的可扩展性产生负面影响。
较小的、特定于指令的模块更容易理解和导航。 当每个指令(例如,create
和 cancel
)及其关联的组件(例如其 Accounts
结构)位于单独的文件中时,开发人员可以更轻松地理解和维护代码库。 此模块化还降低了合并冲突的可能性,尤其是在多个团队成员同时处理不同的指令时。 此外,随着程序的大小或复杂性增加,当前的平面结构可能会变得越来越难以管理,从而使调试或重构更容易出错。
考虑通过将每个指令放入单独的模块或文件中来拆分 fusion_swap
程序。 这将有助于提高代码库的清晰度、促进团队协作并提高长期可维护性。
更新: 已确认,未解决。 1inch 团队表示:
已记录 - 我们已决定暂时不进行更改。
在整个代码库中,发现了多个包含技术上不正确或具有误导性信息的文档字符串的实例:
/// Account to store order conditions
(存在于 fusion-swap/src/lib.rs
中的 415、501、577 和 638 行)。 此描述不准确地表明托管账户存储订单条件。 实际上,它是用作托管源代币账户的权限的 PDA。 它的地址是从订单详细信息(order_hash
)派生的,但它不直接存储订单配置。 更清晰的描述是:/// PDA derived from order details, acting as the authority for the escrow ATA
/// size(timestamp) + size(rate_bump) < 64
(存在于 auction.rs
上的 37 和 50 行)。 此语句在事实上是不正确的。 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!