L'Escrow

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_escrowAjoutez ensuite pinocchio, pinocchio-system, pinocchio-token et pinocchio-associated-token :
cargo add pinocchio pinocchio-system pinocchio-token pinocchio-associated-token-accountDé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"]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 :
src
├── instructions
│ ├── make.rs
│ ├── helpers.rs
│ ├── mod.rs
│ ├── refund.rs
│ └── take.rs
├── errors.rs
├── lib.rs
└── state.rsLe 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 :
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 :
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 :
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 :
Calcul exact de la taille:
LENcalcule précisément la taille du compte en additionnant la taille de chaque champChargement en toute sécurité:
loadfournit un moyen sûr de charger et de valider les données de l'escrowOptimisations des performances:
#[inline(always)]sur les getters pour une performance maximaleMé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
Sécurité de la mémoire: Validation appropriée de la taille des données du compte et du propriétaire
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.