从以太坊智能合约到 Solana 程序:两个常见的安全陷阱及其他
以太坊是一个众所周知的支持”图灵完备”智能合约的区块链。Solana 是一个快速发展的区块链,它还支持智能合约——称为 Solana 程序,在每秒交易量和成本方面似乎优于以太坊。
为什么 Solana 程序比以太坊智能合约更快?那么主要区别是什么?本文解释了两者之间的本质区别,并说明了 Solana 程序中的两个常见安全缺陷。
代码和数据,耦合还是解耦合?
在计算机程序中,总是有两件事:
- 代码:由计算机的 CPU、GPU 或其他计算单元执行的指令;
- 数据:对代码的输入或由代码处理的程序状态。
以太坊和 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
是代码,状态变量 counter
是 account
的数据,作为输入传递给函数。
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