Представьте: контракт на 40 строк, все юнит-тесты зелёные, forge test проходит, slither молчит. Команда деплоит в mainnet — и через неделю баланс пуст. Не потому что кто-то взломал хеш-функцию, а потому что в withdraw() стоит msg.sender.call{value: amount}("") до обнуления баланса. Классическая реентрантность, которую видит любой ревьюер за 30 секунд — но не видит CI.
Миф «компилируется + тесты зелёные = безопасен» живёт потому, что тесты проверяют happy path, а эксплойт — это unhappy path, которого в тестах нет. Аудит нужен не только протоколам на $100M: логическая ошибка на $5K в контракте тоже стоит $5K. Разница — в цене ошибки, а не в её наличии.
Подборка ниже — это 12 промтов, которые превращают LLM в ревьюера, работающего по чек-листу известных классов уязвимостей смарт-контрактов. Не «проверь безопасность» (бесполезно), а конкретные вопросы, на которые модель обязана ответить конкретным кодом или «не найдено».
Прежде чем копировать: как заставить LLM не врать про уязвимости
LLM по умолчанию склонна «находить» проблемы ради галочки — галлюцинирует критические баги в безопасном коде. Три правила, которые работают во всех промтах ниже:
- Требуйте цитату строки. Любая находка — только с фрагментом кода из вашего контракта и номером строки. Нет цитаты — нет находки.
- Разделяйте «подтверждено» и «требует проверки». Модель обязана явно помечать уровень уверенности.
- Давайте контекст явно. Не «вот контракт», а «вот контракт + вот как он используется + вот какие активы держит».
Без этого вы получите список из 20 «уязвимостей», 18 из которых — фантазии. С промтами ниже — 3–5 находок, из которых 2 реальные.
Промты для аудита смарт-контрактов: от threat model до PoC
1. Threat model до чтения кода
Промт:
Ты — аудитор смарт-контрактов. До чтения кода составь threat model для контракта со следующими характеристиками:
- сеть: [Ethereum / L2 / BSC]
- хранимые активы: [например, ERC-20 пул, ETH в escrow]
- роли: [owner, admin, user, keeper]
- внешние вызовы: [какие контракты вызывает, какие вызывают его]
Выведи таблицу: | Актив | Кто может его вывести | Через какую функцию | Что должно сломаться у атакующего, чтобы это стало возможным |
Отдельно перечисли 3 самых дорогих сценария потери средств для этого типа контракта.
Не читай код, пока не составлена таблица.
Зачем: threat model задаёт рамку. Без неё LLM ищет всё подряд и находит мусор. С ней — ищет в 3–5 точках, где реально лежат деньги.
2. Поиск реентрантности по изменениям состояния
Промт:
Проанализируй контракт на реентрантность. Алгоритм:
1. Найди все внешние вызовы (call, transfer, transferFrom, вызовы внешних контрактов).
2. Для каждого вызова найди все изменения state (запись в storage) в той же функции.
3. Если запись идёт ПОСЛЕ внешнего вызова — флаг "CEI нарушен", цитата строк.
4. Проверь cross-function реентрантность: может ли атакующий вызвать другую функцию контракта во время внешнего вызова и увидеть несогласованное состояние.
5. Проверь read-only реентрантность: есть ли view-функции, читающие состояние между вызовом и записью.
Формат: | Функция | Строка вызова | Строка записи | Тип | Уверенность (подтверждено/проверить) |
Для каждой находки — минимальный сценарий атаки в 3 строках.
Зачем: реентрантность — не «есть/нет», а «какое состояние видит атакующий». Классический паттерн call до balance = 0 — только первый уровень; cross-function и read-only reentrancy ловятся отдельным проходом.
3. Оракул и манипуляция ценой
Промт:
Проверь контракт на манипуляцию оракулом. Вопросы:
1. Откуда берётся цена? (Chainlink, Uniswap TWAP, spot-резерв, собственный расчёт)
2. Если spot или резерв — можно ли цену сдвинуть в рамках одной транзакции через флеш-кредит? Оцени, какой объём нужен для сдвига на 1%.
3. Есть ли проверка staleness (updatedAt, heartbeat)? Что будет, если оракул не обновится 2 часа?
4. Есть ли проверка min/max ответа? Что будет при ответе 0 или при отрицательной цене?
5. Используется ли одна и та же цена для deposit и withdraw?
Формат по каждому пункту: цитата кода + вердикт (защищено / уязвимо / нужен контекст).
Зачем: оракульные дыры — не «оракул плохой», а «цена читается из точки, которую можно сдвинуть». Промт заставляет модель проверить именно точку чтения и её защиту. Подробнее о том, почему децентрализованный оракул не спасает от флеш-кредита — в статье Безопасность блокчейна и аудит смарт-контрактов. Подробнее — в статье Firestore Security Rules и Supabase RLS.
4. Контроль доступа и роли
Промт:
Составь полную матрицу доступа. Для каждой функции контракта:
- кто может вызвать (anyone / owner / role / только через другой контракт)
- есть ли modifier, цитата
- что функция меняет
- что будет, если owner скомпрометирован — какой максимальный ущерб
Отдельно проверь:
1. initialize() — защищён ли от повторного вызова (initializer modifier, не только bool flag)?
2. Есть ли функции без модификатора, которые должны быть protected?
3. Может ли контракт быть уничтожен (selfdestruct) или funds выведены одной функцией?
Формат: таблица + список "функции без защиты, которые меняют состояние".
Зачем: неверный контроль доступа — самый частый источник потерь в контрактах, которые «работают». initialize() без защиты = захват контракта за одну транзакцию.
5. Целочисленные округления и порядок операций
Промт:
Проверь арифметику на округления. Для каждой формулы с делением:
1. В какую сторону округляется результат — в пользу протокола или пользователя?
2. Можно ли эксплуатировать округление через многократные мелкие операции (dust-атака)?
3. Есть ли проверка на 0 после деления?
4. Порядок операций: (a*b)/c vs a*(b/c) — где теряется точность?
5. Если контракт использует разные decimals у токенов — нормализованы ли они?
Для каждой формулы: пример с конкретными числами, показывающий потерю или эксплойт.
Зачем: округление в пользу пользователя при каждой операции = постепенный слив. В одном вызове — копейки, в тысяче — заметная сумма.
6. Слив газа и griefing
Промт:
Проверь контракт на gas-инъекции и griefing-векторы:
1. Есть ли unbounded цикл по массиву, размер которого контролирует пользователь? Оцени, при каком размере он перестаёт проходить в блок.
2. Есть ли внешний вызов в цикле (call/transfer внутри for)?
3. Может ли атакующий сделать операцию для другого пользователя невыполнимой (блокировка withdraw, отравление очереди)?
4. Есть ли function, где стоимость газа растёт с числом пользователей — и кто её оплачивает?
Формат: | Функция | Вектор | Кто платит газ | Максимальный ущерб | Уверенность |
Зачем: gas-инъекция не крадёт деньги напрямую — она делает протокол неработоспособным или вынуждает платить за атаку. Для escrow и очередей это критично.
7. Проверка внешних вызовов и возвращаемых значений
Промт:
Проверь все внешние вызовы:
1. Проверяется ли возвращаемое значение у call/transfer/transferFrom?
2. Используется ли low-level call без проверки success?
3. Есть ли вызовы к адресам, которые пользователь передаёт как параметр? Может ли он подставить malicious-контракт?
4. ReentrancyGuard на всех функциях, где есть внешний вызов?
5. Для ERC-20: используется ли SafeERC20 или проверка на токены без возврата значения (USDT)?
Каждый пункт: цитата или "не применимо, потому что…".
Зачем: token.transfer() без проверки return — тихая потеря средств на токенах, которые возвращают false вместо revert. Классика, которую CI не ловит.
8. Проверка событий и off-chain логики
Промт:
Проверь, что все изменения критичного состояния сопровождаются событиями. Список критичных изменений: изменение owner/ролей, изменение параметров (fee, лимиты), движение средств, пауза/анпауза.
Для каждого: есть ли event, хватает ли полей для восстановления состояния off-chain?
Отдельно: может ли бот/индексер, слушающий события, принять неверное решение из-за пропущенного или неполного event?
Зачем: аудит — не только про кражу. Если ваш мониторинг не видит вывод средств, вы узнаете об эксплойте из Twitter.
9. Сравнение с известными паттернами
Промт:
Сравни контракт с четырьмя известными классами атак и для каждого ответь "защищён / уязвим / не применимо":
1. Reentrancy (включая cross-function и read-only)
2. Oracle/price manipulation
3. Access control (включая initialize и delegatecall)
4. Integer issues (overflow, rounding, decimals)
Для каждого класса — не общие слова, а ссылка на конкретную функцию контракта и строку. Если класс не применим — объясни, почему (нет внешних вызовов, нет оракула и т.д.).
Зачем: это финальная сверка после точечных промтов выше. Ловит то, что проскочило в узких проверках.
10. PoC-тест на Foundry для найденной уязвимости
Промт:
Напиши PoC-тест на Foundry для найденной уязвимости [описание].
Требования:
- тест должен ПАДАТЬ на текущем коде и ПРОХОДИТЬ после фикса
- использовать forge-std (Test.sol), vm.prank, vm.deal
- атакующий контракт — отдельный контракт в тесте, если нужна реентрантность
- в комментарии — что именно тест доказывает
- без моков, которые скрывают уязвимость
Выведи полный файл test/Exploit.t.sol и команду запуска.
Зачем: находка без PoC — гипотеза. Тест, который падает на уязвимом коде, превращает её в факт и одновременно становится регрессионным тестом после фикса.
11. Проверка фикса, а не только находки
Промт:
Вот уязвимость [описание] и вот предложенный фикс [diff].
1. Закрывает ли фикс именно этот вектор, или только выглядит так?
2. Не вводит ли фикс новую уязвимость (например, reentrancy guard меняет порядок вызовов)?
3. Есть ли обходной путь, который фикс не закрывает?
4. Какой минимальный PoC докажет, что фикс работает?
Ответь по пунктам с цитатами diff.
Зачем: половина повторных эксплойтов — это неверный фикс. Модель, проверяющая свой же предложенный фикс, ловит «заплатку», которая закрывает симптом.
12. Финальный отчёт для команды
Промт:
Собери результаты всех проверок в отчёт:
| # | Класс | Функция | Строка | Уверенность | Критичность (критично/высоко/средне/низко) | PoC есть? | Фикс предложен? |
Отдельно: 3 находки, которые нужно чинить до деплоя, и 3, которые можно отложить с обоснованием.
В конце — что НЕ проверено этим аудитом (внешние зависимости, экономика протокола, off-chain часть).
Зачем: отчёт с честным разделом «что не проверено» — признак зрелого аудита. LLM без этого промта склонна объявлять «всё проверено», что опаснее пропущенной дыры.
Что эти промты не заменяют
LLM хорошо ловит паттерны: CEI-нарушения, отсутствие modifier, spot-цену. Плохо — экономику протокола (может ли инфляция доли в LP-пуле обнулить чужой вклад), сложные многоконтрактные взаимодействия и всё, что требует запуска кода. Для этого нужен либо фаззинг (Foundry, Echidna), либо человек с опытом.
Но 80% реальных эксплойтов в небольших контрактах — это те самые паттерны из списка. Прогнать 12 промтов перед деплоем — 2–3 часа работы, которые стоят дешевле, чем один потерянный контракт. И дешевле, чем полный аудит, который для контракта на $20K просто не окупается.
Начните с промта №1 на контракте, который собираетесь деплоить на этой неделе. Если threat model уже показывает точку, где лежат все деньги без защиты, — дальше можно не идти: сначала фикс, потом остальные 11 промтов.