Back to Blog

LiNEAR 安全審計報告

Code Auditing
April 8, 2022
12 min read

報告清單

項目 說明
客戶 LiNEAR Protocol
目標 LiNEAR

版本歷史

版本 日期 說明
1.0 2022 年 4 月 1 日 首次發布

1. 簡介

1.1 關於目標合約

資訊 說明
類型 智能合約
語言 Rust
方法 半自動與人工驗證

本次審計的代碼庫包含 LiNEAR ^1

審計過程具備迭代性。具體而言,我們將審計修復已發現問題的提交(commit)。若發現新問題,我們將繼續此流程。審計期間的提交 SHA 值如下所示。本審計報告對初始版本(即版本 1)以及用於修復報告中問題的新代碼(後續版本)負責。

1.2 安全模型

為了評估風險,我們遵循產業界與學術界廣泛採用的標準或建議,包括 OWASP 風險評級方法論 ^2 和常見缺陷枚舉 (Common Weakness Enumeration, CWE) ^3。整體的風險嚴重程度可能性影響決定。具體而言,可能性用於估計攻擊者發現並利用特定漏洞的可能性,而影響用於衡量成功利用後的後果。

在本報告中,可能性和影響均分為兩個等級,即,它們的組合如下表 1.1 所示。

相應地,本報告測量的嚴重程度分為三個類別:。為求完整性,我們也使用待定 (Undetermined) 來涵蓋風險無法明確判定的情況。

此外,已發現問題的狀態將歸入以下四類之一:

  • 待定 (Undetermined):尚未收到回應。

  • 已讀取 (Acknowledged):客戶已收到問題,但尚未確認。

  • 已確認 (Confirmed):客戶已認可該問題,但尚未修復。

  • 已修復 (Fixed):客戶已確認並修復該問題。

2. 審計發現

我們在智能合約中總共發現 4 個潛在問題。我們亦提出了 4 項建議,如下:

  • 高風險:0

  • 中風險:2

  • 低風險:2

  • 建議:4

ID 嚴重程度 說明 類別 狀態
1 精度損失 軟體安全 已修復
2 用戶的可用餘額可能被暫時鎖定 DeFi 安全 已確認
3 向受益人無限制發放獎勵 DeFi 安全 已修復
4 用戶的贖回請求可能無法及時響應 DeFi 安全 已修復
7 - epoch_update_rewards 函數可能因受益人無上限而失效 建議 已修復
8 - 冗餘代碼 建議 已確認
6 - ft_transfer_call 函數中遺漏了對 prepaid_gas 的檢查 建議 已修復
9 - 中心化設計的風險 建議 已確認

詳細資訊請見後續章節。

2.1 軟體安全

2.1.1 潛在問題 1:精度損失

資訊 說明
狀態 版本 2 已修復
引入版本 版本 1

說明 在 internal_calculate_distribution 函數的第 125 行,計算 reward_per_session 變數時,先執行了除法,後執行乘法。

fn internal_calculate_distribution(
        &self,
        farm: &Farm,
        total_staked: Balance,
    ) -> Option<RewardDistribution> {
        if farm.start_date > env::block_timestamp() {
            // Farm hasn't started.
            return None;
        }
        let mut distribution = farm.last_distribution.clone();
        if distribution.undistributed == 0 {
            // Farm has ended.
            return Some(distribution);
        }
        distribution.reward_round = (env::block_timestamp() - farm.start_date) / SESSION_INTERVAL;
        let reward_per_session =
            farm.amount / (farm.end_date - farm.start_date) as u128 * SESSION_INTERVAL as u128;

列表 2.1:contracts/linear/src/farm.rs

影響 Rust 語言中的整數除法會進行截斷。在此案例中,整數運算先除後乘可能會導致精度損失。

建議 I 修改計算順序,改為先乘後除。

建議 II 在添加農場時預先計算 reward_per_session 的值。這是因為除非擁有者停止,否則 farm.amount、farm.end_date 和 farm.start_date 在耕作過程中不會改變。

2.2 DeFi 安全

2.2.1 潛在問題 2:用戶的可用餘額可能被暫時鎖定

資訊 說明
狀態 已確認
引入版本 版本 1

說明 用戶存入的 NEAR 將直接加到用戶的「未質押」(unstaked)餘額中。因此,如果用戶調用 unstake/unstake_all 函數,該數量的可用 NEAR 將在未來 0 到 8 個紀元(epoch)內被鎖定。

pub(crate) fn internal_deposit(&mut self, amount: Balance) {
		let account_id = env::predecessor_account_id();
		let mut account = self.internal_get_account(&account_id);
		account.unstaked += amount;
		self.internal_save_account(&account_id, &account);
  
		Event::Deposit {
			account_id: &account_id,
			amount: &U128(amount),
			new_unstaked_balance: &U128(account.unstaked),
		}
		.emit();
	} 

列表 2.2:contracts/linear/src/internal.rs

影響 若用戶不了解此合約的工作流程並直接與之交互,用戶的可用餘額可能會被暫時鎖定。

建議 I 在 Account 結構中添加另一個屬性(例如 available_amount)來維護可用的 NEAR。

項目方回饋 這是刻意設計的,基本上遵循了質押池(staking pool)原始的介面與設計。為了解決此潛在問題,我們可以進行優化,添加另一個欄位 'unstaking' 以區分 'unstaked',並在為該帳戶發起下一次贖回操作前,將 'unstaking' 移入 'unstaked'。但目前我們傾向不進行此項變更,以保持與質押池一致的工作流程。作為臨時方案,當用戶從前端進行贖回時,UI 將提示用戶,若帳戶中存有 'unstaked' 的金額,請先進行提現。

2.2.2 潛在問題 3:向受益人無限制發放獎勵

資訊 說明
狀態 版本 2 已修復
引入版本 版本 1

說明 在設置新受益人時,該合約的 assert_valid 函數沒有檢查所有受益人的總權重。

pub fn set_beneficiary(&mut self, account_id: AccountId, fraction: Fraction) {
		self.assert_owner();
		fraction.assert_valid();
		self.beneficiaries.insert(&account_id, &fraction);
	} 

列表 2.3:contracts/linear/src/owner.rs

pub fn assert_valid(&self) {
		require!(self.denominator != 0, ERR_FRACTION_BAD_DENOMINATOR);
		require!(
			self.numerator <= self.denominator,
			ERR_FRACTION_BAD_NUMERATOR
		);
	} 

列表 2.4:contracts/linear/src/utils.rs

影響 一旦受益人的總權重超過 100%,在執行 epoch_update_rewards 操作後,為受益人鑄造的 LiNEAR 可能會降低 LiNEAR 的價格。

建議 I 引入合理的閾值來限制發放給受益人的獎勵總額。

2.2.3 潛在問題 4:用戶的贖回請求可能無法及時響應

資訊 說明
狀態 版本 2 已修復
引入版本 版本 1

說明 get_num_epoch_to_unstake 函數返回的紀元數量在某些情況下應該加倍。例如,如果驗證節點質押池中未處於掛起狀態的總質押 NEAR 不足,用戶將無法在 4 個紀元後提取所有請求的贖回 NEAR。

pub fn get_num_epoch_to_unstake(&self, _amount: u128) -> EpochHeight {
		// the num of epoches can be doubled or trippled if not enough stake is available
		NUM_EPOCHS_TO_UNLOCK
	} 

列表 2.5:contracts/linear/src/validator_pool.rs

影響 用戶的贖回請求可能無法總是及時得到滿足。

建議 I 實施一種策略,根據驗證節點質押池的狀態來預測用戶的贖回等待時間。

2.3 其他建議

2.3.1 epoch_update_rewards 函數可能因受益人無上限而失效

資訊 說明
狀態 版本 2 已修復
引入版本 版本 1

說明 受益人的數量沒有限制。在這種情況下,調用 internal_distribute_staking_rewards 函數的 epoch_update_rewards 函數可能會耗盡執行所需的 Gas。

pub fn epoch_update_rewards(&mut self, validator_id: AccountId) {
	let min_gas = GAS_EPOCH_UPDATE_REWARDS + GAS_EXT_GET_BALANCE + GAS_CB_VALIDATOR_GET_BALANCE;
	require!(
		env::prepaid_gas() >= min_gas,
		format!("{}. require at least {:?}", ERR_NO_ENOUGH_GAS, min_gas)
	);

	let validator = self
		.validator_pool
		.get_validator(&validator_id)
		.expect(ERR_VALIDATOR_NOT_EXIST);

	if validator.staked_amount == 0 && validator.unstaked_amount == 0 {
		return;
	}

	validator
		.refresh_total_balance()
		.then(ext_self_action_cb::validator_get_balance_callback(
			validator.account_id,
			env::current_account_id(),
			NO_DEPOSIT,
			GAS_CB_VALIDATOR_GET_BALANCE,
		));
}

列表 2.6:contracts/linear/src/epoch_actions.rs

/// When there are rewards, a part of them will be
	/// given to executor, manager or treasury by minting new LiNEAR tokens.
	pub(crate) fn internal_distribute_staking_rewards(&mut self, rewards: Balance) {
		let hashmap: HashMap<AccountId, Fraction> = self.internal_get_beneficiaries();
		for (account_id, fraction) in hashmap.iter() {
			let reward_near_amount: Balance = fraction.multiply(rewards);
			// mint extra LiNEAR for him
			self.internal_mint_beneficiary_rewards(&account_id, reward_near_amount);
		}
	} 

列表 2.7:contract/src/internal.rs

影響 當受益人數量過多時,由於 Gas 限制,epoch_update_rewards 函數可能會失效。

建議 I 建議增加合理的閾值來限制受益人的數量。

2.3.2 冗餘代碼

資訊 說明
狀態 已確認
引入版本 版本 1

說明 參數 registration_only 是冗餘的,因為 storage_deposit 函數並未針對該參數實現任何邏輯。

fn storage_deposit(
		&mut self,
		account_id: Option<AccountId>,
		registration_only: Option<bool>,
	) -> StorageBalance {
		let amount: Balance = env::attached_deposit();
		let account_id = account_id.unwrap_or_else(env::predecessor_account_id);
		if let Some(account) = self.accounts.get(&account_id) {
			log!("The account is already registered, refunding the deposit");
			if amount > 0 {
				Promise::new(env::predecessor_account_id()).transfer(amount);
			}
		} else {
			let min_balance = self.storage_balance_bounds().min.0;
			if amount < min_balance {
				env::panic_str("The attached deposit is less than the minimum storage balance");
			}
  
			self.internal_register_account(&account_id);
			let refund = amount - min_balance;
			if refund > 0 {
				Promise::new(env::predecessor_account_id()).transfer(refund);
			}
		}
		self.internal_storage_balance_of(&account_id).unwrap()
	} 

列表 2.8:contracts/linear/src/fungible_token/storage.rs

建議 I 建議在 storage_deposit 函數中移除此未使用的參數。

項目方回饋 確實如此。這同樣發生在 near-contract-standards crate 的標準 FT 實現中。為了保持與標準 storage_deposit(account_id, registration_only) 介面的一致性,我們將保留未使用的 registration_only 參數。

2.3.3 ft_transfer_call 函數中遺漏對 prepaid_gas 的檢查

資訊 說明
狀態 版本 2 已修復
引入版本 版本 1

說明 應檢查 prepaid_gas 以確保其足以支持 ft_on_transfer 和 ft_resolve_transfer 等目標函數。

#[payable]
	fn ft_transfer_call(
		&mut self,
		receiver_id: AccountId,
		amount: U128,
		memo: Option<String>,
		msg: String,
	) -> PromiseOrValue<U128> {
		assert_one_yocto();
		let sender_id = env::predecessor_account_id();
		let amount = amount.into();
		self.internal_ft_transfer(&sender_id, &receiver_id, amount, memo);
		// Initiating receiver's call and the callback
		ext_fungible_token_receiver::ft_on_transfer(
			sender_id.clone(),
			amount.into(),
			msg,
			receiver_id.clone(),
			NO_DEPOSIT,
			env::prepaid_gas() - GAS_FOR_FT_TRANSFER_CALL,
		)
		.then(ext_ft_self::ft_resolve_transfer(
			sender_id,
			receiver_id,
			amount.into(),
			env::current_account_id(),
			NO_DEPOSIT,
			GAS_FOR_RESOLVE_TRANSFER,
		))
		.into()
	}
 

列表 2.9:contracts/linear/src/fungible_token/core.rs

建議 I 增加對 prepaid_gas 的檢查。

2.3.4 中心化設計的風險

資訊 說明
狀態 已確認
引入版本 版本 1

說明 該項目具有潛在的中心化問題。

建議 I 建議在合約中引入去中心化設計,例如多重簽名或 DAO。

項目方回饋 I 是的。這已在計畫中,如 github.com/linear-protocol/LiNEAR/issues/60 所述。

建議 II 項目所有人需要確保 OWNER_ROLE/MANAGERS_ROLE 私鑰的安全性,並使用多重簽名方案來降低單點故障的風險。

項目方回饋 II 是的。我們一直在制定安全策略,以降低單點故障的風險。

3. 注意事項與備註

3.1 免責聲明

本審計報告並不構成投資建議或個人推薦。它不考慮(也不應被解釋為考慮或影響)代幣、代幣銷售或任何其他產品、服務或其他資產的潛在經濟價值。任何實體均不應以任何方式依賴本報告,包括作為購買或出售任何代幣、產品、服務或其他資產的決策依據。

本審計報告亦非對任何特定項目或團隊的背書,報告也無法保證任何特定項目的安全性。本審計不保證能夠發現智能合約中存在的所有安全問題,即評估結果不保證未來不會發現進一步的安全問題。由於一次審計難以全面覆蓋,我們始終建議進行獨立審計並建立公開的漏洞獎勵計畫,以確保智能合約的安全性。

本審計的範圍僅限於第 1.1 節中提到的代碼。除非明確說明,否則語言本身(例如 Solidity 語言)、底層編譯工具鏈及計算基礎設施的安全性均不在審計範圍內。

3.2 審計程序

我們根據以下程序執行審計。

  • 漏洞檢測:我們首先使用自動代碼掃描工具對智能合約進行掃描,然後對自動工具報告的問題進行人工驗證(拒絕或確認)。

  • 語義分析:我們研究智能合約的業務邏輯,並使用自動模糊測試工具(由我們的研究團隊開發)對可能的漏洞進行進一步調查。我們還會與獨立審計員一起手動分析可能的攻擊路徑,以交叉驗證結果。

  • 建議:我們從良好程式設計實踐的角度為開發人員提供實用的建議,包括 Gas 優化、代碼風格等。

我們在下方列出主要的具體核對清單。

3.2.1 軟體安全

  • 重入 (Reentrancy)

  • 拒絕服務 (DoS)

  • 存取控制

  • 資料處理與資料流

  • 例外狀況處理

  • 不受信任的外部呼叫與控制流

  • 初始化一致性

  • 事件操作

  • 易錯的隨機性

  • 代理系統的不當使用

3.2.2 DeFi 安全

  • 語義一致性

  • 功能一致性

  • 存取控制

  • 業務邏輯

  • 代幣操作

  • 緊急應變機制

  • 預言機安全

  • 白名單與黑名單

  • 經濟影響

  • 批量轉帳

3.2.3 NFT 安全

  • 重複項目

  • 代幣接收者驗證

  • 鏈下元資料安全

3.2.4 其他建議

  • Gas 優化

  • 代碼品質與風格

::: Note 上述核對清單為主要內容。根據項目的具體功能,我們在審計過程中可能會使用更多的核對項目。 :::

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit