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:
Deserialisierung einer dynamischen Anzahl von Konten basierend auf der Anzahl der Darlehen, die der Benutzer aufnehmen möchte.
Speichern all dieser Darlehen im
loan"Scratch"-Konto und Berechnung des endgültigen Saldos, den dasprotocol_token_accounthaben muss.Überprüfung der Rückzahlung: Verwendung der Anweisungsintrospektion, um zu bestätigen, dass eine gültige Rückzahlungsanweisung am Ende der Transaktion existiert
Ü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 seinprotocol: 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 dasprotocol_token_accountund den endgültigenbalancezu speichern, den es haben muss. Muss veränderbar seintoken_program: das Token-Programm. Muss ausführbar sein
Hier ist die Implementierung:
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 diefind_program_address()Funktion verwenden zu müssen.fee: der Gebührensatz (in Basispunkten), den Benutzer für die Kreditaufnahme zahlenamounts: ein dynamisches Array von Darlehensbeträgen, da der Benutzer mehrere Darlehen in einer Transaktion anfordern kann
Hier ist die Implementierung:
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.
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.
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.
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:
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.
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.
// 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);
}