Back to Blog

Отчёт о тестировании безопасности Radiant V2

Code Auditing
March 23, 2023
33 min read

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

Пункт Описание
Клиент Radiant Capital
Цель Radiant V2

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

Версия Дата Описание
1.0 15 марта 2023 г. Первая версия
2.0 21 марта 2023 г. Вторая версия

1. Введение

1.1 О тестировании безопасности

Мы были приглашены компанией Radiant Capital для проведения тестирования безопасности (в роли красной команды) смарт-контрактов Radiant V2 с целью выявления потенциальных рисков. Как ответственная команда, Radiant Capital серьёзно относится к безопасности. Поэтому команда решила приложить дополнительные усилия для защиты смарт-контрактов, несмотря на то что они уже прошли аудит у нескольких компаний по безопасности ^1.

Обратите внимание, что тестирование безопасности отличается от аудита безопасности как по целям, так и по требованиям. В частности, тестирование безопасности направлено на обнаружение дополнительных/нестандартных уязвимых точек путём имитации действий злоумышленников с целью нарушения работы программы/протокола, тогда как аудит безопасности направлен на проведение относительно всестороннего контроля безопасности путём перечисления возможных поверхностей атаки. Таким образом, тестирование безопасности может не охватывать некоторые сложные логические ошибки, которые могли бы быть выявлены при аудите безопасности, в силу ограниченности времени и ресурсов.

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

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

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

Обратите внимание, что данный отчёт охватывает только смарт-контракты в папке radiant_v2.1.1/contracts данного репозитория, включая:

  • bounties
  • deployments
  • flashloan
  • leverage
  • lock
  • oracles
  • staking
  • zap
  • eligibility
  • misc
  • oft
  • protocol
  • stargate

После обновления в Версии 8 файлы, охваченные данным тестированием безопасности, включают:

  • lending/AaveOracle.sol
  • lending/AaveProtocolDataProvider.sol
  • lending/ATokensAndRatesHelper.sol
  • lending/StableAndVariableTokensHelper.sol
  • lending/UiPoolDataProviderV2V3.sol
  • lending/UiPoolDataProvider.sol
  • lending/WETHGateway.sol
  • lending/WalletBalanceProvider.sol
  • lending/configuration
  • lending/flashloan
  • lending/lendingpool
  • lending/tokenization
  • radiant/accessories
  • radiant/eligibility
  • radiant/oracles
  • radiant/staking
  • radiant/token
  • radiant/zap

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

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

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

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

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

  • Неопределённый Ответ ещё не получен.

  • Принят к сведению Пункт был получен клиентом, но ещё не подтверждён.

  • Подтверждён Пункт был признан клиентом, но ещё не исправлен.

  • Исправлен Пункт был подтверждён и исправлен клиентом.

2. Автоматизированное тестирование безопасности

2.1 Автоматизированное статическое тестирование безопасности

Мы используем собственный инструмент статического анализа на основе Slither для проверки наличия уязвимостей. После ручной проверки результатов проблем обнаружено не было. Подробные результаты тестирования можно найти в Таблице 4.1 в Приложении.

2.2 Автоматизированное динамическое тестирование безопасности

Мы используем методы фаззинга для проверки надёжности, устойчивости и точности целевых контрактов. В частности, начальное состояние в процессе фаззинга определяется на основе семантики функций и тестовых скриптов контракта. Для имитации среды в блокчейне мы также поддерживаем набор адресов, взаимодействовавших с контрактами LendingPool и MultiFeeDistribution.

Наш фаззер также учитывает семантику функций при генерации последовательностей транзакций. Например, функция stake в контракте MultiFeeDistribution и функция deposit в контракте LendingPool, вероятно, вызываются первыми в последовательности. Мутация параметров функций и последовательности направляется покрытием кода контракта. Если определённый параметр или последовательность обеспечивает более высокое покрытие кода, он будет иметь более высокий приоритет для мутации в следующем раунде фаззинга. Для исследования путей, ограниченных магическими числами, мы собираем значения, считываемые из хранилища (т.е. инструкция SLOAD) во время выполнения, и используем их для генерации параметров функций в процессе мутации.

Всего мы сгенерировали 100 000 тестовых случаев и использовали 31 оракул, который применяется для обнаружения сбоев. Каждый тестовый случай содержит 30 транзакций с указанным порядком. В итоге нами была обнаружена одна критическая проблема (т.е. Раздел 3.2.6), которая также была выявлена в ходе ручного тестирования безопасности. Подробные результаты тестирования можно найти в Таблицах 4.2, 4.3 и 4.4 в Приложении.

3. Ручное тестирование безопасности

Мы прикладываем ручные усилия для понимания общей архитектуры и взаимодействий между различными модулями, а затем проводим тестирование безопасности на основе наших знаний о потенциальных поверхностях атаки, полученных из предыдущих исследований и опыта.

Всего мы обнаружили семнадцать потенциальных проблем. Кроме того, у нас есть три рекомендации и одна заметка:

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

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

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

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

  • Заметки: 1

ID Критичность Описание Категория Статус
1 Средняя Отсутствует зарезервированный интерфейс для сброса указателей на функции Безопасность ПО Исправлено
2 Средняя Некорректное вычисление оракула Безопасность DeFi Исправлено
3 Высокая Потенциальный слив средств через BaseBounty Безопасность DeFi Исправлено
4 Низкая Потенциально недействительные расписания эмиссии Безопасность DeFi Исправлено
5 Низкая Пропускаемые расписания эмиссии Безопасность DeFi Подтверждено
6 Средняя Изменяемый курс обмена во время миграции Безопасность DeFi Исправлено
7 Высокая Некорректная реализация _transfer() (I) Безопасность DeFi Исправлено
8 Низкая Отсутствие проверки Period в UniV2TwapOracle Безопасность DeFi Исправлено
9 Средняя Невозвращаемые пылевые токены Безопасность DeFi Исправлено
10 Средняя Некорректная реализация _transfer() (II) Безопасность DeFi Исправлено
11 Средняя Манипулируемые составные вознаграждения Безопасность DeFi Исправлено
12 Средняя Отсутствие контроля доступа в setLeverager() Безопасность DeFi Исправлено
13 Средняя Отсутствие проверки проскальзывания в addLiquidityWETHOnly() Безопасность DeFi Подтверждено
14 Низкая Отсутствие проверки borrowRatio в loopETH() Безопасность DeFi Исправлено
15 Низкая Отсутствие проверки длины между assets и poolIDs в setPoolIDs() Безопасность DeFi Исправлено
16 Низкая Отсутствие отзыва привилегии mint в addBountyContract() Безопасность DeFi Подтверждено
17 Низкая Minters могут быть назначены только один раз Безопасность DeFi Подтверждено
18 - Оптимизация газа (zapVestingToLp() в Mfd) Рекомендация Исправлено
19 - Непустой резерв баунти в BountyManager Рекомендация Исправлено
20 - Несогласованное именование в requiredUsdValue() Рекомендация Подтверждено
21 - Заметка об устаревшем MFDPlus Заметка Подтверждено

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

3.1 Безопасность программного обеспечения

3.1.1 Потенциальная проблема 1: Отсутствует зарезервированный интерфейс для сброса указателей на функции

Пункт Описание
Критичность Средняя
Статус Исправлено в Версии 7
Введено в Версии 1

Описание Три функции — getLpMfdBounty(), getChefBounty() и getAutoCompoundBounty() — вызываются через указатели на функции в контракте BountyManager. При этом наследование от OwnableUpgradable указывает на то, что данный контракт является реализацией прокси. Это означает, что реализация контракта может быть обновлена в будущем, что порождает проблему, связанную с указателями на функции.

function initialize(
        address _rdnt,
        address _weth,
        address _lpMfd,
        address _mfd,
        address _chef,
        address _priceProvider,
        address _eligibilityDataProvider,
        uint256 _hunterShare,
        uint256 _baseBountyUsdTarget,
        uint256 _maxBaseBounty,
        uint256 _bountyBooster
    ) external initializer {
        require(_rdnt != address(0));
        require(_weth != address(0));
        require(_lpMfd != address(0));
        require(_mfd != address(0));
        require(_chef != address(0));
        require(_priceProvider != address(0));
        require(_eligibilityDataProvider != address(0));
        require(_hunterShare <= 10000);
        require(_baseBountyUsdTarget != 0);
        require(_maxBaseBounty != 0);
 
        rdnt = _rdnt;
        weth = _weth;
        lpMfd = _lpMfd;
        mfd = _mfd;
        chef = _chef;
        priceProvider = _priceProvider;
        eligibilityDataProvider = _eligibilityDataProvider;
 
        HUNTER_SHARE = _hunterShare;
        baseBountyUsdTarget = _baseBountyUsdTarget;
        bountyBooster = _bountyBooster;
        maxBaseBounty = _maxBaseBounty;
 
        bounties[1] = getLpMfdBounty;
        bounties[2] = getChefBounty;
        bounties[3] = getAutoCompoundBounty;
        bountyCount = 3;
 
        slippageLimit = 10;
        minDLPBalance = uint256(5).mul(10 ** 18);
 
 
        __Ownable_init();
        __Pausable_init();
    } 

Листинг 3.1: BountyManager.sol

Воздействие При изменении смещений вышеупомянутых трёх функций указатели на функции не смогут работать должным образом, и вся логика контракта может быть нарушена.

Рекомендация Контракт должен предоставлять интерфейсы для сброса указателей на функции.

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

3.2.1 Потенциальная проблема 2: Некорректное вычисление оракула

Пункт Описание
Критичность Средняя
Статус Исправлено в Версии 11
Введено в Версии 1 и Версии 4

Описание Функция consult() в контракте ComboOracle используется для вычисления средней цены из нескольких источников. В реализации версии 1 для вычисления итоговой цены используется среднее арифметическое, которое может быть манипулировано путём воздействия на один из оракулов-источников.

function consult() public view override returns (uint256 price) {
        require(sources.length != 0);

        uint256 sum;
        for (uint256 i = 0; i < sources.length; i++) {
            uint256 price = sources[i].consult();
            require(price != 0, "source consult failure");
            sum = sum.add(price);
        }
        price = sum.div(sources.length);
    }

Листинг 3.2: ComboOracle.sol

В реализации версии 4, когда средняя цена превышает наименьшую цену×1,025, возвращается наименьшая цена. Однако возвращаемое значение всё ещё может быть манипулировано, если результат, возвращённый одним из оракулов-источников, аномально низок.

/**
    * @notice Calculated price
    * @return price Average price of several sources.
    */
   function consult() public view override returns (uint256 price) {
       require(sources.length != 0);

       uint256 sum;
       uint256 lowestPrice;
       for (uint256 i = 0; i < sources.length; i++) {
           uint256 price = sources[i].consult();
           require(price != 0, "source consult failure");
           if (lowestPrice == 0) {
               lowestPrice = price;
           } else {
               lowestPrice = lowestPrice > price ? price : lowestPrice;
           }
           sum = sum.add(price);
       }
       price = sum.div(sources.length);
       price = price > ((lowestPrice * 1025) / 1000) ? lowestPrice : price;
   }

Листинг 3.3: ComboOracle.sol

Воздействие Цена, возвращаемая контрактом ComboOracle, может быть манипулирована, что позволяет злоумышленнику извлечь из этого прибыль.

Рекомендация Мы предлагаем использовать медианное значение вместо среднего значения. Если оракулов-источников всего два и возникает значительное расхождение, разумнее отменить транзакцию, когда среднее значение цены значительно превышает наименьшую цену.

Обратная связь Будет использоваться только два оракула-источника. Если возникнет значительное расхождение, мы используем OZ Defender Sentinel для приостановки связанных контрактов.

Заметка Контракт ComboOracle удалён и больше не используется.

3.2.2 Потенциальная проблема 3: Потенциальный слив средств через BaseBounty

Пункт Описание
Критичность Высокая
Статус Исправлено в Версии 4
Введено в Версии 1

Описание Пользователь может заблокировать токены (т.е. RDNT) на фиксированный срок для получения вознаграждений. По истечении срока блокировки другие пользователи могут вызвать функцию executeBounty() для повторной блокировки токенов этого пользователя с целью получения BaseBounty, если у данного пользователя включена функция AutoRelock. В процессе повторной блокировки истёкшие блокировки очищаются и повторно вносятся в пул во внутренней функции _cleanWithdrawableLocks(). Однако существует переменная maxLockWithdrawPerTxn, ограничивающая максимальное количество блокировок, которые могут быть очищены. В таком случае неочищенные истёкшие блокировки могут всё ещё существовать даже после выполнения функции executeBounty(). Это может позволить обойти проверку в строке 106 функции claimBounty() в контракте MFDPlus. В результате issueBaseBounty будет установлено как true и возвращено обратно.

**
    * @notice Withdraw all lockings tokens where the unlock time has passed
    */
   function _cleanWithdrawableLocks(
       address user,
       uint256 totalLock,
       uint256 totalLockWithMultiplier
   ) internal returns (uint256 lockAmount, uint256 lockAmountWithMultiplier) {
       LockedBalance[] storage locks = userLocks[user];

       if (locks.length != 0) {
           uint256 length = locks.length <= maxLockWithdrawPerTxn ? locks.length : maxLockWithdrawPerTxn;
           for (uint256 i = 0; i < length; ) {
               if (locks[i].unlockTime <= block.timestamp) {
                   lockAmount = lockAmount.add(locks[i].amount);
                   lockAmountWithMultiplier = lockAmountWithMultiplier.add(
                       locks[i].amount.mul(locks[i].multiplier)
                   );
                   locks[i] = locks[locks.length - 1];
                   locks.pop();
                   length = length - 1;
               } else {
                   i = i + 1;
               }
           }
           if (locks.length == 0) {
               lockAmount = totalLock;
               lockAmountWithMultiplier = totalLockWithMultiplier;
               delete userLocks[user];

               userlist.removeFromList(user);
           }
       }
   }

Листинг 3.4: MultiFeeDistribution.sol

В частности, злоумышленник может застейкать 1 wei токена с одинаковым сроком истечения несколько раз, что значительно превышает maxLockWithdrawPerTxn. После этого злоумышленник может установить действие как getLpMfdBounty и многократно вызывать executeBounty(). Поскольку количество очищаемых блокировок ограничено maxLockWithdrawPerTxn, BaseBounty в контракте BountyManager может быть полностью слит злоумышленником.

Воздействие Злоумышленник может слить все средства из контракта BountyManager в одной транзакции, нарушая задуманный механизм баунти.

Рекомендация Убедитесь, что функция _cleanWithdrawableLocks() может очистить все истёкшие блокировки, и установите минимальную сумму стейкинга в функции _stake().

3.2.3 Потенциальная проблема 4: Потенциально недействительные расписания эмиссии

Пункт Описание
Критичность Низкая
Статус Исправлено в Версии 10
Введено в Версии 1

Описание В контракте ChefIncentivesController функция setEmissionSchedule() вызывается владельцем для установки расписаний для различных ставок вознаграждений. В этом случае время начала для каждого расписания (_startTimeOffsets[i] + startTime) должно быть проверено на то, что оно больше текущей метки времени. Однако проверяется только первый элемент в _startTimeOffsets, чего недостаточно. Кроме того, _startTimeOffsets[i] преобразуется из uint256 в uint128 при добавлении в emissionSchedule, что может привести к усечению, если исходные входные данные слишком велики.

function setEmissionSchedule(
        uint256[] calldata _startTimeOffsets,
        uint256[] calldata _rewardsPerSecond
    ) external onlyOwner {
        uint256 length = _startTimeOffsets.length;
        require(length > 0 && length == _rewardsPerSecond.length, "empty or mismatch params");
        if (startTime > 0) {
            require(_startTimeOffsets[0] > block.timestamp.sub(startTime), "invalid start time");
        }
 
        for (uint256 i = 0; i < length; i++) {
            emissionSchedule.push(
                EmissionPoint({
                    startTimeOffset: uint128(_startTimeOffsets[i]),
                    rewardsPerSecond: uint128(_rewardsPerSecond[i])
                })
            );
        }
        emit EmissionScheduleAppended(_startTimeOffsets, _rewardsPerSecond);
    } 

Листинг 3.5: ChefIncentivesController.sol

Воздействие Если _startTimeOffsets не расположены в порядке возрастания, некоторые обещанные вознаграждения не будут распределены пользователям. Если _startTimeOffsets[i] выходит за пределы диапазона uint128, будет добавлено недействительное расписание эмиссии.

Рекомендация Убедитесь, что _startTimeOffsets расположены в порядке возрастания и все элементы находятся в диапазоне uint128.

3.2.4 Потенциальная проблема 5: Пропускаемые расписания эмиссии

Пункт Описание
Критичность Низкая
Статус Подтверждено
Введено в Версии 1

Описание В контракте ChefIncentivesController функция setScheduleRewardsPerSecond() будет перебирать emissionSchedule для поиска целевого расписания с наибольшим индексом, которое уже началось, и обновлять ставку вознаграждения соответствующим образом. Однако в этом случае некоторые расписания эмиссии могут быть пропущены.

function setScheduledRewardsPerSecond() internal {
		if (!persistRewardsPerSecond) {
			uint256 length = emissionSchedule.length;
			uint256 i = emissionScheduleIndex;
			uint128 offset = uint128(block.timestamp.sub(startTime));
			for (; i < length && offset >= emissionSchedule[i].startTimeOffset; i++) {}
			if (i > emissionScheduleIndex) {
				emissionScheduleIndex = i;
				_massUpdatePools();
				rewardsPerSecond = uint256(emissionSchedule[i - 1].rewardsPerSecond);
			}
		}
	}

Листинг 3.6: ChefIncentivesController.sol

Воздействие Если функция setScheduledRewardsPerSecond() долго не вызывается, некоторые обещанные вознаграждения могут не быть распределены пользователям.

Рекомендация Функция setScheduledRewardsPerSecond() вызывается внутри функций claim() и _handleActionAfterForToken(), поэтому единственный способ пропустить расписание эмиссии — это отсутствие взаимодействий с протоколом в течение эпохи эмиссии.

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

Пункт Описание
Критичность Средняя
Статус Исправлено в Версии 5
Введено в Версии 1

Описание Контракт Migration реализован для обмена пользователями tokenV1 на tokenV2 по указанному exchangeRate. Однако в процессе миграции этот exchangeRate всё ещё может быть скорректирован владельцем через функцию setExchangeRate().

/**
    * @notice Migrate from V1 to V2
    * @param amount of V1 token
    */
   function exchange(uint256 amount) external whenNotPaused {
       uint256 v1Decimals = tokenV1.decimals();
       uint256 v2Decimals = tokenV2.decimals();

       uint256 outAmount = amount.mul(1e4).div(exchangeRate).mul(10**v2Decimals).div(10**v1Decimals);
       tokenV1.safeTransferFrom(_msgSender(), address(this), amount);
       tokenV2.safeTransfer(_msgSender(), outAmount);

       emit Migrate(_msgSender(), amount, outAmount);
   }

Листинг 3.7: Migration.sol

Воздействие Это будет несправедливо по отношению к другим пользователям, если exchangeRate изменится в процессе миграции.

Рекомендация После начала миграции exchangeRate должен быть зафиксирован.

3.2.6 Потенциальная проблема 7: Некорректная реализация _transfer() (I)

Пункт Описание
Критичность Высокая
Статус Исправлено в Версии 7
Введено в Версии 1

Описание В контракте IncentivizedERC20 функция _transfer() не учитывает ситуацию, когда отправитель и получатель могут быть одним и тем же аккаунтом (так называемый перевод самому себе). В частности, если отправитель равен получателю, баланс отправителя будет перезаписан при обновлении баланса получателя. В этом случае злоумышленник может бесконечно увеличивать свой баланс, переводя средства на свой собственный аккаунт многократно.

function _transfer(
        address sender,
        address recipient,
        uint256 amount
      ) internal virtual {
        require(sender != address(0), 'ERC20: transfer from the zero address');
        require(recipient != address(0), 'ERC20: transfer to the zero address');
    
        _beforeTokenTransfer(sender, recipient, amount);
    
        uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
        uint256 recipientBalance = _balances[recipient].add(amount);
    
        if (address(_getIncentivesController()) != address(0)) {
          // uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionBefore(sender);
          if (sender != recipient) {
            _getIncentivesController().handleActionBefore(recipient);
          }
        }
    
        _balances[sender] = senderBalance;
        _balances[recipient] = recipientBalance;
    
        if (address(_getIncentivesController()) != address(0)) {
          uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
          if (sender != recipient) {
            _getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
          }
        }
      }

Листинг 3.8: IncentivizedERC20.sol

Воздействие Токены могут быть выпущены в неограниченном количестве.

Рекомендация Реализуйте функцию _transfer() корректно. Например, в соответствии со стандартной реализацией _transfer() для ERC20 в OpenZeppelin.

_balances[sender] = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
_balances[recipient] = _balances[recipient].add(amount);

Листинг 3.9: ERC20.sol в OpenZeppelin

3.2.7 Потенциальная проблема 8: Отсутствие проверки Period в UniV2TwapOracle

Пункт Описание
Критичность Низкая
Статус Исправлено в Версии 9
Введено в Версии 1

Описание В контракте UniV2TwapOracle атрибут _period не проверяется в функциях initialize() и setPeriod().

function initialize(
        address _pair,
        address _rdnt,
        address _ethChainlinkFeed,
        uint _period,
        uint _consultLeniency,
        bool _allowStaleConsults
    ) external initializer {
        __Ownable_init();

        pair = IUniswapV2Pair(_pair);
        token0 = pair.token0();
        token1 = pair.token1();
        price0CumulativeLast = pair.price0CumulativeLast(); // Fetch the current accumulated price value (1 / 0)
        price1CumulativeLast = pair.price1CumulativeLast(); // Fetch the current accumulated price value (0 / 1)
        uint112 reserve0;
        uint112 reserve1;
        (reserve0, reserve1, blockTimestampLast) = pair.getReserves();
        require(reserve0 != 0 && reserve1 != 0, 'UniswapPairOracle: NO_RESERVES'); // Ensure that there's liquidity in the pair

        PERIOD = _period;
        CONSULT_LENIENCY = _consultLeniency;
        ALLOW_STALE_CONSULTS = _allowStaleConsults;

        baseInitialize(_rdnt, _ethChainlinkFeed);
    }

    function setPeriod(uint _period) external onlyOwner {
        PERIOD = _period;
    }

Листинг 3.10: UniV2TwapOracle.sol

Воздействие В этом случае оракул может возвращать неожиданное значение, если _period слишком мал.

Рекомендация Установите минимальное ограничение на _period в функциях initialize и setPeriod.

3.2.8 Потенциальная проблема 9: Невозвращаемые пылевые токены

Пункт Описание
Критичность Средняя
Статус Исправлено в Версии 5
Введено в Версии 1

Описание В контракте UniswapPoolHelper функция zapWETH() предназначена для помощи пользователю в конвертации токенов WETH в LP-токены. Она вызывает функцию addLiquidityWETHOnly() для добавления ликвидности в пул для получения LP-токенов. В этом процессе могут образовываться пылевые токены, которые должны возвращаться пользователям. Однако UniswapPoolHelper не реализует такую функциональность для обработки этих пылевых токенов.

function zapWETH(uint256 amount)
    public
    returns (uint256 liquidity)
{
    IWETH WETH = IWETH(wethAddr);
    WETH.transferFrom(msg.sender, address(liquidityZap), amount);
    liquidity = liquidityZap.addLiquidityWETHOnly(amount, address(this));
    IERC20 lp = IERC20(lpTokenAddr);
    
    liquidity = lp.balanceOf(address(this));
    lp.safeTransfer(msg.sender, liquidity);
}

Листинг 3.11: UniswapPoolHelper.sol

Воздействие Пылевые токены останутся в контракте и могут быть извлечены другими через функцию zapTokens(0,0).

Рекомендация Реализуйте функцию возврата пылевых токенов после добавления ликвидности.

3.2.9 Потенциальная проблема 10: Некорректная реализация _transfer() (II)

Пункт Описание
Критичность Средняя
Статус Исправлено в Версии 9
Введено в Версии 7

Описание В контракте IncentivizedERC20 функция _transfer() вызывает функцию handle_ActionAfter() для соответствующего обновления статуса пользователя в контракте ChefIncentivesController. Однако параметр senderBalance не будет обновлён, если отправитель равен получателю, что является некорректным поведением.

function _transfer(
        address sender,
        address recipient,
        uint256 amount
      ) internal virtual {
        require(sender != address(0), 'ERC20: transfer from the zero address');
        require(recipient != address(0), 'ERC20: transfer to the zero address');
    
        _beforeTokenTransfer(sender, recipient, amount);
    
        uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
    
        if (address(_getIncentivesController()) != address(0)) {
          // uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionBefore(sender);
          if (sender != recipient) {
            _getIncentivesController().handleActionBefore(recipient);
          }
        }
    
        _balances[sender] = senderBalance;
        uint256 recipientBalance = _balances[recipient].add(amount);
        _balances[recipient] = recipientBalance;
    
        if (address(_getIncentivesController()) != address(0)) {
          uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
          if (sender != recipient) {
            _getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
          }
        }
      }

Листинг 3.12: IncentivizedERC20.sol

Воздействие Когда пользователи переводят средства самим себе, их состояние в контракте ChefIncentivesController не будет обновлено должным образом, что создаст дополнительные проблемы с вознаграждениями.

Рекомендация Исправьте senderBalance в функции handleActionAfter().

3.2.10 Потенциальная проблема 11: Манипулируемые составные вознаграждения

Пункт Описание
Критичность Средняя
Статус Исправлено в Версии 10
Введено в Версии 5

Описание В контракте MFDPlus функция _convertPendingRewardsToWeth() обменивает вознаграждения пользователя на WETH через роутер Uniswap для повторной блокировки. Однако после обмена не выполняется проверка проскальзывания.

IERC20(underlying).safeApprove(uniRouter, removedAmount);
    uint256[] memory amounts = IUniswapV2Router02(uniRouter)
    .swapExactTokensForTokens(
         removedAmount,
         0, // slippage handled after this function
         mfdHelper.getRewardToBaseRoute(underlying),
         address(this),
         block.timestamp + 10
     );

Листинг 3.13: MFDPlus.sol

Воздействие Злоумышленник может опередить транзакцию для манипулирования ценой и получения прибыли.

Рекомендация Добавьте проверку проскальзывания в функцию claimCompound().

3.2.11 Потенциальная проблема 12: Отсутствие контроля доступа в setLeverager()

Пункт Описание
Критичность Средняя
Статус Исправлено в Версии 9
Введено в Версии 1

Описание Функция setLeverager() в контракте LendingPool не имеет контроля доступа.

uint256[] memory amounts = IUniswapV2Router02(uniRouter)
    .swapExactTokensForTokens(
         removedAmount,
         0, // slippage handled after this function
         mfdHelper.getRewardToBaseRoute(underlying),
         address(this),
         block.timestamp + 10
     );

Листинг 3.14: LendingPool.sol

Воздействие Если leverager изначально не был установлен, злоумышленник может установить leverager на любой адрес, тем самым получив контроль над логикой функции depositWithAutoDLP().

Рекомендация Установите leverager в функции initialize() или добавьте контроль доступа для функции setLeverager().

3.2.12 Потенциальная проблема 13: Отсутствие проверки проскальзывания в addLiquidityWETHOnly()

Пункт Описание
Критичность Средняя
Статус Подтверждено
Введено в Версии 1

Описание Пользователь может использовать либо заёмные токены WETH (либо собственные токены ETH), либо вестинговые токены RDNT в контрактах MFD для получения LP-токенов (т.е. WETH-RDNT).

Однако при добавлении ликвидности в пул расчёт необходимых токенов основывается на количестве резервов в пуле, которым можно манипулировать. В этом случае, если у пользователя есть только токены WETH, будет вызвана функция addLiquidityWETHOnly() для обмена половины токенов WETH на токены RDNT в несбалансированном пуле без проверки проскальзывания.

function addLiquidityWETHOnly(uint256 _amount, address payable to)
    public
    returns (uint256 liquidity)
{
    require(to != address(0), "LiquidityZAP: Invalid address");
    uint256 buyAmount = _amount.div(2);
    require(buyAmount > 0, "LiquidityZAP: Insufficient ETH amount");

    (uint256 reserveWeth, uint256 reserveTokens) = getPairReserves();
    uint256 outTokens = UniswapV2Library.getAmountOut(
        buyAmount,
        reserveWeth,
        reserveTokens
    );

    _WETH.transfer(_tokenWETHPair, buyAmount);

    (address token0, address token1) = UniswapV2Library.sortTokens(
        address(_WETH),
        _token
    );
    IUniswapV2Pair(_tokenWETHPair).swap(
        _token == token0 ? outTokens : 0,
        _token == token1 ? outTokens : 0,
        address(this),
        ""
    );

    return _addLiquidity(outTokens, buyAmount, to);
}

Листинг 3.15: LiquidityZap.sol

function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
       require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
       require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
       uint amountInWithFee = amountIn.mul(997);
       uint numerator = amountInWithFee.mul(reserveOut);
       uint denominator = reserveIn.mul(1000).add(amountInWithFee);
       amountOut = numerator / denominator;
   }  

Листинг 3.16: UniswapV2Library.sol

Воздействие Злоумышленник может опередить транзакцию для манипулирования ценой и получения прибыли.

Рекомендация Проверяйте проскальзывание в функции addLiquidityWETHOnly() или убедитесь, что она может вызываться только контрактом UniswapPoolHelper.

3.2.13 Потенциальная проблема 14: Отсутствие проверки borrowRatio в loopETH()

Пункт Описание
Критичность Низкая
Статус Исправлено в Версии 10
Введено в Версии 1

Описание Функция loopETH() используется для кредитного плеча и принимает параметр borrowRatio для указания коэффициента заимствования. Однако borrowRatio не проверяется до начала цикла.

function loopETH(
        uint256 interestRateMode,
        uint256 borrowRatio,
        uint256 loopCount
    ) external payable {
        uint16 referralCode = 0;
        uint256 amount = msg.value;
        if (IERC20(address(weth)).allowance(address(this), address(lendingPool)) == 0) {
            IERC20(address(weth)).safeApprove(address(lendingPool), type(uint256).max);
        }
        if (IERC20(address(weth)).allowance(address(this), address(treasury)) == 0) {
            IERC20(address(weth)).safeApprove(treasury, type(uint256).max);
        }

        uint256 fee = amount.mul(feePercent).div(RATIO_DIVISOR);
        _safeTransferETH(treasury, fee);
        
        amount = amount.sub(fee);

        weth.deposit{value: amount}();
        lendingPool.deposit(address(weth), amount, msg.sender, referralCode);

        for (uint256 i = 0; i < loopCount; i += 1) {
            amount = amount.mul(borrowRatio).div(RATIO_DIVISOR);
            lendingPool.borrow(address(weth), amount, interestRateMode, referralCode, msg.sender);
            weth.withdraw(amount);

            fee = amount.mul(feePercent).div(RATIO_DIVISOR);
            _safeTransferETH(treasury, fee);

            weth.deposit{value: amount.sub(fee)}();
            lendingPool.deposit(address(weth), amount.sub(fee), msg.sender, referralCode);
        }

        zapWETHWithBorrow(wethToZap(msg.sender), msg.sender);
    }

Листинг 3.17: Leverager.sol

Воздействие borrowRatio может превысить RATIO_DIVISOR, что противоречит первоначальному замыслу.

Рекомендация Убедитесь, что borrowRatio меньше или равен RATIO_DIVISOR.

3.2.14 Потенциальная проблема 15: Отсутствие проверки длины между assets и poolIDs в setPoolIDs()

Пункт Описание
Критичность Низкая
Статус Исправлено в Версии 10
Введено в Версии 1

Описание Функция setPoolIDs() позволяет владельцу устанавливать различные poolIDs для разных активов. Однако длины этих двух массивов не проверяются на равенство.

// Set pool ids of assets
    function setPoolIDs(address[] memory assets, uint256[] memory poolIDs) external onlyOwner {
        for (uint256 i = 0; i < assets.length; i += 1) {
            poolIdPerChain[assets[i]] = poolIDs[i];
        }
        emit PoolIDsUpdated(assets, poolIDs);
    } 

Листинг 3.18: StarBorrow.sol

Воздействие Активам не будут назначены правильные poolIDs.

Рекомендация Убедитесь, что длины массивов assets и poolIDs равны.

3.2.15 Потенциальная проблема 16: Отсутствие отзыва привилегии mint в addBountyContract()

Пункт Описание
Критичность Низкая
Статус Подтверждено
Введено в Версии 1

Описание Функция addBountyContract() используется для установки нового BountyManager. Однако исходный контракт баунти по-прежнему сохраняет привилегию mint, что противоречит первоначальному замыслу.

function addBountyContract(address _bounty) external onlyOwner {
       BountyManager = _bounty;
       minters[_bounty] = true;
   }

Листинг 3.19: Leverager.sol

Воздействие Устаревший BountyManager по-прежнему имеет привилегии mint.

Рекомендация Отзовите привилегию mint у исходного контракта BountyManager.

Обратная связь Функция addBountyContract будет вызвана только один раз для инициализации BountyManager.

3.2.16 Потенциальная проблема 17: Minters могут быть назначены только один раз

Пункт Описание
Критичность Низкая
Статус Подтверждено
Введено в Версии 1

Описание Minters используется для записи тех, кто имеет разрешение на доступ к функциям mint() и addReward(). Однако когда один из minters (например, контракт ChefIncentivesController) обновляется, устаревшие minters не могут быть удалены.

function setMinters(address[] memory _minters) external onlyOwner {
        require(!mintersAreSet);
        for (uint256 i; i < _minters.length; i++) {
            minters[_minters[i]] = true;
        }
        mintersAreSet = true;
    }

Листинг 3.20: MultiFeeDistribution.sol

Воздействие Устаревшие minters не могут быть удалены при их обновлении.

Рекомендация Реализуйте привилегированную функцию для изменения minters.

Обратная связь Поскольку BountyManager, ChefIncentivesController и MultiFeeDistribution будут обновляемыми, minters всегда сохраняют один и тот же адрес прокси.

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

3.3.1 Потенциальная проблема 18: Оптимизация газа (zapVestingToLp() в Mfd)

Пункт Описание
Статус Исправлено в Версии 10
Введено в Версии 1

Описание Функция zapVestingToLp() может вызываться только контрактом LockZap для передачи заблокированных доходов пользователя. Она перебирает массив earnings пользователя начиная с индекса 0 и проверяет, превышает ли unlockTime текущую метку времени. Если да, этот доход удаляется из массива и передаётся. Однако поскольку unlockTime в массиве возрастает с индексом, было бы эффективнее начинать перебор с конца массива к его началу. Если unlockTime меньше текущей метки времени, цикл можно прервать.

function zapVestingToLp(address _user)
        external
        override
        returns (uint256 zapped)
    {
        require(msg.sender == lockZap);

        LockedBalance[] storage earnings = userEarnings[_user];
        uint256 length = earnings.length;

        for (uint256 i = 0; i < length; ) {
            // only vesting, so only look at currently locked items
            if (earnings[i].unlockTime > block.timestamp) {
                zapped = zapped.add(earnings[i].amount);
                // remove + shift array size
                earnings[i] = earnings[earnings.length - 1];
                earnings.pop();
                length = length.sub(1);
            } else {
                i = i.add(1);
            }
        }

        rdntToken.safeTransfer(lockZap, zapped);

        Balances storage bal = balances[_user];
        bal.earned = bal.earned.sub(zapped);
        bal.total = bal.total.sub(zapped);

        return zapped;
    }

Листинг 3.21: MultiFeeDistribution.sol

Рекомендация Начните перебор с конца массива earnings к его началу. Если unlockTime меньше текущей метки времени, цикл можно прервать.

3.3.2 Потенциальная проблема 19: Непустой резерв баунти в BountyManager

Пункт Описание
Статус Исправлено в Версии 10
Введено в Версии 1

Описание В функции _sendBounty(), если в контракте BountyManager недостаточно токенов RDNT для перевода, будет сгенерировано событие BountyReseveEmpty(), и контракт будет приостановлен. Однако возможна ситуация, когда ещё остались некоторые токены RDNT, что не соответствует сгенерированному событию.

function _sendBounty(address _to, uint256 _amount)
		internal
		returns (uint256)
	{
		if (_amount == 0) {
			return 0;
		}

		uint256 bountyReserve = IERC20(rdnt).balanceOf(address(this));
		if(_amount > bountyReserve) {
			emit BountyReserveEmpty(bountyReserve);
			_pause();
		} else {
			IERC20(rdnt).safeTransfer(address(mfd), _amount);
			IMFDPlus(mfd).mint(_to, _amount, true);
			return _amount;
		}
	}

Листинг 3.22: BountyManager.sol

Рекомендация Переведите оставшиеся токены RDNT, даже если их недостаточно.

3.3.3 Потенциальная проблема 20: Несогласованное именование в requiredUsdValue()

Пункт Описание
Статус Подтверждено
Введено в Версии 1

Описание Функция requiredUsdValue() используется для проверки требуемой заблокированной стоимости пользователя, желающего получить право на вознаграждения за владение RTokens. Расчёт основан на стоимости залога пользователя, которая возвращается функцией getUserAccountData(). Однако возвращаемое значение называется totalCollateralETH, что не соответствует наименованию в функции requiredUsdValue() (т.е. totalCollateralUSD).

Рекомендация Стандартизируйте соглашения об именовании функций с правильным названием токена. Например, переименуйте requiredUsdValue() в requiredEthValue().

Обратная связь Мы предпочитаем сохранять контракты AAVE как можно более близкими к оригиналу, поэтому имя не изменялось.

3.4 Заметки

3.4.1 Потенциальная проблема 21: Устаревший MFDPlus

Пункт Описание
Статус Подтверждено
Введено в Версии 10

Описание Контракт MFDPlus больше не используется. Логика компаундирования перенесена в контракт AutoCompounder, а остальная логика — в контракт MiddleFeeDistribution.

4. Приложение

4.1 Результаты автоматизированного статического тестирования безопасности

Таблица 4.1: Результаты автоматизированного статического тестирования безопасности. Found обозначает количество проблем, выявленных инструментами. FP означает количество ложных срабатываний после нашей ручной верификации.

ID Детектор Описание Воздействие Found FP Результат
1 arbitrary-send-erc20 Вызов transferFrom с произвольным from Высокое 1 1 Пройдено
2 array-by-reference Изменение массива в хранилище по значению Высокое 0 0 Пройдено
3 incorrect-shift Неверный порядок параметров в инструкции сдвига Высокое 0 0 Пройдено
4 multiple-constructors Несколько схем конструктора Высокое 0 0 Пройдено
5 name-reused Повторное использование имени контракта Высокое 0 0 Пройдено
6 protected-vars Изменение переменных напрямую без контроля доступа Высокое 0 0 Пройдено
7 rtlo Использование управляющего символа Right-To-Left-Override Высокое 0 0 Пройдено
8 shadowing-state Затенение переменных состояния Высокое 1 1 Пройдено
9 suicidal Функции, позволяющие любому уничтожить контракт Высокое 0 0 Пройдено
10 uninitialized-state Неинициализированные переменные состояния Высокое 3 3 Пройдено
11 uninitialized-storage Неинициализированные переменные хранилища Высокое 0 0 Пройдено
12 unprotected-upgrade Незащищённый обновляемый контракт Высокое 1 1 Пройдено
13 arbitrary-send-erc20-permit transferFrom использует произвольный from с разрешением Высокое 0 0 Пройдено
14 arbitrary-send-eth Функции, отправляющие Ether на произвольные адреса Высокое 0 0 Пройдено
15 controlled-array-length Заражённое присваивание длины массива Высокое 0 0 Пройдено
16 controlled-delegatecall Управляемое назначение delegatecall Высокое 0 0 Пройдено
17 delegatecall-loop Оплачиваемые функции, использующие delegatecall внутри цикла Высокое 0 0 Пройдено
18 msg-value-loop Использование msg.value внутри цикла Высокое 0 0 Пройдено
19 reentrancy-eth Уязвимости повторного входа (кража эфира) Высокое 5 5 Пройдено
20 storage-array Ошибка компилятора для знаковых целочисленных массивов в хранилище Высокое 0 0 Пройдено
21 unchecked-transfer Непроверенная передача токенов Высокое 12 12 Пройдено
22 weak-prng Слабый генератор псевдослучайных чисел Высокое 0 0 Пройдено
23 domain-separator-collision Обнаруживает токены ERC20, имеющие функцию, сигнатура которой совпадает с DOMAIN_SEPARATOR() из EIP-2612 Среднее 0 0 Пройдено
24 enum-conversion Обнаруживает опасное преобразование перечислений Среднее 0 0 Пройдено
25 erc20-interface Некорректные интерфейсы ERC20 Среднее 0 0 Пройдено
26 erc721-interface Некорректные интерфейсы ERC721 Среднее 0 0 Пройдено
27 incorrect-equality Опасные строгие равенства Среднее 23 23 Пройдено
28 locked-ether Контракты, блокирующие эфир Среднее 1 1 Пройдено
29 mapping-deletion Удаление из маппинга, содержащего структуру Среднее 0 0 Пройдено
30 shadowing-abstract Затенение переменных состояния из абстрактных контрактов Среднее 0 0 Пройдено
31 tautology Тавтология или противоречие Среднее 0 0 Пройдено
32 write-after-write Неиспользуемая запись Среднее 3 3 Пройдено
33 boolean-cst Неправильное использование булевой константы Среднее 0 0 Пройдено
34 constant-function-asm Константные функции, использующие ассемблерный код Среднее 0 0 Пройдено
35 constant-function-state Константные функции, изменяющие состояние Среднее 0 0 Пройдено
36 divide-before-multiply Неточный порядок арифметических операций Среднее 20 20 Пройдено
37 reentrancy-no-eth Уязвимости повторного входа (без кражи эфира) Среднее 12 12 Пройдено
38 reused-constructor Повторно используемый базовый конструктор Среднее 0 0 Пройдено
39 tx-origin Опасное использование tx.origin Среднее 1 1 Пройдено
40 unchecked-lowlevel Непроверенные низкоуровневые вызовы Среднее 0 0 Пройдено
41 unchecked-send Непроверенная отправка Среднее 0 0 Пройдено
42 uninitialized-local Неинициализированные локальные переменные Среднее 33 33 Пройдено
43 unused-return Неиспользуемые возвращаемые значения Среднее 19 19 Пройдено

4.2 Результаты автоматизированного динамического тестирования безопасности

Таблица 4.2: Проверенные свойства для логики, связанной с кредитованием

ID Свойство Результат
1 Вызов deposit никогда не приводит к уменьшению количества RToken у onBehalfOf Пройдено
2 Вызов withdraw никогда не приводит к увеличению количества RToken у msg.sender Пройдено
3 Вызов borrow со стабильным режимом процентной ставки никогда не приводит к уменьшению StableDebtToken у onBehalfOf. Пройдено
4 Вызов borrow с переменным режимом процентной ставки никогда не приводит к уменьшению VariableDebtToken у onBehalfOf. Пройдено
5 Вызов borrow с onBehalfOf, не равным msg.sender, никогда не приводит к увеличению лимита заимствований msg.sender. Пройдено
6 Вызов repay со стабильным режимом процентной ставки никогда не приводит к увеличению StableDebtToken у onBehalfOf. Пройдено
7 Вызов repay с переменным режимом процентной ставки никогда не приводит к увеличению VariableDebtToken у onBehalfOf. Пройдено
8 liquidityIndex никогда не уменьшается. Пройдено
9 liquidityIndex остаётся постоянным в пределах одного блока. Пройдено
10 variableBorrowIndex никогда не уменьшается. Пройдено
11 variableBorrowIndex остаётся постоянным в пределах одного блока. Пройдено
12 Уменьшение сумм залога никогда не приводит к тому, что коэффициент здоровья становится меньше 1. Пройдено
13 Увеличение сумм заимствования никогда не приводит к тому, что коэффициент здоровья становится меньше 1. Пройдено

Таблица 4.3: Проверенные свойства для логики, связанной со стейкингом

ID Свойство Результат
1 Общий баланс пользователя всегда равен сумме заблокированного баланса, разблокированного баланса и заработанного баланса. Пройдено
2 Заблокированный баланс пользователя всегда равен сумме количества userLocks Пройдено
3 Баланс lockedWithMultiplier пользователя всегда равен сумме количества userLocks, умноженного на множитель userLocks Пройдено
4 lockedSupply всегда равен сумме заблокированных балансов пользователей Пройдено
5 lockedSupplyWithMultiplier всегда равен сумме балансов lockedWithMultiplier пользователей Пройдено
6 rewardPerTokenStored никогда не уменьшается. Пройдено
7 rewardPerTokenStored остаётся постоянным в пределах одного блока. Пройдено
8 totalSupply всегда равен сумме количества пользователей Пройдено
9 accRewardPerShare никогда не уменьшается. Пройдено
10 accRewardPerShare остаётся постоянным в пределах одного блока. Пройдено

Таблица 4.4: Проверенные свойства для прочих функций

ID Свойство Результат
1 Баланс WETH и RDNT контракта LockedZap всегда равен нулю. Пройдено
2 Баланс WETH и RDNT контракта LiquidityZap всегда равен нулю. Пройдено
3 Баланс WETH и RDNT контракта BalancerPoolHelper всегда равен нулю. Пройдено
4 Баланс WETH и RDNT контракта UniswapPoolHelper всегда равен нулю. Пройдено
5 Вызов loop всегда делает пользователя правомочным для получения вознаграждений Пройдено
6 Вызов loopETH всегда делает пользователя правомочным для получения вознаграждений Пройдено
7 Вызов executeBounty с _execute равным false никогда не приводит к изменению состояния хранилища. Пройдено
8 Вызов transfer с отправителем, равным получателю, никогда не приводит к изменению баланса. Не пройдено в Версии 1. Пройдено в Версии 7

5 Уведомления и примечания

5.1 Отказ от ответственности

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

Настоящий отчёт не является одобрением какого-либо конкретного проекта или команды, и данный отчёт не гарантирует безопасность какого-либо конкретного проекта. Данное тестирование безопасности не даёт никаких гарантий обнаружения всех проблем безопасности смарт-контрактов, т.е. результат оценки не гарантирует отсутствия дальнейших выявленных проблем безопасности. Поскольку тестирование безопасности не может считаться всесторонним, мы всегда рекомендуем проводить независимые аудиты и публичную программу вознаграждения за обнаружение ошибок для обеспечения безопасности смарт-контрактов.

Область действия данного тестирования безопасности ограничена кодом, указанным в Разделе 1.2. Если явно не указано иное, безопасность самого языка программирования (например, языка Solidity), базового инструментария компиляции и вычислительной инфраструктуры выходит за рамки данного тестирования.

5.2 Процедура аудита

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

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

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

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

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

5.2.1 Безопасность программного обеспечения

  • Повторный вход

  • DoS

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

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

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

  • Ненадёжные внешние вызовы и поток управления

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

  • Операции с событиями

  • Ненадёжная случайность

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

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

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

  • Согласованность функциональности

  • Управление правами доступа

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

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

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

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

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

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

  • Массовая передача

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

  • Дублирующиеся элементы

  • Верификация получателя токена

  • Безопасность метаданных вне блокчейна

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

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

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

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

Sign up for the latest updates
~$5.98M Потеряно: Aztec, Raydium и другие | Еженедельник BlockSec
Security Insights

~$5.98M Потеряно: Aztec, Raydium и другие | Еженедельник BlockSec

Еженедельный отчёт о безопасности блокчейна (8–15 июня 2026 г.): 4 инцидента в Ethereum и Solana, общие потери ~$5,98 млн. Aztec Connect: отсутствие валидации входных данных привело к рассинхронизации rollup и L1. Raydium: уязвимость в AMM v3 позволила дренировать 4 пула.

Анализ уязвимости Zcash Orchard | Еженедельник BlockSec
Security Insights

Анализ уязвимости Zcash Orchard | Еженедельник BlockSec

Критическая уязвимость в цепи Orchard Zcash: отсутствие ограничения равенства в гаджете ECC halo2 позволяло незаметно подделывать ZEC через двойное расходование. Уязвимость существовала 4+ лет, обнаружена ИИ-аудитом (Anthropic Opus 4.8, исследователь Тейлор Хорнби), устранена экстренным обновлением NU6.2.

Информационный бюллетень — май 2026 г.
Security Insights

Информационный бюллетень — май 2026 г.

В мае 2026 года в DeFi произошло 3 взлома: Echo Protocol ($76,7 млн, компрометация ключа), StablR ($12,8 млн, брешь в multisig) и Verus-Ethereum Bridge ($11,7 млн, ошибка проверки типов). Общий ущерб — около $101,2 млн.

Best Security Auditor for Web3

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

BlockSec Audit