Rust
Vault Pinocchio

Vault Pinocchio

103 Graduates

Vault Pinocchio

Vault

Pinocchio Vault Challenge

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_vault

Tambahkan Pinocchio:

cargo add pinocchio pinocchio-system

Deklarasikan tipe crate di Cargo.toml untuk menghasilkan artefak deployment di target/deploy:

toml
[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:

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),
    }
}

Deposit

Instruksi deposit melakukan langkah-langkah berikut:

  1. Memverifikasi vault kosong (memiliki nol lamport) untuk mencegah deposit ganda

  2. Memastikan jumlah deposit melebihi minimum rent-exempt untuk akun dasar

  3. Mentransfer lamport dari pemilik ke vault menggunakan CPI ke System Program

Pertama, mari kita definisikan struct akun untuk deposit:

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 })
    }
}

Mari kita uraikan setiap pemeriksaan akun:

  1. owner: Harus menjadi penandatangan karena mereka perlu mengotorisasi transaksi

  2. vault:

    • 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:

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 })
    }
}

Di sini kita hanya memeriksa bahwa jumlahnya tidak sama dengan nol.

Akhirnya, mari kita implementasikan instruksi deposit:

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(())
    }
}

Withdraw

Instruksi withdraw melakukan langkah-langkah berikut:

  1. Memverifikasi vault berisi lamport (tidak kosong)

  2. Menggunakan PDA vault untuk menandatangani transfer atas namanya sendiri

  3. Mentransfer semua lamport dari vault kembali ke pemilik

Pertama, mari kita definisikan struct akun untuk withdraw:

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] })
    }
}

Mari kita uraikan setiap pemeriksaan akun:

  1. owner: Harus menjadi penandatangan karena mereka perlu mengotorisasi transaksi

  2. vault:

    • Harus dimiliki oleh System Program

    • Harus diturunkan dari seeds yang benar

    • Harus cocok dengan alamat PDA yang diharapkan

  3. bumps: Kita menyimpan bump seed untuk digunakan dalam penandatanganan PDA

Sekarang mari kita implementasikan instruksi withdraw:

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(())
    }
}

Keamanan penarikan ini dijamin oleh dua faktor:

  1. PDA vault diturunkan menggunakan kunci publik pemilik, memastikan hanya penyetor asli yang dapat melakukan penarikan

  2. 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-sbf

Ini menghasilkan file .so langsung di folder target/deploy Anda.

Sekarang klik tombol take challenge dan letakkan file tersebut di sana!

Siap mengambil tantangan?
Daftar Isi
Lihat Sumber
Blueshift © 2025Commit: e573eab