Rust
Pinocchio Flash Loan

Pinocchio Flash Loan

14 Graduates

Darlehen

Die loan Anweisung ist die erste Hälfte unseres Flash-Loan-Systems. Sie führt vier kritische Schritte aus, um sicheres und atomares Verleihen zu gewährleisten:

  1. Deserialisierung einer dynamischen Anzahl von Konten basierend auf der Anzahl der Darlehen, die der Benutzer aufnehmen möchte.

  2. Speichern all dieser Darlehen im loan "Scratch"-Konto und Berechnung des endgültigen Saldos, den das protocol_token_account haben muss.

  3. Überprüfung der Rückzahlung: Verwendung der Anweisungsintrospektion, um zu bestätigen, dass eine gültige Rückzahlungsanweisung am Ende der Transaktion existiert

  4. Überweisung von Geldern: Übertragung aller angeforderten Darlehensbeträge vom Treasury des Protokolls auf das Konto des Kreditnehmers

Erforderliche Konten

  • borrower: der Benutzer, der den Flash-Loan anfordert. Muss ein Unterzeichner sein

  • protocol: eine Program Derived Address (PDA), die den Liquiditätspool des Protokolls für eine bestimmte Gebühr besitzt.

  • loan: das "Scratch"-Konto, das verwendet wird, um das protocol_token_account und den endgültigen balance zu speichern, den es haben muss. Muss veränderbar sein

  • token_program: das Token-Programm. Muss ausführbar sein

Hier ist die Implementierung:

rust
pub struct LoanAccounts<'a> {
    pub borrower: &'a AccountInfo,
    pub protocol: &'a AccountInfo,
    pub loan: &'a AccountInfo,
    pub instruction_sysvar: &'a AccountInfo,
    pub token_accounts: &'a [AccountInfo],
}

impl<'a> TryFrom<&'a [AccountInfo]> for LoanAccounts<'a> {
    type Error = ProgramError;

    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let [borrower, protocol, loan, instruction_sysvar, _token_program, _system_program, token_accounts @ ..] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };

        if !pubkey_eq(instruction_sysvar.key(), &INSTRUCTIONS_ID) {
            return Err(ProgramError::UnsupportedSysvar);
        }

        // Verify that the number of token accounts is valid
        if (token_accounts.len() % 2).ne(&0) || token_accounts.len().eq(&0) {
            return Err(ProgramError::InvalidAccountData);
        }

        if loan.try_borrow_data()?.len().ne(&0) {
            return Err(ProgramError::InvalidAccountData);
        }

        Ok(Self {
            borrower,
            protocol,
            loan,
            instruction_sysvar,
            token_accounts,
        })
    }
}

Da token_accounts ein dynamisches Array von Konten ist, übergeben wir sie ähnlich wie bei remaining_accounts.

Um sicherzustellen, dass die Struktur korrekt ist, fügen wir eine Validierung hinzu. Jedes Darlehen erfordert ein protocol_token_account und ein borrower_token_account, daher müssen wir überprüfen, ob das Array Konten enthält und die Anzahl der Konten durch zwei teilbar ist.

Anweisungsdaten

Unser Flash-Loan-Programm muss variable Datenmengen verarbeiten können, je nachdem, wie viele Darlehen ein Benutzer gleichzeitig aufnehmen möchte. Hier ist die Datenstruktur, die wir benötigen:

  • bump: Ein einzelnes Byte, das verwendet wird, um die Protokoll-PDA abzuleiten, ohne die find_program_address() Funktion verwenden zu müssen.

  • fee: der Gebührensatz (in Basispunkten), den Benutzer für die Kreditaufnahme zahlen

  • amounts: ein dynamisches Array von Darlehensbeträgen, da der Benutzer mehrere Darlehen in einer Transaktion anfordern kann

Hier ist die Implementierung:

rust
pub struct LoanInstructionData<'a> {
    pub bump: [u8; 1],
    pub fee: u16,
    pub amounts: &'a [u64],
}

impl<'a> TryFrom<&'a [u8]> for LoanInstructionData<'a> {
    type Error = ProgramError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        // Get the bump
        let (bump, data) = data.split_first().ok_or(ProgramError::InvalidInstructionData)?;

        // Get the fee
        let (fee, data) = data.split_at_checked(size_of::<u16>()).ok_or(ProgramError::InvalidInstructionData)?;

        // Verify that the data is valid
        if data.len() % size_of::<u64>() != 0 {
            return Err(ProgramError::InvalidInstructionData);
        }

        // Get the amounts
        let amounts: &[u64] = unsafe {
            core::slice::from_raw_parts(
                data.as_ptr() as *const u64,
                data.len() / size_of::<u64>()
            )
        };

        Ok(Self { bump: [*bump], fee: u16::from_le_bytes(fee.try_into().map_err(|_| ProgramError::InvalidInstructionData)?), amounts })
    }
}

Wir verwenden die Funktionen split_first und split_at_checked, um sequentiell die bump und fee aus den Instruktionsdaten zu extrahieren. Dies ermöglicht uns, die verbleibenden Bytes zu verarbeiten und sie direkt in einen u64Slice mittels der Funktion core::slice::from_raw_parts() für effizientes Parsing umzuwandeln.

Die Ableitung der protocol Program Derived Address mit der fee erzeugt isolierte Liquiditätspools für jede Gebührenstufe und eliminiert die Notwendigkeit, Gebührendaten in Konten zu speichern. Dieses Design ist sowohl sicher als auch optimal, da jede PDA mit einer spezifischen Gebühr nur die Liquidität besitzt, die mit dieser Gebührenrate verbunden ist. Wenn jemand eine ungültige Gebühr übergibt, wird das entsprechende Token-Konto für diese Gebührenkategorie leer sein, was automatisch dazu führt, dass die Überweisung aufgrund unzureichender Mittel fehlschlägt.

Instruction Logic

Nach der Deserialisierung von instruction_data und accounts überprüfen wir, dass die Anzahl der amounts der Anzahl der token_accounts geteilt durch zwei entspricht. Dies stellt sicher, dass wir die korrekte Anzahl von Konten für die angeforderten Darlehen haben.

rust
pub struct Loan<'a> {
    pub accounts: LoanAccounts<'a>,
    pub instruction_data: LoanInstructionData<'a>,
}

impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Loan<'a> {
    type Error = ProgramError;

    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = LoanAccounts::try_from(accounts)?;
        let instruction_data = LoanInstructionData::try_from(data)?;

        // Verify that the number of amounts matches the number of token accounts
        if instruction_data.amounts.len() != accounts.token_accounts.len() / 2 {
            return Err(ProgramError::InvalidInstructionData);
        }

        Ok(Self {
            accounts,
            instruction_data,
        })
    }
}

Als nächstes erstellen wir den signer_seeds, der benötigt wird, um Tokens an den Kreditnehmer zu übertragen und ein loanKonto zu erstellen. Die Größe dieses Kontos wird mit size_of::<LoanData>() * self.instruction_data.amounts.len() berechnet, um sicherzustellen, dass es alle Darlehensdaten für die Transaktion aufnehmen kann.

rust
impl<'a> Loan<'a> {
    pub const DISCRIMINATOR: &'a u8 = &0;

    pub fn process(&mut self) -> ProgramResult {
        // Get the fee
        let fee = self.instruction_data.fee.to_le_bytes();

        // Get the signer seeds
        let signer_seeds = [
            Seed::from("protocol".as_bytes()),
            Seed::from(&fee),
            Seed::from(&self.instruction_data.bump),
        ];
        let signer_seeds = [Signer::from(&signer_seeds)];

        // Open the LoanData account and create a mutable slice to push the Loan struct to it
        let size = size_of::<LoanData>() * self.instruction_data.amounts.len();
        let lamports = Rent::get()?.minimum_balance(size);

        CreateAccount {
            from: self.accounts.borrower,
            to: self.accounts.loan,
            lamports,
            space: size as u64,
            owner: &ID,
        }.invoke()?;

        //..
    }
}

Dann erstellen wir einen veränderbaren Slice aus den Daten des loanKontos. Wir werden diesen Slice in einer forSchleife befüllen, während wir jedes Darlehen und die entsprechende Überweisung verarbeiten:

rust
let mut loan_data = self.accounts.loan.try_borrow_mut_data()?;
let loan_entries = unsafe {
    core::slice::from_raw_parts_mut(
        loan_data.as_mut_ptr() as *mut LoanData,
        self.instruction_data.amounts.len()
    )
};

Schließlich durchlaufen wir alle Darlehen. In jeder Iteration erhalten wir die protocol_token_account und borrower_token_account, berechnen das dem Protokoll geschuldete Guthaben, speichern diese Daten im loanKonto und übertragen die Tokens.

rust
for (i, amount) in self.instruction_data.amounts.iter().enumerate() {
    let protocol_token_account = &self.accounts.token_accounts[i * 2];
    let borrower_token_account = &self.accounts.token_accounts[i * 2 + 1];

    // Get the balance of the protocol's token account and add the fee to it so we can save it to the loan account
    let balance = get_token_amount(&protocol_token_account.try_borrow_data()?);
    let balance_with_fee = balance.checked_add(
        amount.checked_mul(self.instruction_data.fee as u64)
            .and_then(|x| x.checked_div(10_000))
            .ok_or(ProgramError::InvalidInstructionData)?
    ).ok_or(ProgramError::InvalidInstructionData)?;

    // Push the Loan struct to the loan account
    loan_entries[i] = LoanData {
        protocol_token_account: *protocol_token_account.key(),
        balance: balance_with_fee,
    };

    // Transfer the tokens from the protocol to the borrower
    Transfer {
        from: protocol_token_account,
        to: borrower_token_account,
        authority: self.accounts.protocol,
        amount: *amount,
    }.invoke_signed(&signer_seeds)?;
}

Zum Abschluss verwenden wir Instruktionsintrospection, um die notwendigen Überprüfungen durchzuführen. Wir verifizieren, dass die letzte Instruktion in der Transaktion eine repay Instruktion ist und dass sie dasselbe loan Konto verwendet wie unsere aktuelle loan Instruktion.

rust
// Introspecting the Repay instruction
let instruction_sysvar = unsafe { Instructions::new_unchecked(self.accounts.instruction_sysvar.try_borrow_data()?) };
let num_instructions = instruction_sysvar.num_instructions();
let instruction = instruction_sysvar.load_instruction_at(num_instructions as usize - 1)?;

if instruction.get_program_id() != &crate::ID {
    return Err(ProgramError::InvalidInstructionData);
}

if unsafe { *(instruction.get_instruction_data().as_ptr()) } != *Repay::DISCRIMINATOR {
    return Err(ProgramError::InvalidInstructionData);
}

if unsafe { instruction.get_account_meta_at_unchecked(1).key } != *self.accounts.loan.key() {
    return Err(ProgramError::InvalidInstructionData);
}

Die Verwendung eines loan Kontos und die Strukturierung der Instruktionsintrospection auf diese Weise stellt sicher, dass wir in der repay Instruktion keine tatsächliche Introspection durchführen müssen, da alle Rückzahlungsprüfungen vom loan Konto übernommen werden.

Next PageZurückzahlen
ODER DIREKT ZUR HERAUSFORDERUNG
Bereit für die Herausforderung?
Blueshift © 2025Commit: e573eab