О чем материал
Рассмотрим исследование гибридного подхода для детекции уязвимостей в смарт-контрактах, сочетающего классические алгоритмы, LLM и графовый анализ.
Развитие блокчейн-технологий стремительно ускорило распространение смарт-контрактов, однако обеспечение их безопасности остается острой проблемой для бизнеса. Мы провели исследование и разработали архитектуру решения для выявления уязвимостей в смарт-контрактах с использованием LLM и статического анализа кода.
Цель исследования — создать автоматизированную систему анализа кода и генерации детекторов уязвимостей Solidity-контрактов на основе данных из открытых источников.
Сбор и подготовка датасета
Для построения эффективной системы анализа необходим качественный и разнообразный датасет. Отметим основные сложности при его сборе:
- Релевантность данных. Смарт-контракты из открытых источников могут быть устаревшими или неприменимыми на практике.
- Выявление уязвимостей. Разнообразие атак усложняет обнаружение всех типов уязвимостей, особенно новых или же скрытых ошибок в коде.
- Ложноположительные результаты. Ошибки в разметке могут привести к обучению модели на некорректных примерах.
Подготовленный датасет включает 27 073 Solidity-контракта для анализа безопасности и выявления уязвимостей:
- Источники данных — GitHub (tintinweb.github.io) и Etherscan — позволяют включить как реальные, так и аннотированные уязвимые контракты. В выборку вошло 10 000 контрактов из сети Ethereum.
- Состав датасета — 21 360 уязвимых и 5713 безопасных контрактов.
- Отбор: исключены прокси-контракты и контракты без исходного кода.
- Основные атрибуты: адрес контракта, исходный код, байт-код, список функций, импортируемые библиотеки и метки уязвимостей.
- Структура контрактов: большинство содержат небольшое количество функций, но встречаются и сложные структуры более чем со 100 функциями.
При разработке алгоритмов обнаружения уязвимых Solidity-контрактов ML-методами был учтен дисбаланс данных (преобладание уязвимых контрактов), чтобы избежать сильного смещения модели и сохранить точность классификации.
Архитектура решения
Анализатор включает несколько ключевых компонентов.
- Построение Data Flow Graph (DFG) с использованием инструментов slither и aderyn. Это граф зависимостей, который визуализирует связи между переменными, функциями и операциями внутри контракта.
- Дообучение модели GraphCodeBERT-Base-Solidity-Vulnerability на полученном датасете.
Эта модель использует подходы графового анализа кода для более точно выявления уязвимостей. Например:
contract Vulnerable {
uint public balance;
function withdraw(uint amount) public {
require(amount <= balance);
msg.sender.transfer(amount);
balance -= amount;
}
}
- Узел balance — переменная, отражающая текущее финансовое состоянием контракта.
- Узел amount — переменная, которую вводит пользователь для выполнения операции (количество средств, которые он хочет вывести).
- Узел msg.sender.transfer(amount) — операция перевода средств, потенциально уязвима для Reentrancy-атак.
- Связь require(amount ← balance) — это проверка условий, которая должна предотвратить вывод средств, превышающих баланс. Однако она не защищает от повторного вызова функции перед обновлением balance.
В примере графа потока данных (см. рис. 1) связь между переменными balance, amount и операцией msg.sender.transfer(amount) помогает модели заметить потенциальную уязвимость типа Reentrancy. Рассмотрим, как Data Flow Graph (DFG) может это сделать:
- DFG выявляет неправильный порядок операций: msg.sender.transfer(amount) выполняется до изменения переменной balance. Это нарушение паттерна Check Effect Iteraction (CEI), который используется для предотвращения Reentrancy-атак.
- Если атакующий контракт вызывает withdraw() рекурсивно до обновления balance, он сможет вывести больше средств, чем положено.
- Нарушение паттерна CEI создает уязвимость, т. к. контракт не обновляет состояние (снижение баланса) до того, как выполняются внешние взаимодействия (перевод средств).
- Статический анализ
Помогает выявлять потенциальные уязвимости в смарт-контрактах, оценивая их структуру и признаки использования опасных операций. Разберем основные методы анализа:
- Проверка на опасные операции: tx.origin (уязвимость, приводящая к фишингу), delegatecall (может привести к захвату управления и модификации хранилища контракта), call и send (небезопасные методы отправки ETH; могут не завершиться успешно, но при этом не откатывают всю транзакцию).
- Анализ дополнительных фич: извлечение из кода контрактов признаков, которые могут быть связаны с уязвимостями, для глубокого анализа.
Выделим ключевые категории признаков:
Анализ модификаторов и контроля доступов:
- has_onlyOwner — проверяет, используется ли модификатор onlyOwner (контроль доступа);
- has_access_modifier — проверка на наличие модификаторов onlyOwner, onlyAdmin (модификаторы ролей);
- num_modifiers — количество модификаторов.
Анализ вызовов и внешних взаимодействий:
- num_external_calls — вызовы call, delegatecall, send, transfer. Также возможны неявные call-вызовы к функциям контрактов;
- num_external_contract_calls — вызовы других контрактов (contractInstance.method()).
- num_delegatecall — количество delegatecall (опасный вызов внешних контрактов);
- num_msg_sender — количество обращений к msg.sender;
- num_msg_value — количество обращений к msg.value;
- num_payable_functions — количество функций с payable.
Общая структура кода:
- num_functions — количество объявленных функций;
- num_public_functions — количество публичных функций;
- num_if_statements — количество if-условий;
- num_loops — количество циклов (for, while);
- avg_function_length — средняя длина функций;
- code_length — длина кода контракта;
- total_contract_length — полная длина контракта в строках;
- num_lines — общее количество строк кода;
- num_contracts — количество контрактов в файле.
Использование проверок и событий:
- num_require — количество проверок require (условий);
- num_assert — количество жестких проверок assert;
- num_revert — количество revert (откатов транзакций);
- num_events — количество событий;
- has_event — есть ли в коде хотя бы одно событие.
Использование опасных конструкций:
- num_selfdestruct — количество вызовов selfdestruct (удаление контракта);
- num_decimals — количество объявлений переменной decimals.
Лингвистический анализ кода:
- w2v_avg_scalar (Word2Vec Average Scalar) — усредненное значение всех компонент векторов слов в коде контракта. В нашем случае применяем как обобщенное представление важности слов, встречающихся в контракте;
- fasttext_avg_scalar (FastText Average Scalar) — аналогичный показатель для FastText-модели, которая, в отличие от Word2Vec, учитывает морфологию и подсловные (n-gram) структуры, что улучшает обработку редких слов и морфологически сложных языков.
Корреляция признаков
После выделения признаков переходим к анализу их взаимодействий. Для этого построим тепловую карту корреляции признаков (см. рис. 2).

Разберем полученные значения:
- Как видим из рисунка, сильная отрицательная корреляция с целевой переменной (Label) наблюдается у признаков num_functions (−0,43), num_lines (−0,37), code_length (−0,37), num_contracts (−0,34). То есть более сложные контракты (по количеству строк, функций и длине кода) реже содержат уязвимости.
- Умеренная отрицательная корреляция у w2v_avg_scalar (−0,28), num_events (−0,28), num_msg_sender (−0,19): контракты с большим количеством событий и семантически сложные контракты (Word2Vec) менее подверженных уязвимостям.
- Слабая корреляция с уязвимостями у num_require (−0,11), num_revert (−0,13) говорит о том, что наличие проверок require() и revert() не является определяющим фактором безопасности.
- Сильная внутренняя корреляция между структурными характеристиками кода у code_length и num_lines (0,97), num_functions и num_lines (0,78): чем больше строк в контракте, тем больше в нем функций и кода.
- Относительно высокая корреляция у num_require и num_msg_sender (0,48): контракты с большим количеством проверок require() чаще взаимодействуют с msg.sender.
- Корреляция у num_events и num_functions (0,37): больше функций в контракте означает больше событий.
- Незначительная корреляция у num_delegatecall, num_selfdestruct, num_assert. Эти признаки не имеют значимой связи с другими переменными. Это может означать, что они встречаются редко и не оказывают сильного влияния на модель.
Резюмируем: более сложные контракты реже содержат уязвимости, а частое использование msg.sender и require() не является гарантией их безопасности. Эти результаты необходимо учитывать при обучении моделей, особенно при выборе признаков для классификации.
Машинное обучение
Пайплайн машинного обучения
- Предобработка данных
- Отбор значимых признаков на основе анализа корреляции.
- Преобразование текстовых данных (w2v_avg_scalar, fasttext_avg_scalar).
- Нормализация числовых признаков.
- Обучение моделей
- Тестирование традиционных алгоритмов (Logistic Regression, Random Forest, Gradient Boosting, Support Vector Machine).
- Дообучение GraphCodeBERT-Base-Solidity-Vulnerability.
- Ансамблирование
- Hard Voting (жесткое голосование) — выбор класса большинством моделей.
- Soft Voting (взвешенное голосование) — усреднение вероятностей.
- Оценка качества
- Метрики: Accuracy, False Positives, False Negatives, ROC-AUC.
- Анализ ошибок классификации (FP, FN).
Дообучение модели GraphCodeBERT-Base-Solidity-Vulnerability
Рассмотрим, как Solidity-код трансформируется в разные графовые представления для дообучения GraphCodeBERT-Base-Solidity-Vulnerability.
Возьмем контракт, который позволяет владельцу (owner) вывести средства с баланса контракта. Однако если управление над owner получит злоумышленник, станет возможен непреднамеренный вывод средств.
pragma solidity ^0.8.0;
contract Vulnerable {
address owner;
constructor() {
owner = msg.sender;
}
function withdraw() public {
require(msg.sender == owner, "Not the owner");
payable(msg.sender).transfer(address(this).balance);
}
}
- Абстрактное синтаксическое дерево (AST)
AST представляет код как иерархическое дерево, где каждый узел соответствует конструкции языка. В нашем случае дерево будет выглядеть так:
FunctionDefinition(withdraw)
├── RequireStatement
│ ├── Identifier(msg.sender)
│ ├── BinaryOperation(==)
│ ├── Identifier(owner)
├── ExpressionStatement
├── FunctionCall(transfer)
├── MemberAccess(payable(msg.sender))
├── MemberAccess(address(this).balance)
Вершина FunctionDefinition(withdraw) содержит оператор require(), который проверяет условие на равенство между msg.sender и owen. Затем, если проверка проходит, выполняется перевод средств через transfer().
- Граф потока управления (CFG)
Control Flow Graph показывает, как выполняется код в зависимости от условий. Он позволяет проанализировать, как данные проходят через различные участки кода.
START → check (msg.sender == owner?) → YES → transfer() → END
↘ NO → REVERT
Анализ CFG:
- Если msg.sender == owner, то средства переводятся.
- Если нет, выполнение прерывается (REVERT).
Этот граф используется для анализа ветвлений и потенциальных уязвимостей, например пропущенных проверок.
- Граф потока данных
DFG отображает, как данные передаются между переменными и функциями.
msg.sender ---> owner (constructor)
|
├──> require(msg.sender == owner)
|
├──> payable(msg.sender)
| |
| ├──> transfer()
| |
address(this).balance ------┘
Анализ DFG:
- msg.sender передается в owner в конструкторе.
- В функции withdraw() проходит проверка msg.sender перед выполнением transfer().
- balance контракта используется в transfer() — это может быть уязвимостью в Reentrancy-сценариях.
- Гибридное представление: граф + токены
GraphCodeBERT представляет графы в виде списка ребер:
[
{"source": "msg.sender", "target": "transfer", "edge_type": "flows_to"},
{"source": "require", "target": "transfer", "edge_type": "controls"}
]
Кроме графовой структуры, модель использует токены:
[CLS] function withdraw() public { require ( msg . sender == owner , " Not the owner " ) ;
payable ( msg . sender ) . transfer ( address ( this ) . balance ) ; } [SEP]
Гибридное представление (граф + токены) позволяет модели учитывать как структуру кода, так и его семантическое значение.
Оценка качества

На рис. 3 представлены диаграммы для традиционных алгоритмов и их ансамблей, демонстрирующие среднюю точность моделей (слева) и их стандартное отклонение (справа). Расчеты проведены с использованием K-Fold и Shuffle Split кросс-валидации:
- Все модели показали высокую точность (около 0,85–0,93).
- Ансамблевые методы (Hard Voting и Soft Voting) достигают лучших результатов, улучшая предсказания за счет комбинирования нескольких моделей.
- Разница между методами K-Fold и Shuffle Split минимальна, что подтверждает стабильность моделей.
- Gradient Boosting имеет наибольший разброс (самый высокий Std), что указывает на нестабильность предсказаний модели на разных разбиениях данных.
- Logistic Regression, Hard Voting и Soft Voting демонстрируют наименьшее отклонение, что свидетельствует о большей надежности их предсказаний.
- Разница между K-Fold и Shuffle Split заметна, но Shuffle Split дает более стабильные результаты (меньший разброс).

Accuracy (Test) | False Positives (FP) | False Negatives (FN) | ROC-AUC Score | Confusion Matrix | |
---|---|---|---|---|---|
Logistic Regression | 0.879778 | 520 | 131 | 0.931623 | TN=619, FP=520, FN=131, TP=4145 |
Random Forest | 0.936473 | 176 | 168 | 0.9741 | TN=963, FP=176, FN=168, TP=4108 |
Gradient Boosting | 0.907849 | 274 | 225 | 0.955142 | TN=865, FP=274, FN=225, TP=4051 |
Support Vector Machine | 0.897692 | 372 | 182 | 0.945679 | TN=767, FP=372, FN=182, TP=4094 |
angusleung100/GraphCodeBERT-Base-Solidity-Vulnerability (дообученная) | 0.7156 | 1148 | 392 | None | TN=30, FP=1148, FN=392, TP=3845 |
Ensemble (Hard Voting) | 0.914866 | 296 | 165 | None | TN=843, FP=296, FN=165, TP=4111 |
Ensemble (Soft Voting) | 0.916343 | 297 | 156 | 0.96663 | TN=842, FP=297, FN=156, TP=4120 |
GraphCodeBert Solidity | 0.715605 | 1148 | 392 | TN=30, FP=1147, FN=392, TP=3845 | |
Ensemble (All Models + GraphCodeBERT) | 0.910619 | 290 | 194 | TN=888, FP=290, FN=194, TP=4043 |
Из табл. 1 можно сделать следующие выводы:
- Logistic Regression показала самую низкую точность (0,8798) среди традиционных методов, допустив 520 FP и 131 FN.
- Random Forest достиг наивысшей точности (0,9365) среди простых моделей, однако число FP (176) и FN (168) остается значительным.
- Gradient Boosting продемонстрировал сбалансированные результаты с точностью 0,9078, но увеличил FN (225 случаев).
- Support Vector Machine показал результаты хуже, чем Random Forest, но лучше, чем Logistic Regression (точность 0.8977).
- GraphCodeBERT (дообученная модель) значительно уступает классическим моделям (точность 0,7156), имея очень высокий уровень FP (1148) и FN (392). Это говорит о переобучении на предобученных данных и сложности адаптации к реальному Solidity-коду.
- Ансамблевые методы показали лучшие результаты:
- Hard Voting: точность 0.9149, 296 FP и 165 FN.
- Soft Voting: наилучший баланс FP/FN, точность 0.9163, 297 FP и 156 FN.
- Ансамбль с GraphCodeBERT ухудшил результаты (точность 0,9106).
Наилучшие результаты показал алгоритм Random Forest. График важности признаков (см. рис. 5) показывает, что ключевую роль в классификации уязвимостей Solidity-контрактов играют количество функций (num_functions) и сложность кода (num_contracts, num_lines, code_length). Использование require, revert и событий также влияет на предсказания модели, но в меньшей степени. Вызовы selfdestruct, delegatecall и других рискованных операций не являются доминирующими признаками, хотя могут быть полезны в отдельных случаях. Добавление семантических признаков (Word2Vec, FastText) усиливает модель, что показывает важность текстового анализа кода.

Анализ ошибок классификации False Positives и False Negatives
Анализ FP и FN для каждого алгоритма и ансамблевой модели помогает определить лучшую архитектуру.
False Positives — ложные срабатывания:
- Во многих FP-контрактах присутствуют лицензии SPDX (GPL, MIT, BSL-1.1 и др.)
- Среди FP встречаются примеры из OpenZeppelin, которые вряд ли содержат уязвимости.
- Часть ложных срабатываний вызвана сложными контрактами с импортами, где статический анализ ошибочно интерпретировал их как потенциально уязвимые.
False Negatives — пропущенные уязвимости:
- В FN часто встречаются функции multicall, mint, swap, withdraw, что указывает на высокую вероятность критических багов.
- Некоторые FN связаны с reentrance- и DoS-уязвимостями. Моделям сложно их детектировать.
- Обнаружены контракты с проблемами безопасности, описанными в отчетах CTF и багбаунти, но оставшиеся незамеченными нашими моделями.
Выводы и дальнейшие планы
Использование ансамблевого подхода Hard Voting Ensemble вместе с GraphCodeBERT-Base-Solidity-Vulnerability позволяет детектировать уязвимости в смарт-контрактах, которые пропускает ансамбль классических моделей.
Представленный нами подход автоматизирует процесс выявления уязвимостей, снижая вероятность атак благодаря сочетанию ансамблевого и графового методов на дообученной GraphCodeBERT-Base-Solidity-Vulnerability.
В дальнейшем мы планируем доработать модель:
- интеграцией генеративных моделей (GAN) для адаптации новых методов атак и защитных механизмов;
- расширением датасета для более точного файн-тюнинга LLM;
- динамическим анализом (включая фаззинг) для обнаружения логических уязвимостей, которые сложно выявить статическим анализом;
- прогнозированием нестандартных сценариев эксплуатации уязвимостей, которые могут быть неочевидны для традиционных статических анализаторов.
Так как большинство статических анализаторов ищут известные шаблоны уязвимостей, но неспособны выявлять новые и сложные атаки, также целесообразно:
- применять обучение с подкреплением (RL) для обучения агентов поиску новых уязвимостей;
- разрабатывать методы автоматического тестирования, анализирующие возможные сценарии эксплуатации.