Solana[part17]_Solana质押-编写应用级别的质押合约-解质押部分

Solana质押-编写应用级别的质押合约-解质押部分

核心代码地址提交:

https://github.com/SoniaChan33/anchor_social/commit/b6ca707c33992ef5e72d1be5e4b6edc14e385b98

1. 解质押功能核心逻辑

解质押是质押流程的反向操作,核心目标是将质押的 NFT 归还给用户,并销毁质押时获得的流动性代币。具体流程通过 nft_unstake 函数实现,步骤如下:

1.1 权限校验

函数首先验证解质押操作的合法性,确保:

  • 解质押的 NFT 与质押记录中的 NFT 匹配(stake_info.nft_mint_account == nft_mint_account.key());
  • 发起解质押的用户是原质押人(stake_info.staker == authority.key())。
    若不满足,返回 UnstakeError::NoAuthority 错误。
// 权限校验代码(源自nft_unstake.rs)
require!(
    &ctx.accounts.stake_info.nft_mint_account == &ctx.accounts.nft_mint_account.key(),
    UnstakeError::NoAuthority,
);
require!(
    &ctx.accounts.stake_info.staker == &ctx.accounts.authority.key(),
    UnstakeError::NoAuthority,
);

1.2 NFT 转回用户账户

通过 transfer 操作将暂存于合约托管账户(program_receipt_ata)的 NFT 转回用户的关联账户(nft_associated_token_account)。

  • 由于合约托管账户的权限归 stake_info(PDA 账户),需使用 stake_info 的种子(StakeInfo::SEED_PREFIX + nft_mint_account.key() + bump)生成签名,确保操作合法性。
// NFT 转移代码(源自nft_unstake.rs)
let nft_mint_account = ctx.accounts.nft_mint_account.key();
let signer_seeds: &[&[&[u8]]] = &[&[
    StakeInfo::SEED_PREFIX.as_bytes(),
    nft_mint_account.as_ref(),
    &[ctx.bumps.stake_info],
]];
transfer(
    CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        Transfer {
            from: ctx.accounts.program_receipt_ata.to_account_info(),
            to: ctx.accounts.nft_associated_token_account.to_account_info(),
            authority: ctx.accounts.stake_info.to_account_info(),
        },
    )
    .with_signer(signer_seeds),
    1, // NFT 数量固定为1
)?;

1.3 流动性代币销毁

用户解质押时,需销毁质押时获得的流动性代币(通过 burn 操作),数量通过 stake_info.salvage_value(10000) 计算(基于质押时长)。

// 代币销毁代码(源自nft_unstake.rs)
let amount = ctx.accounts.stake_info.salvage_value(10000);
burn(
    CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        Burn {
            mint: ctx.accounts.token_mint_account.to_account_info(),
            from: ctx.accounts.associated_token_account.to_account_info(),
            authority: ctx.accounts.authority.to_account_info(),
        },
    ),
    amount,
)?;

2. 账户结构说明(NFTUnStake

NFTUnStake 结构体定义了解质押所需的所有账户,各账户作用如下:

账户名 作用描述
stake_info 存储质押记录(质押人、NFT 地址、质押时间),以 StakeInfo::SEED_PREFIX + NFT Mint 地址 为种子的 PDA 账户。
program_receipt_ata 合约托管 NFT 的关联账户,权限归 stake_info,用于暂存质押的 NFT。
token_mint_account 流动性代币的 Mint 账户,以 MyToken::SEED_PREFIX 为种子的 PDA 账户,用于销毁代币。
associated_token_account 用户持有的流动性代币账户,用于销毁质押时获得的代币。
nft_mint_account 被质押 NFT 的 Mint 账户,标识唯一 NFT。
nft_associated_token_account 用户接收 NFT 的关联账户,解质押后 NFT 转回此处。
authority 解质押发起者(原质押人),需签名授权操作。
系统程序(token_program 等) 提供代币转移、销毁等基础功能的系统程序。

3. 利息计算逻辑(salvage_value 方法)

解质押时销毁的代币数量通过 StakeInfo::salvage_value 计算,逻辑为:基于质押时长(当前 Epoch 与质押时 Epoch 的差值),按每 Epoch 2% 的比例计算(原代码实现)。

// 利息计算代码(源自stake.rs)
pub fn salvage_value(&self, amount: u64) -> u64 {
    let now = Clock::get().unwrap().epoch;
    // 每一个epoch减2%(原代码逻辑)
    let p = max(0, (now - self.staked_at) * 2) as f64 / 100.0;
    (amount as f64 * p) as u64
}

4. 关键注意事项

4.1 利息计算的潜在问题

  • 浮点数精度风险:原代码使用 f64 计算比例(如 (now - staked_at) * 2 as f64 / 100.0),可能导致精度丢失(例如 10000 * 0.02 理论为 200,但浮点数运算可能产生偏差)。
  • Epoch 差值为 0 的问题:若测试网络中 Epoch 长期未更新(默认配置 slots-per-epoch 较大),now - self.staked_at 为 0,导致 salvage_value 返回 0,用户无实际获利。

4.2 测试网络配置建议

为解决 Epoch 不更新的问题,启动本地测试网时需手动设置较小的 slots-per-epoch,确保 Epoch 能快速推进:

# 每32个slot为1个Epoch(加速Epoch更新,便于测试利息计算)
solana-test-validator --slots-per-epoch 32 -r \
  --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s metadata.so

可通过以下命令验证 Epoch 状态:

solana epoch-info --url localhost # 查看当前Epoch和slot数

4.3 权限与安全性

  • 解质押必须验证 stake_info 中的质押人与发起者一致,防止非所有者操作。
  • NFT 转移时需使用 stake_info 的 PDA 签名(signer_seeds),确保只有合约有权限操作托管的 NFT,避免权限泄露。

总结

解质押功能通过权限校验、NFT 转回、流动性代币销毁完成闭环,核心依赖 stake_info 记录的质押关系和 salvage_value 计算的销毁数量。需特别注意浮点数计算精度和测试网 Epoch 配置,以确保用户获利逻辑可正常验证。