Pinjaman
Instruksi loan adalah bagian pertama dari sistem flash loan kita. Instruksi ini melakukan empat langkah penting untuk memastikan peminjaman yang aman dan atomik:
Mendeserialkan sejumlah akun secara dinamis berdasarkan jumlah pinjaman yang ingin diambil oleh pengguna.
Menyimpan semua pinjaman ini di akun "scratch"
loandan menghitung saldo akhir yang harus dimiliki olehprotocol_token_account.Memverifikasi pembayaran kembali: Menggunakan introspeksi instruksi untuk mengonfirmasi bahwa instruksi pembayaran kembali yang valid ada di akhir transaksi
Mentransfer dana: Memindahkan semua jumlah pinjaman yang diminta dari treasury protokol ke akun peminjam
Required Accounts
borrower: pengguna yang meminta flash loan. Harus sebagai Penandatanganprotocol: Program Derived Address (PDA) yang memiliki pool likuiditas protokol untuk biaya tertentu.loan: akun "scratch" yang digunakan untuk menyimpanprotocol_token_accountdanbalanceakhir yang harus dimilikinya. Harus dapat diubahtoken_program: program token. Harus dapat dieksekusi
Berikut implementasinya:
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,
})
}
}Karena token_accounts adalah array akun dinamis, kita meneruskannya dengan cara yang mirip dengan remaining_accounts.
Untuk memastikan strukturnya benar, kita menambahkan validasi. Setiap pinjaman memerlukan protocol_token_account dan borrower_token_account, jadi kita harus memverifikasi bahwa array tersebut berisi akun dan jumlah akun dapat dibagi dua.
Instruction Data
Program flash loan kita perlu menangani jumlah data yang bervariasi tergantung pada berapa banyak pinjaman yang ingin diambil pengguna secara bersamaan. Berikut struktur data yang kita butuhkan:
bump: Satu byte yang digunakan untuk menurunkan PDA protokol tanpa harus menggunakan fungsifind_program_address().fee: tingkat biaya (dalam basis poin) yang dibayar pengguna untuk meminjamamounts: array dinamis dari jumlah pinjaman, karena pengguna dapat meminta beberapa pinjaman dalam satu transaksi
Berikut implementasinya:
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 })
}
}Kami menggunakan fungsi split_first dan split_at_checked untuk secara berurutan mengekstrak bump dan fee dari data instruksi, memungkinkan kita memproses byte yang tersisa dan mengubahnya langsung menjadi slice u64 menggunakan fungsi core::slice::from_raw_parts() untuk parsing yang efisien.
Instruction Logic
Setelah mendeserialkan instruction_data dan accounts, kita memeriksa bahwa jumlah amounts sama dengan jumlah token_accounts dibagi dua. Ini memastikan kita memiliki jumlah akun yang benar untuk pinjaman yang diminta.
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,
})
}
}Selanjutnya, kita membuat signer_seeds yang diperlukan untuk mentransfer token ke peminjam dan membuat akun loan. Ukuran akun ini dihitung menggunakan size_of::<LoanData>() * self.instruction_data.amounts.len() untuk memastikan dapat menampung semua data pinjaman untuk transaksi tersebut.
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()?;
//..
}
}Kemudian kita membuat slice yang dapat diubah dari data akun loan. Kita akan mengisi slice ini dalam loop for saat kita memproses setiap pinjaman dan transfer yang sesuai:
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()
)
};Akhirnya, kita melakukan loop melalui semua pinjaman. Dalam setiap iterasi, kita mendapatkan protocol_token_account dan borrower_token_account, menghitung saldo yang harus dibayarkan ke protokol, menyimpan data ini di akun loan, dan mentransfer token.
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)?;
}Kami menyelesaikannya dengan menggunakan introspeksi instruksi untuk melakukan pemeriksaan yang diperlukan. Kami memverifikasi bahwa instruksi terakhir dalam transaksi adalah instruksi repay dan bahwa instruksi tersebut menggunakan akun loan yang sama dengan instruksi loan kita saat ini.
// 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);
}