保險庫

保險庫讓用戶可以安全地存儲資產。保險庫是去中心化金融(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 中生成部署工件:
[lib]
crate-type = ["lib", "cdylib"]模板
讓我們從基本程序結構開始。我們將在 lib.rs 中實現所有內容,因為這是一個簡單的程序。以下是包含核心組件的初始模板:
#![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),
}
}存款
存款指令執行以下步驟:
驗證保險庫為空(lamports 為零),以防止重複存款
確保存款金額超過基本賬戶的免租金最低限額
使用對系統程序的 CPI,將 lamports 從擁有者轉移到保險庫
首先,讓我們定義存款的賬戶結構:
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 })
}
}讓我們分解每個賬戶檢查:
owner:必須是簽名者,因為他們需要授權交易vault:必須由系統程序擁有
必須為零 lamports(確保是“新”存款)
必須從正確的種子派生
必須匹配預期的 PDA 地址
現在讓我們實作指令數據結構:
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 })
}
}在這裡,我們只需檢查金額是否不為零。
最後,讓我們實作存款指令:
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(())
}
}提取
提取指令執行以下步驟:
驗證保管庫內是否有 lamports(是否非空)
使用保管庫的 PDA 以其自身名義簽署轉賬
將保管庫內的所有 lamports 轉回給擁有者
首先,讓我們定義提取的帳戶結構:
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] })
}
}讓我們逐一解析每個帳戶檢查:
owner:必須是簽署者,因為他們需要授權交易vault:必須由系統程序擁有
必須從正確的種子派生
必須與預期的 PDA 地址匹配
bumps:我們存儲了用於 PDA 簽署的 bump 種子
現在讓我們實作提取指令:
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(())
}
}此提取的安全性由以下兩個因素保證:
保管庫的 PDA 是使用擁有者的公鑰派生的,確保只有原始存款人可以提取
我們提供給
invoke_signed的種子驗證了 PDA 簽署轉賬的能力
結論
您現在可以根據我們的單元測試測試您的程序並領取您的 NFT!
首先,使用以下命令在終端中構建您的程序:
cargo build-sbf這會在您的 target/deploy 資料夾中直接生成一個 .so 文件。
現在點擊 take challenge 按鈕,然後將文件拖放到那裡!