O Vault

Um vault permite que os usuários armazenem seus ativos de forma segura. Um vault é um bloco fundamental na DeFi que, em sua essência, permite que os usuários armazenem seus ativos (lamports neste caso) de forma segura e que apenas esse mesmo usuário possa sacar depois.
Neste desafio, construiremos um vault simples de lamports que demonstra como trabalhar com contas básicas, Program Derived Addresses (PDAs) e Cross-Program Invocation (CPI). Se você não está familiarizado com o Pinocchio, deve começar lendo a Introdução ao Pinocchio para se familiarizar com os conceitos principais que vamos usar neste programa.
Instalação
Antes de começar, certifique-se de que Rust e Pinocchio estejam instalados. Então, no seu terminal execute:
# criar workspace
cargo new blueshift_vault --lib --edition 2021
cd blueshift_vaultAdicione Pinocchio:
cargo add pinocchio pinocchio-systemDeclare os tipos de crate no Cargo.toml para gerar artefatos de deploy em target/deploy:
[lib]
crate-type = ["lib", "cdylib"]Template
Vamos começar com a estrutura básica do programa. Vamos implementar tudo no lib.rs já que este é um programa direto. Aqui está o template inicial com os componentes principais que precisaremos:
#![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),
}
}Depósito
A instrução de depósito executa os seguintes passos:
Verifica se o vault está vazio (tem zero lamports) para prevenir depósitos duplicados
Garante que o valor do depósito exceda o mínimo isento de aluguel para uma conta básica
Transfere lamports do dono para o vault usando uma CPI para o System Program
Primeiro, vamos definir a struct de contas para depósito:
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);
};
// Verificações de Contas
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);
}
// Retornar as contas
Ok(Self { owner, vault })
}
}Vamos detalhar cada verificação de conta:
owner: Deve ser um signer já que precisa autorizar a transaçãovault:Deve ser de propriedade do System Program
Deve ter zero lamports (garante depósito "fresco")
Deve ser derivado das seeds corretas
Deve corresponder ao endereço PDA esperado
Agora vamos implementar a struct de dados da instrução:
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());
// Verificações de Instrução
if amount.eq(&0) {
return Err(ProgramError::InvalidInstructionData);
}
Ok(Self { amount })
}
}Aqui apenas verificamos que o valor é diferente de zero.
Finalmente, vamos implementar a instrução de depósito:
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(())
}
}Saque
A instrução de saque executa os seguintes passos:
Verifica se o vault contém lamports (não está vazio)
Usa o PDA do vault para assinar a transferência em seu próprio nome
Transfere todos os lamports do vault de volta para o dono
Primeiro, vamos definir a struct de contas para saque:
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);
};
// Verificações Básicas de Contas
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()], &crate::ID);
if &vault_key != vault.key() {
return Err(ProgramError::InvalidAccountOwner);
}
Ok(Self { owner, vault, bumps: [bump] })
}
}Vamos detalhar cada verificação de conta:
owner: Deve ser um signer já que precisa autorizar a transaçãovault:Deve ser de propriedade do System Program
Deve ser derivado das seeds corretas
Deve corresponder ao endereço PDA esperado
bumps: Armazenamos a seed de bump para uso na assinatura PDA
Agora vamos implementar a instrução de saque:
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 {
// Criar seeds do assinante PDA
let seeds = [
Seed::from(b"vault"),
Seed::from(self.accounts.owner.key().as_ref()),
Seed::from(&self.accounts.bumps),
];
let signers = [Signer::from(&seeds)];
// Transferir todos os lamports do vault para o dono
Transfer {
from: self.accounts.vault,
to: self.accounts.owner,
lamports: self.accounts.vault.lamports(),
}
.invoke_signed(&signers)?;
Ok(())
}
}A segurança deste saque é garantida por dois fatores:
O PDA do vault é derivado usando a chave pública do dono, garantindo que apenas o depositante original pode sacar
A capacidade do PDA de assinar a transferência é verificada através das seeds que fornecemos ao
invoke_signed
Conclusão
Agora você pode testar seu programa contra nossos testes unitários e reivindicar seus NFTs!
Comece compilando seu programa usando o seguinte comando no seu terminal:
cargo build-sbfIsso gerou um arquivo .so diretamente na sua pasta target/deploy.
Agora clique no botão take challenge e solte o arquivo lá!