Rust
Vault avec Pinocchio

Vault avec Pinocchio

103 Graduates

Vault avec Pinocchio

Pinocchio Vault Challenge

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_vault

Ajoutez Pinocchio:

cargo add pinocchio pinocchio-system

Déclarez les types de crate dans Cargo.toml pour générer les artefacts de déploiement dans target/deploy :

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

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

L'instruction deposit effectue les étapes suivantes :

  1. Vérifie que le vault est vide (contient zéro lamport) afin d'empêcher les doubles dépôts

  2. S'assure que le montant du dépôt dépasse la rente minimale pour qu'un compte de base soit exempt de rente

  3. 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 :

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

Détaillons chaque vérification de compte :

  1. owner: Doit être signataire car il doit autoriser la transaction

  2. vault:

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

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

Ici, nous vérifions uniquement que le montant est différent de zéro.

Enfin, implémentons l'instruction 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

L'instruction withdraw effectue les étapes suivantes :

  1. Vérifie que le vault contient des lamports (n'est pas vide)

  2. Utilise le PDA du vault pour signer le transfert en son nom

  3. Transfère tous les lamports du vault au propriétaire

Tout d'abord, définissons la structure de compte pour 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] })
    }
}

Détaillons chaque vérification de compte :

  1. owner: Doit être signataire car il doit autoriser la transaction

  2. vault:

    • Doit appartenir au Programme Système

    • Doit être dérivé des bonnes seeds

    • Doit correspondre à l'adresse de PDA attendue

  3. bumps: Nous stockons la seed de saut pour l'utiliser dans la signature du PDA

Implémentons maintenant l'instruction 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(())
    }
}

La sécurité de ce retrait est garantie par deux mesures :

  1. 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

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

Cela 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 !

Prêt à relever le challenge ?
Blueshift © 2025Commit: e573eab