Back to Blog

Отчет об аудите безопасности LiNEAR

Code Auditing
April 8, 2022
11 min read

Манифест отчета

Элемент Описание
Клиент LiNEAR Protocol
Цель LiNEAR

История версий

Версия Дата Описание
1.0 1 апреля 2022 г. Первый выпуск

1. Введение

1.1 О целевых контрактах

Информация Описание
Тип Смарт-контракт
Язык Rust
Подход Полуавтоматическая и ручная проверка

Репозиторий, прошедший аудит, включает LiNEAR ^1.

Процесс аудита является итеративным. В частности, мы проверяем коммиты, в которых исправлены обнаруженные проблемы. Если возникают новые проблемы, мы продолжаем этот процесс. Значения SHA коммитов во время аудита представлены ниже. Наш отчет об аудите относится к начальной версии (т.е. версии 1), а также к новому коду (в последующих версиях), исправляющему проблемы, указанные в отчете.

1.2 Модель безопасности

Для оценки риска мы следуем стандартам или рекомендациям, широко принятым в отрасли и академической среде, включая методологию оценки рисков OWASP ^2 и классификацию распространенных слабых мест (CWE) ^3. Общая степень серьезности риска определяется вероятностью и воздействием. В частности, вероятность используется для оценки того, насколько вероятно, что конкретная уязвимость может быть обнаружена и использована злоумышленником, в то время как воздействие используется для измерения последствий успешной эксплуатации.

В этом отчете вероятность и воздействие классифицируются по двум рейтингам: высокий и низкий соответственно, а их комбинации показаны в Таблице 1.1.

Соответственно, серьезность, измеренная в этом отчете, классифицируется по трем категориям: Высокая (High), Средняя (Medium), Низкая (Low). Для полноты картины также используется статус Не определено (Undetermined), чтобы охватить ситуации, когда риск не может быть точно определен.

Кроме того, статус обнаруженной проблемы относится к одной из следующих четырех категорий:

  • Не определено (Undetermined): ответа пока нет.

  • Принято к сведению (Acknowledged): проблема получена клиентом, но еще не подтверждена.

  • Подтверждено (Confirmed): проблема признана клиентом, но еще не исправлена.

  • Исправлено (Fixed): проблема подтверждена и исправлена клиентом.

2. Результаты

Всего в смарт-контракте мы обнаружили 4 потенциальные проблемы. Мы также подготовили 4 рекомендации:

  • Высокий риск: 0

  • Средний риск: 2

  • Низкий риск: 2

  • Рекомендации: 4

ID Серьезность Описание Категория Статус
1 Средняя Потеря точности Безопасность ПО Исправлено
2 Низкая Доступный баланс пользователя может быть временно заблокирован Безопасность DeFi Подтверждено
3 Средняя Неограниченное распределение вознаграждений бенефициарам Безопасность DeFi Исправлено
4 Низкая Запросы пользователей на разблокировку (unstaking) могут быть не выполнены вовремя Безопасность DeFi Исправлено
7 - Функция epoch_update_rewards может не работать из-за неограниченного числа бенефициаров Рекомендация Исправлено
8 - Избыточный код Рекомендация Подтверждено
6 - Отсутствует проверка prepaid_gas в функции ft_transfer_call Рекомендация Исправлено
9 - Риск централизованного проектирования Рекомендация Подтверждено

Подробности приведены в следующих разделах.

2.1 Безопасность ПО

2.1.1 Потенциальная проблема 1: Потеря точности

Информация Описание
Статус Исправлено в версии 2
Добавлено в версии 1

Описание В строке 125 функции internal_calculate_distribution деление выполняется до умножения при расчете переменной 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 эпох.

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 Добавить еще один атрибут (например, available_amount) в структуру Account для хранения доступных NEAR.

Комментарий от проекта Это реализовано намеренно, в соответствии с исходным интерфейсом и дизайном стейкинг-пула. Чтобы решить потенциальную проблему, мы можем добавить поле 'unstaking', чтобы отличать его от 'unstaked', и переносить средства из 'unstaking' в 'unstaked' перед началом следующего процесса разблокировки для этого аккаунта. Но на данный момент мы предпочитаем не вносить подобные изменения, чтобы сохранить согласованность рабочего процесса со стейкинг-пулом. Как временное решение: при выводе средств пользователем через фронтенд, интерфейс будет напоминать пользователям вывести имеющиеся средства, если на аккаунте есть баланс '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%, LiNEAR, выпущенные (minted) для бенефициаров, могут снизить цену LiNEAR после выполнения действия epoch_update_rewards.

Рекомендация I Ввести разумный порог для ограничения общего объема вознаграждений, распределяемых среди бенефициаров.

2.2.3 Потенциальная проблема 4: Запросы пользователей на разблокировку могут быть не выполнены вовремя

Информация Описание
Статус Исправлено в версии 2
Добавлено в версии 1

Описание Количество эпох, возвращаемое функцией get_num_epoch_to_unstake, в некоторых краевых случаях должно удваиваться. Например, если суммарного количества NEAR, поставленных в стейкинг-пулах валидаторов и не находящихся в статусе ожидания, недостаточно, пользователи не смогут вывести все запрошенные разблокированные NEAR через 4 эпохи.

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

Описание Число бенефициаров не ограничено. В этом случае функция epoch_update_rewards, вызывающая функцию internal_distribute_staking_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 может не работать из-за нехватки газа, если бенефициаров слишком много.

Рекомендация 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.

Комментарий от проекта Это верно. То же самое происходит в стандартной реализации FT в стандарте near contract-standards. Мы сохраним неиспользуемый параметр registration_only, чтобы сохранить соответствие стандартному интерфейсу storage_deposit(account_id, registration_only).

2.3.3 Отсутствует проверка prepaid_gas в функции ft_transfer_call

Информация Описание
Статус Исправлено в версии 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 Рекомендуется внедрить в контракт децентрализованные механизмы управления, такие как мультиподпись (multi-sig) или 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 Процедура аудита

Мы выполняем аудит в соответствии со следующей процедурой.

  • Обнаружение уязвимостей Мы сначала сканируем смарт-контракты с помощью автоматических анализаторов кода, а затем вручную проверяем (отклоняем или подтверждаем) сообщенные ими проблемы.

  • Семантический анализ Мы изучаем бизнес-логику смарт-контрактов и проводим дальнейшее исследование возможных уязвимостей с использованием автоматического инструмента фаззинга (разработанного нашей исследовательской группой). Мы также вручную анализируем возможные сценарии атак вместе с независимыми аудиторами для перекрестной проверки результатов.

  • Рекомендации Мы предоставляем разработчикам полезные советы с точки зрения качественного программирования, включая оптимизацию газа, стиль написания кода и т. д.

Ниже приведены основные контрольные точки:

3.2.1 Безопасность ПО

  • Reentrancy (повторный вход)

  • DoS-атаки

  • Контроль доступа

  • Обработка данных и потоки данных

  • Обработка исключений

  • Небезопасные внешние вызовы и потоки управления

  • Согласованность инициализации

  • Работа с событиями (Events)

  • Ошибочная генерация случайных чисел

  • Неправильное использование системы прокси

3.2.2 Безопасность DeFi

  • Семантическая согласованность

  • Функциональная согласованность

  • Контроль доступа

  • Бизнес-логика

  • Операции с токенами

  • Механизмы экстренного реагирования

  • Безопасность оракулов

  • Белые и черные списки

  • Экономическое воздействие

  • Пакетные переводы

3.2.3 Безопасность NFT

  • Дублирование элементов

  • Проверка получателя токена

  • Безопасность офчейн-метаданных

3.2.4 Дополнительные рекомендации

  • Оптимизация газа

  • Качество и стиль кода

::: Note Предыдущие пункты являются основными контрольными точками. В процессе аудита мы можем использовать дополнительные критерии в зависимости от функциональности проекта. :::

Best Security Auditor for Web3

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

BlockSec Audit