Rust
Escrow avec Pinocchio

Escrow avec Pinocchio

47 Graduates

L'Escrow

Pinocchio Escrow Challenge

Un escrow est un outil financier puissant qui permet des échanges sécurisés de jetons entre deux parties.

Imaginez un coffre-fort numérique où un utilisateur peut bloquer un jeton A en attendant qu'un autre utilisateur dépose un jeton B avant que l'échange ne soit terminé.

Cela crée un environnement sans confiance où aucune des parties n'a à craindre que l'autre ne revienne sur sa décision.

Dans ce défi, nous allons mettre en œuvre ce concept à travers trois instructions simples mais puissantes :

  • Créer (Make) : Le créateur (premier utilisateur) définit les conditions de l'échange et dépose le montant convenu du jeton A dans un coffre-fort sécurisé (vault). C'est comme si vous mettiez votre objet dans un coffre-fort et que vous fixiez les conditions de l'échange.

  • Accepter (Take) : Le preneur (deuxième utilisateur) accepte l'offre en transférant le montant promis du jeton B au créateur, et reçoit en retour le jeton A verrouillé. C'est le moment où les deux parties complètent leur part du marché.

  • Rembourser (Refund) : Si le créateur change d'avis ou si aucun preneur n'est trouvé, il peut annuler l'offre et récupérer son jeton A. C'est comme si vous récupériez votre objet dans le coffre-fort si l'échange n'aboutit pas.

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

Commençons par créer un nouvel environnement Rust :

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

Ajoutez ensuite pinocchio, pinocchio-system, pinocchio-token et pinocchio-associated-token :

cargo add pinocchio pinocchio-system pinocchio-token pinocchio-associated-token-account

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"]

Vous êtes maintenant prêt à écrire votre programme d'escrow.

Modèle

Cette fois, nous allons diviser le programme en petits modules dédiés au lieu de tout mettre dans le lib.rs. L'arborescence des dossiers se présente sensiblement comme suit :

text
src
├── instructions
│       ├── make.rs
│       ├── helpers.rs
│       ├── mod.rs
│       ├── refund.rs
│       └── take.rs
├── errors.rs
├── lib.rs
└── state.rs

Le point d'entrée, qui se trouve dans lib.rs, ressemble beaucoup à ce que nous avons fait dans les dernières leçons, nous allons donc le passer en revue rapidement :

rust
use pinocchio::{account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, ProgramResult};
entrypoint!(process_instruction);

pub mod instructions;
pub use instructions::*;

pub mod state;
pub use state::*;

// 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((Make::DISCRIMINATOR, data)) => Make::try_from((data, accounts))?.process(),
        Some((Take::DISCRIMINATOR, _)) => Take::try_from(accounts)?.process(),
        Some((Refund::DISCRIMINATOR, _)) => Refund::try_from(accounts)?.process(),
        _ => Err(ProgramError::InvalidInstructionData)
    }
}

État (State)

Nous allons nous rendre dans le fichier state.rs où se trouvent toutes les données de notre Escrow. Nous allons décomposer ce point en deux parties : la définition de la structure et son implémentation.

Tout d'abord, intéressons-nous à la définition de la structure :

rust
use pinocchio::{program_error::ProgramError, pubkey::Pubkey};
use core::mem::size_of;

#[repr(C)]
pub struct Escrow {
    pub seed: u64,        // Random seed for PDA derivation
    pub maker: Pubkey,    // Creator of the escrow
    pub mint_a: Pubkey,   // Token being deposited
    pub mint_b: Pubkey,   // Token being requested
    pub receive: u64,     // Amount of token B wanted
    pub bump: [u8;1]      // PDA bump seed
}

L'attribut #[repr(C)] garantit que notre structure a une configuration mémoire prédictible, ce qui est crucial pour des données on-chain. Chaque champ a une utilité spécifique :

  • seed: Un nombre aléatoire qui permet à un créateur de créer plusieurs comptes d'escrow avec la même paire de jetons

  • maker: L'adresse du portefeuille qui a créé l'escrow et qui recevra les jetons

  • mint_a: L'adresse de mint du jeton déposé

  • mint_b: L'adresse de mint du jeton demandé

  • receive: Le montant exact du jeton B que le créateur souhaite recevoir

  • bump: Un seul octet utilisé dans la dérivation du PDA pour s'assurer que l'adresse n'est pas sur la courbe Ed25519

Examinons maintenant l'implémentation et toutes ses fonctions d'aide :

rust
impl Escrow {
    pub const LEN: usize = size_of::<u64>()
    + size_of::<Pubkey>()
    + size_of::<Pubkey>()
    + size_of::<Pubkey>()
    + size_of::<u64>()
    + size_of::<[u8;1]>();

    #[inline(always)]
    pub fn load_mut(bytes: &mut [u8]) -> Result<&mut Self, ProgramError> {
        if bytes.len() != Escrow::LEN {
            return Err(ProgramError::InvalidAccountData);
        }
        Ok(unsafe { &mut *core::mem::transmute::<*mut u8, *mut Self>(bytes.as_mut_ptr()) })
    }

    #[inline(always)]
    pub fn load(bytes: &[u8]) -> Result<&Self, ProgramError> {
        if bytes.len() != Escrow::LEN {
            return Err(ProgramError::InvalidAccountData);
        }
        Ok(unsafe { &*core::mem::transmute::<*const u8, *const Self>(bytes.as_ptr()) })
    }

    #[inline(always)]
    pub fn set_seed(&mut self, seed: u64) {
        self.seed = seed;
    }

    #[inline(always)]
    pub fn set_maker(&mut self, maker: Pubkey) {
        self.maker = maker;
    }

    #[inline(always)]
    pub fn set_mint_a(&mut self, mint_a: Pubkey) {
        self.mint_a = mint_a;
    }

    #[inline(always)]
    pub fn set_mint_b(&mut self, mint_b: Pubkey) {
        self.mint_b = mint_b;
    }

    #[inline(always)]
    pub fn set_receive(&mut self, receive: u64) {
        self.receive = receive;
    }

    #[inline(always)]
    pub fn set_bump(&mut self, bump: [u8;1]) {
        self.bump = bump;
    }

    #[inline(always)]
    pub fn set_inner(&mut self, seed: u64, maker: Pubkey, mint_a: Pubkey, mint_b: Pubkey, receive: u64, bump: [u8;1]) {
        self.seed = seed;
        self.maker = maker;
        self.mint_a = mint_a;
        self.mint_b = mint_b;
        self.receive = receive;
        self.bump = bump;
    }
}

L'implementation présente plusieurs caractéristiques importantes :

  1. Calcul exact de la taille: LEN calcule précisément la taille du compte en additionnant la taille de chaque champ

  2. Chargement en toute sécurité: load fournit un moyen sûr de charger et de valider les données de l'escrow

  3. Optimisations des performances:

    • #[inline(always)] sur les getters pour une performance maximale

    • Méthodes unsafe pour les cas où l'on sait que le borrow est sûr

    • Définition efficace de la valeur des champs avec set_inner

  4. Sécurité de la mémoire: Validation appropriée de la taille des données du compte et du propriétaire

  5. Documentation: Des commentaires clairs expliquant l'objectif et les considérations de sécurité de chaque méthode

Cette mise en œuvre garantit que l'état de notre escrow est à la fois sûr et efficace et qu'il y a une validation appropriée et des optimisations de performance lorsque cela est nécessaire.

Next PageCréer
OU PASSER AU CHALLENGE
Prêt à relever le challenge ?
Blueshift © 2025Commit: e573eab