报告清单
| 项目 | 描述 |
|---|---|
| 客户 | LiNEAR Protocol |
| 目标 | LiNEAR |
版本历史
| 版本 | 日期 | 描述 |
|---|---|---|
| 1.0 | 2022年4月1日 | 首次发布 |
1. 引言
1.1 关于目标合约
| 信息 | 描述 |
|---|---|
| 类型 | 智能合约 |
| 语言 | Rust |
| 方法 | 半自动和手动验证 |
已审计的存储库包括 LiNEAR ^1。
审计过程是迭代的。具体来说,我们将审计 用于修复已发现问题的提交。如果存在新问题,我们将继续此过程。审计期间的提交 SHA 值如下所示。我们的审计报告负责初始 版本(即版本 1)以及用于 修复审计报告中问题的代码(以下版本)。

1.2 安全模型
为了评估风险,我们遵循广泛 被业界和学术界采用的标准或建议,包括 OWASP 风险评级方法 ^2 和常见弱点枚举 ^3。风险的总体 严重性由可能性和影响决定。 具体来说,可能性用于估计某个漏洞被攻击者发现和利用的可能性,而影响则用于衡量成功利用漏洞的后果。
在本报告中,可能性和影响都分为两个评级,即高和低,它们的组合如 表 1.1 所示。

因此,本报告中测量的严重性分为三类:高、中、低。为 了完整起见,当风险无法很好地确定时,也使用未确定来涵盖这种情况。
此外,已发现问题的状态将属于以下四类之一:
-
未确定 尚未回复。
-
已确认 客户已收到该问题,但尚未确认。
-
已确认 客户已识别出该问题,但尚未修复。
-
已修复 该问题已得到客户确认并修复。
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 将直接添加到用户的未质押余额中。因此,如果用户调用 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。
项目反馈 这是按设计进行的,基本上遵循质押池的原始接口和设计。为了解决潜在问题,我们可以进行增强,添加另一个字段 'unstaking' 以区别于 'unstaked',并在我们开始该账户的下一个取消质押流程之前,将 'unstaking' 移入 'unstaked'。但目前,我们宁愿不进行更改,以保持工作流程与质押池一致。作为一种变通方法,当用户从前端取消质押时,UI 会在用户账户中有 'unstaked' 金额时提醒用户先提取。
2.2.2 潜在问题 3:对受益人的无限奖励分配
| 信息 | 描述 |
|---|---|
| 状态 | 版本 2 中已修复 |
| 由...引入 | 版本 1 |
描述 此合约在设置新受益人时,不检查所有受益人的总权重。
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%,为受益人铸造的 LiNEAR 可能会在执行 epoch_update_rewards 操作后降低 LiNEAR 的价格。
建议 I 引入合理的阈值来限制分配给受益人的总奖励。
2.2.3 潜在问题 4:用户的取消质押请求可能无法及时得到满足
| 信息 | 描述 |
|---|---|
| 状态 | 版本 2 中已修复 |
| 由...引入 | 版本 1 |
描述 描述 get_num_epoch_to_unstake 函数返回的 epoch 数量在某些极端情况下应加倍。例如,如果验证者质押池中未处于挂起状态的总 NEAR 不足,用户将无法在 4 个 epoch 后提取所有已取消质押的 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
影响 由于受益人过多,epoch_update_rewards 函数可能因 gas 不足而无法正常工作。
建议 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 实现也有同样的情况。我们将保留未使用的 registration_only 参数,以保持与标准 storage_deposit(account_id, 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 免责声明
本审计报告不构成投资建议或个人推荐。它不考虑,也不应被解释为考虑或对代币、代币销售或任何其他产品、服务或其他资产的潜在经济性产生任何影响。任何实体均不应以任何方式依赖本报告,包括用于做出任何买卖任何代币、产品、服务或其他资产的决定。
本审计报告并非对任何特定项目或团队的认可,报告不保证任何特定项目的安全性。本次审计不保证能够发现智能合约的所有安全问题,即评估结果不保证不存在任何进一步的安全问题。由于一次审计不能被认为是全面的,我们始终建议进行独立审计和公开的 bug 赏金计划,以确保智能合约的安全性。
本次审计范围仅限于第 1.1 节中提到的代码。除非另有明确规定,否则语言本身(例如,solidity 语言)、底层编译工具链和计算基础设施的安全性均不在审计范围之内。
3.2 审计流程
我们根据以下流程进行审计。
-
漏洞检测 我们首先使用自动代码分析器扫描智能合约,然后手动验证(拒绝或确认)它们报告的问题。
-
语义分析 我们研究智能合约的业务逻辑,并使用自动模糊测试工具(由我们的研究团队开发)对可能的漏洞进行进一步调查。我们还与独立审计师一起手动分析可能的攻击场景,以交叉检查结果。
-
建议 我们从良好的编程实践的角度为开发人员提供一些有用的建议,包括 gas 优化、代码风格等。
我们在以下展示主要具体检查点。
3.2.1 软件安全
-
重入攻击
-
DoS(拒绝服务)
-
访问控制
-
数据处理和数据流
-
异常处理
-
不受信任的外部调用和控制流
-
初始化一致性
-
事件操作
-
易出错的随机性
-
代理系统的使用不当
3.2.2 DeFi 安全
-
语义一致性
-
功能一致性
-
访问控制
-
业务逻辑
-
代币操作
-
紧急机制
-
预言机安全
-
白名单和黑名单
-
经济影响
-
批量转账
3.2.3 NFT 安全
-
重复项
-
代币接收者验证
-
链下元数据安全
3.2.4 附加建议
-
Gas 优化
-
代码质量和风格
::: 注意 之前的检查点是主要的。在审计过程中,我们可能会根据项目的功能使用更多检查点。 :::



