LiNEAR 安全审计报告

这是我们于2022年4月为LiNEAR进行的安 全审计报告。

LiNEAR 安全审计报告

报告清单

项目 描述
客户 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 中缺少对预付 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' 中。但目前,我们倾向于暂时不做更改,以保持工作流程与质押池一致。作为一种变通方法,当用户从前端取款时,如果他们账户中有 'unstaked' 金额,UI 将提醒用户先取款。

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

影响 当受益人过多时,由于 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 实现也是如此。我们将保留未使用的 registration_only 参数,以保持与标准 storage_deposit(account_id, registration_only) 接口的一致性。

2.3.3 函数 ft_transfer_call 中缺少对预付 gas 的检查

信息 描述
状态 在版本 2 中已修复
由...引入 版本 1

描述 应检查预付 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 检查预付 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 Bounty 计划,以确保智能合约的安全性。

本次审计的范围仅限于第 1.1 节中提到的代码。除非另有明确说明,语言本身的安全性(例如 Solidity 语言)、底层编译工具链和计算基础设施均不在审计范围内。

3.2 审计流程

我们按照以下流程进行审计。

  • 漏洞检测 我们首先使用自动代码分析器扫描智能合约,然后手动验证(拒绝或确认)它们报告的问题。

  • 语义分析 我们研究智能合约的业务逻辑,并使用自动模糊测试工具(由我们的研究团队开发)对潜在漏洞进行进一步调查。我们还由独立审计员手动分析可能的攻击场景,以交叉检查结果。

  • 建议 我们从良好的编程实践的角度为开发人员提供一些有用的建议,包括 gas 优化、代码风格等。

我们在下面展示了主要的具体检查点。

3.2.1 软件安全

  • 重入攻击

  • 拒绝服务 (DoS)

  • 访问控制

  • 数据处理和数据流

  • 异常处理

  • 不受信任的外部调用和控制流

  • 初始化一致性

  • 事件操作

  • 易出错的随机性

  • 不当使用代理系统

3.2.2 DeFi 安全

  • 语义一致性

  • 功能一致性

  • 访问控制

  • 业务逻辑

  • 代币操作

  • 紧急机制

  • 预言机安全

  • 白名单和黑名单

  • 经济影响

  • 批量转账

3.2.3 NFT 安全

  • 重复项

  • 代币接收者验证

  • 链下元数据安全

3.2.4 附加建议

  • Gas 优化

  • 代码质量和风格

::: 注意 之前的检查点是主要的。我们可能会根据项目的实际情况在审计过程中使用更多检查点。 :::

Sign up for the latest updates