Back to Blog

Уязвимости Revest Finance: больше, чем просто повторный вход

Code Auditing
March 31, 2022
8 min read

27 марта 2022 года DeFi-проект для стейкинга Revest Finance в сети Ethereum подвергся атаке из-за механизма обратного вызова (call-back) ERC-1155, в результате которой были украдены токены на сумму около 2 млн долларов (а именно BLOCKS, ECO, LYXe и RENA). Мы проанализировали атаку в первую очередь и опубликовали наш анализ в тот же вечер (UTC+8).

На самом деле, во время написания поста в Twitter у нас оставались сомнения по поводу одной функции в контракте Revest TokenVault. Мы изучили контракт, пытаясь понять принцип его работы. Позже мы обнаружили, что это еще одна критическая уязвимость нулевого дня, которую можно использовать гораздо более простым способом, что может привести к таким же огромным убыткам (как и в случае с уже произошедшей атакой).

Мы немедленно связались с командой Revest Finance, они быстро отреагировали и предложили временное решение для устранения уязвимости. Убедившись, что уязвимость невозможно активировать, мы решили опубликовать этот блог.

Далее в этом блоге будут рассмотрены три части: механизм работы Revest Finance, оригинальная атака через реентерабельность и новая уязвимость нулевого дня.

Что такое FNFT в Revest Finance

Financial Non-Fungible Token (FNFT) от Revest Finance делает возможной доверительную передачу будущих прав на заблокированные активы. Входной контракт (контракт Revest) предоставляет три различных интерфейса для минта (выпуска) FNFT путем блокировки базовых активов:

  • mintTimeLock: базовый актив будет разблокирован по прошествии определенного периода времени.
  • mintValueLock: базовый актив будет разблокирован, когда его стоимость поднимется выше или упадет ниже установленного значения.
  • mintAddressLock: базовый актив будет разблокирован указанным аккаунтом.

Контракт Revest связывает три других контракта для блокировки и разблокировки базовых активов.

  • FNFTHandler: наследует стандарт ERC-1155. При каждой блокировке он создает новый FNFT с увеличивающимся fnftId. Блокировка определяет общий запас (total supply) нового FNFT при создании. FNFT нельзя создать другим способом, но их можно сжечь для разблокировки базовых активов.

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

  • TokenVault: получает и отправляет базовые активы, а также записывает метаданные для каждого FNFT, такие как стоимость конкретного FNFT.

Возьмем mintAddressLock в качестве примера, чтобы проиллюстрировать процесс создания FNFT.

Рисунок 1
Рисунок 2

На двух рисунках выше показано, как FNFT создается, минтится и сжигается. В частности, пользователь А блокирует 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 старого замка и указанной суммы, как показано в коде ниже:

// Теперь мы переводим в хранилище токенов
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 из старого замка в новый.

(Если указанное количество превышает общий запас, транзакция будет отменена)

Что такое уязвимость реентерабельности (Re-entrancy)

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

Рисунок 5
Рисунок 6
Рисунок 7

Эти три рисунка в основном описывают весь процесс атаки через реентерабельность. В частности, злоумышленник сначала блокирует ноль токенов RENA, чтобы сминтить 2 1-FNFT, которые не имеют никакой ценности. Во-вторых, злоумышленник снова блокирует ноль токенов RENA, но минтит 360 000 2-FNFT, которые (сейчас) также не имеют ценности. На последнем шаге злоумышленник входит в функцию depositAdditionalToFNFT контракта Revest через механизм обратного вызова 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 старого. Однако код говорит нам, что она не только записывает depositAmount нового замка, но и меняет depositAmount старого.

Мы считаем, что это серьезная логическая уязвимость нулевого дня, и написали PoC-код для ее проверки. Следующие три рисунка описывают, как работает PoC.

Рисунок 8
Рисунок 9
Рисунок 10

В частности, злоумышленник сначала блокирует ноль RENA, чтобы сминтить 360 000 1-FNFT. После этого злоумышленник напрямую вызывает функцию depositAdditionalToFNFT для создания нового замка. Из-за ошибки в логике контракт TokenVault некорректно меняет depositAmount старого замка с нуля на 1e18. В результате злоумышленник получает 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 может смягчить возможные атаки, упомянутые в этом блоге.

Выводы

Обеспечение безопасности DeFi-проекта — непростая задача. Помимо аудита кода, мы считаем, что сообщество должно использовать проактивный метод для мониторинга состояния проекта и блокировать атаку еще до того, как она произойдет.

О компании BlockSec

BlockSec — передовая компания в области блокчейн-безопасности, основанная в 2021 году группой всемирно известных экспертов по безопасности. Компания стремится повысить безопасность и удобство использования нового мира Web3 для содействия его массовому внедрению. С этой целью BlockSec предоставляет услуги по аудиту смарт-контрактов и сетей EVM, платформу Phalcon для разработки безопасности и проактивной блокировки угроз, платформу MetaSleuth для отслеживания и расследования средств, а также расширение MetaDock для эффективной работы Web3-разработчиков в криптомире.

На сегодняшний день компания обслужила более 300 уважаемых клиентов, таких как MetaMask, Uniswap Foundation, Compound, Forta и PancakeSwap, и привлекла десятки миллионов долларов США в двух раундах финансирования от выдающихся инвесторов, включая Matrix Partners, Vitalbridge Capital и Fenbushi Capital.

Официальный сайт: https://blocksec.com/

Официальный аккаунт в Twitter: https://twitter.com/BlockSecTeam

Best Security Auditor for Web3

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

BlockSec Audit