Solana小技巧 - 如何限制CPI call

Solana小技巧 - 如何限制CPI call

前言

Solana上由一個Program (Smart contract) 去call另一個Program的做法稱為"Cross-Program Invocation", 通常簡稱為CPI (詳解)。

這篇小技巧講解的, 是在Program上我們如何做到限制Program只能由特定Program所invoke, 甚或是完全阻擋CPI call。


Program中讀取當下Transaction Instruction

Solana的Program中是有方法讀取當下Transaction的serialized資訊的。

(如果你不太認識Transaction/Instruction的概念, 可以先看看我之前寫的這篇介紹 。 )

只要用這個function:
solana_program::sysvar::instructions::get_instruction_relative (相關文檔)

就能讀取當下執行的instruction資訊。

pub struct Instruction {
    pub program_id: Pubkey,
    pub accounts: Vec<AccountMeta, Global>,
    pub data: Vec<u8, Global>,
}

Instruction結構裡有個"program_id"的property, 代表著這個Instruction的最外層入口Program是哪個。

限制最外層instruction program ID

假設當下執行中的program ID是aaaa吧...... 然後我們就去看看最外層Instruction結構的program ID對照下。

(1) 對不上的情況

如果我們看到最外層Instruction結構的program ID是bbbb, 那就100%肯定這一定是經CPI invoke aaaa 的, 要不要限制或阻擋悉隨尊便。

(2) 兩者一致

但假若最外層Instruction結構的program ID也同樣看到是aaaa呢? 那就代表:

  • 可能性1: 這個Instruction就是直接call aaaa的, 沒有經任何CPI。

  • 可能性2: 這個Instruction是先call到這program aaaa的某operation, 然後再經CPI invoke aaaa 自己的當下operation。

  • 可能性3: 這個Instruction是先call到這program aaaa的某operation, 然後再經CPI invoke 到其他的Program, ..., 最後別的Program再經CPI invoke到 aaaa 的當下operation。

一般而言, 依靠可能性1及可能性2, 只要針對Instruction外層program ID做限制, 我們能某程度上做到限制Program只能直接call, 或是只限由可信的program(s)去call, 從而阻擋外部CPI。

可能性3其實也是不存在的。可以參看官方文檔 談到reentrancy問題,現在reentrancy是有限制令這不可能發生的。


程式碼例子

https://github.com/metaplex-foundation/metaplex-program-library/blob/58d10c46e66ca9d9c6288999ca9289c986587c7f/candy-machine/program/src/processor/mint.rs#L151-L164

這段程式碼是metaplex中的實例。

看看handle_mint_nft 這function。

留意115行, 它就是在用get_instruction_relative 讀取當下instruction資訊。

再看看之後用到這variable的是在151-154行。

這裡就是針對最外層入口program ID做檢查, 限制只能由3個內部Program去invoke, 否則就會走進限制的logic。


自己寫的程式碼例子

Anchor上寫防止CPI大概是這樣做:

  1. Accounts裡加個account是指向sysvar (Sysvar1nstructions1111111111111111111111111 ), 用來之後讀取instruction資訊用的。
#[derive(Accounts)]
#[instruction()]
pub struct ABCAccounts<'info> {
    ......

    /// CHECK: safe
    #[account(address = sysvar::instructions::id())]
    pub instruction_sysvar_account: UncheckedAccount<'info>,

    ......
}
  1. 你可以在每個operation底下去個別檢查的。
    但我這裡例子是寫了個macro rule, 強制要求ctx.program_id , 否則throw error。
macro_rules! block_cpi {
        ($ctx:expr) => {{
            let instruction_sysvar_account = &$ctx.accounts.instruction_sysvar_account;
            let instruction_sysvar_account_info = instruction_sysvar_account.to_account_info();
            let instruction = solana_program::sysvar::instructions::get_instruction_relative(
                0,
                &instruction_sysvar_account_info,
            )
            .unwrap();
            require!(
                instruction.program_id == *$ctx.program_id,
                ProgramError::CpiNotAllowed
            );
        }};
    }

參考網址

https://solana.stackexchange.com/questions/2447/is-there-any-way-to-tell-if-a-program-was-invoked-via-cpi-in-a-transaction