27 марта 2022 года стейкинговый проект DeFi Revest Finance на Ethereum подвергся атаке из-за механизма обратного вызова ERC-1155. Были украдены токены на сумму около 2 миллионов долларов (а именно: BLOCKS, ECO, LYXe и RENA). Мы проанализировали атаку в первую очередь и Твиттере той ночью (UTC+8).На самом деле, на момент написания статьи в Твиттере у нас все еще были некоторые сомнения относительно функции контракта Revest TokenVault. Мы изучили контракт, пытаясь понять его функциональность. Позже мы обнаружили, что существует еще одна критическая уязвимость нулевого дня , которую можно эксплуатировать гораздо более простым способом и которая может привести к таким же огромным потерям ( как и случившаяся атака ).
Затем мы немедленно связались с командой Revest Finance, и они быстро отреагировали и предложили обходной путь для устранения уязвимости. Убедившись, что уязвимость нельзя использовать, мы решили выпустить этот блог.
Следующая часть этого блога состоит из трех частей: механизм Revest Finance, исходная атака с повторным входом и новая уязвимость нулевого дня .
Что такое Revest Finance FNFT
Финансовый невзаимозаменяемый токен (FNFT) Revest Finance делает возможной бездоверительную передачу будущих прав на заблокированные активы. Вход (контракт ) Revest предоставляет три разных интерфейса для FNFT путем блокировки базовых активов:- mintTimeLock: базовый актив будет разблокирован через определенный период времени.
- mintValueLock: базовый актив будет разблокирован, когда его стоимость поднимется выше или упадет ниже установленного значения.
- mintAddressLock: базовый актив будет разблокирован с помощью установленной учетной записи.
- FNFTHandler : унаследован от ERC-1155 . Он создает новый FNFT с увеличением fnftId для каждого замка. Блокировка прописывает общий запас нового FNFT при создании. FNFT нельзя добыть другим способом, но можно сжечь для разблокировки базовых активов.
- LockManager : записывает условия разблокировки для каждого замка при создании и решает, можно ли разблокировать замок при разблокировке.
- TokenVault : получает и отправляет базовые активы и записывает метаданные для каждого FNFT, например значение указанного FNFT.
Рисунок 1
Рисунок 2
Два приведенных выше рисунка в основном описывают, как создается, чеканится и сжигается FNFT. В частности, пользователь A блокирует 100 WETH в Revest Finance, который создает соответствующий FNFT с fnftIdкак 1. Наконец, он отчеканит 100 1-FNFT указанным получателям с указанными акциями.
Обратите внимание, что после разблокировки базового актива каждый 1-FNFT может быть сожжен для получения одного (*1e18) WETH. Как показано на рисунке 2, пользователь B выводит 25 (* 1e18) WETH, сжигая 25 1-FNFT.
Кроме того, Revest предоставляет еще один интерфейс с именем depositAdditionalToFNFT, который влечет за собой две уязвимости, которые будут обсуждаться далее.
Сначала мы используем следующие два рисунка, чтобы описать нормальное использование этой функции.
Рисунок 3
Рисунок 4
Функция depositAdditionalToFNFTиспользуется для привязки дополнительных базовых активов к существующей блокировке (указанной параметром fnftId). Разумно (рисунок 3), он требует, чтобы указанное количество было таким же, как общее предложение указанного FNFT, а затем равномерно распределяет добавленные активы по каждому указанному FNFT.
В противном случае (рис. 4) создается новая блокировка с последним fnftId, сжигает указанное количество старых FNFT и чеканит указанное количество новых FNFT, а затем записывает новые блокировки depositAmountкак сумма старых замков depositAmountи указанную сумму, как показано в следующем коде.
Код:
// Now, we transfer to the token vault
if(fnft.asset != address(0)){
IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);emit FNFTAddionalDeposited(_msgSender(), newFNFTId, quantity, amount);
С depositAmount записанная в TokenVault , указывает количество базового актива, которое может вывести один указанный FNFT, эта операция переносит значение указанного количества старого FNFT из старого замка в новый замок. (указанное количество, превышающее общее предложение, отменит транзакцию)
Что такое уязвимость повторного входа
В этой части мы проиллюстрируем, как работает атака с повторным входом, и обсудим основную причину и метод исправления.
Рисунок 5
Рисунок 6
Рисунок 7
Приведенные выше три рисунка в основном описывают весь процесс повторной атаки. В частности, злоумышленник сначала блокирует нулевой токен RENA, чтобы отчеканить 2 1-FNFT, которые не имеют значения. Во-вторых, злоумышленник снова блокирует нулевой токен RENA, но выпускает 360 000 2-FNFT, которые также не имеют ценности (сейчас). На последнем этапе злоумышленник повторно входит в Revest контракта DepositAdditionalToFNFT функцию FNFTHandler , унаследованный от стандарта токена ERC-1155, который depositAmountзамка с fnftIdкак 2 до обновления fnftId. В результате злоумышленник получает 360 001 2-FNFT с depositAmountкак 1e18, что означает, что он может вывести 360 001 * 1e18 RENA из TokenVault . Кроме того, единственная стоимость - 1e18 RENA.
Метод исправления
Коды Revest Finance полностью соответствуют классической схеме повторного входа: используйте fnftId-> внешний вызов с механизмом обратного вызова -> обновление fnftId. Поэтому самый простой способ решить проблемы — сломать шаблон. Код показан ниже:
Код:
function mint(
address account,
uint id,
uint amount,
bytes memory data
) external override onlyRevestController {
require(amount > 0, "Invalid amount");
require(supply[id] == 0, "Repeated mint for the same FNFT");
supply[id] += amount;
fnftsCreated += 1;
_mint(account, id, amount, data);
}
Во-первых, он перемещает операцию обновления перед внешним вызовом ( _mint), что может избежать атаки.
Во-вторых, поскольку система не допускает чеканки с нулевым FNFT и повторной чеканки одной и той же FNFT, она добавляет две проверки, чтобы убедиться, что система работает должным образом, что может повысить безопасность системы.
Новая уязвимость нулевого дня
При анализе кода Revest Finance функция handleMultipleDepositsв TokenVault нас смущает, код которого показан ниже.
Код:
function handleMultipleDeposits(
uint fnftId,
uint newFNFTId,
uint amount
) external override onlyRevestController {
require(amount >= fnfts[fnftId].depositAmount, 'E003');
IRevest.FNFTConfig storage config = fnfts[fnftId];
config.depositAmount = amount;
mapFNFTToToken(fnftId, config);
if(newFNFTId != 0) {
mapFNFTToToken(newFNFTId, config);
}
}
Во время вызова функции depositAdditionalToFNFT функция handleMultipleDeposits изменяет depositAmount старого замка или записывает его в новый. Когда newFNFTId равен нулю, она не записывает depositAmount нового замка, поскольку это операция добавления дополнительных активов к существующему замку.
Согласно нашему пониманию протокола, когда newFNFTId не равен нулю, нужно только записать сумму депозита нового замка и не нужно изменять сумму депозита старого. Однако код показывает, что он не только записывает DepositAmount нового замка, но и изменяет DepositAmount старого, что противоречит нашему пониманию.
Мы считаем, что это серьезная логическая уязвимость нулевого дня, и затем пишем PoC для проверки этого. Следующие три рисунка описывают, как работает PoC.
Рисунок 8
Рисунок 9
Рисунок 10
В частности, злоумышленник сначала блокирует ноль RENA, чтобы отчеканить 360 000 1-FNFT. После этого злоумышленник напрямую вызывает depositAdditionalToFNFTФункция для создания нового замка. Из-за уязвимости, TokenVaultдоговор неправильно изменяет depositAmountстарого замка с нуля до 1е18. В результате злоумышленник получает 359 999 1-FNFT на сумму 359 999 RENA. Очевидно, что PoC намного проще, чем реальная атака с повторным входом, поскольку она не требует повторного входа.
Обходной путь для устранения уязвимости
Это логическая ошибка, и мы рекомендуем использовать следующий код для ее исправления.
Код:
function handleMultipleDeposits(
uint fnftId,
uint newFNFTId,
uint amount
) external override onlyRevestController {
require(amount >= fnfts[fnftId].depositAmount, 'E003');
IRevest.FNFTConfig memory config = fnfts[fnftId];
config.depositAmount = amount;
if(newFNFTId != 0) {
mapFNFTToToken(newFNFTId, config);
} else {
mapFNFTToToken(fnftId, config);
}
}
Поскольку два уязвимых контракта: TokenVault и FNFTHandler хранят множество критических состояний, проект не может повторно развернуть TokenVault контракт FNFTHandler без миграции состояний. Чтобы избежать дальнейшей атаки на эту уязвимость, проект повторно развернул облегченную версию Revest контракта , которая отключает более сложные функции, чтобы уменьшить поверхности, доступные любому потенциальному злоумышленнику. После проверки обходного пути мы считаем, что облегченный Revest может смягчить возможные атаки, упомянутые в этом блоге.
Источник: https://blocksecteam.medium.com/revest-finance-vulnerabilities-more-than-re-entrancy-1609957b742f