Vault

Vault memungkinkan pengguna untuk menyimpan aset mereka dengan aman. Vault adalah blok bangunan fundamental dalam DeFi yang, pada intinya, memungkinkan pengguna untuk menyimpan aset mereka (lamport dalam kasus ini) dengan aman yang hanya dapat ditarik kembali oleh pengguna yang sama.
Dalam tantangan ini, kita akan membangun lamport vault sederhana yang menunjukkan cara bekerja dengan akun dasar, Program Derived Addresses (PDA), dan Cross-Program Invocation (CPI). Jika Anda belum familiar dengan Pinocchio, Anda sebaiknya mulai dengan membaca Pengantar Pinocchio untuk membiasakan diri dengan konsep inti yang akan kita gunakan dalam program ini.
Installation
Sebelum Anda memulai, pastikan Rust dan Pinocchio sudah terpasang. Kemudian di terminal Anda jalankan:
# create workspace
cargo new blueshift_vault --lib --edition 2021
cd blueshift_vaultTambahkan Pinocchio:
cargo add pinocchio pinocchio-systemDeklarasikan tipe crate di Cargo.toml untuk menghasilkan artefak deployment di target/deploy:
[lib]
crate-type = ["lib", "cdylib"]Template
Mari mulai dengan struktur program dasar. Kita akan mengimplementasikan semuanya di lib.rs karena ini adalah program yang sederhana. Berikut adalah template awal dengan komponen inti yang kita butuhkan:
#![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),
}
}Deposit
Instruksi deposit melakukan langkah-langkah berikut:
Memverifikasi vault kosong (memiliki nol lamport) untuk mencegah deposit ganda
Memastikan jumlah deposit melebihi minimum rent-exempt untuk akun dasar
Mentransfer lamport dari pemilik ke vault menggunakan CPI ke System Program
Pertama, mari kita definisikan struct akun untuk deposit:
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 })
}
}Mari kita uraikan setiap pemeriksaan akun:
owner: Harus menjadi penandatangan karena mereka perlu mengotorisasi transaksivault:Harus dimiliki oleh System Program
Harus memiliki nol lamport (memastikan deposit "baru")
Harus diturunkan dari seeds yang benar
Harus cocok dengan alamat PDA yang diharapkan
Sekarang mari kita implementasikan struct data instruksi:
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 })
}
}Di sini kita hanya memeriksa bahwa jumlahnya tidak sama dengan nol.
Akhirnya, mari kita implementasikan instruksi deposit:
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(())
}
}Withdraw
Instruksi withdraw melakukan langkah-langkah berikut:
Memverifikasi vault berisi lamport (tidak kosong)
Menggunakan PDA vault untuk menandatangani transfer atas namanya sendiri
Mentransfer semua lamport dari vault kembali ke pemilik
Pertama, mari kita definisikan struct akun untuk withdraw:
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] })
}
}Mari kita uraikan setiap pemeriksaan akun:
owner: Harus menjadi penandatangan karena mereka perlu mengotorisasi transaksivault:Harus dimiliki oleh System Program
Harus diturunkan dari seeds yang benar
Harus cocok dengan alamat PDA yang diharapkan
bumps: Kita menyimpan bump seed untuk digunakan dalam penandatanganan PDA
Sekarang mari kita implementasikan instruksi withdraw:
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(())
}
}Keamanan penarikan ini dijamin oleh dua faktor:
PDA vault diturunkan menggunakan kunci publik pemilik, memastikan hanya penyetor asli yang dapat melakukan penarikan
Kemampuan PDA untuk menandatangani transfer diverifikasi melalui seeds yang kita berikan ke
invoke_signed
Conclusion
Anda sekarang dapat menguji program Anda terhadap unit test kami dan mengklaim NFT Anda!
Mulailah dengan membangun program Anda menggunakan perintah berikut di terminal Anda:
cargo build-sbfIni menghasilkan file .so langsung di folder target/deploy Anda.
Sekarang klik tombol take challenge dan letakkan file tersebut di sana!