貸款
loan 指令是我們閃電貸系統的第一部分。它執行四個關鍵步驟以確保安全和原子性貸款:
根據用戶想要借貸的數量,反序列化動態數量的賬戶。
將所有這些貸款保存到
loan"暫存" 賬戶中,並計算protocol_token_account需要擁有的最終餘額。驗證還款:使用指令內省確認交易結尾處存在有效的還款指令。
資金轉移:將協議金庫中的所有請求貸款金額轉移到借款人的賬戶。
所需賬戶
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,因此我們必須驗證數組包含賬戶,並且賬戶數量是二的倍數。
指令數據
我們的閃電貸程序需要根據用戶想要同時借貸的數量處理可變數量的數據。以下是我們需要的數據結構:
bump:一個單字節,用於在不使用find_program_address()函數的情況下派生協議 PDA。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,從而允許我們處理剩餘的字節,並使用 core::slice::from_raw_parts() 函數將其直接轉換為 u64 切片,以實現高效解析。
指令邏輯
在反序列化 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);
}