贷款
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);
}