Перейти к основному содержанию
Change page

Безопасность умных контрактов

Последнее обновление страницы: 26 февраля 2026 г.

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

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

Хотя цифры разнятся, по оценкам, общая сумма украденных или потерянных ценностей из-за дефектов безопасности в умных контрактах легко превышает 1 миллиард долларов. Сюда входят громкие инциденты, такие как взлом DAO (opens in a new tab) (украдено 3,6 млн ETH, что по сегодняшним ценам составляет более 1 млрд долларов), взлом кошелька с мультиподписью Parity (opens in a new tab) (хакеры похитили 30 млн долларов) и проблема с замороженным кошельком Parity (opens in a new tab) (более 300 млн долларов в ETH заблокированы навсегда).

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

Предварительные условия

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

Руководство по созданию безопасных умных контрактов Ethereum

1. Разработка надлежащих средств контроля доступа

В умных контрактах функции, помеченные как public или external, могут вызываться любыми внешними аккаунтами (EOA) или аккаунтами контрактов. Указание публичной видимости для функций необходимо, если вы хотите, чтобы другие могли взаимодействовать с вашим контрактом. Однако функции, помеченные как private, могут вызываться только функциями внутри умного контракта, а не внешними аккаунтами. Предоставление каждому участнику сети доступа к функциям контракта может вызвать проблемы, особенно если это означает, что кто угодно может выполнять конфиденциальные операции (например, чеканку новых токенов).

Чтобы предотвратить несанкционированное использование функций умного контракта, необходимо внедрить безопасные средства контроля доступа. Механизмы контроля доступа ограничивают возможность использования определенных функций в умном контракте только для утвержденных субъектов, таких как аккаунты, ответственные за управление контрактом. Патерн Ownable и управление на основе ролей — это два полезных патерна для реализации контроля доступа в умных контрактах:

Патерн Ownable

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

Управление доступом на основе ролей

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

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

Использование кошельков с мультиподписью

Другой подход к реализации безопасного контроля доступа — использование аккаунта с мультиподписью для управления контрактом. В отличие от обычного EOA, аккаунты с мультиподписью принадлежат нескольким субъектам и требуют для выполнения транзакций подписи от минимального числа аккаунтов, например, 3 из 5.

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

2. Используйте операторы require(), assert() и revert() для защиты операций контракта

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

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

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

revert(): revert() может использоваться в операторе if-else, который вызывает исключение, если требуемое условие не выполняется. В приведенном ниже примере контракта для защиты выполнения функций используется revert():

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Предоставлено недостаточно Ether.");
9 // Выполнение покупки.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
Показать все

3. Тестируйте умные контракты и проверяйте правильность кода

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

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

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

Лучший подход — это сочетание модульного тестирования с тестированием на основе свойств, выполняемым с использованием статического и динамического анализа. Статический анализ опирается на низкоуровневые представления, такие как графы потоков управления (opens in a new tab) и абстрактные синтаксические деревья (opens in a new tab), для анализа достижимых состояний программы и путей выполнения. В то же время методы динамического анализа, такие как фаззинг умных контрактов (opens in a new tab), выполняют код контракта со случайными входными значениями для обнаружения операций, нарушающих свойства безопасности.

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

4. Запросите независимую проверку вашего кода

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

Аудиты

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

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

Вознаграждения за обнаружение ошибок

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

При верном использовании, такие вознаграждения мотивируют членов хакерского сообщества проверить ваш код на уязвимости. Реальным примером является «ошибка бесконечных денег», которая позволила бы атакующему создать неограниченное количество эфира на Optimism (opens in a new tab), протоколе Леер 2, работающем на Ethereum. К счастью, «белый» хакер обнаружил уязвимость (opens in a new tab) и уведомил команду, заработав при этом крупное вознаграждение (opens in a new tab).

Хорошая стратегия — установить размер выплат по программе bug bounty пропорционально сумме средств, за которую вы отвечаете. Этот подход, названный «масштабируемая программа вознаграждения за обнаружение ошибок (opens in a new tab)», предоставляет финансовые стимулы для ответственного раскрытия уязвимостей вместо их эксплуатации.

5. Следуйте лучшим практикам при разработке умных контрактов

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

  • Храните весь код в системе контроля версий, такой как git

  • Вносите все изменения в код через запросы на слияние

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

  • Используйте среду разработки для тестирования, компиляции и развертывания умных контрактов

  • Прогоняйте свой код через базовые инструменты анализа кода, такие как Cyfrin Aderyn (opens in a new tab), Mythril и Slither. В идеале это следует делать перед слиянием каждого запроса на слияние и сравнивать различия в результатах

  • Убедитесь, что ваш код компилируется без ошибок и компилятор Solidity не выдает предупреждений

  • Правильно документируйте свой код (используя NatSpec (opens in a new tab)) и описывайте детали архитектуры контракта на понятном языке. Это облегчит аудит и проверку вашего кода другими людьми.

6. Внедряйте надежные планы аварийного восстановления

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

Обновления контрактов

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

Механизмы обновления контрактов работают по-разному, но «патерн прокси» является одним из наиболее популярных подходов к обновлению умных контрактов. Патерны прокси (opens in a new tab) разделяют состояние и логику приложения между двумя контрактами. Первый контракт (называемый «прокси-контракт») хранит переменные состояния (например, балансы пользователей), в то время как второй контракт (называемый «логический контракт») содержит код для выполнения функций контракта.

Аккаунты взаимодействуют с прокси-контрактом, который перенаправляет все вызовы функций логическому контракту с помощью низкоуровневого вызова delegatecall() (opens in a new tab). В отличие от обычного вызова сообщения, delegatecall() гарантирует, что код, работающий по адресу логического контракта, выполняется в контексте вызывающего контракта. Это означает, что логический контракт всегда будет записывать данные в хранилище прокси-контракта (а не в свое собственное хранилище), а исходные значения msg.sender и msg.value сохраняются.

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

Подробнее об обновлении контрактов.

Аварийные остановки

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

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

  1. Глобальная логическая переменная, указывающая, находится ли умный контракт в остановленном состоянии или нет. Эта переменная устанавливается в значение false при настройке контракта, но изменится на true после остановки контракта.

  2. Функции, которые ссылаются на логическую переменную при своем выполнении. Такие функции доступны, когда умный контракт не остановлен, и становятся недоступными при срабатывании функции аварийной остановки.

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

После того, как контракт активирует аварийную остановку, определенные функции станут недоступными для вызова. Это достигается путем обертывания избранных функций в модификатор, который ссылается на глобальную переменную. Ниже приведен пример (opens in a new tab), описывающий реализацию этого патерна в контрактах:

1// Этот код не прошел профессиональный аудит и не дает никаких гарантий безопасности или корректности. Используйте на свой страх и риск.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Здесь проверьте авторизацию msg.sender
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Здесь происходит логика депозита
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Здесь происходит экстренный вывод средств
36 }
37}
Показать все

Этот пример показывает основные особенности аварийных остановок:

  • isStopped — это логическая переменная, которая в начале имеет значение false, а когда контракт переходит в аварийный режим, — true.

  • Модификаторы функций onlyWhenStopped и stoppedInEmergency проверяют переменную isStopped. stoppedInEmergency используется для управления функциями, которые должны быть недоступны, когда контракт уязвим (например, deposit()). Вызовы этих функций будут просто отменены.

onlyWhenStopped используется для функций, которые должны быть доступны для вызова во время чрезвычайной ситуации (например, emergencyWithdraw()). Такие функции могут помочь разрешить ситуацию, поэтому они исключены из списка «ограниченных функций».

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

Мониторинг событий

События (opens in a new tab) позволяют отслеживать вызовы функций умного контракта и контролировать изменения переменных состояния. Идеально запрограммировать ваш умный контракт так, чтобы он генерировал событие всякий раз, когда какая-либо сторона предпринимает критически важное для безопасности действие (например, вывод средств).

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

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

7. Проектирование безопасных систем управления

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

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

Один из способов предотвращения проблем, связанных с ончейн-управлением, — использование временной блокировки (opens in a new tab). Временная блокировка не позволяет умному контракту выполнять определенные действия до истечения определенного времени. Другие стратегии включают присвоение «веса голоса» каждому токену в зависимости от того, как долго он был заблокирован, или измерение голосующей силы адреса в исторический период (например, 2-3 блока в прошлом) вместо текущего блока. Оба метода снижают вероятность быстрого накопления голосующей силы для влияния на результаты ончейн-голосований.

Подробнее о проектировании безопасных систем управления (opens in a new tab), различных механизмах голосования в DAO (opens in a new tab) и распространенных векторах атак на DAO с использованием DeFi (opens in a new tab) по приведенным ссылкам.

8. Сведите сложность кода к минимуму

Традиционные разработчики программного обеспечения знакомы с принципом KISS («делай проще, дурак»), который советует избегать ненужной сложности в проектировании программного обеспечения. Это следует из давнего убеждения, что «сложные системы дают сбой сложными способами» и более подвержены дорогостоящим ошибкам.

Простота особенно важна при написании умных контрактов, учитывая, что они потенциально контролируют большие объемы ценностей. Совет по достижению простоты при написании умных контрактов — по возможности повторно использовать существующие библиотеки, такие как OpenZeppelin Contracts (opens in a new tab). Поскольку эти библиотеки были тщательно проверены и протестированы разработчиками, их использование снижает вероятность появления ошибок при написании новой функциональности с нуля.

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

9. Защита от распространенных уязвимостей умных контрактов

Повторный вход

EVM не допускает параллелизма, то есть два контракта, участвующие в вызове сообщения, не могут выполняться одновременно. Внешний вызов приостанавливает выполнение вызывающего контракта и его память до тех пор, пока вызов не вернется, после чего выполнение продолжается в обычном режиме. Этот процесс можно формально описать как передачу потока управления (opens in a new tab) другому контракту.

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

Рассмотрим простой умный контракт («Victim»), который позволяет любому вносить и выводить эфир:

1// Этот контракт уязвим. Не используйте в продакшене
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
Показать все

Этот контракт предоставляет функцию withdraw(), позволяющую пользователям выводить ETH, ранее внесенные в контракт. При обработке вывода средств контракт выполняет следующие операции:

  1. Проверяет баланс ETH пользователя
  2. Отправляет средства на вызывающий адрес
  3. Сбрасывает их баланс до 0, предотвращая дальнейшие выводы средств пользователем

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

Если withdraw() вызывается из внешнего аккаунта (EOA), функция выполняется, как и ожидалось: msg.sender.call.value() отправляет ETH вызывающему. Однако, если msg.sender является аккаунтом умного контракта и вызывает withdraw(), отправка средств с помощью msg.sender.call.value() также вызовет запуск кода, хранящегося по этому адресу.

Представьте, что по адресу контракта развернут следующий код:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Показать все

Этот контракт предназначен для выполнения трех действий:

  1. Принять депозит с другого аккаунта (вероятно, EOA атакующего)
  2. Внести 1 ETH в контракт Victim
  3. Вывести 1 ETH, хранящийся в умном контракте

Здесь нет ничего плохого, за исключением того, что у Attacker есть еще одна функция, которая снова вызывает withdraw() в Victim, если оставшегося газа от входящего msg.sender.call.value больше 40 000. Это дает Attacker возможность повторно войти в Victim и вывести больше средств до завершения первого вызова withdraw. Цикл выглядит следующим образом:

1- EOA атакующего вызывает `Attacker.beginAttack()` с 1 ETH
2- `Attacker.beginAttack()` вносит 1 ETH в `Victim`
3- `Attacker` вызывает `withdraw()` в `Victim`
4- `Victim` проверяет баланс `Attacker` (1 ETH)
5- `Victim` отправляет 1 ETH в `Attacker` (что вызывает функцию по умолчанию)
6- `Attacker` снова вызывает `Victim.withdraw()` (обратите внимание, что `Victim` не уменьшил баланс `Attacker` после первого вывода средств)
7- `Victim` проверяет баланс `Attacker` (который все еще составляет 1 ETH, потому что он не применил эффекты первого вызова)
8- `Victim` отправляет 1 ETH в `Attacker` (что вызывает функцию по умолчанию и позволяет `Attacker` повторно войти в функцию `withdraw`)
9- Процесс повторяется до тех пор, пока у `Attacker` не закончится газ, после чего `msg.sender.call.value` возвращается, не вызывая дополнительных выводов средств
10- `Victim` наконец применяет результаты первой транзакции (и последующих) к своему состоянию, поэтому баланс `Attacker` устанавливается в 0
Показать все

Суть в том, что, поскольку баланс вызывающего не устанавливается в 0 до завершения выполнения функции, последующие вызовы будут успешными и позволят вызывающему вывести свой баланс несколько раз. Такой тип атаки может быть использован для опустошения умного контракта, как это произошло во время взлома DAO в 2016 году (opens in a new tab). Атаки повторного входа до сих пор остаются критической проблемой для умных контрактов, как показывают публичные списки эксплойтов повторного входа (opens in a new tab).

Как предотвратить атаки повторного входа

Один из подходов к борьбе с повторным входом — следование патерну «проверки-эффекты-взаимодействия» (opens in a new tab). Этот патерн упорядочивает выполнение функций таким образом, что код, выполняющий необходимые проверки перед продолжением выполнения, идет первым, за ним следует код, который манипулирует состоянием контракта, а код, взаимодействующий с другими контрактами или EOA, идет последним.

Патерн «проверки-эффект-взаимодействие» используется в исправленной версии контракта Victim, показанной ниже:

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}

Этот контракт выполняет проверку баланса пользователя, применяет эффекты функции withdraw() (сбрасывая баланс пользователя до 0) и переходит к взаимодействию (отправке ETH на адрес пользователя). Это гарантирует, что контракт обновит свое хранилище до внешнего вызова, устраняя условие повторного входа, которое сделало возможной первую атаку. Контракт Attacker все еще мог бы вызвать NoLongerAVictim, но поскольку balances[msg.sender] был установлен в 0, дополнительные выводы средств вызовут ошибку.

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

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Заблокировано от повторного входа.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // Эта функция защищена мьютексом, поэтому повторные вызовы из `msg.sender.call` не могут снова вызвать `withdraw`.
14 // Оператор `return` возвращает `true`, но все равно выполняет оператор `locked = false` в модификаторе
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "Нет баланса для вывода.");
17
18 balances[msg.sender] -= _amount;
19 (bool success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Показать все

Вы также можете использовать систему pull payments (opens in a new tab), которая требует от пользователей вывода средств из умных контрактов, вместо системы «push payments», которая отправляет средства на аккаунты. Это исключает возможность непреднамеренного запуска кода по неизвестным адресам (а также может предотвратить некоторые атаки типа «отказ в обслуживании»).

Целочисленные недополнения и переполнения

Целочисленное переполнение происходит, когда результат арифметической операции выходит за пределы допустимого диапазона значений, что приводит к его «перебросу» на наименьшее представимое значение. Например, uint8 может хранить значения только до 2^8-1=255. Арифметические операции, которые приводят к значениям выше 255, переполнятся и сбросят uint до 0, подобно тому, как одометр на автомобиле сбрасывается до 0, как только он достигает максимального пробега (999999).

Целочисленные недополнения происходят по схожим причинам: результат арифметической операции оказывается ниже допустимого диапазона. Допустим, вы попытались уменьшить 0 в uint8, результат просто перескочит на максимальное представимое значение (255).

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

1pragma solidity ^0.7.6;
2
3// Этот контракт предназначен для использования в качестве хранилища с временной блокировкой.
4// Пользователь может вносить средства в этот контракт, но не может выводить их в течение как минимум недели.
5// Пользователь также может продлить время ожидания сверх 1-недельного периода.
6
7/*
81. Разверните TimeLock
92. Разверните Attack с адресом TimeLock
103. Вызовите Attack.attack, отправив 1 эфир. Вы сразу сможете
11 вывести свой эфир.
12
13Что произошло?
14Атака вызвала переполнение TimeLock.lockTime, что позволило вывести средства
15до истечения 1-недельного периода ожидания.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Недостаточно средств");
33 require(block.timestamp > lockTime[msg.sender], "Время блокировки не истекло");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Не удалось отправить Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 если t = текущее время блокировки, то нам нужно найти x, такое что
56 x + t = 2**256 = 0
57 тогда x = -t
58 2**256 = type(uint).max + 1
59 тогда x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
Показать все
Как предотвратить целочисленные недополнения и переполнения

Начиная с версии 0.8.0, компилятор Solidity отклоняет код, который приводит к целочисленным недополнениям и переполнениям. Однако контракты, скомпилированные с более низкой версией компилятора, должны либо выполнять проверки в функциях, включающих арифметические операции, либо использовать библиотеку (например, SafeMath (opens in a new tab)), которая проверяет на недополнение/переполнение.

Манипуляция оракулом

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

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

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

Цены на DEX часто точны, в основном благодаря арбитражерам, восстанавливающим паритет на рынках. Однако они подвержены манипуляциям, особенно если ончейн-оракул рассчитывает цены активов на основе исторических торговых патернов (как это обычно бывает).

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

Как предотвратить манипуляции оракулом

Минимальное требование для избежания манипуляций с оракулом (opens in a new tab) — использование децентрализованной сети оракулов, которая запрашивает информацию из нескольких источников, чтобы избежать единых точек отказа. В большинстве случаев децентрализованные оракулы имеют встроенные криптоэкономические стимулы для поощрения узлов оракула сообщать верную информацию, что делает их более безопасными, чем централизованные оракулы.

Если вы планируете запрашивать цену актива у ончейн-оракула, рассмотрите возможность использования того, который реализует механизм средневзвешенной по времени цены (TWAP). Оракул TWAP (opens in a new tab) запрашивает цену актива в два разных момента времени (которые вы можете изменять) и рассчитывает спотовую цену на основе полученного среднего значения. Выбор более длительных периодов времени защищает ваш протокол от манипулирования ценами, поскольку крупные ордера, выполненные недавно, не могут повлиять на цены активов.

Ресурсы по безопасности умных контрактов для разработчиков

Инструменты для анализа умных контрактов и проверки правильности кода

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

  • Инструменты формальной верификацииИнструменты для проверки функциональной корректности в умных контрактах и проверки инвариантов.

  • Сервисы аудита умных контрактовСписок организаций, предоставляющих услуги по аудиту умных контрактов для проектов разработки на Ethereum.

  • Платформы вознаграждений за обнаружение ошибокПлатформы для координации программ вознаграждений за обнаружение ошибок и поощрения ответственного раскрытия критических уязвимостей в умных контрактах.

  • Fork Checker (opens in a new tab)бесплатный онлайн-инструмент для проверки всей доступной информации о форкнутом контракте.

  • ABI Encoder (opens in a new tab)бесплатный онлайн-сервис для кодирования функций вашего контракта Solidity и аргументов конструктора.

  • Aderyn (opens in a new tab) — статический анализатор Solidity, обходящий абстрактные синтаксические деревья (AST) для выявления предполагаемых уязвимостей и вывода проблем в удобном для восприятия формате Markdown.

Инструменты для мониторинга умных контрактов

  • Tenderly Real-Time Alerting (opens in a new tab)инструмент для получения уведомлений в реальном времени о необычных или неожиданных событиях в ваших умных контрактах или кошельках.

Инструменты для безопасного администрирования умных контрактов

  • Safe (opens in a new tab)кошелек на основе умного контракта, работающий на Ethereum, который требует одобрения транзакции минимальным количеством людей (M из N).

  • OpenZeppelin Contracts (opens in a new tab)библиотеки контрактов для реализации административных функций, включая владение контрактом, обновления, контроль доступа, управление, возможность приостановки и многое другое.

Сервисы аудита умных контрактов

  • ConsenSys Diligence (opens in a new tab)сервис аудита умных контрактов, помогающий проектам по всей экосистеме блокчейна убедиться, что их протоколы готовы к запуску и созданы для защиты пользователей.

  • CertiK (opens in a new tab)фирма по безопасности блокчейна, пионер в использовании передовых технологий формальной верификации на умных контрактах и блокчейн-сетях.

  • Trail of Bits (opens in a new tab)компания по кибербезопасности, которая сочетает исследования в области безопасности с мышлением атакующего, чтобы снизить риски и укрепить код.

  • PeckShield (opens in a new tab)компания по безопасности блокчейна, предлагающая продукты и услуги для обеспечения безопасности, конфиденциальности и удобства использования всей экосистемы блокчейна.

  • QuantStamp (opens in a new tab)сервис аудита, способствующий массовому внедрению технологии блокчейн через услуги по оценке безопасности и рисков.

  • OpenZeppelin (opens in a new tab)компания по безопасности умных контрактов, предоставляющая аудиты безопасности для распределенных систем.

  • Runtime Verification (opens in a new tab)компания по безопасности, специализирующаяся на формальном моделировании и верификации умных контрактов.

  • Hacken (opens in a new tab)аудитор кибербезопасности Web3, применяющий 360-градусный подход к безопасности блокчейна.

  • Nethermind (opens in a new tab)услуги аудита Solidity и Cairo, обеспечивающие целостность умных контрактов и безопасность пользователей в Ethereum и Starknet.

  • HashEx (opens in a new tab)HashEx специализируется на аудите блокчейна и умных контрактов для обеспечения безопасности криптовалют, предоставляя такие услуги, как разработка умных контрактов, тестирование на проникновение, консалтинг в области блокчейна.

  • Code4rena (opens in a new tab)конкурентная платформа для аудита, которая стимулирует экспертов по безопасности умных контрактов находить уязвимости и помогать делать web3 более безопасным.

  • CodeHawks (opens in a new tab)конкурентная аудиторская платформа, проводящая соревнования по аудиту умных контрактов для исследователей безопасности.

  • Cyfrin (opens in a new tab)мощный центр безопасности Web3, инкубирующий криптобезопасность через продукты и услуги по аудиту умных контрактов.

  • ImmuneBytes (opens in a new tab)фирма по безопасности Web3, предлагающая аудиты безопасности для блокчейн-систем с помощью команды опытных аудиторов и лучших в своем классе инструментов.

  • Oxorio (opens in a new tab)аудиты умных контрактов и услуги по безопасности блокчейна с опытом в EVM, Solidity, ZK, кросс-чейн технологиях для криптофирм и проектов DeFi.

  • Inference (opens in a new tab)аудиторская компания по безопасности, специализирующаяся на аудите умных контрактов для блокчейнов на базе EVM. Благодаря своим опытным аудиторам они выявляют потенциальные проблемы и предлагают действенные решения для их устранения до развертывания.

Платформы вознаграждений за обнаружение ошибок

  • Immunefi (opens in a new tab)платформа вознаграждений за обнаружение ошибок для умных контрактов и проектов DeFi, где исследователи безопасности просматривают код, раскрывают уязвимости, получают вознаграждение и делают криптовалюту безопаснее.

  • HackerOne (opens in a new tab)платформа для координации уязвимостей и вознаграждений за обнаружение ошибок, которая связывает компании с пентестерами и исследователями кибербезопасности.

  • HackenProof (opens in a new tab)экспертная платформа вознаграждений за ошибки для криптопроектов (DeFi, умные контракты, кошельки, CEX и другие), где специалисты по безопасности обеспечивают сортировочные услуги и исследователям платят за соответствующие, проверенные сообщения об ошибках.

  • Sherlock (opens in a new tab)гарант безопасности умных контрактов в Web3, с выплатами для аудиторов, управляемыми через умные контракты, чтобы обеспечить справедливую оплату за обнаружение соответствующих ошибок.

  • CodeHawks (opens in a new tab)конкурентная платформа вознаграждений за обнаружение ошибок, где аудиторы принимают участие в конкурсах и челленджах по безопасности, а (скоро) и в своих собственных частных аудитах.

Публикации известных уязвимостей и эксплойтов умных контрактов

Задания для изучения безопасности умных контрактов

  • Awesome BlockSec CTF (opens in a new tab)отобранный список военных игр, задач и соревнований Capture The Flag (opens in a new tab) в области безопасности блокчейна, а также разборы решений.

  • Damn Vulnerable DeFi (opens in a new tab)военная игра для изучения наступательной безопасности умных контрактов DeFi и развития навыков в поиске ошибок и аудите безопасности.

  • Ethernaut (opens in a new tab)военная игра на основе Web3/Solidity, где каждый уровень — это умный контракт, который нужно «взломать».

  • HackenProof x HackTheBox (opens in a new tab)задание по взлому умных контрактов в стиле фэнтезийного приключения. Успешное выполнение задания также дает доступ к частной программе вознаграждений за обнаружение ошибок.

Лучшие практики по обеспечению безопасности умных контрактов

Учебные пособия по безопасности умных контрактов

Была ли эта статья полезной?