Манифест отчёта
| Пункт | Описание |
|---|---|
| Клиент | 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 Дополнительные рекомендации
-
Оптимизация газа
-
Качество и стиль кода
Примечание: Перечисленные выше контрольные точки являются основными. В процессе аудита мы можем использовать дополнительные контрольные точки в зависимости от функциональности проекта.



