
Le Vault
Un vault permet aux utilisateurs de stocker leurs actifs en toute sécurité. Un vault est un composant fondamental de la DeFi qui permet aux utilisateurs de stocker en toute sécurité leurs actifs (dans notre cas, des lamports) que seul cet utilisateur peut retirer plus tard.
Dans ce challenge, nous allons créer un vault à lamport simple qui montre comment travailler avec les comptes de base, les Adresses Dérivées de Programmes (PDAs) et les Invocations de Programme Croisé (CPI). Si vous n'êtes pas familier avec Pinocchio, vous devriez commencer par lire l'Introduction à Pinocchio pour vous familiariser avec les concepts de base que nous allons utiliser dans ce programme.
Installation
Avant de commencer, assurez-vous que Rust et Pinocchio sont installés. Exécutez ensuite dans votre terminal :
# create workspace
cargo new blueshift_vault --lib --edition 2021
cd blueshift_vaultAjoutez Pinocchio:
cargo add pinocchio pinocchio-systemDéclarez les types de crate dans Cargo.toml pour générer les artefacts de déploiement dans target/deploy :
[lib]
crate-type = ["lib", "cdylib"]Modèle
Commençons par la structure de base du programme. Nous allons tout implémenter dans lib.rs puisqu'il s'agit d'un programme simple. Voici le modèle initial avec les composants clés dont nous aurons besoin :
#![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
L'instruction deposit effectue les étapes suivantes :
Vérifie que le vault est vide (contient zéro lamport) afin d'empêcher les doubles dépôts
S'assure que le montant du dépôt dépasse la rente minimale pour qu'un compte de base soit exempt de rente
Transfert des lamports du payeur au vault à l'aide d'un CPI au Programme Système
Tout d'abord, définissons la structure de compte pour 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 })
}
}Détaillons chaque vérification de compte :
owner: Doit être signataire car il doit autoriser la transactionvault:Doit appartenir au Programme Système
Doit avoir zéro lamport (garantit un premier dépôt)
Doit être dérivé des bonnes seeds
Doit correspondre à l'adresse de PDA attendue
Implémentons maintenant la structure des données d'instruction :
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 })
}
}Ici, nous vérifions uniquement que le montant est différent de zéro.
Enfin, implémentons l'instruction 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
L'instruction withdraw effectue les étapes suivantes :
Vérifie que le vault contient des lamports (n'est pas vide)
Utilise le PDA du vault pour signer le transfert en son nom
Transfère tous les lamports du vault au propriétaire
Tout d'abord, définissons la structure de compte pour 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] })
}
}Détaillons chaque vérification de compte :
owner: Doit être signataire car il doit autoriser la transactionvault:Doit appartenir au Programme Système
Doit être dérivé des bonnes seeds
Doit correspondre à l'adresse de PDA attendue
bumps: Nous stockons la seed de saut pour l'utiliser dans la signature du PDA
Implémentons maintenant l'instruction 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(())
}
}La sécurité de ce retrait est garantie par deux mesures :
Le PDA du vault est dérivé à partir de la clé publique du propriétaire, garantissant ainsi que seul le déposant initial peut effectuer des retraits
La capacité du PDA à signer le transfert est vérifiée à l'aide des seeds que nous fournissons à
invoke_signed
Conclusion
Vous pouvez maintenant tester votre programme à l'aide de nos tests unitaires et réclamer votre NFT !
Commencez par compiler votre programme en utilisant la commande suivante dans votre terminal :
cargo build-sbfCela générera un fichier .so directement dans votre dossier target/deploy.
Cliquez ensuite sur le bouton Relever le challenge et déposez le fichier !