Rust
Pinocchio Flash Loan

Pinocchio Flash Loan

14 Graduates

貸款

loan 指令是我們閃電貸系統的第一部分。它執行四個關鍵步驟以確保安全和原子性貸款:

  1. 根據用戶想要借貸的數量,反序列化動態數量的賬戶。

  2. 將所有這些貸款保存到 loan "暫存" 賬戶中,並計算 protocol_token_account 需要擁有的最終餘額。

  3. 驗證還款:使用指令內省確認交易結尾處存在有效的還款指令。

  4. 資金轉移:將協議金庫中的所有請求貸款金額轉移到借款人的賬戶。

所需賬戶

  • borrower:請求閃電貸的用戶。必須是簽署者

  • protocol:一個程序派生地址 (PDA),擁有協議的特定費用流動性池。

  • loan:用於保存 protocol_token_account 和它需要擁有的最終 balance 的 "暫存" 賬戶。必須是可變的。

  • token_program:代幣程序。必須是可執行的。

以下是實現:

rust
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:一個動態貸款金額數組,因為用戶可以在一筆交易中請求多筆貸款。

以下是實現方式:

rust
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_firstsplit_at_checked 函數,依次提取指令數據中的 bumpfee,從而允許我們處理剩餘的字節,並使用 core::slice::from_raw_parts() 函數將其直接轉換為 u64 切片,以實現高效解析。

使用 fee 推導出 protocol 程序派生地址,為每個費率級別創建了獨立的流動性池,無需在賬戶中存儲費率數據。這種設計既安全又高效,因為每個具有特定費率的 PDA 只擁有與該費率相關的流動性。如果有人傳遞無效的費率,該費率範圍對應的代幣賬戶將為空,從而自動導致轉賬因資金不足而失敗。

指令邏輯

在反序列化 instruction_dataaccounts 後,我們檢查 amounts 的數量是否等於 token_accounts 的數量除以二。這確保我們擁有請求貸款所需的正確賬戶數量。

rust
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() 計算的,以確保它能夠容納交易的所有貸款數據。

rust
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 循環中填充此切片,處理每筆貸款及其相應的轉賬:

rust
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_accountborrower_token_account,計算協議應得的餘額,將此數據保存到 loan 賬戶中,並轉移代幣。

rust
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 帳戶。

rust
// 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);
}

使用 loan 帳戶並以這種方式結構化指令內省,確保我們實際上不需要在 repay 指令中執行任何內省,因為所有的還款檢查都將由 loan 帳戶處理。

Next Page還款
或跳過到挑戰
準備好參加挑戰了嗎?
Blueshift © 2025Commit: e573eab