報告清單
| 項目 | 說明 |
|---|---|
| 客戶 | 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 上述核對清單為主要內容。根據項目的具體功能,我們在審計過程中可能會使用更多的核對項目。 :::



