前言
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 invokeaaaa
自己的當下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是有限制令這不可能發生的。
程式碼例子
這段程式碼是metaplex中的實例。
看看handle_mint_nft
這function。
留意115行, 它就是在用get_instruction_relative
讀取當下instruction資訊。
再看看之後用到這variable的是在151-154行。
這裡就是針對最外層入口program ID做檢查, 限制只能由3個內部Program去invoke, 否則就會走進限制的logic。
自己寫的程式碼例子
Anchor上寫防止CPI大概是這樣做:
- 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>,
......
}
- 你可以在每個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
);
}};
}