Сховище

Сховище дозволяє користувачам безпечно зберігати свої активи. Сховище є фундаментальним будівельним блоком у 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:
[lib]
crate-type = ["lib", "cdylib"]Template
Почнімо з базової структури програми. Ми реалізуємо все в 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),
}
}Deposit
Інструкція депозиту виконує такі кроки:
Перевіряє, що сховище порожнє (має нуль лампортів), щоб запобігти подвійним депозитам
Забезпечує, щоб сума депозиту перевищувала мінімум, звільнений від орендної плати для базового облікового запису
Переказує лампорти від власника до сховища, використовуючи CPI до системної програми
Спочатку визначимо структуру облікового запису для депозиту:
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:Має належати системній програмі
Має мати нуль лампортів (забезпечує "свіжий" депозит)
Має бути отриманий з правильних сідів
Має відповідати очікуваній адресі 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(())
}
}Withdraw
Інструкція зняття коштів виконує такі кроки:
Перевіряє, що сховище містить лампорти (не порожнє)
Використовує PDA сховища для підпису переказу від свого імені
Переказує всі лампорти зі сховища назад власнику
Спочатку визначимо структуру облікового запису для зняття коштів:
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: Ми зберігаємо bump-сід для використання в підписі PDA
Тепер реалізуємо інструкцію зняття коштів:
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 сховища отримується з використанням публічного ключа власника, що гарантує, що тільки початковий вкладник може зняти кошти
Здатність PDA підписувати переказ перевіряється через сіди, які ми надаємо для
invoke_signed
Conclusion
Тепер ви можете протестувати свою програму за допомогою наших юніт-тестів і отримати свої NFT!
Почніть з побудови вашої програми, використовуючи таку команду в терміналі:
cargo build-sbfЦе згенерувало файл .so безпосередньо у вашій папці target/deploy.
Тепер натисніть на кнопку take challenge і перетягніть туди файл!