Rust
Pinocchio Vault

Pinocchio Vault

103 Graduates

Pinocchio Vault

Сховище

Завдання Pinocchio Сховище

Сховище дозволяє користувачам безпечно зберігати свої активи. Сховище є фундаментальним будівельним блоком у DeFi, який у своїй основі дозволяє користувачам безпечно зберігати свої активи (у цьому випадку лампорти), які пізніше може вилучити лише той самий користувач.

У цьому завданні ми створимо просте сховище лампортів, яке демонструє, як працювати з базовими обліковими записами, адресами, похідними від програми (PDA), та міжпрограмним викликом (CPI). Якщо ви не знайомі з Pinocchio, вам слід почати з читання Вступу до Pinocchio, щоб ознайомитися з основними концепціями, які ми будемо використовувати в цій програмі.

Installation

Перш ніж почати, переконайтеся, що Rust і Pinocchio встановлені. Потім у терміналі виконайте:

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

Додайте Pinocchio:

cargo add pinocchio pinocchio-system

Оголосіть типи крейту в Cargo.toml, щоб згенерувати артефакти розгортання в target/deploy:

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

Template

Почнімо з базової структури програми. Ми реалізуємо все в 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),
    }
}

Deposit

Інструкція депозиту виконує такі кроки:

  1. Перевіряє, що сховище порожнє (має нуль лампортів), щоб запобігти подвійним депозитам

  2. Забезпечує, щоб сума депозиту перевищувала мінімум, звільнений від орендної плати для базового облікового запису

  3. Переказує лампорти від власника до сховища, використовуючи CPI до системної програми

Спочатку визначимо структуру облікового запису для депозиту:

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:

    • Має належати системній програмі

    • Має мати нуль лампортів (забезпечує "свіжий" депозит)

    • Має бути отриманий з правильних сідів

    • Має відповідати очікуваній адресі 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(())
    }
}

Withdraw

Інструкція зняття коштів виконує такі кроки:

  1. Перевіряє, що сховище містить лампорти (не порожнє)

  2. Використовує PDA сховища для підпису переказу від свого імені

  3. Переказує всі лампорти зі сховища назад власнику

Спочатку визначимо структуру облікового запису для зняття коштів:

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: Ми зберігаємо bump-сід для використання в підписі PDA

Тепер реалізуємо інструкцію зняття коштів:

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. Здатність PDA підписувати переказ перевіряється через сіди, які ми надаємо для invoke_signed

Conclusion

Тепер ви можете протестувати свою програму за допомогою наших юніт-тестів і отримати свої NFT!

Почніть з побудови вашої програми, використовуючи таку команду в терміналі:

cargo build-sbf

Це згенерувало файл .so безпосередньо у вашій папці target/deploy.

Тепер натисніть на кнопку take challenge і перетягніть туди файл!

Готові прийняти завдання?
Blueshift © 2025Commit: e573eab