Solana程序安全漏洞实战分析:真实案例与防御指南
本文深入分析Solana生态中实际发生的安全事件,揭示其独特的执行模型导致的漏洞类别。

引言
Solana 已悄然成为最活跃的链上程序生态系统之一。过去几年间,它从一个高吞吐量的新秀,发展成一个承载着主要 DeFi 协议、消费者支付应用、交易基础设施和机构级应用的平台。
该网络目前已是生产环境中吞吐量最高的区块链之一——这是源于一种与 EVM 链根本不同的架构。
但规模也带来了风险敞口。如今,数十亿美元的流动性流经 Solana 生态系统,使得保护用户和协议资金成为一项关键关注点——这需要超越单纯 EVM 合约审计所提供的安全视角。Solana 独特的执行模型产生了一类在以太坊上没有明确对应的漏洞,仅凭通用的 DeFi 审计经验不足以捕获它们。
本文分析了在 Solana 上引发事故的真正原因:真实的利用案例、最常见漏洞的架构根源,以及审计人员在生产程序中遇到最多的漏洞类别的实用指南。
真正导致 Solana 上「攻击」的原因是什么?
与以太坊一样,并非所有事故都源于链上逻辑的错误。很大一部分损失源于密钥泄露、基础设施或受信任的链下服务。代表性案例:
- Drift Protocol(约 2.86 亿美元)—— 未窃取种子短语情况下的签名泄露
- Step Finance(约 4000 万美元)—— 签名 / 金库泄露
- Upbit(约 3000 万美元)—— 中心化交易所;热钱包泄露
- SwissBorg(约 4200 万美元)—— SwissBorg 应用;第三方质押 API(Kiln)泄露
同时,许多利用事件确实可追溯到 Solana 程序的缺陷。例如:
- Loopscale,借贷协议(约 600 万美元)—— 伪造的价格预言机 / 验证不足
- Texture Finance(约 200 万美元)—— 伪造的 SPL 目标地址 / 验证不足
- GoonFi(约 25.4 万美元)—— AMM 接受了错误的可信价格馈送 / 缺少合理性范围检查
深入分析具体利用案例
Solana 的一个架构特性悄然造就了一整类错误。在 EVM 风格的系统中,如果合约已经知道正确的地址,从外部传入相同的地址然后再验证它通常是多余的。合约可以直接使用其存储中或硬编码在逻辑中的地址。
Solana 的工作方式则不同。程序不会自己去「获取」它的账户。指令将读取或写入的每个账户都由交易构建者提供。这不是 API 的便利性,而是执行模型的核心部分。
这很容易与开发者的正常直觉相冲突:「用户传递了我们需要的账户,那我们就用它吧。」但攻击者的直觉则恰恰相反:「如果我传递一个不同的账户会怎样?」结果是,Solana 程序必须不断证明它们收到的账户正是协议打算使用的账户。缺少这种证明并非边缘情况,而是最常见的 Solana 特定漏洞类别之一。
Loopscale 和 Texture 都存在用户提供数据验证不足的问题。GoonFi 展示了邻近的故障模式:即使账户路径没有被攻击者明显伪造,如果程序在没有足够健全性检查的情况下接受可信的定价状态,它仍然可能不安全。其机制和风险概况有所不同。
关于资料来源的说明:这些项目的完整链上程序源代码并未公开,这在 Solana 上是常见情况。以下分析基于链上交易证据、官方事后分析报告和团队声明,以及从先前网页版本中恢复的客户端代码等辅助材料。
Loopscale
Loopscale 允许用户使用抵押品借款。抵押品估值依赖于预言机价格,这些预言机的地址——在 Solana 上不可避免——随交易的账户包一起传递。对于大多数资产,程序限制了哪些预言机地址是允许的,但在事件发生前不久,增加了对 RateX 本金通证的支持,并且该代码路径上的验证比需要的要弱。
攻击者提供了市价不到 1 美分的抵押品,然后接入了将其定价为本质上无上限的伪造预言机,从而可以抽走几乎所有可用流动性——即用伪造的抵押品「借走」全部可访问的资金池。
在产品层面,Loopscale 通常在后端组装交易分支,该后端也负责限制预言机地址。但这并非一个足够的安全控制措施:攻击者可以在不调用你的 API 的情况下直接向网络提交恶意指令。
协议经验法则:任何能实质性改变指令结果的输入都必须在链上加密锚定或枚举。如果无论如何依赖「可信的」链下组装路径,那么在链上仍必须证明提供的值符合该路径本应产生的结果——通过明确的白名单、程序 ID、PDA 等方式,具体取决于设计。Loopscale 缺少这种绑定;损失约为 600 万美元。
Texture
这里的漏洞更为直接——并且具有典型的 Solana 风格。某个代码路径从用户提供的账户中获取 LP 铸造的接收者——这在 CPI/账户列表机制下是预期的——但未能强制其与协议预期的保险库匹配。攻击者将 LP 发行路由到了任意地址,而不是协议控制的托管账户。
在 Solidity 中,保险库地址通常是硬编码或派生得出的,因此在开发者已经固定了规范目标的地方,最终用户无法替换为恶意接收者。在 Solana 上,账户仍然必须随交易一起传递,然而 Texture 程序忽略了强制「将 LP 铸造到我们的保险库」——这是一个价值约 200 万美元的现实提醒:账户验证应出现在每个强制性的开发者/审计检查清单上。
协议经验法则:每当某个指令铸造、转移、托管或贷记价值时,目标账户是安全不变量的一部分。仅仅账户具有正确的 mint 或 Token-program 形状是不够的;程序必须证明目标地址是预期的保险库、托管账户、仓位、策略或用户账户。对于协议控制的流程,这通常意味着将目标地址绑定到存储配置、PDA 种子、保险库权限、mint 和市场/策略状态——而不仅仅是接受交易提供的任何 Token 账户。
GoonFi
GoonFi 是另一种类型的事故。它看起来不像「经典的」由攻击者驱动的利用——即获胜交易本身夹带了假账户,而程序盲目地信任它。受影响的 WSOL/USDC 市场根据一个与 GoonFi 关联的外部数据馈送进行定价,该馈送提供了报价的两端——类买价和类卖价值。
公开来源没有解释该馈送的链下输入来自哪里。然而,在链上,序列足够清晰:该馈送首先发布了大约 82.7 的正常 SOL/USDC 值,然后突然写入了一个约 2.658 的报价,之后进一步退化到接近零和零。这不是市场波动;这是一种病态的馈送状态。搜索者看到了机会,从 GoonFi 以远低于市场普遍价格的价格购买 WSOL,然后在其他地方平仓。
目前的链上证据并未显示这些搜索者导致了错误价格。主要的受益者看起来更像是针对公开可见的错误定价做出反应的套利 / MEV 账户,而不是馈送破坏的源头。这种区别很重要:事故仍然抽干了流动性池,但故障模式似乎是 AMM 接受了不良的可信定价,而不仅仅是恶意的调用数据。
协议经验法则:如果一个活跃市场可能因为一个价格馈送更新而波动,那么发布者路径和消费程序都需要健全性检查。在同一秒内从约 82.7 跳变到约 2.658——超过 30 倍——本应触发断路器,活跃市场应拒绝接近零或零的报价,除非这些报价具有非常明确且安全的含义。
Solana 程序中最常见的问题是什么?
公开的事故只是冰山一角。许多关键漏洞在审计期间就被发现,从未上线生产环境。查看公共审计报告中最常出现的问题,可以更全面地了解 Solana 程序出错的地方。
Solana 程序中的漏洞可以是 Solana 特定的,也可以是通用的。通用问题是审计人员在其他链(包括 EVM 生态系统)上也会遇到的错误类型。这里我们重点关注 Solana 特定的部分。
一个 Solana 特定的漏洞几乎从不始于一行错误的算术代码。它通常始于对账户图的错误建模。程序收到一组账户:有些签署交易,有些是可写的,有些是 PDA,有些属于 SPL Token 或 Token-2022,有些被转发到 CPI。如果代码只验证了账户的本地形状,而没有验证其在账户图中的角色、权限、mint、所有者、种子、程序 ID、生命周期状态或其在 remaining_accounts 中的动态位置,用户通常可以提供一组「几乎正确」的账户。
这就是为什么导致严重或高危发现的前几类 Solana 特定问题看起来都像是同一主题的不同形式:程序对账户的信任超出了应有的限度。
Token、Mint、保险库和 Token-2022 语义
此类包括协议对 SPL Token、Token-2022、mint/保险库关系、ATA(关联 Token 账户)、wSOL/原生 SOL、转账费用、委托、冻结权限或净额与总额核算的错误建模。
一个典型的现实形态是:再平衡或保险库流程检查了 mint,但没有检查 Token 账户所有者、权限、保险库绑定或 Token 程序。本地看起来账户正确,但价值却转移到了协议中的错误角色。
典型形式:
- 指令更新了内部会计记录,但没有执行实际的 Token 转账。
- 保险库、mint 或抵押品 ID 未绑定到预期的策略或市场。
- wSOL/SOL 转换产生了 DoS、租金或账户关闭的边缘情况。
- 标准的 Token-2022 转账费用或协议级宿主费用使得总额与实际收到的净额不同。
- 不正确的 mint 验证允许创建使用了错误 Token 的池、交易对或仓位。
- 费用所有者、保险库权限或 Token 账户所有者在错误的位置被检查,或未完全检查。
如何检查:
- 为每条指令构建一个 Token 矩阵:mint、Token 账户、保险库、权限、Token 程序、小数位数、扩展策略和预期所有者。
- 不仅要验证
token_account.mint,还要验证关系:保险库 -> mint -> 策略/市场/配置。 - 对于 Token-2022,明确列出支持的和禁止的扩展。
- 测试总额与净额转账行为:协议费、宿主费、转账费、预扣金额和扩展特定行为。
- 分别测试 wSOL/原生 SOL 路径:创建 ATA、同步原生、关闭账户和租金接收者。
- 对于每次转账,将前后余额与内部会计记录进行比较。
- 对于每个费用账户,验证 mint、Token 程序、所有者、权限以及与预期策略、市场或配置的绑定。
账户图完整性
此类涉及账户之间关系不正确。一个账户可能具有正确的所有者、判别器和布局,但仍然可能是错误的对象:他人的仓位、错误的市场、另一个配置、不相关的 tick 数组、假的用户状态或错误的债务。
典型形式:
- 源和目标仓位可以是同一个账户。
- 账户属于正确的程序,但未关联到所需的 mint、配置或市场。
- 用户状态看起来有效,但不属于该用户或策略。
- 关闭/最终确定指令接受了一个形状正确但来自错误的节点共识网络(NCN)、纪元或分支的账户。
- 重复的账户让一个逻辑对象满足了两不应相同的角色。
如何检查:
- 为每条指令构建一个账户矩阵:角色、所有者、判别器、种子、签名者/可写标志和关系检查。
- 验证所有隐含关系:
position.market == market.key(),vault.mint == config.mint,user_state.owner == signer。 - 添加同形替换的负面测试:另一个市场、另一个用户、另一个保险库、另一个配置。
- 当操作不能是自身操作时,检查源 != 目标。
- 在 Anchor 中,不要将
Account<T>视为足够:has_one、constraint、seeds和手动关系检查必须覆盖业务图。
外壳:格式错误的数据、Panic 和运行时外壳
外壳类是业务逻辑在正常输入时可能正确,但当程序收到格式错误或意外的账户数据时则可能崩溃。在 DeFi 程序审计中,这通常意味着未检查的反序列化、缺少长度检查、错误的判别器、过时的零拷贝布局假设、未检查的索引、不安全的切片,或通过无需权限的指令可到达的 panic 路径。
典型形式:
unwrap()或不可靠的假设导致 panic。- 解析器期望某个长度或格式,但在访问前未验证。
- 接受了错误大小、版本或尾部字节的零拷贝或手动反序列化的账户。
- DoS 并非来自经济攻击,而是来自任何用户都能触发的异常执行路径。
- 代码在快乐路径上稳定,但缺乏针对恶意字节的负面测试语料库。
如何检查:
- 搜索
unwrap、expect、未检查的索引、切片、unsafe和手动反序列化。 - 在每次读取账户数据前,验证长度、判别器、所有者、版本和尾部字节策略。
- 添加格式错误的语料库测试:短数据、长数据、错误的判别器、随机字节、重复段落、无效的 TLV。
- 对于解析器/运行时代码,使用模糊测试和差异测试。
- 单独检查 panic 是否会成为无需权限指令的 DoS。
PDA 命名空间、种子和初始化竞争
此类包括将确定性地址误认为是正确逻辑对象的证明的错误。PDA 种子定义了命名空间,invoke_signed 可以将 PDA 转换为程序权限,生命周期规则决定了同一地址是否可以安全地初始化、关闭或重用。如果种子不完整,bump 非规范,PDA 未绑定到父实体,或者可以创建多个全局配置账户,则协议可能最终出现第二个「有效」对象。
典型形式:
- 允许多个全局配置账户。
- 缺少 PDA 验证导致重复或未经授权的转账。
- 派生地址冲突或命名空间过于宽泛。
- PDA 可以被错误的参与者关闭或重新初始化。
- 种子/权限不匹配使得仓位无法管理或破坏提款。
如何检查:
- 构建 PDA 注册表:实体、种子、bump 来源、初始化者、关闭权限和签名者使用情况。
- 确保种子包含所有必需的域分隔符:协议、市场、mint、用户、配置、链 ID、nonce。
- 为每个 PDA 添加针对为另一个用户、市场或配置派生的 PDA 的负面测试。
- 检查规范 bump 使用情况,以及如果在以后重用 bump 时的存储 bump 处理。
- 测试预初始化、重复初始化、关闭/重建和
init_if_needed路径。
CPI 信任边界
CPI 类涵盖了程序间边界上的错误。Solana 的 CPI 不仅传递调用,还传递一组 AccountInfo 结构、可写/签名者、程序账户,有时还有签名者种子。如果调用者在没有白名单和后检查的情况下信任目标程序或转发的账户,外部程序语义将成为攻击面的一部分。
一个典型的集成失败是:定价、桥接或路由路径接受由调用者提供的、具有相同接口的外部程序或市场账户。调用成功了,但可信的事实却来自不可信的程序。
典型形式:
- 攻击者可以通过 CPI 替换市场、永续合约或 Token 程序账户并窃取资产。
- 保险库 Token 被耗尽,因为 CPI 权限/账户集设置错误。
- 部分执行或外部调用使系统处于中间状态。
- 跨 CPI 边界发生全链应用(OAPP)或执行者账户重分配。
- 签名者种子或可写账户被转发得比外部调用所需的范围更广。
如何检查:
- 构建 CPI 映射:目标程序、转发的账户、签名者种子、可写账户和预期的后置条件。
- 验证 Token 程序、DEX、预言机、桥接和外部协议的允许列表中的程序 ID。
- 如果外部程序可能已修改账户,在 CPI 之后重新加载/重新读取账户。
- 最小化转发的可写账户和签名者。
- 测试恶意 CPI 目标和同形账户替换。
租金、重新分配、关闭和残留状态
此类涵盖了 Solana 在 Lamports 和账户存储层面的生命周期行为:租金支付者、关闭接收者、重新分配大小、过时字节、强制 ATA 创建、关闭/重建。这些错误通常看起来像是微小的生命周期细节,但在无需权限的流程中可能演变成租金提取、DoS 或状态混淆。
典型形式:
- 强制 ATA 创建允许租金提取。
- 关闭指令未验证账户是否属于所需上下文。
- wSOL 关闭/同步路径破坏了订单流。
- 打包数据的长度或重新分配逻辑允许错误的大小。
- 自定义程序错误阻塞清理/关闭流程。
如何检查:
- 对于每个关闭路径,说明谁可以关闭,Lamports 流向何处,以及哪些引用被失效。
- 测试关闭后重建和用相同地址重建的流程。
- 验证重新分配行为:新大小、租金补充/退款以及将过时字节归零。
- 对于 ATA 创建,检查谁支付租金,以及攻击者是否可以强制他人支付。
- 对于 wSOL/原生 SOL,验证同步/关闭/租金接收者策略。
账户所有权和判别器
此类是一个基础但关键的检查:账户必须属于预期的程序并具有预期的类型/布局。在 Solana 上,这不是表面功夫。外部所有者或错误的判别器意味着程序正在读取由另一个信任域创建的字节。
典型形式:
- 预言机/范围程序账户未经验证。
- 质押池接受了未初始化或临时的质押账户。
- Serum/DEX 市场或预言机账户未对照预期的所有者/类型进行检查。
- 程序读取了其所有者/判别器与预期程序不匹配的状态。
如何检查:
- 对于每个账户,显式验证所有者、判别器/类型、数据长度和相关的可执行标志。
- 不要仅仅因为账户能反序列化就信任它。
- 对于预言机、DEX、范围或外部程序账户,验证程序 ID 和市场/数据馈送身份。
- 添加对布局正确但所有者错误的账户的测试。
- 检查尾部字节和版本控制策略。
签名者和可写标志语义
此类涵盖了关于谁实际签署了某个操作以及哪个账户被允许更改的错误。在 Solana 上,签名者标志位于账户元数据上,而权限可能是一个存储的密钥、PDA 签名者、多签、委托或治理地址。这些层级容易混淆。
典型形式:
- 缺少多签签名者检查。
- 多签签名者未与指令账户匹配。
- 执行者/OAPP 权限可以被重新分配。
- 在 Solana 特定的验证流程中绕过签名验证。
如何检查:
- 对于每次状态变更,记下预期的参与者:用户签名者、管理员签名者、PDA 签名者或多签阈值。
- 检查签名者账户是否匹配存储的权限,而不仅仅是它签署了交易。
- 对于多签,测试缺少签名者、重复签名者、顺序错误、额外签名者以及签名者数量低于阈值的情况。
- 检查可写最小化原则:不必要的可写账户可能成为攻击面。
- 对于 PDA 签名者,验证种子和程序派生权限的来源。
结论
审计 Solana 程序已成为保护 Solana 生态系统中用户资金的重要组成部分。上面讨论的事故和漏洞模式表明,在 Solana 上进行有效的安全工作需要超越通用的 DeFi 经验。该背景知识仍然很重要,但它必须与对 Solana 架构及其协议构建和运行方式的深入理解相结合。对于在 Solana 上构建的团队来说,这使得专业的审计专业知识成为一项切实的必要条件,而非锦上添花。
- 原文链接: x.com/MixBytes/status/20...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~