Back to Blog

Безопасная разработка смарт-контрактов (2) — Как использовать цифровую подпись в NFT (маркетплейсах) правильно

12 августа 2022 г.
6 min read

1. Введение

Цифровая подпись используется для обеспечения подлинности и целостности. Как описано в этой статье, «Действительная цифровая подпись, при выполнении всех необходимых условий, дает получателю очень высокую уверенность в том, что сообщение было создано известным отправителем (подлинность) и что сообщение не было изменено при передаче (целостность)».

Цифровые подписи широко используются в смарт-контрактах, например, при минтинге в «белые списки» (allowlist mint) и на NFT-маркетплейсах с книгой ордеров (order-book). Это позволяет сократить транзакционные издержки (подпись вне сети и проверка внутри сети). Однако ошибки разработчиков при использовании цифровых подписей создают риски для NFT-рынка. В этом блоге мы хотим обсудить проблемы, связанные с неправильным использованием цифровых подписей в экосистеме NFT.

2. Применение

Цифровая подпись широко используется для минтинга по «белым спискам» (только пользователи с действительными подписями могут создавать NFT) в NFT-контрактах и на NFT-маркетплейсах для проверки ордеров (только ордера с ожидаемыми подписями могут быть исполнены). Подписание данных происходит вне сети (off-chain) для экономии газа. Ниже мы проиллюстрируем эти два сценария использования.

2.1. Минтинг по белому списку (Allowlist Mint)

«NFT-минтинг» — это процедура создания NFT в блокчейне. Большинство NFT-проектов стремятся продвигать свои продукты; они предпочитают мотивировать пользователей с помощью минтинга по белому списку (также называемого предпродажей и т.д.). Люди, выигравшие место, могут сминтить токены по более низкой цене (или даже бесплатно). Цифровая подпись используется для разграничения пользователей из белого списка и обычных пользователей («публичных минтёров»). Ниже приведен пример реализации минтинга по белому списку.

function mint_approved(
        vData memory info,
        uint256 number_of_items_requested,
        uint16 _batchNumber
    ) external {
        ...
        require(verify(info), "Unauthorised access secret");
        ...
    }
    function verify(vData memory info) public view returns (bool) {
        require(info.from != address(0), "INVALID_SIGNER");
        bytes memory cat =
            abi.encode(
                info.from,
                info.start,
                info.end,
                info.eth_price,
                info.dust_price,
                info.max_mint,
                info.mint_free
            );
        bytes32 hash = keccak256(cat);
        require(info.signature.length == 65, "Invalid signature length");
        bytes32 sigR;
        bytes32 sigS;
        uint8 sigV;
        bytes memory signature = info.signature;        assembly {
            sigR := mload(add(signature, 0x20))
            sigS := mload(add(signature, 0x40))
            sigV := byte(0, mload(add(signature, 0x60)))
        }        bytes32 data =
            keccak256(
                abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
            );
        address recovered = ecrecover(data, sigV, sigR, sigS);
        return signer == recovered;
    }

Этот фрагмент кода взят из проекта Association NFT (в котором была уязвимость — не копируйте этот код). Функция mint_approved() призвана реализовать минтинг по белому списку: владелец проекта подписывает сообщение о минтинге (переменная info) и отправляет его разрешенному пользователю. Затем пользователь вызывает approved_mint с подписанной переменной. Контракт проверяет, подписано ли сообщение проектом (signer == recovered). Если это так, вызывающему разрешается сминтить NFT (что НЕ безопасно, так как отсутствует проверка того, является ли тот, кто вызывает функцию, тем самым человеком, который находится в белом списке).

2.2. Проверка ордеров

Проверка ордеров — еще одно применение цифровых подписей в экосистеме NFT. NFT-маркетплейсы играют важную роль, предоставляя функционал для торговли. Поскольку каждый NFT-токен уникален, модель автоматических маркет-мейкеров (AMM) сложно применить к NFT-рынкам. Таким образом, большинство маркетплейсов, например OpenSea, LooksRare и X2Y2, используют модель «книги ордеров».

Торговля по книге ордеров проста. Есть мейкер (человек, который хочет продать актив по определенной цене) и тейкер (человек, который хочет купить актив по цене продавца). В этом случае происходит совпадение ордеров. Процесс на NFT-маркетплейсах аналогичен. Единственное отличие — процедура выставления ордера: маркетплейсы используют цифровые подписи для проверки ордеров. На рис. 1 показан пример процесса торговли на одном из маркетплейсов: OpenSea.

**Рис 1. Процесс торговли на OpenSea**
**Рис 1. Процесс торговли на OpenSea**

В частности, продавец подписывает ордер на продажу и сохраняет его на сервере OpenSea. Покупатель может получить информацию о подписанном ордере и вызвать контракт NFT-рынка, передав подписанный ордер в качестве параметров. Маркет-контракт проверяет ордер, чтобы убедиться, что продавец его подписал (поскольку транзакцию инициирует покупатель), что предотвращает покупку актива без согласия продавца.

3. Инциденты безопасности

Принцип Хортона — это максима для криптографических систем, которую можно выразить как «Аутентифицируйте то, что имеется в виду, а не то, что говорится» или «подразумевайте то, что подписываете, и подписывайте то, что подразумеваете». Он требует полного и точного подписания действия. Если подпись неполная или неточная, последствия будут катастрофическими.

3.1 Association NFT

Вернемся к контракту NBA NFT из раздела 2.1. Функция verify выполняет стандартную проверку подписи, но в ней пропущен КРИТИЧЕСКИ важный компонент. Проверка подписи подтверждает лишь то, что сообщение подписано проектом. Однако нет никакой проверки того, что человек, предоставляющий подпись контракту, является тем же, кто был указан в сообщении. В результате любой может использовать ту же подпись, чтобы пройти проверку и сминтить NFT.

3.2 OpenSea

Другая проблема безопасности связана с OpenSea. В начале 2022 года исследователи обнаружили потенциальную уязвимость контракта маркетплейса OpenSea (версия wyvern 2.2), который реализует основной функционал торговли NFT.

В протоколе Wyvern пользователи создают листинги (предложения о продаже) или офферы (предложения о покупке) вне сети, а их подписи проверяются внутри сети. Офферы Wyvern содержат много параметров, которые объединяются в одну байтовую строку для вычисления дайджеста. Затем контракт проверяет подпись этого дайджеста. Метод агрегации параметров просто объединяет их в байтовую строку следующими методами:

index = ArrayUtils.unsafeWriteAddress(index, order.target);
index = ArrayUtils.unsafeWriteUint8(index, uint8(order.howToCall));
index = ArrayUtils.unsafeWriteBytes(index, order.calldata);
index = ArrayUtils.unsafeWriteBytes(index, order.replacementPattern);
index = ArrayUtils.unsafeWriteAddress(index, order.staticTarget);
index = ArrayUtils.unsafeWriteBytes(index, order.staticExtradata);
index = ArrayUtils.unsafeWriteAddress(index, order.paymentToken);

Например, если параметры состоят из 2 компонентов: (address, bytes), и параметры равны (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xc098"), агрегированные байты будут выглядеть как 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098, просто аdress + bytes. Кажется простым и понятным, верно?

Теперь рассмотрим более сложный пример, структура параметров — (address, bytes, bytes).

параметр 1: _(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef")_.

параметр 2: _(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef")_.

Агрегированные байты:

параметр 1: _0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.

параметр 2: _0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.

Ого! Два разных набора параметров имеют одинаковый результат агрегации, что означает, что их дайджесты ОДИНАКОВЫ, в результате чего одна и та же подпись может подтвердить два разных набора параметров.

Это происходит из-за наличия многих компонентов переменной длины в параметрах. Злоумышленник может обрезать часть переменных и присоединить их к предыдущим или последующим компонентам. К сожалению, контракты Wyvern содержат много параметров переменной длины, как показано ниже:

......
    address target;
    /* HowToCall. */
    AuthenticatedProxy.HowToCall howToCall;
    /* Calldata. */
    bytes calldata;
    /* Calldata replacement pattern, or an empty byte array for no replacement. */
    bytes replacementPattern;
    /* Static call target, zero-address for no static call. */
    address staticTarget;
    /* Static call extra data. */
    bytes staticExtradata;
    ......

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

Оба упомянутых инцидента безопасности нарушают принцип Хортона. В частности, контракт NBA не включает информацию о минтёре в подписанное сообщение (или не проверяет соответствие этой информации с фактически вызывающим), а контракт Wyvern подписывает неструктурированные параметры, из-за чего значение действия может быть изменено при том, что представление («слова») параметров остается прежним.

4. Рекомендации

Следуйте принципу Хортона: подписывайте то, что подразумеваете, а не то, что утверждаете. Подпись должна содержать исчерпывающую и точную информацию.

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

Читайте другие статьи из этой серии

Sign up for the latest updates
Потери около 5,57 млн USD: атаки на X100 Freezer и NEM | Еженедельный обзор BlockSec
Security Insights

Потери около 5,57 млн USD: атаки на X100 Freezer и NEM | Еженедельный обзор BlockSec

Этот еженедельный отчёт охватывает два инцидента безопасности в сетях Ethereum и BNB Chain за период 19.10.2026–25.10.2026. В нём рассматриваются захват управления X100 Freezer, профинансированный с помощью флеш-кредита, и ошибка при расчёте налога, затронувшая пул ликвидности токена NEM в сети BNB Chain. Оценочные потери в рамках отчёта составляют в общей сложности около 5,57 млн USD: ~4,75 млн USD по рассматриваемой транзакции X100 и ~820 тыс. USD в результате инцидента с пулом NEM. В эту сумму не входят отдельно опубликованные данные о последующих выводах средств из X100.

Потери на сумму около 3,87 млн USD: взломы Autheo Network и UNCX | Еженедельный обзор BlockSec
Security Insights

Потери на сумму около 3,87 млн USD: взломы Autheo Network и UNCX | Еженедельный обзор BlockSec

В этом еженедельном отчёте рассматриваются 3 инцидента безопасности за период 12.10.2026–18.10.2026. Общий оценочный ущерб составил около 3,87 млн USD. Ущерб Autheo Network от компрометации закрытого ключа составил примерно 1,76 млн USD, Qubic Wallet потерял примерно 1,30 млн USD из-за ненадёжной генерации seed, а ошибка учёта в контрактах Smart Token Vesting V2 сети UNCX Network на Ethereum, BNB Chain, Base и Robinhood Chain привела к ущербу примерно в 807 тыс. USD. В подробном анализе инцидента UNCX объясняется, как некорректный расчёт совокупного количества долей смешивал транши вестинга, созданные при разных соотношениях токенов и долей, и позволял выводить из общих пулов больше положенного.

Что такое сертификация SEAL? Подтверждение операционной безопасности в криптоиндустрии
Security Insights

Что такое сертификация SEAL? Подтверждение операционной безопасности в криптоиндустрии

Сертификация SEAL обеспечивает независимое подтверждение операционной безопасности криптопроекта в согласованных пределах проверки. Узнайте о шести модулях программы, требованиях к подтверждающим материалам, пределах действия сертификации и процессе оценки, который проводит BlockSec.