Anchor、反序列化与内存复制

本文分析了在使用Anchor框架开发Solana程序时,当交易的to token和quote token相同时,由于Anchor的序列化机制可能导致的问题。文章通过WooFi Sherlock contest中的一个例子,说明了重复账户在序列化时可能导致数据覆盖,并提供了一种解决方案,即在处理重复账户时,只更新其中一个变量。

本文将使用 WooFi Sherlock 竞赛 中的这份报告作为底层示例。

#[derive(Accounts)]
pub struct Swap<'info> {
    // ... 所有账户定义 ...
    woopool_from: Box<Account<'info, WooPool>>,
    woopool_to: Box<Account<'info, WooPool>>,
    woopool_quote: Box<Account<'info, WooPool>>,
    // ... 更多账户 ...
}

pub fn handler(ctx: Context<Swap>, from_amount: u128,
  min_to_amount: u128) -> Result<()>
{
  // ... 逻辑

在上面的代码片段中,Swap 结构体是由 handler 函数表示的 swap 指令的输入上下文。这允许用户将 from token 兑换为 to token,其中 quote 是兑换的单位。

例如,我想将 ABC 兑换为 XYZ,但没有 ABC/XYZ 预言机。但存在两个预言机 ABC/USDC 和 XYZ/USDC。那么我可以卖出 ABC 换取 USDC,并用 USDC 购买 XYZ。最后,这类似于将 ABC 兑换为 XYZ,但多了一个步骤;USDC 在这里是 quote token。

正常流程

回到我们的 swap 指令,用户可以通过在 woopool_fromwoopool_towoopool_quote 中提供正确的账户来描述他们的 swap。如果我们保留之前的例子:

  • woopool_from = ABC WooPool
  • woopool_to = XYZ WooPool
  • woopool_quote = USDC WooPool

在 swap 操作期间,woopool_quote 接收费用,而 woopool_fromwoopool_to 会更新其 token 数量。

woopool_from.add_reserve(from_amount).unwrap();
woopool_to.sub_reserve(to_amount).unwrap();

// 将费用记录到账户中
woopool_quote.sub_reserve(swap_fee).unwrap();
woopool_quote.add_unclaimed_fee(swap_fee).unwrap();

异常流程

但是,如果 swap 路径是直接的会发生什么?假设我们的用户想要将 ABC(from token)兑换为 USDC(to token),那么 USDC 也可以作为 quote token,这意味着用户将向指令提供:

  • woopool_from = ABC WooPool
  • woopool_to = USDC WooPool
  • woopool_quote = USDC WooPool

我们可以在这里看到 woopool_towoopool_quote 与同一个账户相关。这就是事情变得有趣的地方:这些账户是如何提供给指令的?

当 Anchor 处理你的账户时,它遵循三个步骤:

  1. 反序列化(De-serialize):将原始账户数据转换为 Rust 结构体
  2. 执行(Execute):使用这些结构体作为变量来运行你的指令
  3. 序列化(Serialize):将修改后的结构体写回账户

问题在于最后一步,当数据写回账户时:

  1. 首先,woopool_to 被序列化,其数据写回到 USDC WooPool 账户。
  2. 然后,woopool_quote 被序列化,并写回到 USDC WooPool 账户,覆盖了之前的操作!

那么,这里的解决方案是什么?

解决此问题的一种方法是仅将更改应用于其中一个变量,即 woopool_towoopool_quote,以用于影响 USDC WooPool 的两个操作。

我们可以对 woopool_to 执行两项操作,而不是更新 woopool_to 上的 token 余额并将费用分配给 woopool_quote

woopool_from.add_reserve(from_amount).unwrap();
woopool_to.sub_reserve(to_amount).unwrap();

// 将费用记录到账户中
if ctx.accounts.woopool_to.key() == ctx.accounts.woopool_quote.key() {
    woopool_to.sub_reserve(swap_fee).unwrap();
    woopool_to.add_unclaimed_fee(swap_fee).unwrap();
} else {
 // ...
}

这个解决方案有效,因为:

  • 在处理重复账户时,我们只修改一个副本
  • 所有更改都发生在同一个内存结构体上
  • 当 Anchor 将其写回时,我们所有的更改都会被保留

你可以使用这个Solana Playground亲自尝试一下

说明:

  1. 构建,然后部署程序。
  2. 右键单击 “tests/ deploy.test.ts” 并选择 “Test”

总的来说,Anchor 非常强大,但它不是魔法。理解它的序列化机制对于构建安全、可预测的 Solana 程序至关重要。通过正确管理内存和整合状态更改,你可以避免那些会悄无声息地破坏你的逻辑的陷阱。

想了解更多?请继续关注更多深入的内容、真实的错误和智能合约最佳实践

X / LinkedIn 上关注 @AdevarLabs - 安全交付!

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

0 条评论

请先 登录 后评论
Adevar labs
Adevar labs
Blockchain Security Firm | Rust, Solidity, Move, and beyond. Vulnerability discovery, practical remediation, and complete audit reports | Ship Safely.