Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
К разработчикам смарт контрактов будут предъявлять новые требования при найме?

Сегодняшний разработчик смарт-контрактов — это уже не просто человек за клавиатурой, который пишет код на Solidity и проверяет его через Foundry. Современный специалист должен понимать, как работают децентрализованные системы, как находить и устранять уязвимости, и при этом держать руку на пульсе технологических изменений. Одно из таких изменений — появление инструментов, которые позволяют искусственному интеллекту взаимодействовать с блокчейном и смарт-контрактами. И ключевую роль здесь начинают играть MCP-серверы — Model Context Protocol серверы. Это, по сути, мост между языковыми моделями и внешними системами, включая блокчейн, API и базы данных. Они позволяют ИИ безопасно получать данные и выполнять действия, не выходя за рамки заданного контекста.

Современные языковые модели уже умеют писать смарт-контракты на Solidity и других языках без дополнительных инструментов — они обучаются на огромных объёмах кода и могут воспроизводить шаблоны, включая использование библиотек вроде OpenZeppelin. Однако одно дело — сгенерировать код по памяти, и совсем другое — гарантировать, что он актуален, безопасен и соответствует последним версиям проверенных шаблонов. Именно здесь на помощь приходит MCP-сервер. Он позволяет модели не просто "вспоминать" код, а запрашивать его напрямую из доверенного источника — например, из официальных репозиториев OpenZeppelin. Сервер выступает в роли посредника, предоставляя модели доступ к точным, аудированным шаблонам и строго регламентируя, какие действия и с какими параметрами могут быть выполнены. Это превращает ИИ из пассивного генератора текста в активного участника разработки, способного безопасно и точно взаимодействовать с внешними системами.

Одна из ведущих компаний в области безопасности — Trail of Bits — уже занялась вопросами защиты таких взаимодействий. Они разработали mcp-context-protector — специальный слой безопасности, который защищает от атак, когда вредоносный сервер может подменить инструкции для модели. Например, через описания инструментов можно внедрить prompt-инъекцию и заставить модель выполнять нежелательные действия. Их решение работает как прокси: оно проверяет все, что приходит от сервера, до того как это попадёт в контекст модели. Это особенно важно, когда вы доверяете стороннему MCP-серверу, потому что теперь можно быть уверенным, что в коде или инструкциях не скрыто ничего опасного.

Но не только про безопасность. OpenZeppelin, известный своими шаблонами для безопасных контрактов, запустил свой MCP-сервер, который позволяет разработчикам генерировать готовые контракты на Solidity, Cairo и других языках. Представьте: вы просто говорите ассистенту — "создай ERC-20 токен с именем MyToken и общим объёмом 1 миллион", — а он, через MCP-сервер OpenZeppelin, получает шаблон, подставляет параметры и возвращает вам готовый, аудированный код. Это экономит время, снижает риск ошибок и делает процесс разработки доступнее даже для новичков.

Что особенно ценно — эти серверы не просто дают доступ к функциям, они делают это в стандартизированной и безопасной форме. MCP-сервер OpenZeppelin не просто генерирует код, он делает это на основе проверенных шаблонов, которые уже используются тысячами проектов. А благодаря интеграции с безопасными протоколами, вы можете быть уверены, что даже если ИИ что-то напутает, базовая архитектура останется надёжной. Это как если бы у вас был старший разработчик, который всегда проверяет ваш код перед тем, как он попадёт в продакшн.

Для разработчика это означает, что скоро будет недостаточно просто знать Solidity. Важно будет понимать, как работают внешние инструменты, как интегрировать их через MCP, и как обеспечивать безопасность этих взаимодействий. Умение настраивать, проверять и использовать MCP-серверы станет частью повседневной работы. И те, кто начнёт изучать это уже сейчас, получат серьёзное преимущество — не только в скорости разработки, но и в качестве итогового продукта.
🔥73
Всё это — часть более широкого сдвига: мы переходим от ручного кодирования к агент-ориентированной разработке, где ИИ становится активным участником процесса. Но с ростом возможностей растёт и ответственность. Поэтому важно не просто подключать MCP-серверы, а понимать, как они работают, что они делают и как их можно взломать. Будущее смарт-контрактов — это не только безопасный код, но и безопасные инструменты, которые помогают этот код создавать.

#llm #solidity
👍5🔥1
Обновление в Solidity

Об интересном обновлении в Solidity 0.8.29 я узнал на днях из чатика аудиторов, спасибо @SovaSlava!

В этой версии языка появилась поддержка указания кастомных слотов хранилища смарт контрактов, как на скрине выше.

Контракты в Solidity по умолчанию размещают свои переменные состояния, начиная с нулевого слота хранилища, и далее последовательно, в соответствии с порядком определения и иерархией наследования. Однако, начиная с новой версией языка, появилась возможность явно задать начальный слот для размещения хранилища с помощью спецификатора layout at. Это позволяет разработчику управлять тем, с какого именно слота будут начинаться переменные состояния контракта, включая те, что унаследованы от базовых контрактов. Такая гибкость открывает новые возможности для сложных сценариев, особенно в системах с динамической логикой или при работе с прокси-паттернами, где требуется точный контроль над расположением данных в хранилище.

Рассмотрим конкретный пример:

contract C layout at 2**255 - 42 {
uint x;
}


contract C layout at 2**255 - 42 задаёт начальный слот для хранилища как значение, равное 2 в степени 255 минус 42. Это число находится в верхней половине диапазона uint256, но всё ещё достаточно далеко от максимального значения, чтобы оставить место для размещения хотя бы нескольких переменных. Переменная uint x, будучи первой в контракте, будет размещена именно в этом слоте — то есть в слоте с номером 578...66. Это демонстрирует, что выражение в спецификаторе layout at может быть сложным, но при этом оно должно быть вычислимо на этапе компиляции.

Спецификатор layout at должен содержать выражение и результат должен быть значением типа uint256. Это может быть простая константа, сумма или битовый сдвиг, но не может включать вызовы функций или переменные. Компилятор проверяет, не выходит ли суммарный объём статических данных за пределы адресного пространства хранилища. Например, если базовый слот установлен слишком близко к type(uint256).max, и при этом контракт имеет много переменных, компилятор выдаст ошибку, чтобы предотвратить «переполнение» хранилища. Однако динамические структуры, такие как массивы переменной длины и маппинги, не подпадают под эту проверку, поскольку их данные размещаются не линейно, а по хеш-адресам, вычисляемым на основе ключей и базового слота.

Важно понимать, что спецификатор layout at можно указать только для самого верхнего контракта в дереве наследования, и он влияет на все переменные состояния во всей иерархии. Если контракт A наследуется B, а B — C, и только C имеет layout at, то все переменные из A, B и C будут сдвинуты на одну и ту же величину. При этом абстрактные контракты, интерфейсы и библиотеки не могут использовать этот механизм, так как они либо не имеют собственного хранилища, либо не предназначены для развертывания как независимые экземпляры. Также не затрагиваются временные переменные, помеченные как transient, поскольку они хранятся не в постоянном хранилище.

На практике этот механизм может быть использован в системах, где требуется изолировать хранилище контракта от других данных, например, в реализациях UUPS (Universal Upgradeable Proxy Standard), где логика обновления должна гарантированно не затрагивать определённые слоты. Другой пример — многомодульные системы, где каждый модуль размещается в выделенной области хранилища, и использование layout at позволяет избежать случайных пересечений. Однако следует быть осторожным: использование слотов вблизи верхней границы адресного пространства может затруднить будущие обновления или привести к коллизиям при использовании inline assembly для ручной записи данных. Также стоит помнить, что layout и at пока не являются зарезервированными словами, но их использование в качестве идентификаторов в будущем станет невозможным, поэтому лучше избегать их в именах переменных и функций уже сейчас.

#solidity
18🔥3👍2
Взломы контрактов с помощью LLM все ближе

Пока одни аудиторы в Твиттере смеются над теми кто использует LLM в своей работе, другие учатся ее использовать на 100%.

На днях попалась очень интересная статья, где группа разработчиков создала агента для взлома смарт контрактов:

https://arxiv.org/abs/2507.05558v2

Постараюсь кратко пересказать ее.

Исследователи представили A1 — агентную систему на основе больших языковых моделей, способную автоматически генерировать эксплойты для уязвимостей в смарт-контрактах DeFi. В отличие от традиционных инструментов анализа, которые лишь указывают на потенциальные баги, A1 строит полный Proof-of-Concept атаки, включая цепочку транзакций, использование flash loan и извлечение реальной экономической выгоды. Это позволяет не просто обнаружить уязвимость, а доказать её эксплуатируемость, что кардинально повышает значимость находки. Система работает итеративно, используя обратную связь от выполнения на симулированной блокчейн-среде, и за несколько шагов уточняет и доводит стратегию до успешного результата.

A1 была протестирована на наборе из 36 реальных инцидентов, включая атаки через манипуляцию цен, реентранси и уязвимости в механизмах распределения токенов. В среднем система достигла успеха в 72,2% случаев, а с использованием самых мощных моделей, таких как OpenAI o3-pro, этот показатель вырос до 88,5%. Общая сумма, которую удалось воспроизвести как извлечённую прибыль, составила 9,33 миллиона долларов. При этом A1 продемонстрировала результативность, сопоставимую с современными фаззерами, такими как ItyFuzz и Efcf, но при этом с меньшим количеством ложных срабатываний, поскольку каждый успех подтверждён реальным экономическим эффектом.

Одним из ключевых аспектов работы является экономический анализ рентабельности использования A1 как в атакующих, так и в защитных целях. Показано, что при реалистичных предположениях — например, при наличии баг-баунти в размере 10% от ущерба — использование A1 может быть прибыльным для аудиторов и защитников. Однако система выявила тревожную асимметрию: атакующим проще окупить затраты на запуск таких агентов, поскольку им не нужно ожидать официального вознаграждения и они могут сразу извлекать прибыль. Это означает, что технология, хотя и доступна обеим сторонам, фактически смещает преимущество в пользу злоумышленников.

Система A1 интегрируется с рядом специализированных инструментов: для получения исходного кода, анализа состояния контрактов, нормализации дохода в базовую валюту и выполнения транзакций в изолированной среде. Она поддерживает как Ethereum, так и BSC, и способна обрабатывать сложные сценарии, включая работу с proxy-контрактами и учёт рыночной ликвидности при построении маршрутов обмена. При этом производительность сильно зависит от выбранной LLM: например, Gemini Flash работает быстро, но менее точно, тогда как o3-pro даёт лучшие результаты, но требует в среднем 34 минуты на один запуск.

Работа поднимает важные вопросы о будущем безопасности смарт-контрактов. Если агенты на основе ИИ могут уже сейчас воспроизводить реальные атаки, это означает, что традиционные методы аудита и анализа требуют модернизации. Авторы подчёркивают, что A1 — не замена фаззерам, а дополнение, особенно эффективное в сценариях, требующих глубокого логического и экономического анализа. Исследование демонстрирует, что граница между программным анализом, машинным рассуждением и экономикой в DeFi продолжает стираться, и дальнейшее развитие в этой области неизбежно потребует более сложных, агентных систем, способных мыслить стратегически и адаптироваться к динамике реальных систем.

#llm #solidity
2😱10
Сколько нужно учиться в день?

Вопрос о том, сколько времени нужно уделять обучению каждый день, наверное, один из самых частых перед стартом курса. Люди хотят понять, как правильно распределить усилия, чтобы двигаться вперёд с ощутимым прогрессом.  

На протяжении долгого времени мой ответ был примерно одинаковым: учиться стоит столько, сколько потребуется для глубокого понимания темы. Если вы разобрались за полчаса — отлично, если на это ушло два часа или даже больше — тоже прекрасно. У каждого свой темп, и универсальных временных рамок здесь быть не может.  

Недавно я сам погрузился в новую для себя область — машинное и глубокое обучение. Это совершенно новая сфера, где я начинаю практически с чистого листа. Математика даётся непросто, и я чувствую себя тем, кто только делает первые шаги, как и многие из вас, осваивая, например, Solidity с самых основ.  

За последний месяц я потратил на обучение 33 часа. Поскольку в выходные стараюсь отключаться от компьютера, занятия проходят только в будние дни. Казалось бы, в среднем это чуть больше часа в день — но на деле всё обстоит иначе.  

Процесс совсем не равномерный: бывает, что в один день я только знакомлюсь с новой темой, а в другой — полностью погружаюсь в неё, уделяя теории и практике по пять-шесть часов подряд.  

Для меня ключевое — не количество затраченного времени, а качество усвоения. Могу ли я объяснить пройденное простыми словами, так, чтобы понял человек без технического бэкграунда? Если да — значит, тема усвоена, можно двигаться дальше. Если нет — возвращаюсь к материалу, выписываю вопросы, ищу дополнительные источники, пока не почувствую уверенность.  

За эти 33 часа я освоил базовый синтаксис Python, познакомился с такими модулями, как math и itertools, разобрался с основами линейной алгебры — видами векторов и матриц, операциями над ними — и начал работать с библиотекой numpy.  

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

Надеюсь, из моего опыта видно, что путь обучения у каждого индивидуален. Нет единого графика, по которому все обязательно добьются результата за определённое время.  

Я по-прежнему убеждён: регулярность куда важнее интенсивности. Главное — не пропадать, двигаться вперёд хоть понемногу, но каждый день. И уже через месяц-два вы с удивлением оглянетесь назад и увидите, как много успели освоить.

#study
7👍4🔥1
Новый способ взаимодействия Foundry и AI

Недавно наткнулся на интересный проект под названием foundry-mcp-server, который предлагает новый способ взаимодействия с экосистемой Foundry. Этот репозиторий представляет собой сервер, реализующий протокол MCP (Model Context Protocol), что позволяет интегрировать внешние инструменты, например, AI-ассистенты, непосредственно в процесс разработки смарт-контрактов. Идея заключается в том, чтобы сделать взаимодействие между разработчиком и инструментами анализа кода более гибким и контекстно-зависимым, используя стандартный протокол обмена данными.

Особенность foundry-mcp-server в том, что он не просто предоставляет API, а создает среду, в которой AI-модели могут получать доступ к состоянию проекта Foundry, запускать тесты, анализировать контракты и даже предлагать исправления на основе реального контекста. Это особенно полезно при тестировании — например, можно настроить ассистента, который автоматически генерирует edge-case тесты на основе анализа кода или выявляет потенциальные уязвимости, используя статический анализ в сочетании с семантическим пониманием. Такой подход может значительно ускорить процесс ревью и повысить качество кода.

С технической точки зрения, сервер написан на Rust и использует стандартные инструменты Foundry, такие как cast и anvil, для выполнения операций в блокчейн-среде. Он запускается локально и предоставляет HTTP-интерфейс, через который внешние клиенты могут запрашивать информацию о проекте, выполнять команды и получать результаты. Например, можно запросить список всех тестов в проекте, получить ABI контракта или запустить конкретный тест с определёнными параметрами. Всё это делается с сохранением полного контекста — включая состояние сети, переменные окружения и результаты предыдущих операций.

Одним из ключевых преимуществ этой архитектуры является её расширяемость. Поскольку MCP — это открытый протокол, разработчики могут подключать к серверу различные инструменты: от IDE-плагинов до автономных агентов, которые самостоятельно исследуют код и предлагают оптимизации. Это открывает путь к созданию «умных» систем разработки, где AI не просто подсказывает, а активно участвует в процессе — например, перебирает различные сценарии атак на контракт или автоматически генерирует документацию на основе кода и комментариев.

На данный момент проект находится на ранней стадии, но уже демонстрирует большой потенциал. Он может стать важным звеном в цепочке инструментов для безопасной разработки смарт-контрактов, особенно в сочетании с системами CI/CD и автоматизированным тестированием. Если вы работаете с Foundry и интересуетесь автоматизацией процессов разработки, стоит посмотреть на foundry-mcp-server как на экспериментальную, но перспективную платформу для построения следующего поколения инструментов для Solidity.

Примеры его работы можно посмотреть в этой ветке Твиттера.

#foundry
🔥91🤔1
Кто-нибудь хочет присоединиться к модулю по Foundry?

На этой неделе завершается основная обучающая часть Летнего модуля. Следующая неделя знаменует собой начало нового блока — практическое освоение экосистемы Foundry для начинающих.

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

Если вы заинтересованы в участии в модуле по Foundry, пожалуйста, дайте знать в опросе ниже.

Старт на следующей неделе.
Стоимость: 3000 рублей, 40 USDT
Старт продаж в пятницу.

Напомню темы модуля:


Неделя 1

1. Установка Foundry: что входит в программу и для чего это нужно
2. Знакомство с cast командами. Чем они нужны и как работают
3. Сложные команды cast, которые пригодятся разработчику
4. Работа с chisel. Побитовые сдвиги и форк сети
5. Настройки Foundry: профили, библиотеки, пути


Неделя 2

6. Какие тесты бывают: правила написания и формирования папки проекта
7. Создание setUp() функции для удобного написания тестов. Роли пользователей, управление временем в тестах
8. Логика assert условий
9. Работа с ошибками и событиями в тестах
10. Логирование результатов теста и вывод в консоль


Неделя 3

11. Фазз тесты
12. Форк тесты. Интеграции с другими протоколами
13. Мутационные тесты
14. Тестирование подписей в контрактах
15. Деплой в разные сети. Написание простых скриптов


Неделя 4. Бонусные уроки

16. Интеграция с Hardhat
17. Хранение приватных ключей
18. Современные программы тестирования
19. Финальный практикум

Если есть какие-либо вопросы, буду рад ответить в комментариях!

#foundry
🔥31👍1
Кто-нибудь хочет присоединиться к модулю по Foundry?

P.S. Ученикам текущего Летнего модуля голосовать не нужно.
Final Results
45%
Да, уже готов!
55%
Не в этот раз (не хочу, не нужно, уже знаю)
Атака Denial-of-Service. Часть 1

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

В блоге Cyfrin мне попался прекрасный цикл статей от крутого аудитора и судьи Hans, который разбирает уязвимости из "Solodit Checklist". Давайте вместе изучим их.

Итак, первая уязвимость, которую мы рассмотрим это "DoS".

Почему нам стоит беспокоиться о DoS-атаках?

Разве DoS-атаки — это проблема только для традиционных централизованных серверов? Что-то, о чем должны переживать только крупные корпорации, верно? Нет!

На самом деле, в децентрализованных финансах (DeFi) DoS-атаки могут быть разрушительными. Представьте себе этот кошмарный сценарий: ваш тщательно разработанный стейкинг-контракт, созданный для щедрого вознаграждения пользователей, внезапно… перестает работать.

Он встает колом, потому что какой-то злоумышленник забрасывает его транзакциями. Пользователи не могут вывести свои средства, и вся система становится безнадежно неработоспособной. А что насчет вашей репутации? Она падает быстрее, чем цена мемкоина после rug pull'а (а мы все знаем, каково это).

DoS-атаки используют уязвимости в вашем коде, делая смарт-контракт бесполезным — либо для конкретных пользователей, либо для всех. Они как цифровые баррикады, мешающие честным людям получить доступ к услугам вашего протокола! В сфере, где доверие решает все, успешная DoS-атака может стать полной катастрофой.

Для наглядности представьте: вы вложили душу в создание прекрасной кофейни. Это лучшая кофейня в мире. Но тут кто-то приходит и начинает заказывать тысячи пустых чашек кофе, забивая всю систему и не давая реальным клиентам получить свой кофеин. Бесит, правда?

Denial-of-Service (DoS) атака: общий обзор

DoS-атаки обычно используют недостатки в логике вашего смарт-контракта, а не грубую силу. Это тонкая манипуляция, которая вас побеждает.

Вот как это работает:

1. Эксплуатация логики — злоумышленники находят хитрые (или, точнее, вредоносные) способы вызвать изменения состояния, которые приводят к откату транзакций или бесконечным циклам, фактически замораживая контракт. Это происходит чаще, чем кажется.

2. Исчерпание ресурсов — представьте себе это как трагедию общего достояния, но с комиссиями за газ. Атакующий заваливает контракт запросами, сжигая огромное количество газа и делая его использование невыгодным для обычных пользователей. Помните: каждое действие в блокчейне требует газа, а лавина транзакций может взвинтить эти расходы.

Теперь, когда мы кратко разобрали основы (и, надеюсь, убедили вас, что DoS-атаки — реальная угроза), давайте углубимся в конкретные способы защиты.

SOL-AM-DOSA-1: Используется ли паттерн вывода средств для защиты от DoS?

Иными словами: Вы применяете "pull" вместо "push" подход для вывода средств?

Классическая ошибка (которую я видел слишком много раз в конкурсах по аудиту) — это передача ETH пользователям в функции вывода средств. Кажется, все просто, правда? Совершенно не так! Давайте посмотрим на код:

// Anti-pattern: Pushing ETH
function batchWithdraw() public {
address[] memory users = getUsers(); // imaginary function to get users
for (uint i = 0; i < users.length; i++) {
uint amount = balances[users[i]];
if (amount > 0) {
balances[users[i]] = 0;
(bool success, ) = users[i].call{value: amount}(""); // Potential DoS!
require(success, "Transfer failed"); // If any transfer fails, the entire batch reverts
}
}
}


Почему это плохо?

Если msg.sender — это контракт, который ревертится при получении ETH (по любой причине — возможно, из-за злонамеренных действий), то вся функция вывода средств будет ревертиться также для всех! Это создает реальную ситуацию DoS, поскольку легитимные пользователи не могут вывести средства из-за одного неработающего адреса. Представьте себе хаос!

‍Решение: модель pull
Вместо push, позвольте пользователям самим забирать свои токены. Они инициируют перевод из контракта. Это как дать им ключ от хранилища. Выглядит это так:

// Pull Pattern: Much Safer!
mapping(address => uint) public withdrawableBalances;

// Step 1: Admin marks funds as withdrawable without actually sending them
function startBatchWithdrawal() public {
address[] memory users = getUsers(); // imaginary function to get users
for (uint i = 0; i < users.length; i++) {
uint amount = balances[users[i]];
if (amount > 0) {
balances[users[i]] = 0;
withdrawableBalances[users[i]] += amount;
}
}
}

// Step 2: Each user withdraws their own funds individually
function withdraw() public {
uint amount = withdrawableBalances[msg.sender];
require(amount > 0, "No funds to withdraw");
withdrawableBalances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}


Теперь, если вывод средств пользователя не удается, это затрагивает только его самого. Контракт продолжает функционировать для всех остальных. Это и есть правильная децентрализация! Оператор require проверяет, был ли перевод успешным. В случае неудачи транзакция отменяется только для запрашивающего пользователя, что оказывает минимальное влияние на текущие операции.

В примере, приведенном в пункте чеклиста, подробно описан сценарий, в котором комиссии переводятся владельцу до вывода средств пользователем. Если адрес владельца случайно установлен на нулевой адрес или если владелец является, э-э-э, злонамеренным контрактом, который ревертит перевод токенов, вывод средств пользователями не удастся. Это неприятная ошибка для отладки! Минимальный пример и PoC, написанные в Foundry, доступны здесь.

Далее разберем еще два случая атаки.

#dos
👍21
Атака Denial-of-Service. Часть 2

Существует ли минимальная сумма транзакции?

Намек: Вы предотвращаете «пылевые» транзакции (крошечные, незначительные суммы), которые забивают ваш контракт и делают все неиспользуемым?

Здесь речь идет о предотвращении спама, просто и понятно. Если ваш контракт позволяет пользователям взаимодействовать с ним на любую сумму (даже на крошечные, незначительные доли токенов), злоумышленники могут затопить его бесчисленными транзакциями с нулевой или почти нулевой стоимостью. Все эти транзакции потребляют газ и делают законные операции гораздо более дорогостоящими. Это увеличение затрат может потенциально достичь предела лимита газа в блоке. Если это произойдет, нормальные пользователи не смогут взаимодействовать с вашим протоколом. Вот как выглядит уязвимый код:

// Vulnerable to dust attacks
struct WithdrawalRequest {
address user;
uint amount;
}
WithdrawalRequest[] public withdrawalRequests;

// Anyone can submit withdrawal requests for ANY amount (even 1 wei!)
function requestWithdrawal(uint amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;

// Add to the global withdrawal queue - no minimum amount check!
withdrawalRequests.push(WithdrawalRequest(msg.sender, amount));
}

function processWithdrawals() external onlyOwner {
for (uint256 i = 0; i < withdrawalRequests.length; i++) { // This amplifies the attack even more because it tries to handle all the requests at once
WithdrawalRequest memory request = withdrawalRequests[i];
request.user.transfer(request.amount);
}
withdrawalRequests = new WithdrawalRequest[](0);
}



Решение: ввести минимальное требование!

Простой оператор require может значительно улучшить ситуацию. Введите пороговое значение с помощью оператора require и действуйте как охранник, проверяющий удостоверение личности на входе в клуб. Нам также необходимо обеспечить возможность обработки запросов на снятие средств пакетами. Вот как это сделать:

uint public minimumWithdrawal = 0.1 ether;

function requestWithdrawal(uint amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
require(amount >= minimumWithdrawal, "Amount below minimum withdrawal threshold"); // Bouncer!

balances[msg.sender] -= amount;
withdrawalRequests.push(WithdrawalRequest(msg.sender, amount));
}

function processWithdrawals(uint count) external onlyOwner { // We can process in batches now
for (uint256 i = 0; i < count; i++) {
WithdrawalRequest memory request = withdrawalRequests[i];
request.user.transfer(request.amount);
}
for (uint i = 0; i < withdrawalRequests.length - count; i++) {
withdrawalRequests[i] = withdrawalRequests[i + count];
}
withdrawalRequests.length = withdrawalRequests.length - count;
}


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

#foundry #dos
👍81
Атака Denial-of-Service. Часть 3

Как протокол обрабатывает токены с функцией черного списка?

Уточнение: Вы учитываете последствия использования токенов (например, USDC и подобных), которые могут вносить пользовательские адреса в черный список?

Это гораздо более сложный вопрос, который имеет огромное значение в современном мире регулируемых стейблкоинов. Некоторые токены имеют функцию черного списка. Это означает, что центральный орган владеющий токеном может заморозить или полностью заблокировать использование токена для определенных адресов.

Почему это потенциальная бомба замедленного действия?

Представьте себе контракт сообщества, в котором друзья, члены семьи или инвестиционные партнеры объединяют свои токены в «группу стейкинга». Звучит кооперативно и эффективно, не так ли? Каждому члену назначается процент вознаграждения в зависимости от его вклада.

Но вот, где дело становится опасно интересным: что произойдет, если только один член вашей стейкинг-группы попадет в черный список токена? Возможно, ваш двоюродный брат оказался в каком-то списке регулирующих органов, или адрес вашего друга был помечен из-за какой-то совершенно не связанной с этим транзакции. Для остальных членов группы это не имеет большого значения, верно? Неверно!

Когда ваша группа пытается вывести свои вознаграждения, вся транзакция терпит крах! Почему? Потому что контракт пытается распределить вознаграждения всем участникам в рамках одной транзакции. Если один перевод не удается, вся операция отменяется! Совокупные токены вашей группы на сумму 100 ETH? Полностью заблокированы! Ваши запланированные выводы? Невозможны! И все из-за того, что один из участников попал в черный список, что может не иметь никакого отношения к вашей стейкинговой деятельности!

Знаете, что еще хуже? Обычно нет возможности удалить члена, занесенного в черный список, или перераспределить доли. Средства группы фактически замораживаются до тех пор, пока черный список токенов не будет обновлен — а это может никогда не произойти, если занесение в черный список было сделано по регуляторным причинам.

Это не теоретическая проблема — это происходит в реальных контрактах сегодня! Единственная точка отказа, затрагивающая сразу нескольких пользователей. Механизм коллективного наказания, о котором никто не просил! Страшно, не правда ли?

Вот как это выглядит в коде:
🐳1
contract GroupStaking {
IERC20 public token;

struct StakingGroup {
uint256 id;
uint256 totalAmount;
address[] members;
uint256[] weights;
bool exists;
}

// Mapping from group ID to group data
mapping(uint256 => StakingGroup) public stakingGroups;
// Current group ID counter
uint256 public nextGroupId = 1;

constructor(IERC20 _token) {
token = _token;
}

// Create a new staking group
function createStakingGroup(address[] calldata _members, uint256[] calldata _weights) external returns (uint256) {
require(_members.length > 0, "Empty members list");
require(_members.length == _weights.length, "Members and weights length mismatch");

// Validate weights sum to 100%
uint256 totalWeight = 0;
for (uint256 i = 0; i < _weights.length; i++) {
totalWeight += _weights[i];
}
require(totalWeight == 100, "Weights must sum to 100");

uint256 groupId = nextGroupId;
stakingGroups[groupId] = StakingGroup({
id: groupId,
totalAmount: 0,
members: _members,
weights: _weights,
exists: true
});

nextGroupId++;
return groupId;
}

// Stake tokens to a group
function stakeToGroup(uint256 _groupId, uint256 _amount) external {
require(stakingGroups[_groupId].exists, "Group does not exist");
require(token.transferFrom(msg.sender, address(this), _amount), "Transfer failed");

stakingGroups[_groupId].totalAmount += _amount;
}

// Withdraw tokens from a group with rewards distributed according to weights
function withdrawFromGroup(uint256 _groupId, uint256 _amount) external {
StakingGroup storage group = stakingGroups[_groupId];
require(group.exists, "Group does not exist");
require(group.totalAmount >= _amount, "Insufficient group balance");

// Only a group member can initiate a withdrawal
bool isMember = false;
for (uint256 i = 0; i < group.members.length; i++) {
if (group.members[i] == msg.sender) {
isMember = true;
break;
}
}
require(isMember, "Not a group member");

// Update the group's total amount
group.totalAmount -= _amount;

// Distribute the withdrawn amount to all members according to their weights
// VULNERABLE: If any member is blacklisted, the entire distribution fails
for (uint256 i = 0; i < group.members.length; i++) {
uint256 memberShare = (_amount * group.weights[i]) / 100;
if (memberShare > 0) {
token.transfer(group.members[i], memberShare);
}
}
}

// Get group info
function getGroupInfo(uint256 _groupId) external view returns (
uint256 id,
uint256 totalAmount,
address[] memory members,
uint256[] memory weights
) {
StakingGroup storage group = stakingGroups[_groupId];
require(group.exists, "Group does not exist");

return (
group.id,
group.totalAmount,
group.members,
group.weights
);
}
}


Решение: учитывайте возможность попадания в черный список.

К сожалению, здесь нет единственно «правильного» ответа. Каждая ситуация уникальна. Все зависит от структуры вашего протокола, терпимости к рискам и даже от ваших юридических соглашений с пользователями. По крайней мере, имейте в виду такую возможность и разработайте резервный механизм, обеспечивающий соблюдение нормативных требований.

Этот пункт чеклиста заставляет вас критически подумать о внешних зависимостях и их потенциальном влиянии на основные функции вашего протокола. Речь идет о том, чтобы предвидеть худшие сценарии и иметь хотя бы какой-то запасной план.

Минимальный пример и PoC, написанные в Foundry, доступны здесь.

#dos
👍52🐳2
Атака Denial-of-Service. Часть 4

Может ли злоумышленник заблокировать или предотвратить обработку очереди транзакций, чтобы вызвать сбой в работе сервиса?

Проблема: если ваш смарт-контракт использует очереди для обработки задач, злоумышленник может манипулировать определенным статусом в очереди, чтобы предотвратить правильную обработку.

‍Исправление: Ваш механизм обработки очередей требует надежной обработки ошибок и механизмов резервного копирования, чтобы обеспечить продолжение процесса даже при возникновении проблем.

Рассмотрим пример очереди вывода средств:

1. Пользователи запрашивают вывод средств, и некоторые флаги указывают, что запрос активен.

2. Злоумышленники могут использовать resetUserStatus после того, как их вывод средств поставлен в очередь, и помешать другим пользователям сделать то же самое.

 // VULNERABLE FUNCTION: Can be exploited
function resetUserStatus() external {
// Anyone can reset their status while remaining in the queue
withdrawalRequested[msg.sender] = false;
// Note: User is not removed from the queue!
}

// Process the next withdrawal in the queue
function processNextWithdrawal() external {
require(withdrawalQueue.length > currentIndex, "No withdrawals to process");

// Get the next withdrawal
Withdrawal memory withdrawal = withdrawalQueue[currentIndex];

// VULNERABLE: This check can be exploited by an attacker by resetting the status
require(withdrawalRequested[withdrawal.user], "Withdrawal no longer requested");

// Process the withdrawal
uint256 amount = withdrawal.amount;
require(balances[withdrawal.user] >= amount, "Insufficient balance");

// Update balance
balances[withdrawal.user] -= amount;

// Reset withdrawal request
withdrawalRequested[withdrawal.user] = false;

// Send funds
(bool success, ) = payable(withdrawal.user).call{value:amount}("");
require(success, "Failed to send funds");

// Move to next in queue
currentIndex++;
}


Для того, чтобы воспользоваться этой уязвимостью, злоумышленник может выполнить следующие действия:

- Инициировать запрос на снятие средств.
- Вызвать функцию resetUserStatus.
- Функция processNextWithdrawal вернется в исходное состояние, что приведет к непрерывной DoS-атаке.

Устранение уязвимости:

- Ограничить изменение статуса withdrawalRequested администратором.
- Обеспечить проверку валидности, чтобы избежать транзакций с нулевым значением.
- Реализовать резервную функцию для обработки непредвиденных ошибок.

#dos #queue
🔥31👍1
Атака Denial-of-Service. Часть 5

Могут ли токены с низким количеством десятичных знаков (decimal) вызвать DoS?

Проблема: токены с низким decimals могут привести к проблемам с целочисленным делением, в результате чего происходит округление в меньшую сторону до нуля.

Представьте себе контракт потоковой передачи токенов (streaming contract), который распределяет токены в течение определенного периода времени. Если tokensPerSecond округляется до нуля из-за целочисленного деления с токенами с низким количеством десятичных знаков, функция распределения будет заблокирована.

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

Пример:

Рассмотрим контракт TokenStream, который передает определенное количество токенов пользователям, где:

- total_tokens необходимо перевести в контракт;
- token_per_second может быть округлено до нуля, поскольку мы используем токен с 1 десятичным знаком;
- функция distributeTokens будет отменена.

contract TokenStream {
IERC20 public token;
uint256 public streamDuration;
uint256 public tokensPerSecond;

constructor(IERC20 _token, uint256 _streamDuration, uint256 _tokensPerSecond) {
token = _token;
streamDuration = _streamDuration;
tokensPerSecond = _tokensPerSecond;
}

function distributeTokens(address recipient) external {
uint256 balance = token.balanceOf(address(this));
uint256 amount = tokensPerSecond * streamDuration;

uint256 tokensToSend = amount > balance ? balance : amount;

require(tokensToSend > 0, "Insufficient tokens to stream");
token.transfer(recipient, tokensToSend);
}
}

contract LowDecimalToken is ERC20 {
constructor() ERC20("LowDecimalToken", "LDT") {
_mint(msg.sender, 100000 * (10 ** decimals()));
}

function decimals() public view virtual override returns (uint8) {
return 1; // Simulate a low decimal token
}
}


Тест testDOSWithLowDecimalTokens в TokenStreamTest в этом случае вернется к исходному состоянию.

‍Исправление: убедитесь, что контракт правильно обрабатывает токены с небольшими decimals, масштабируя математические формулы для смягчения округления целых чисел во время вычислений.

#dos #decimals
👍2🤔2
Атака Denial-of-Service. Часть 6

Безопасно ли протокол обрабатывает взаимодействия с внешними контрактами?

Проблема: многие смарт-контракты полагаются на взаимодействия с внешними контрактами. Неожиданное поведение внешних контрактов может привести к сбою всей системы. Неспособность обработать эти внешние ошибки приводит к уязвимости DoS.

Исправление: обеспечьте надежную обработку ошибок при взаимодействии с внешними контрактами, чтобы защитить целостность протокола независимо от их производительности.

Пример:

Рассмотрим контракт, который взаимодействует с внешним фидом цен Chainlink. Без надлежащей обработки ошибок с помощью try/catch любой откат транзакции из внешнего фида будет каскадироваться вверх, и весь вызов будет ревертиться.

contract PriceDependentContract {
AggregatorV3Interface public priceFeed;

constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}

// Vulnerable function that retrieves the price without handling potential Chainlink reverts
function getPrice() public view returns (uint256) {
(, int256 price, , , ) = priceFeed.latestRoundData(); // Vulnerable line: No error handling
require(price > 0, "Price must be positive");
return uint256(price);
}

function calculateSomethingImportant() public view returns (uint256) {
uint256 price = getPrice();
// ... some important calculation using the price
return price * 2;
}


Как исправить: оберните вызовы внешних контрактов в блоки try/catch для обработки возвращенных ошибок и реализуйте резервный вариант или кэшированное значение.

Примечание: существует особый случай, когда внешний контракт намеренно тратит газ, что может привести к сбою блока catch! Мы обсудим это позже.

#dos #external
👍4
Всех с Днем Знаний!

Знаете, я тут подумал о Solidity и о том, как всё изменилось. Раньше это было как на американских горках: каждый месяц выходило какое-то обновление, добавлялись новые фичи, разрабатывались программы и плагины и все пытались изобрести свою, самую хитрую архитектуру смарт контракта. Сейчас же всё как-то устаканилось. Язык стал взрослым и надежным, как старый добрый швейцарский армейский нож — в нем есть всё нужное, и он просто работает без сюрпризов (ну, при должном подходе).

Из-за этого все сейчас как будто пришли к негласному соглашению: «давайте делать всё по проверенным шаблонам». Зачем рисковать и выдумывать велосипед, если есть готовые, протестированные всеми практики реализации стандартов вроде тех же ERC-20 для токенов? Это как собирать мебель из IKEA — есть инструкция, и если ей следовать, то всё будет стоять крепко и не развалится. Так гораздо безопаснее и спокойнее для всех.

И знаете, что самое крутое? Сейчас учиться — одно удовольствие! Раньше уроки устаревали, не успев выйти. А сейчас можно открыть любой годный курс или гайд и быть уверенным, что основы уже не поменяются через полгода. Не нужно бежать за поездом, который постоянно ускоряется. Можно спокойно разбираться в нюансах, учиться правильно тестировать код и понимать, как избегать классических ловушек.

В общем, мне кажется, мы сейчас живем в золотое время для разработчика. Экосистема повзрослела, перестала бросаться из крайности в крайность и стала надежным фундаментом. Можно наконец-то не отвлекаться на постоянные новшества, а просто сосредоточиться и строить что-то классное и долговечное. По-моему, это офигенный прогресс!

Solidity прекрасен! Вы прекрасны! С Днем Знаний!

#solidity
👏21👍51
Аудит смарт контрактов изменился, нужно меняться и вам

Аудит смарт-контрактов за последние годы изменился до неузнаваемости. Раньше всё было проще: смотришь код, ловишь очевидные ошибки — переполнения, недостаточные проверки доступа, утечки средств. Баги были грубые, но и последствия от них — большие. Потом, по мере того как проекты становились сложнее, начали всплывать уязвимости не в синтаксисе, а в самой логике. Что-то работает технически правильно, но при этом позволяет вытянуть все деньги из пула, если чуть-чуть подтолкнуть систему в нужную сторону. И тут уже нужно было не просто читать код, а понимать, как он живёт в сети, как взаимодействует с другими контрактами, как реагирует на разные сценарии.

На помощь пришли инструменты. Появились штуки вроде Ceritora — формальная верификация, где ты математически доказываешь, что контракт делает то, что должен. Потом halmos, medusa — фреймворки, которые могут перебирать миллионы путей выполнения, чтобы найти, где что сломается. И, конечно, статические анализаторы вроде Slither или 4nalyer — они как радар: быстро сканируют код и подсвечивают подозрительные места. Удобно, полезно, теперь это база.

Но настоящий виток — это, конечно, нейросети. Сейчас почти все с ними в той или иной форме работают. Кто-то просто кидает контракт в chatGPT и верит, что модель сама найдёт уязвимости. Ну, знаешь, типа: «вот, посмотри, тут всё норм?» — и доверяется ответу. Другие используют нейросети, чтобы красиво оформить отчёт — сформулировать мысли чётко, без воды, чтобы клиент или судьи поняли суть.

А есть те, кто подошёл иначе. Для них нейросеть — не ментор и не помощник, который говорит: «смотри сюда». Это инструмент, как мощный микроскоп или осциллограф. Ты сам решаешь, что проверять, куда копать, а модель помогает ускорить процесс. Вместо того чтобы тратить три часа на то, чтобы разобраться, зачем нужна та или иная функция, как она связана с другими, что может пойти не так — ты можешь за пару минут получить гипотезу, проверить её, перепроверить, построить сценарии атаки.

Но это, честно говоря, сложнее. Потому что теперь ты отвечаешь не только за код, но и за то, что говорит модель. Нужно уметь задавать правильные вопросы, понимать, когда она ошибается, когда просто «догадывается», а когда реально анализирует. Нужно держать контроль.

И вот в этом и есть разница. Кто-то позволяет нейросети вести себя за руку, а кто-то сам идёт вперёд, а модель просто делает путь быстрее и светлее. Как ты с этим справишься — покажет, насколько ты действительно сильный аудитор. Не потому что ты знаешь все инструменты, а потому что умеешь ими пользоваться, не теряя головы.

#audit
1👍12🔥4👏1🤔1
Немного про мой процесс самообучения

Когда я начал погружаться в machine learning и deep learning — совершенно новые для меня области, — меня снова охватило то самое ощущение неуверенности, с которым я уже сталкивался при изучении Solidity. Это состояние, когда чувствуешь, что не хватает базы, не понимаешь, с чего начать, куда двигаться, и возникает ощущение, будто ты отстаёшь, даже если только стартовал.

Уже около месяца я систематически изучаю ML/DL. За это время освоил Python, разобрался с NumPy и Pandas, изучил базовые принципы нейронных сетей и ряд других тем. Однако математическая часть — линейная алгебра, градиентный спуск, операции с векторами и матрицами — до сих пор даётся с трудом. Уже несколько недель я возвращаюсь к одним и тем же концепциям, пытаясь не просто запомнить, а именно понять.

И здесь я пришёл к важному выводу: обучение — это не про количество прочитанных уроков или просмотренных видео. Это про глубину понимания. Можно пройти десять курсов подряд, но если ты не можешь объяснить тему своими словами, не можешь воспроизвести логику, не понимаешь, почему формула выглядит именно так — значит, знания не усвоены. Они временно залегли в памяти, но не стали частью мышления.

Я давно для себя определил: настоящий прогресс начинается тогда, когда ты перестаёшь просто потреблять информацию и начинаешь с ней работать. Если что-то непонятно — нельзя просто пропустить, надеясь, что «всё встанет на свои места позже». Нужно остановиться, разобраться, докопаться. В случае с математикой ML я не ограничиваюсь учебником. Каждый непонятный символ, каждый логический переход, каждая формула, которую не могу воспроизвести самостоятельно, становится поводом для глубокого погружения. Я ищу дополнительные материалы, смотрю лекции, задаю вопросы нейросетям, прося расписать выводы по шагам, объяснить интуицию за формулами, показать аналогии.

Например, сейчас я читаю книгу Дайзенрота «Математика в машинном обучении». Но чтение — это только отправная точка. На каждой странице у меня десятки пометок: что непонятно, где теряется логика, какие шаги пропущены. И каждая такая пометка требует отдельной работы. Иногда на одну страницу уходит несколько часов: диалоги с ИИ, поиск объяснений, конспектирование, переписывание формул с комментариями. И даже после всего этого чувствуется, что где-то что-то ускользает — и это нормально. Главное, что я не просто читаю, а работаю с материалом.

Вот в чём разница между поверхностным изучением и настоящим пониманием: первое заканчивается там, где заканчивается урок, второе — продолжается за его пределами. Нужно быть готовым тратить время, задавать вопросы, искать разные объяснения, пока не щёлкнет. Потому что только тогда знания становятся твоими.

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

Я убеждён: ключ к освоению сложных тем — в настойчивом стремлении к пониманию. Не в том, сколько вы прочитали, а в том, насколько глубоко вы проработали материал. Даже если сегодня что-то не до конца ясно — главное, что вы не прошли мимо, а попытались разобраться. Со временем эти усилия складываются, и однажды вы замечаете, что всё, что раньше казалось запутанным, теперь выстраивается в единую, логичную картину.

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

#edu
🔥175💯1
Front-running атаки. Часть 1

Сегодня мы посмотрим на другую, не менее коварную уязвимость: атаки с опережением или Front-Running атаки.

Для иллюстрации представьте, что вы находитесь на оживленном фермерском рынке, где цены меняются в зависимости от спроса. Вы замечаете выгодную сделку по редким трюфелям за 50 долларов, которые, как вы знаете, в других местах стоят 100 долларов. Когда вы подходите, чтобы купить их, инсайдер рынка, который может видеть все поступающие заказы клиентов, замечает ваше намерение. Он быстро проскакивает вперед в очереди, покупает трюфели за 50 долларов, а затем сразу же предлагает продать их вам за 90 долларов. Вы все равно получаете трюфели, но теперь этот посредник заработал 40 долларов, которые могли бы быть вашими.

Это и есть фронт-раннинг в двух словах — увидеть чужую ожидающую транзакцию в публичной системе, а затем вставить свою собственную транзакцию перед ней, чтобы извлечь выгоду из изменения цены, о котором вы знаете.

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

Понимание front-running атак

Прежде чем перейти к самому чек-листу, давайте начнем с некоторых важных определений.

1. Атака с опережением: злоумышленник наблюдает за ожидающей транзакцией в мемпуле. Чтобы получить выгоду из этой транзакции, он быстро выполняет свою собственную транзакцию, опережая ее. Обычно для этого он предлагает более высокую цену газа, чтобы его транзакция была обработана первой.

2. Паттерн «Get-or-create»: паттерн проектирования смарт контракта, который предполагает либо извлечение существующих активов (например, существующей торговой пары на DEX), либо создание новых, если они не существуют. Этот паттерн может быть уязвим для фронт-раннинга, если он не защищен должным образом.

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

4. Атака «пыль»: специфическая форма фронт-раннинга, при которой злоумышленник отправляет транзакцию с незначительной суммой («пыль») перед законной транзакцией. Это приводит к сбою (реверсии) законной транзакции из-за измененного состояния или невыполненных предварительных условий.

5. Схема «Commit-reveal»: криптографический протокол, разработанный для обеспечения справедливости и секретности в ситуациях, когда участники должны предоставить информацию, не раскрывая ее сразу. Он состоит из двух отдельных этапов: этапа «commit», на котором участники предоставляют криптографическое обязательство (обычно хэш их предполагаемого действия в сочетании со случайным секретом или nonce), и этапа «reveal», на котором они раскрывают исходное действие и секрет по истечении определенного периода времени. Этот процесс гарантирует, что ни один участник не может изменить свою заявку или реагировать на действия других, пока все обязательства не будут окончательно оформлены. Некоторые версии протокола гарантируют, что только лицо, которое взяло на себя обязательство, может раскрыть его позже. Это предотвращает вмешательство других лиц и обеспечивает ответственность участников.

SOL-AM-FrA-1: Защищены ли шаблоны «get-or-create» от атак front-running?
3🔥1
Рассмотрим сценарий с фабричным контрактом (ExploitableContract), предназначенным для управления экземплярами VulnerablePool. Предполагается, что пользователь (идентифицируемый как _poolCreator) вызывает getOrCreatePool, указывая _initialPrice. Если пул для этого создателя не существует, контракт создает его. В противном случае он возвращает существующий пул. Этот паттерн «get-or-create» является распространенным выбором при проектировании смарт контрактов. Представьте себе сценарий, в котором необходимо создать пул для обмена, и кто-то хочет добавить ликвидность.

Процесс может выглядеть следующим образом:

- Проверить, существует ли пул.
- Если пул существует, приступить к взаимодействию (например, добавить ликвидность).
- Если пул не существует, создать его с желаемыми параметрами, а затем приступить к взаимодействию.

И вот тут-то и начинаются проблемы! При наивной реализации в рамках одной транзакции паттерн «get-or-create» может быть уязвим для фронт-раннинга. Злоумышленник может отслеживать мемпул на предмет транзакций, пытающихся создать определенный актив (например, наш VulnerablePool, связанный с адресом _poolCreator).

Увидев параметры, которые планирует использовать жертва, злоумышленник может быстро отправить свою собственную транзакцию, вызывая ту же функцию (getOrCreatePool), но с другими, вредоносными параметрами (например, с подправленной _initialPrice). Заплатив более высокую комиссию за газ, злоумышленник гарантирует, что его транзакция будет выполнена первой. Это создает актив под идентификатором жертвы, но с параметрами злоумышленника, перехватывая этап создания.

Когда исходная транзакция жертвы наконец выполняется, проверка (address(pools[_poolCreator]) == address(0)) обнаруживает, что ресурс уже существует. Этап создания пропускается, и функция возвращает созданный злоумышленником ресурс с измененными параметрами. Не подозревающая об этом жертва затем взаимодействует с этим скомпрометированным ресурсом.

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

Вот этот паттерн в коде:

pragma solidity ^0.8.0;


import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";


contract VulnerablePool {
address public poolCreator;
uint256 public initialPrice;


constructor(address _poolCreator, uint256 _initialPrice) {
poolCreator = _poolCreator;
initialPrice = _initialPrice;
}
}


contract ExploitableContract {
mapping(address => VulnerablePool) public pools;


function getOrCreatePool(address _poolCreator, uint256 _initialPrice)
public returns (VulnerablePool)
{
if (address(pools[_poolCreator]) == address(0)) {
// An attacker can front-run this transaction with different initialPrice
pools[_poolCreator] = new VulnerablePool(_poolCreator, _initialPrice);
}
return pools[_poolCreator];
}


function viewPoolInitialPrice(address _poolCreator) public view returns (uint256) {
if(address(pools[_poolCreator]) != address(0)) {
return pools[_poolCreator].initialPrice();
} else {
return 0;
}
}
}


Ниже приведен тестовый случай, демонстрирующий эту уязвимость. Тест имитирует атаку «фронт-раннинг» путем создания пула с начальной ценой 50, в то время как жертва намеревалась создать его с начальной ценой 100. Злоумышленник успешно осуществляет «фронт-раннинг» транзакции жертвы, и в результате жертва взаимодействует с пулом злоумышленника, а не со своим собственным.
👍4