Skip to main content

🎰 NFT质押工作机制详解

🎉 欢迎来到质押的世界!

哇哦!你已经来到第三周的终点线了!🏁 是时候把你这些天学到的超能力运用到真正的项目中了 —— 让我们为你的 buildoors NFT项目打造一个超酷的质押系统

🎯 今天的任务: 构建一个完整的NFT质押程序(除了代币功能,那是下周的大餐!)


🎮 游戏规则

🎯 你的使命(如果你选择接受的话)

创建一个能够追踪每个用户质押状态的智能程序。想象它就像一个超级账本,记录着:

  • 🎫 谁质押了什么NFT
  • ⏰ 什么时候质押的
  • 💰 应该获得多少奖励

💡 小贴士: 现在我们先不碰真正的代币转移,那部分下周再说。这周我们专注于状态管理


🏗️ 架构设计 - 四大金刚指令

1️⃣ InitializeStakeAccount(初始化质押账户)🏠

/// 🏗️ 创建用户的质押小金库
///
/// 这个指令会创建一个特殊的账户,就像给每个用户
/// 建造一个专属的保险箱,用来存放他们的质押信息
pub fn initialize_stake_account(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
msg!("🎉 Creating a brand new stake account!");

// 🔑 PDA种子配方:
// - 用户的公钥(谁的保险箱)
// - NFT代币账户(放什么宝贝)
// 这样每个用户+NFT组合都有独一无二的地址!

// TODO: 实现PDA推导逻辑
// let seeds = &[
// b"stake",
// user_pubkey.as_ref(),
// nft_token_account.as_ref(),
// ];

Ok(())
}

🎯 核心概念: PDA = 用户公钥 + NFT代币账户 = 独一无二的质押地址!


2️⃣ Stake(质押)⚡

/// ⚡ 质押你的NFT - 让它开始赚钱!
///
/// 暂时只更新状态,不实际转移NFT
/// 就像在账本上记录:"小明的猴子NFT从现在开始工作了!"
pub fn stake(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
msg!("🎮 Staking NFT - Let the earning begin!");

// 📝 需要记录的信息:
// 1. is_staked = true (标记为已质押)
// 2. stake_start_time = Clock::get()?.unix_timestamp (记录开始时间)
// 3. last_redeem = stake_start_time (初始化最后领取时间)

// 💡 Pro Tip: 使用 Clock 获取链上时间
let clock = Clock::get()?;
let current_timestamp = clock.unix_timestamp;
msg!("⏰ Staking at timestamp: {}", current_timestamp);

// TODO: 更新状态账户
// account_data.is_staked = true;
// account_data.stake_start_time = current_timestamp;

msg!("✅ NFT successfully staked! Time to earn!");
Ok(())
}

🚀 技巧: 记录时间戳很重要!它决定了用户能获得多少奖励。


3️⃣ Redeem(兑换奖励)💰

/// 💰 领取你的奖励 - 收获的时刻!
///
/// 根据质押时长计算奖励
/// 公式:奖励 = 质押时长 × 奖励率
pub fn redeem(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
msg!("💎 Time to collect your rewards!");

// 🧮 奖励计算公式(简化版)
// let time_elapsed = current_time - last_redeem_time;
// let rewards = time_elapsed * REWARD_RATE; // 比如每秒1个代币

// 📊 示例计算:
// 如果用户质押了1小时(3600秒)
// 奖励 = 3600 * 1 = 3600个代币!

let clock = Clock::get()?;
let current_timestamp = clock.unix_timestamp;

// TODO: 计算奖励
// let time_staked = current_timestamp - account_data.last_redeem;
// let rewards = time_staked as u64 * TOKENS_PER_SECOND;

msg!("🎉 You earned {} tokens!", 0); // 暂时输出0

// 🔄 更新最后领取时间
// account_data.last_redeem = current_timestamp;

Ok(())
}

💡 数学小课堂:

  • 质押10分钟 = 600秒
  • 每秒1个代币 = 600个代币奖励!
  • 记得更新 last_redeem 时间!

4️⃣ Unstake(解除质押)🔓

/// 🔓 解除质押 - 拿回你的NFT!
///
/// 结算所有未领取的奖励并解除质押状态
pub fn unstake(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
msg!("🔄 Unstaking NFT - Thanks for staking!");

// 📋 解除质押检查清单:
// ✅ 计算最终奖励
// ✅ 标记为未质押状态
// ✅ 清空质押时间记录
// ✅ (未来)返还NFT给用户

// 🎁 先结算未领取的奖励
// let final_rewards = calculate_pending_rewards();
// msg!("💰 Final rewards: {}", final_rewards);

// 🔄 重置状态
// account_data.is_staked = false;
// account_data.stake_start_time = 0;
// account_data.last_redeem = 0;

msg!("👋 NFT unstaked successfully! Come back soon!");
Ok(())
}

📦 数据结构设计

🗂️ 质押状态账户结构

/// 📊 质押账户状态 - 记录所有重要信息
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct StakeAccount {
/// 👤 质押者地址
pub owner: Pubkey,

/// 🖼️ NFT铸币地址
pub mint: Pubkey,

/// 🎯 是否已质押
pub is_staked: bool,

/// ⏱️ 质押开始时间(Unix时间戳)
pub stake_start_time: i64,

/// 🕐 上次领取奖励时间
pub last_redeem: i64,

/// 💰 累计未领取奖励(暂存)
pub pending_rewards: u64,

/// 🔢 版本号(方便未来升级)
pub version: u8,
}

impl StakeAccount {
/// 📏 账户大小常量
pub const SIZE: usize = 32 + 32 + 1 + 8 + 8 + 8 + 1;

/// 🎯 检查是否可以领取奖励
pub fn can_redeem(&self, current_time: i64) -> bool {
self.is_staked && current_time > self.last_redeem
}

/// 🧮 计算待领取奖励
pub fn calculate_rewards(&self, current_time: i64, rate: u64) -> u64 {
if !self.is_staked {
return 0;
}
let time_elapsed = (current_time - self.last_redeem) as u64;
time_elapsed * rate
}
}

🛠️ 实用工具箱

⏰ 时间管理神器

use solana_program::clock::Clock;
use solana_program::sysvar::Sysvar;

/// 🕐 获取当前链上时间
pub fn get_current_timestamp() -> Result<i64, ProgramError> {
let clock = Clock::get()?;
Ok(clock.unix_timestamp)
}

/// ⏱️ 计算时间差(秒)
pub fn calculate_time_elapsed(start: i64, end: i64) -> u64 {
(end - start).abs() as u64
}

🎯 PDA生成助手

/// 🔑 生成质押账户PDA
pub fn get_stake_account_pda(
user: &Pubkey,
nft_token_account: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
b"stake",
user.as_ref(),
nft_token_account.as_ref(),
],
program_id,
)
}

💡 专业提示与技巧

🎓 新手常见陷阱

  1. ⚠️ 时间单位混淆

    // ❌ 错误:混淆毫秒和秒
    let rewards = time_ms * TOKENS_PER_SECOND;

    // ✅ 正确:统一使用秒
    let rewards = time_seconds * TOKENS_PER_SECOND;
  2. ⚠️ 忘记更新last_redeem

    // ❌ 错误:忘记更新时间戳
    calculate_rewards();
    // account_data.last_redeem 没更新!

    // ✅ 正确:记得更新
    let rewards = calculate_rewards();
    account_data.last_redeem = current_time;
  3. ⚠️ PDA种子顺序

    // ❌ 错误:种子顺序不一致
    // 初始化用: ["stake", user, nft]
    // 查找用: ["stake", nft, user] // 顺序不对!

    // ✅ 正确:保持一致
    let seeds = &[b"stake", user.as_ref(), nft.as_ref()];

🎮 测试你的程序

🧪 单元测试示例

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_reward_calculation() {
// 🧪 测试奖励计算
let stake_account = StakeAccount {
is_staked: true,
last_redeem: 1000,
// ... 其他字段
};

// 质押10秒,每秒1个代币
let rewards = stake_account.calculate_rewards(1010, 1);
assert_eq!(rewards, 10);

println!("✅ 奖励计算测试通过!");
}
}

🚀 下一步行动

📋 检查清单

  • 🏗️ 实现四个核心指令
  • 📊 设计状态账户结构
  • ⏰ 集成Clock系统变量
  • 🧮 实现奖励计算逻辑
  • 🧪 编写基础测试
  • 🎨 (可选)添加更多功能

🎯 挑战升级

想要更大的挑战?试试这些:

  1. 🎲 随机奖励倍数 - 让质押更刺激!
  2. 📈 递增奖励率 - 质押越久,奖励越高!
  3. 🏆 排行榜系统 - 看谁是质押王!
  4. ⏰ 锁定期机制 - 设置最短质押时间

📚 学习资源

🔗 有用的链接

💬 遇到困难?

🤝 记住: 挣扎是学习的一部分!先自己尝试,实在不行再看答案。每个错误都是成长的机会!


🎊 总结

你现在已经掌握了NFT质押的核心概念!虽然我们还没有处理真正的代币转移,但你已经构建了整个质押系统的骨架

下周,我们将添加真正的代币功能,让这个系统活起来

🌟 座右铭: "质押不仅是锁定资产,更是创造价值!"

准备好了吗?让我们开始编码吧!🚀


Happy Staking! 🎰✨