文章分析了Solana上MEV(矿工可提取价值)的现状与挑战,尽管Solana在架构上对传统MEV攻击有抵抗性,但交易机器人、RPC提供商和验证者等参与者仍通过各种技术手段提取MEV。文章还讨论了开发者可以采取的防御措施,包括防夹三明治攻击、初始化抢跑、逃避惩罚和拒绝服务攻击等,并强调了在Solana上构建应用时进行安全防护的重要性。
在Solana上进行抢跑交易比在Ethereum上难度大得多。原因如下:
Jito 曾经为 MEV 搜索者提供待处理交易的可见性,但在社区强烈反对其对生态系统的负面影响后停止了该服务。一些私有池仍然存在(与 Jito 无关),但访问权限有限。
尽管 Solana 具有上述架构防御,但MEV仍然通过多种策略存在。要理解这一点,我们需要区分参与者(谁提取MEV)和技术(他们如何最大化它)
有几个实体可以提取MEV,我们可以根据它们对交易可见性的控制程度来区分它们。
这些机器人允许用户通过消息应用程序直接交易波动性或新上市的资产。它们处理钱包创建并管理私钥,为用户提供便利和自动化。
但是,由于机器人控制交易构建和签名,它可以夹击或抢跑用户的交易以获取自身利益。并非所有机器人都这样做,但该设计允许这样做,并且不太有信誉的克隆可以并且经常利用这一点。
RPC运营商在用户交易进入区块链之前看到它们。这为他们提供了一个信息优势,允许他们抢跑交易。
这是一个假设但技术上可行的威胁。如果RPC提供商也是一个验证器,那么他们甚至可以更好地利用这种可见性来利用 MEV 机会。
拥有足够大的 stake 的验证器可以成为区块领导者。这是一种资本密集型方法,但由于在他们的 slot 中可以完全查看所有传入交易,因此验证器理论上可以重新排序或审查交易以捕获 MEV。
虽然 Solana 的连续区块构建有助于限制这种行为,但验证器级别的自由裁量权仍然留下了一些操纵空间,特别是如果验证器绕过预期的排序规则。
这些是用于提取 MEV 的一些策略,可供上述一个或多个参与者使用。强大的 MEV 玩家通常会将其中几种组合起来以获得最大优势。
一种基于垃圾邮件的技术,搜索者发送许多只有在满足某些有利可图的条件时才会成功的交易。即使 90%+ 的交易失败,少数成功的交易也可以证明成本是合理的,尤其是在 Solana 上,失败的交易很便宜。
这会产生大量的网络垃圾邮件,但不需要特权访问。任何参与者,包括机器人、验证器或 RPC,都可以使用此方法。
通过与验证器位于同一数据中心,搜索者可以减少延迟并提高其比竞争对手更快地获得有利可图交易的机会。
但是,基于可见性的参与者(如 RPC 提供商或验证器)也可以将他们的基础设施同地部署,将信息优势与执行速度相结合。
位于 CEX 基础设施附近的验证器可以监控链下价格并进行利用套利机会的交易。这种物理上的接近减少了延迟并增加了 MEV 捕获的潜力。
同样,这通常由验证器执行,但由验证器基础设施支持的 RPC 提供商或机器人也可以从此设置中受益。
虽然为了清晰起见,我们已经分开了参与者和技术,但在实践中它们经常重叠。例如,验证器也可以运行 RPC 并在 CEX 和搜索器附近同地部署;交易机器人可以将乐观垃圾邮件与链前可见性相结合;RPC 运营商也可以批量处理搜索者捆绑包以利用时序;等等。
简而言之:基于可见性的参与者可以并且确实使用时序和基于垃圾邮件的技巧。他们控制的基础设施越多,他们可以组合的工具就越多。
Jito 现在已停止,它通过使所有搜索者更容易访问内存池来帮助对抗 MEV 中心化。这减少了垃圾邮件并使乐观策略变得不那么必要。搜索者可以构建有效的、有利可图的捆绑包,而不是依赖随机性,从而降低了进入门槛并更公平地分配了 MEV。
保护交换免受三明治攻击的两种广泛部署的解决方案是滑点保护和有效性时间戳。
以下是一个同时实现滑点和时间戳保护的交换示例:
pub fn swap_tokens_protected(
ctx: Context<SwapTokens>,
amount_in: u64,
min_amount_out: u64, // 滑点保护
max_timestamp: i64 // 时间戳保护
) -> Result<()> {
let current_time = Clock::get()?.unix_timestamp;
// reject if transaction is too old
require!(
current_time <= max_timestamp,
ErrorCode::TransactionExpired
);
let pool = &mut ctx.accounts.pool;
let amount_out = perform_swap(pool, amount_in)?;
// ensure output meets minimum expectation
require!(
amount_out >= min_amount_out,
ErrorCode::SlippageExceeded
);
Ok(())
}
另一种来自抢跑的威胁是账户初始化。
假设你正在定义一个带有协议参数和管理员控制访问字段的配置账户,该账户随后将被程序的其他部分使用和访问。如果攻击者能够在你之前部署该账户并将他控制的地址设置为管理员,他们实际上会劫持它,迫使你重新部署你的程序。
一个简单有效的保护措施是要求初始化期间签名者的公钥与程序的 upgrade_authority 匹配,upgrade_authority 在运行时自动设置为部署者的公钥。此约束是合乎逻辑的,因为初始化通常是部署后的第一步。
use anchor_lang::prelude::*;
use crate::program::MyProgram;
declare_id!("Cum9tTyj5HwcEiAmhgaS7Bbj4UczCwsucrCkxRECzM4e");
#[program]
pub mod my_program {
use super::*;
pub fn set_initial_admin(
ctx: Context<SetInitialAdmin>,
admin_key: Pubkey
) -> Result<()> {
ctx.accounts.admin_settings.admin_key = admin_key;
Ok(())
}
pub fn set_admin(...){...}
pub fn set_settings(...){...}
}
#[account]
#[derive(Default, Debug)]
pub struct AdminSettings {
admin_key: Pubkey
}
#[derive(Accounts)]
pub struct SetInitialAdmin<'info> {
#[account(init, payer = authority, seeds = [b"admin"], bump)]
pub admin_settings: Account<'info, AdminSettings>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(constraint = program.programdata_address()? == Some(program_data.key()))]
pub program: Program<'info, MyProgram>,
#[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
pub program_data: Account<'info, ProgramData>,
pub system_program: Program<'info, System>,
}
一旦建立了管理员密钥,就可以创建其他配置账户(例如,暂停者角色),每个账户都引用受信任的管理员权限。
这是一种用户需要受到惩罚的情况,无论是出于不良行为还是作为收益策略的一部分,但可以通过抢跑应用制裁的交易来逃避损失。
例如,一个 vault,用户在其中存入分配到产生收益的策略中的资产。虽然这些策略在大多数时候都会产生利润,但有时可能会发生损失,这可能会暂时降低 vault 的盈利能力。恶意用户可以观察链状态并在损失发生之前立即提取其资产,然后在之后重新存入其资产。通过这样做,与其他人相比,用户能够增加其在 vault 中的份额,从而从情况中获利。反之亦然:用户可能会发现即将到来的收益飙升,并通过快速存款和取款来夹击该机会以获得即时收益。
另一个例子涉及治理协议,用户可以在其中存入资产以获得投票权,但如果被证明有恶意行为,可能会受到惩罚/削减。如果没有保护,用户可以简单地通过完全提取其 stake 的资产来抢跑削减交易,从而允许他简单地逃避惩罚。
在 vault 示例中,设置微调的存款/取款费用通常足以使大多数这种情况无利可图,因为它要求用户在池中停留最短的时间才能看到产生的收益退还的费用。
在治理案例中,这还不够。这里更好的方法是强制执行操作之间的最短延迟。例如,要求存款和取款之间有 7 天的延迟:虽然这很容易开发,但这种限制可能对用户不友好(并且存款/取款费用具有类似的效果)。
更好的解决方案是实施取款队列,资产在其中停留一段定义的时间(可以是几个小时),并且在退出队列并可供用户使用之前仍然可以削减。
抢跑也可用作 DoS 协议中某些功能的工具。
这实际上是 Solana 中一个众所周知的漏洞,即作为指令的一部分,必须初始化一个账户。
在 Anchor 中,在定义账户时,可以设置一个名为 init 的约束,该约束告诉引擎将初始化一个新的数据账户:
#[account(\
init, // 如果账户已存在将失败\
payer = payer,\
space = 8 + 32\
)]
pub data_account: Account<'info, DataAccount>,
但这里有一个陷阱:如果账户已经初始化,指令将恢复,导致对调用的 DoS。
为防止这种情况发生,可以使用 init_if_needed 约束,它仅在账户尚不存在时才执行初始化。
DoS 发生的另一种方式是,如果交易需要持有特定值才能成功执行。攻击者可以抢跑合法的调用来(暂时或不暂时)更新该值以强制调用恢复。例如,可以在 vault 账户之外为获胜者提供 SOL 奖励,并且程序要求指令提供账户持有的确切值。
攻击者可以通过向 vault 账户发送 1 lamport 来抢跑获胜者,从而使获胜者的请求错误:
pub fn claim_reward(ctx: Context<ClaimReward>, expected_amount: u64) -> Result<()> {
let vault = &ctx.accounts.vault;
// 如果攻击者在此 tx 之前向 vault 发送 1 lamport,
// expected_amount 将是错误的,tx 将失败
require!(
vault.lamports() == expected_amount,
ErrorCode::IncorrectAmount
);
// 将奖励转移给获胜者
如果你正在 Solana 上构建并希望加强你的防御,模糊测试可以发现静态分析可能遗漏的隐藏边缘情况和逻辑缺陷。在 Adevar Labs,我们不仅仅是审计,我们还严格模糊测试 Solana 程序,以便在漏洞被利用之前发现它们。无论你是发布 DEX、vault 还是治理系统,我们都会在这里帮助你安全地发布。
想了解更多?请继续关注更深入的探讨、真实世界的错误和智能合约最佳实践。
- 原文链接: adevarlabs.com/blog/unpa...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!