从以太坊智能合约到 Solana 程序:两个常见的安全陷阱及其他

从以太坊智能合约到 Solana 程序:两个常见的安全陷阱及其他

以太坊是一个众所周知的支持”图灵完备”智能合约的区块链。Solana 是一个快速发展的区块链,它还支持智能合约——称为 Solana 程序,在每秒交易量和成本方面似乎优于以太坊。

为什么 Solana 程序比以太坊智能合约更快?那么主要区别是什么?本文解释了两者之间的本质区别,并说明了 Solana 程序中的两个常见安全缺陷。

代码和数据,耦合还是解耦合?

在计算机程序中,总是有两件事:

  1. 代码:由计算机的 CPU、GPU 或其他计算单元执行的指令;
  2. 数据:对代码的输入或由代码处理的程序状态。

以太坊和 Solana 的一个本质区别在于代码和数据在智能合约中的表示方式。

在以太坊中,数据和代码是耦合在一起的。以太坊中的智能合约包含代码和代码处理的数据。比如下面用 Solidity 编写的 Ownable 合约中,状态变量 _owner 为数据,函数 owner() 为代码。 这种耦合设计对于编写智能合约来说很直观,代码也很容易理解。然而,这使得以太坊难以实现高性能,因为每个智能合约只有一个状态副本,并且所有触及智能合约相同状态的交易都必须按顺序执行。这正是 Solana 解决的问题。

contract Ownable is Context {    
    address private _owner;
    function owner() public view virtual returns (address) { 
        return _owner;    
    }
}

在 Solana 中,数据和代码是解耦的。 Solana 程序仅包含代码,不包含数据。所有数据都作为 Solana 程序的输入提供。比如下面用 Rust 编写的 Solana hello-world 程序,函数 process_instruction 是代码,状态变量 counteraccount 的数据,作为输入传递给函数。

pub fn process_instruction(
    program_id: &Pubkey,    
    accounts: &[AccountInfo],
    _instruction_data: &[u8], 
) -> ProgramResult {      
    let accounts_iter = &mut accounts.iter(); 
    // Get the account to say hello to    
    let account = next_account_info(accounts_iter)?;  
    // Increment the counter   
    let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
    greeting_account.counter += 1;  
    greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
}

这种解耦设计可以实现高性能,因为它允许在不同输入上并行执行 Solana 程序的多个副本。换句话说,从不同用户账户到同一个 Solana 程序的交易可以并行运行。这就解释了为什么 Solana 可以达到惊人的 50000 TPS。

然而,缺点是编写 Solana 程序比以太坊智能合约更难,无论语言差异如何(例如,Rust 与 Solidity)。特别是,Solana 程序打开了以太坊智能合约中不存在的新攻击面,因为输入帐户可能不受信任。接下来说明 Solana 程序中的两个常见安全缺陷:缺少所有者检查和缺少签名者检查。

Solana 程序中的两个常见安全陷阱

在 Solana 中,由于在调用 Solana 程序时所有帐户都作为输入提供,因此用户可以提供任意帐户,并且没有内置阻止恶意用户使用虚假数据这样做。因此,Solana 程序必须检查输入帐户的有效性。使用 Solana 程序库中的函数 process_set_lending_market_owner 来说明这些检查的重要性。

/// Processes an instruction
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    input: &[u8],
) -> ProgramResult {
    let instruction = LendingInstruction::unpack(input)?;
    match instruction {
        LendingInstruction::InitLendingMarket {
            owner,
            quote_currency,
        } => {
            msg!("Instruction: Init Lending Market");
            process_init_lending_market(program_id, owner, quote_currency, accounts)
        }
        LendingInstruction::SetLendingMarketOwner { new_owner } => {
            msg!("Instruction: Set Lending Market Owner");
            process_set_lending_market_owner(program_id, new_owner, accounts)
        }
        LendingInstruction::InitReserve {
            liquidity_amount,
            config,
        } => {
            msg!("Instruction: Init Reserve");
            process_init_reserve(program_id, liquidity_amount, config, accounts)
        }
        LendingInstruction::RefreshReserve => {
            msg!("Instruction: Refresh Reserve");
            process_refresh_reserve(program_id, accounts)
        }
        LendingInstruction::DepositReserveLiquidity { liquidity_amount } => {
            msg!("Instruction: Deposit Reserve Liquidity");
            process_deposit_reserve_liquidity(program_id, liquidity_amount, accounts)
        }
        LendingInstruction::RedeemReserveCollateral { collateral_amount } => {
            msg!("Instruction: Redeem Reserve Collateral");
            process_redeem_reserve_collateral(program_id, collateral_amount, accounts)
        }
        LendingInstruction::InitObligation => {
            msg!("Instruction: Init Obligation");
            process_init_obligation(program_id, accounts)
        }
        LendingInstruction::RefreshObligation => {
            msg!("Instruction: Refresh Obligation");
            process_refresh_obligation(program_id, accounts)
        }
        LendingInstruction::DepositObligationCollateral { collateral_amount } => {
            msg!("Instruction: Deposit Obligation Collateral");
            process_deposit_obligation_collateral(program_id, collateral_amount, accounts)
        }
        LendingInstruction::WithdrawObligationCollateral { collateral_amount } => {
            msg!("Instruction: Withdraw Obligation Collateral");
            process_withdraw_obligation_collateral(program_id, collateral_amount, accounts)
        }
        LendingInstruction::BorrowObligationLiquidity {
            liquidity_amount,
            slippage_limit,
        } => {
            msg!("Instruction: Borrow Obligation Liquidity");
            process_borrow_obligation_liquidity(
                program_id,
                liquidity_amount,
                slippage_limit,
                accounts,
            )
        }
        LendingInstruction::RepayObligationLiquidity { liquidity_amount } => {
            msg!("Instruction: Repay Obligation Liquidity");
            process_repay_obligation_liquidity(program_id, liquidity_amount, accounts)
        }
        LendingInstruction::LiquidateObligation { liquidity_amount } => {
            msg!("Instruction: Liquidate Obligation");
            process_liquidate_obligation(program_id, liquidity_amount, accounts)
        }
        LendingInstruction::FlashLoan { amount } => {
            msg!("Instruction: Flash Loan");
            process_flash_loan(program_id, amount, accounts)
        }
        LendingInstruction::ModifyReserveConfig { new_config } => {
            msg!("Instruction: Modify Reserve Config");
            process_modify_reserve_config(program_id, new_config, accounts)
        }
    }
}

上面的功能是更新借贷市场的所有者为 new_owner 。有两种类型的检查:所有者检查和签名者检查。缺少其中任何一个都可能使该功能容易受到攻击。

所有者检查器

if &lending_market.owner != lending_market_owner_info.key

签名者检查器

if !lending_market_owner_info.is_signer