Rust
Pinocchio pour les nuls

Pinocchio pour les nuls

Comptes

Comme nous l'avons vu dans la section précédente, la validation des comptes avec Pinocchio diffère d'Anchor car nous ne pouvons pas utiliser les types de compte qui effectuent automatiquement les vérifications de propriétaire, de signataire et de discriminateur.

En Rust natif, nous devons effectuer ces validations manuellement. Bien que cela nécessite plus d'attention aux détails, c'est simple à mettre en œuvre :

rust
// SignerAccount type
if !account.is_signer() {
    return Err(PinocchioError::NotSigner.into());
}

Ou pour une vérification de propriétaire :

rust
// SystemAccount type
if !account.is_owned_by(&pinocchio_system::ID) {
    return Err(PinocchioError::InvalidOwner.into());
}

En enveloppant toutes les validations dans l'implémentation TryFrom que nous avons couverte précédemment, nous pouvons facilement identifier les vérifications manquantes et nous assurer que nous écrivons un code sécurisé.

Cependant, écrire ces vérifications pour chaque instruction peut devenir répétitif. Pour résoudre ce problème, nous avons créé un fichier helper.rs qui définit des types similaires à ceux d'Anchor pour simplifier ces validations.

Compte signataire et compte système

Comme nous l'avons vu dans les exemples précédents, les vérifications SystemAccount et SignerAccount sont simples et ne nécessitent pas de validation supplémentaire, nous allons donc ajouter ce qui suit à notre helper.rs :

rust
fn signer_check(account: &AccountInfo) -> Result<(), ProgramError> {
    if !account.is_signer() {
        return Err(PinocchioError::NotSigner.into());
    }

    Ok(())
}

fn system_account_check(account: &AccountInfo) -> Result<(), ProgramError> {
    if !account.is_owned_by(&pinocchio_system::ID) {
        return Err(PinocchioError::InvalidOwner.into());
    }

    Ok(())
}

Ici, nous vérifions simplement si le compte est un signataire ou s'il appartient au programme système.

Comptes Mint et Token

Maintenant, les choses deviennent plus intéressantes. Nous commençons par la vérification habituelle du compte (vérification de propriété et de longueur), mais nous ajoutons également d'autres fonctions spécifiques pour fournir des assistants supplémentaires qui ressemblent aux macros Anchor comme init et init_if_needed.

rust
pub struct Mint;

impl Mint {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&pinocchio_token::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        if account.data_len() != pinocchio_token::state::Mint::LEN {
            return Err(PinocchioError::InvalidAccountData.into());
        }

        Ok(())
    }

    //...
}

Pour les fonctionnalités init et init_if_needed, nous utilisons les CPI CreateAccount et InitializeMint2 en arrière-plan pour initialiser le compte Mint :

rust
impl Mint {
    //...

    fn init(
        account: &AccountInfo, 
        payer: &AccountInfo, 
        decimals: u8, 
        mint_authority: &[u8; 32], 
        freeze_authority: Option<&[u8; 32]>
    ) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::Mint::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::Mint::LEN as u64,
            owner: &pinocchio_token::ID,
        }.invoke()?;

        InitializeMint2 {
            mint: account,
            decimals,
            mint_authority,
            freeze_authority,
        }.invoke()
    }

    fn init_if_needed(
        account: &AccountInfo, 
        payer: &AccountInfo, 
        decimals: u8, 
        mint_authority: &[u8; 32], 
        freeze_authority: Option<&[u8; 32]>
    ) -> ProgramResult {
        mint_account_check(account) {
            Ok(_) => Ok(()),
            Err(_) => init(account, payer, decimals, mint_authority, freeze_authority),
        }
    }
}

Nous faisons ensuite exactement la même chose pour le compte Token :

rust
pub struct Token;

impl Token {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&pinocchio_token::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        if account.data_len().ne(&pinocchio_token::state::TokenAccount::LEN) {
            return Err(PinocchioError::InvalidAccountData.into());
        }

        Ok(())
    }

    fn init(
        account: &AccountInfo, 
        mint: &AccountInfo, 
        payer: &AccountInfo, 
        owner: &[u8; 32]
    ) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::TokenAccount::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::TokenAccount::LEN as u64,
            owner: &pinocchio_token::ID,
        }.invoke()?;

        // Initialize the Token Account
        InitializeAccount3 {
            account,
            mint,
            owner,
        }.invoke()
    }

    fn init_if_needed(
        account: &AccountInfo, 
        mint: &AccountInfo, 
        payer: &AccountInfo, 
        owner: &[u8; 32]
    ) -> ProgramResult {
        match Self::check(account) {
            Ok(_) => Ok(()),
            Err(_) => init_token_account(account, mint, payer, owner),
        }
    }
}

Token2022

Vous avez peut-être remarqué que pour le programme SPL Token Legacy, nous avons seulement effectué une vérification de longueur sur les comptes Mint et Token. Cette approche fonctionne car lorsque vous n'avez que deux types de compte avec des tailles fixes, vous pouvez les distinguer en utilisant uniquement leur longueur.

Pour Token2022, cette approche simple ne fonctionne pas. La taille de Mint peut augmenter et potentiellement dépasser la taille du compte Token lorsque des extensions de token sont ajoutées directement aux données Mint. Cela signifie que nous ne pouvons pas nous fier uniquement à la taille pour différencier les types de comptes.

Pour Token2022, nous pouvons distinguer entre un compte Mint et un compte Token de deux façons :

  • Par la taille : similaire au programme Legacy Token (lorsque les comptes ont des tailles standard)

  • Par discriminateur : un octet spécial situé à la position 165 (un octet plus grand que le compte Token legacy pour éviter les conflits)

Cela conduit à des vérifications de validation modifiées :

rust
// TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
pub const TOKEN_2022_PROGRAM_ID: [u8; 32] = [
    0x06, 0xdd, 0xf6, 0xe1, 0xee, 0x75, 0x8f, 0xde, 0x18, 0x42, 0x5d, 0xbc, 0xe4, 0x6c, 0xcd, 0xda,
    0xb6, 0x1a, 0xfc, 0x4d, 0x83, 0xb9, 0x0d, 0x27, 0xfe, 0xbd, 0xf9, 0x28, 0xd8, 0xa1, 0x8b, 0xfc,
];

const TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET: usize = 165;
pub const TOKEN_2022_MINT_DISCRIMINATOR: u8 = 0x01;
pub const TOKEN_2022_TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 0x02;

pub struct Mint2022;

impl Mint2022 {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        let data = account.try_borrow_data()?;

        if data.len().ne(&pinocchio_token::state::Mint::LEN) {
            if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                return Err(PinocchioError::InvalidAccountData.into());
            }

            if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET].ne(&TOKEN_2022_MINT_DISCRIMINATOR) {
                return Err(PinocchioError::InvalidAccountData.into());
            }
        }

        Ok(())
    }

    fn init(
        account: &AccountInfo, 
        payer: &AccountInfo, 
        decimals: u8, 
        mint_authority: &[u8; 32], 
        freeze_authority: Option<&[u8; 32]>
    ) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::Mint::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::Mint::LEN as u64,
            owner: &TOKEN_2022_PROGRAM_ID,
        }.invoke()?;

        InitializeMint2 {
            mint: account,
            decimals,
            mint_authority,
            freeze_authority,
        }.invoke()
    }

    fn init_if_needed(
        account: &AccountInfo, 
        payer: &AccountInfo, 
        decimals: u8, 
        mint_authority: &[u8; 32], 
        freeze_authority: Option<&[u8; 32]>
    ) -> ProgramResult {
        match Self::check(account) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, payer, decimals, mint_authority, freeze_authority),
        }
    }
}

pub struct Token2022;

impl Token2022 {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        let data = account.try_borrow_data()?;

        if data.len().ne(&pinocchio_token::state::TokenAccount::LEN) {
            if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                return Err(PinocchioError::InvalidAccountData.into());
            }
            if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET].ne(&TOKEN_2022_TOKEN_ACCOUNT_DISCRIMINATOR) {
                return Err(PinocchioError::InvalidAccountData.into());
            }
        }

        Ok(())
    }

    fn init(
        account: &AccountInfo, 
        mint: &AccountInfo,
        payer: &AccountInfo, 
        owner: &[u8; 32]
    ) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::TokenAccount::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::TokenAccount::LEN as u64,
            owner: &TOKEN_2022_PROGRAM_ID,
        }.invoke()?;

        InitializeAccount3 {
            account,
            mint,
            owner,
        }.invoke()
    }

    fn init_if_needed(
        account: &AccountInfo, 
        mint: &AccountInfo, 
        payer: &AccountInfo, 
        owner: &[u8; 32]
    ) -> ProgramResult {
        match Self::check(account) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, mint, payer, owner),
        }
    }
}

Interface Token

Comme nous voulons faciliter le travail avec les programmes Token2022 et Legacy Token sans avoir à les distinguer, nous avons créé un assistant qui suit le même principe de base :

rust
pub struct MintInterface;

impl MintInterface {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            if !account.is_owned_by(&pinocchio_token::ID) {
                return Err(PinocchioError::InvalidOwner.into());
            } else {
                if account.data_len().ne(&pinocchio_token::state::Mint::LEN) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        } else {
            let data = account.try_borrow_data()?;

            if data.len().ne(&pinocchio_token::state::Mint::LEN) {
                if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
                if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET].ne(&TOKEN_2022_MINT_DISCRIMINATOR) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        }

        Ok(())
    }
}

pub struct TokenInterface;

impl TokenInterface {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            if !account.is_owned_by(&pinocchio_token::ID) {
                return Err(PinocchioError::InvalidOwner.into());
            } else {
                if account.data_len().ne(&pinocchio_token::state::TokenAccount::LEN) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        } else {
            let data = account.try_borrow_data()?;

            if data.len().ne(&pinocchio_token::state::TokenAccount::LEN) {
                if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
                if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET]
                    .ne(&TOKEN_2022_TOKEN_ACCOUNT_DISCRIMINATOR)
                {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        }

        Ok(())
    }
}

Compte de token associé

Nous pouvons créer des vérifications pour le programme de token associé. Elles sont très similaires aux vérifications normales du programme Token, mais elles incluent une vérification de dérivation supplémentaire pour s'assurer que le compte est dérivé correctement.

rust
pub struct AssociatedToken;

impl AssociatedToken {
    fn check(
        account: &AccountInfo,
        authority: &AccountInfo,
        mint: &AccountInfo,
        token_program: &AccountInfo,
    ) -> Result<(), ProgramError> {
        TokenAccount::check(account)?;

        if find_program_address(
            &[authority.key(), token_program.key(), mint.key()],
            &pinocchio_associated_token_account::ID,
        ).0.ne(account.key()) {
            return Err(PinocchioError::InvalidAddress.into());
        }

        Ok(())
    }

    fn init(
        account: &AccountInfo, 
        mint: &AccountInfo, 
        payer: &AccountInfo, 
        owner: &AccountInfo, 
        system_program: &AccountInfo, 
        token_program: &AccountInfo
    ) -> ProgramResult {
        Create {
            funding_account: payer,
            account,
            wallet: owner,
            mint,
            system_program,
            token_program,
        }.invoke()
    }

    fn init_if_needed(
        account: &AccountInfo, 
        mint: &AccountInfo, 
        payer: &AccountInfo, 
        owner: &AccountInfo, 
        system_program: &AccountInfo, 
        token_program: &AccountInfo
    ) -> ProgramResult {
        match Self::check(account, payer, mint) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, mint, payer, owner, system_program, token_program),
        }
    }
}

Program Accounts

Enfin, nous implémentons des vérifications et des assistants pour les comptes de programme, y compris les fonctionnalités init et close.

Vous pourriez remarquer quelque chose d'intéressant dans notre implémentation de close : nous redimensionnons le compte à presque rien, ne laissant que le premier octet et le définissant à 255. C'est une mesure de sécurité pour prévenir les attaques de réinitialisation.

Une attaque de réinitialisation se produit lorsqu'un attaquant tente de réutiliser un compte fermé en le réinitialisant avec des données malveillantes. En définissant le premier octet à 255 et en réduisant le compte à une taille presque nulle, nous rendons impossible que le compte soit confondu avec un type de compte valide à l'avenir. C'est un modèle de sécurité courant dans les programmes Solana.

rust
pub struct ProgramAccount;

impl ProgramAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&crate::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        if account.data_len().ne(&crate::state::ProgramAccount::LEN) {
            return Err(PinocchioError::InvalidAccountData.into());
        }

        Ok(())
    }
    
    fn init<'a, T: Sized>(
        payer: &AccountInfo,
        account: &AccountInfo,
        seeds: &[Seed<'a>],
        space: usize,
    ) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(space);

        // Create signer with seeds slice
        let signer = [Signer::from(seeds)];

        // Create the account
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: space as u64,
            owner: &crate::ID,
        }
        .invoke_signed(&signer)?;

        Ok(())
    }

    fn close(account: &AccountInfo, destination: &AccountInfo) -> ProgramResult {
        {
            let mut data = account.try_borrow_mut_data()?;
            data[0] = 0xff;
        }

        *destination.try_borrow_mut_lamports()? += *account.try_borrow_lamports()?;
        account.realloc(1, true)?;
        account.close()
    }
}
Blueshift © 2025Commit: e573eab