Rust
Pinocchio Vault

Pinocchio Vault

103 Graduates

Pinocchio Vault

保險庫

Pinocchio Vault Challenge

保險庫讓用戶可以安全地存儲資產。保險庫是去中心化金融(DeFi)中的基本構建模塊,其核心功能是讓用戶可以安全地存儲資產(在此情況下是 lamports),並且只有該用戶本人可以稍後提取。

在這個挑戰中,我們將構建一個簡單的 lamport 保險庫,展示如何處理基本賬戶、程序派生地址(PDA)和跨程序調用(CPI)。如果你不熟悉 Pinocchio,應該先閱讀Pinocchio 簡介,以熟悉我們在此程序中將使用的核心概念。

安裝

在開始之前,請確保已安裝 Rust 和 Pinocchio。然後在終端中運行:

# create workspace
cargo new blueshift_vault --lib --edition 2021
cd blueshift_vault

添加 Pinocchio:

cargo add pinocchio pinocchio-system

Cargo.toml 中聲明 crate 類型,以在 target/deploy 中生成部署工件:

toml
[lib]
crate-type = ["lib", "cdylib"]

模板

讓我們從基本程序結構開始。我們將在 lib.rs 中實現所有內容,因為這是一個簡單的程序。以下是包含核心組件的初始模板:

rust
#![no_std]

use pinocchio::{account_info::AccountInfo, entrypoint, nostd_panic_handler, program_error::ProgramError, pubkey::Pubkey, ProgramResult};

entrypoint!(process_instruction);
nostd_panic_handler!();

pub mod instructions;
pub use instructions::*;

// 22222222222222222222222222222222222222222222
pub const ID: Pubkey = [
    0x0f, 0x1e, 0x6b, 0x14, 0x21, 0xc0, 0x4a, 0x07,
    0x04, 0x31, 0x26, 0x5c, 0x19, 0xc5, 0xbb, 0xee,
    0x19, 0x92, 0xba, 0xe8, 0xaf, 0xd1, 0xcd, 0x07,
    0x8e, 0xf8, 0xaf, 0x70, 0x47, 0xdc, 0x11, 0xf7,
];

fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    match instruction_data.split_first() {
        Some((Deposit::DISCRIMINATOR, data)) => Deposit::try_from((data, accounts))?.process(),
        Some((Withdraw::DISCRIMINATOR, _)) => Withdraw::try_from(accounts)?.process(),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

存款

存款指令執行以下步驟:

  1. 驗證保險庫為空(lamports 為零),以防止重複存款

  2. 確保存款金額超過基本賬戶的免租金最低限額

  3. 使用對系統程序的 CPI,將 lamports 從擁有者轉移到保險庫

首先,讓我們定義存款的賬戶結構:

rust
pub struct DepositAccounts<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
}

impl<'a> TryFrom<&'a [AccountInfo]> for DepositAccounts<'a> {
    type Error = ProgramError;

    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let [owner, vault, _] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };

        // Accounts Checks
        if !owner.is_signer() {
            return Err(ProgramError::InvalidAccountOwner);
        }

        if !vault.is_owned_by(&pinocchio_system::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }

        if vault.lamports().ne(&0) {
            return Err(ProgramError::InvalidAccountData);
        }

        let (vault_key, _) = find_program_address(&[b"vault", owner.key()], &crate::ID);
        if vault.key().ne(&vault_key) {
            return Err(ProgramError::InvalidAccountOwner);
        }

        // Return the accounts
        Ok(Self { owner, vault })
    }
}

讓我們分解每個賬戶檢查:

  1. owner:必須是簽名者,因為他們需要授權交易

  2. vault

    • 必須由系統程序擁有

    • 必須為零 lamports(確保是“新”存款)

    • 必須從正確的種子派生

    • 必須匹配預期的 PDA 地址

現在讓我們實作指令數據結構:

rust
pub struct DepositInstructionData {
    pub amount: u64,
}

impl<'a> TryFrom<&'a [u8]> for DepositInstructionData {
    type Error = ProgramError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        if data.len() != size_of::<u64>() {
            return Err(ProgramError::InvalidInstructionData);
        }

        let amount = u64::from_le_bytes(data.try_into().unwrap());

        // Instruction Checks
        if amount.eq(&0) {
            return Err(ProgramError::InvalidInstructionData);
        }

        Ok(Self { amount })
    }
}

在這裡,我們只需檢查金額是否不為零。

最後,讓我們實作存款指令:

rust
pub struct Deposit<'a> {
    pub accounts: DepositAccounts<'a>,
    pub instruction_data: DepositInstructionData,
}

impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Deposit<'a> {
    type Error = ProgramError;

    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = DepositAccounts::try_from(accounts)?;
        let instruction_data = DepositInstructionData::try_from(data)?;

        Ok(Self {
            accounts,
            instruction_data,
        })
    }
}

impl<'a> Deposit<'a> {
    pub const DISCRIMINATOR: &'a u8 = &0;

    pub fn process(&mut self) -> ProgramResult {
        Transfer {
            from: self.accounts.owner,
            to: self.accounts.vault,
            lamports: self.instruction_data.amount,
        }
        .invoke()?;

        Ok(())
    }
}

提取

提取指令執行以下步驟:

  1. 驗證保管庫內是否有 lamports(是否非空)

  2. 使用保管庫的 PDA 以其自身名義簽署轉賬

  3. 將保管庫內的所有 lamports 轉回給擁有者

首先,讓我們定義提取的帳戶結構:

rust
pub struct WithdrawAccounts<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
    pub bumps: [u8; 1],
}

impl<'a> TryFrom<&'a [AccountInfo]> for WithdrawAccounts<'a> {
    type Error = ProgramError;

    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let [owner, vault, _] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };

        // Basic Accounts Checks
        if !owner.is_signer() {
            return Err(ProgramError::InvalidAccountOwner);
        }

        if !vault.is_owned_by(&pinocchio_system::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }

        if vault.lamports().eq(&0) {
            return Err(ProgramError::InvalidAccountData);
        }

        let (vault_key, bump) = find_program_address(&[b"vault", owner.key().as_ref()], &crate::ID);
        if &vault_key != vault.key() {
            return Err(ProgramError::InvalidAccountOwner);
        }

        Ok(Self { owner, vault, bumps: [bump] })
    }
}

讓我們逐一解析每個帳戶檢查:

  1. owner:必須是簽署者,因為他們需要授權交易

  2. vault

    • 必須由系統程序擁有

    • 必須從正確的種子派生

    • 必須與預期的 PDA 地址匹配

  3. bumps:我們存儲了用於 PDA 簽署的 bump 種子

現在讓我們實作提取指令:

rust
pub struct Withdraw<'a> {
    pub accounts: WithdrawAccounts<'a>,
}

impl<'a> TryFrom<&'a [AccountInfo]> for Withdraw<'a> {
    type Error = ProgramError;

    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let accounts = WithdrawAccounts::try_from(accounts)?;

        Ok(Self { accounts })
    }
}

impl<'a> Withdraw<'a> {
    pub const DISCRIMINATOR: &'a u8 = &1;

    pub fn process(&mut self) -> ProgramResult {
        // Create PDA signer seeds
        let seeds = [
            Seed::from(b"vault"),
            Seed::from(self.accounts.owner.key().as_ref()),
            Seed::from(&self.accounts.bumps),
        ];
        let signers = [Signer::from(&seeds)];

        // Transfer all lamports from vault to owner
        Transfer {
            from: self.accounts.vault,
            to: self.accounts.owner,
            lamports: self.accounts.vault.lamports(),
        }
        .invoke_signed(&signers)?;

        Ok(())
    }
}

此提取的安全性由以下兩個因素保證:

  1. 保管庫的 PDA 是使用擁有者的公鑰派生的,確保只有原始存款人可以提取

  2. 我們提供給 invoke_signed 的種子驗證了 PDA 簽署轉賬的能力

結論

您現在可以根據我們的單元測試測試您的程序並領取您的 NFT!

首先,使用以下命令在終端中構建您的程序:

cargo build-sbf

這會在您的 target/deploy 資料夾中直接生成一個 .so 文件。

現在點擊 take challenge 按鈕,然後將文件拖放到那裡!

準備好參加挑戰了嗎?
Blueshift © 2025Commit: e573eab