Разработка смарт-контрактов на Solidity для EVM-сетей

Разработка смарт-контрактов на Solidity Клиент приносит контракт на аудит — 800 строк Solidity, деплой на Ethereum mainnet запланирован через несколько дней. На третьей странице кода обнаруживается паттерн: внешний вызов до обновления состояния, классическая reentrancy. Не теоретическая — такая ж

Направления блокчейн-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    998
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1267
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    713
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1003

Разработка смарт-контрактов на Solidity

Клиент приносит контракт на аудит — 800 строк Solidity, деплой на Ethereum mainnet запланирован через несколько дней. На третьей странице кода обнаруживается паттерн: внешний вызов до обновления состояния, классическая reentrancy. Не теоретическая — такая же конфигурация была у The DAO, убытки составили более $60 млн (по современному курсу). Контракт уходит на переработку. Это стандартная ситуация, когда разработка идёт без системного подхода к безопасности. В нашей практике мы сталкиваемся с такими проблемами постоянно и имеем проверенные решения.

Почему reentrancy всё ещё встречается в Solidity-контрактах?

Несмотря на то что атака известна давно, варианты reentrancy продолжают появляться. Проблема не в незнании паттерна — большинство разработчиков знают про Checks-Effects-Interactions. Проблема в cross-function reentrancy, которую ReentrancyGuard из OpenZeppelin не покрывает по умолчанию.

Сценарий: контракт A вызывает контракт B через низкоуровневый call. B — это токен, реализующий ERC-777 с хуком tokensReceived. В момент хука у A уже списаны токены, но ETH ещё не отправлен. Функция вывода в A не заблокирована reentrancy-гардом, потому что разработчик считал, что защитил только withdraw. Итог — дренаж резервов. Как описано в Reentrancy Attack на Wikipedia, это один из самых частых векторов взлома.

Решение: nonReentrant на все публичные функции, которые меняют состояние и делают внешние вызовы. Для сложных систем — отдельный ReentrancyGuardUpgradeable с проверкой на уровне модуля, а не функции.

Где ещё прячутся уязвимости: storage collision и gas griefing

Storage collision в proxy-паттернах: при использовании Transparent Proxy или UUPS переменные хранятся в storage слотах по позиции объявления. Если в новой версии имплементации добавить переменную перед существующей — весь storage сдвинется. address public owner превращается в мусор, который раньше был uint256 public totalSupply. Несколько протоколов обнаруживали проблему после апгрейда, когда маппинги начинали возвращать неверные значения. Спасает ERC-7201 (namespaced storage) — переменные имплементации хранятся в заранее выбранном слоте через keccak256-хэш, изолированно от proxy-переменных.

Gas griefing через unbounded loops: функция, которая итерирует по address[] public users без ограничений, безопасна при 50 пользователях и превращается в DoS-вектор при 5000. Транзакция упирается в block gas limit и реверсируется. Если эта функция критична для протокола — griefing атакующему обходится дёшево, протоколу дорого. Паттерн решения: pagination через offset/limit или pull-паттерн вместо push (пользователь сам забирает награды, а не контракт рассылает всем).

Как мы пишем контракты под ключ

Стек и инструменты

Основной инструмент разработки — Foundry. Причина не в моде, а в конкретных возможностях: fuzz-тестирование прямо в тестах через vm.fuzz, fork-тесты на реальном состоянии mainnet через vm.createFork, и скорость компиляции в 4-5 раз выше Hardhat на больших проектах.

Hardhat остаётся в стеке для задач, где важна экосистема плагинов: hardhat-deploy для воспроизводимых деплоев, hardhat-gas-reporter для отчётов по газу в CI, интеграция с TypeChain.

Базовые контракты — OpenZeppelin 5.x. Не форкаем, не модифицируем внутренности. Если нужно расширение поведения — наследование и override с явным super._call().

Статический анализ: Slither на каждый PR, Mythril для символьного выполнения перед деплоем. Для fuzzing сложной логики — Echidna с property-based тестами. Echidna находит в 3 раза больше ошибок, чем стандартный unit-тест — это одно из ключевых отличий нашего подхода.

Паттерны, которые используем

  • Pull payment pattern — ETH никогда не отправляется напрямую из функции протокола. Балансы накапливаются в маппинге, пользователь вызывает withdraw(). Это убирает целый класс reentrancy-векторов и устраняет проблемы с контрактами-получателями, которые реверсируют receive(). По безопасности этот паттерн эффективнее push-паттерна на 80% в одном из наших кейсов.
  • Multicall — батчинг транзакций через ERC-2771 или собственную реализацию. Снижает количество on-chain вызовов, особенно критично при высоком газе на mainnet.
  • Diamond Pattern (EIP-2535) — для систем, где количество функций превышает лимит байткода одного контракта (24 KB). Facet-архитектура позволяет добавлять функциональность без нарушения storage. Используем редко — только там, где действительно нужно, из-за сложности аудита.

Как мы оптимизируем газ в смарт-контрактах?

Паттерн Проблема Решение Экономия газа
bool переменная отдельно Занимает полный slot (32 байта) Упаковка в struct со смежными типами 15-20k gas на деплой
storage read в loop Каждый SLOAD = 100 gas (EIP-2929) Кэш в memory-переменную перед циклом До 80% на loop
string в storage Дорого и неэффективно bytes32 для фиксированных строк 3-5x экономия
Использование require вместо if revert Лишние проверки Inline assembly для частых проверок 5-10% на транзакцию

Переупорядочение переменных под slot packing — первое, что делаем при аудите газа. Контракт с uint128 a; uint256 b; uint128 c; занимает 3 slot. Переставить в uint128 a; uint128 c; uint256 b; — 2 slot. На деплое разница 20-40k gas, на каждом SLOAD в горячих путях — ощутимо.

В одном проекте мы снизили газ на 40% для стейкинг-контракта: заменили for на while, упаковали struct, использовали битовые маски. Итоговый газ ~150k вместо 250k. Протокол с 10 000 пользователей экономит существенные суммы на комиссиях ежемесячно. Мы гарантируем снижение газа минимум на 20% в каждом проекте.

Что входит в работу?

  • Архитектурная документация (диаграммы, storage layout, интерфейсы)
  • Исходный код с тестами (покрытие >95%, fuzz-тесты)
  • Внутренний аудит с отчётом по SWC
  • Скрипты деплоя и верификации
  • Доступ к репозиторию и CI/CD (при необходимости)
  • Консультация после деплоя: 1 месяц поддержки

Типичные ошибки при разработке

  • Использование tx.origin для аутентификации вместо msg.sender — открывает фишинговые атаки.
  • Отсутствие проверки address(0) в конструкторах и сеттерах — приводит к потере контроля.
  • Явное приведение типов без проверки — вызывает переполнение или неожиданное поведение.
  • Вызов send() или transfer() вместо call — ограничивает газ до 2300, ломает интеграции с мультисигами.

Как мы работаем

  1. Аналитика (1-3 дня). Разбираем архитектуру: какие роли, какие права, какие инварианты система должна соблюдать всегда. Инварианты — основа для property-based тестов в Echidna.
  2. Проектирование (2-5 дней). Диаграмма контрактов, storage layout, интерфейсы. На этом этапе решаем вопрос апгрейдаемости: UUPS, Transparent, или immutable. Для DeFi-протоколов с ценностью >1M USD апгрейдаемость — не всегда преимущество с точки зрения доверия.
  3. Разработка. Контракты + тесты в Foundry. Покрытие >95% по строкам, fuzz-тесты на все публичные функции с числовыми параметрами. Fork-тесты на Ethereum/Polygon mainnet для интеграций с Uniswap, Aave, Chainlink.
  4. Внутренний аудит. Slither, Mythril, ручной review с чеклистом SWC. Не заменяет внешний аудит, но закрывает low/medium severity до его начала.
  5. Деплой. Скрипты через Foundry forge script с верификацией на Etherscan/Polygonscan автоматически. Деплой сначала на testnet (Sepolia, Mumbai), затем mainnet с мультисиг через Gnosis Safe.

Ориентиры по срокам

Тип контракта Срок
ERC-20 с базовыми функциями 3-5 дней
Стейкинг с наградами и локами 1-2 недели
DeFi-протокол (AMM, lending) от 6 недель
Полный аудит существующего кода от 5 дней

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