Позика
Інструкція loan є першою половиною нашої системи миттєвих позик. Вона виконує чотири критичні кроки для забезпечення безпечного та атомарного кредитування:
Десеріалізує динамічну кількість рахунків на основі кількості позик, які користувач хоче взяти.
Зберігає всі ці позики в "чернетковому" рахунку
loanта обчислює кінцевий баланс, який повинен матиprotocol_token_account.Перевіряє повернення: використовує інтроспекцію інструкцій для підтвердження того, що дійсна інструкція повернення існує в кінці транзакції.
Переказує кошти: переміщує всі запитані суми позик із скарбниці протоколу на рахунок позичальника.
Required Accounts
borrower: користувач, який запитує миттєву позику. Має бути підписантом.protocol: адреса, похідна від програми (PDA), яка володіє пулом ліквідності протоколу для конкретної комісії.loan: "чернетковий" рахунок, який використовується для збереженняprotocol_token_accountта кінцевогоbalance, який він повинен мати. Має бути змінюваним.token_program: програма токенів. Має бути виконуваною.
Ось реалізація:
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,
})
}
}Оскільки token_accounts є динамічним масивом рахунків, ми передаємо їх подібно до remaining_accounts.
Щоб забезпечити правильність структури, ми додаємо валідацію. Кожна позика потребує protocol_token_account та borrower_token_account, тому ми повинні перевірити, що масив містить рахунки і що кількість рахунків ділиться на два.
Instruction Data
Наша програма миттєвих позик повинна обробляти змінну кількість даних залежно від того, скільки позик користувач хоче взяти одночасно. Ось структура даних, яка нам потрібна:
bump: один байт, який використовується для отримання PDA протоколу без необхідності використовувати функціюfind_program_address().fee: ставка комісії (у базисних пунктах), яку користувачі сплачують за позику.amounts: динамічний масив сум позик, оскільки користувач може запитати кілька позик в одній транзакції.
Ось реалізація:
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 })
}
}Ми використовуємо функції split_first та split_at_checked для послідовного вилучення bump та fee з даних інструкції, що дозволяє нам обробляти решту байтів і безпосередньо перетворювати їх у зріз u64 за допомогою функції core::slice::from_raw_parts() для ефективного розбору.
Instruction Logic
Після десеріалізації instruction_data та accounts, ми перевіряємо, що кількість amounts дорівнює кількості token_accounts поділеній на два. Це гарантує, що у нас є правильна кількість облікових записів для запитаних позик.
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,
})
}
}Далі ми створюємо signer_seeds, необхідний для переказу токенів позичальнику та створення облікового запису loan. Розмір
цього облікового запису обчислюється за допомогою size_of::<LoanData>() * self.instruction_data.amounts.len(), щоб гарантувати, що він може
зберігати всі дані про позику для транзакції.
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()?;
//..
}
}Потім ми створюємо змінюваний зріз із даних облікового запису loan. Ми заповнимо цей зріз у циклі for, обробляючи кожну позику та відповідний переказ:
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()
)
};Нарешті, ми проходимо через усі позики. У кожній ітерації ми отримуємо protocol_token_account та borrower_token_account, обчислюємо баланс, належний протоколу, зберігаємо ці дані в обліковому записі loan і переказуємо токени.
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)?;
}Наприкінці ми використовуємо інтроспекцію інструкцій для виконання необхідних перевірок. Ми перевіряємо, що остання інструкція в транзакції є інструкцією repay і що вона використовує той самий обліковий запис loan, що й наша поточна інструкція loan.
// 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);
}