Разработка смарт-контрактов на Solidity
Клиент приносит контракт на аудит — 800 строк Solidity, деплой на Ethereum mainnet через неделю. На третьей странице кода обнаруживается паттерн: внешний вызов до обновления состояния, классическая reentrancy. Не теоретическая — такая же конфигурация была у The DAO в 2016-м, 60 миллионов долларов. Контракт уходит на переработку. Это стандартная ситуация, когда Solidity-разработка идёт без системного подхода к безопасности.
Где чаще всего ломается Solidity-контракт
Reentrancy — всё ещё живёт в продакшне
Несмотря на то что атака известна с 2016 года, варианты reentrancy продолжают появляться. Проблема не в незнании паттерна — большинство разработчиков знают про Checks-Effects-Interactions. Проблема в cross-function reentrancy, которую ReentrancyGuard из OpenZeppelin не покрывает по умолчанию.
Сценарий: контракт A вызывает контракт B через низкоуровневый call. B — это токен, реализующий ERC-777 с хуком tokensReceived. В момент хука у A уже списаны токены, но ETH ещё не отправлен. Функция вывода в A не заблокирована reentrancy-гардом, потому что разработчик считал, что защитил только withdraw. Итог — дренаж резервов.
Решение: nonReentrant на все публичные функции, которые меняют состояние и делают внешние вызовы. Для сложных систем — отдельный ReentrancyGuardUpgradeable с проверкой на уровне модуля, а не функции.
Storage collision в proxy-паттернах
При использовании Transparent Proxy или UUPS переменные хранятся в storage слотах по позиции объявления. Если в новой версии имплементации добавить переменную перед существующей — весь storage сдвинется. address public owner превращается в мусор, который раньше был uint256 public totalSupply.
Это не гипотетика: несколько протоколов в 2022-2023 годах обнаруживали проблему после апгрейда, когда маппинги начинали возвращать неверные значения. Спасает 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 (пользователь сам забирает награды, а не контракт рассылает всем).
Как мы пишем Solidity-контракты
Стек и инструменты
Основной инструмент разработки — 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 тестами.
Паттерны, которые используем
Diamond Pattern (EIP-2535) — для систем, где количество функций превышает лимит байткода одного контракта (24 KB). Facet-архитектура позволяет добавлять функциональность без нарушения storage. Используем редко — только там, где действительно нужно, из-за сложности аудита.
Pull payment pattern — ETH никогда не отправляется напрямую из функции протокола. Балансы накапливаются в маппинге, пользователь вызывает withdraw(). Это убирает целый класс reentrancy-векторов и устраняет проблемы с контрактами-получателями, которые реверсируют receive().
Multicall — батчинг транзакций через ERC-2771 или собственную реализацию. Снижает количество on-chain вызовов, особенно критично при высоком газе на mainnet.
Оптимизация газа
Типичные места, где газ уходит впустую:
| Паттерн | Проблема | Решение | Экономия |
|---|---|---|---|
bool переменная отдельно |
Занимает полный slot (32 байта) | Упаковка в struct со смежными типами | 15-20k gas на деплой |
storage read в loop |
Каждый SLOAD = 100 gas (EIP-2929) | Кэш в memory-переменную перед циклом | До 80% на loop |
emit Event без индексации |
Невозможно фильтровать через The Graph | indexed на ключевые поля |
Нет экономии газа, но критично для DX |
string в storage |
Дорого и неэффективно | bytes32 для фиксированных строк |
3-5x экономия |
Переупорядочение переменных под slot packing — первое, что делаем при аудите газа. Контракт с uint128 a; uint256 b; uint128 c; занимает 3 slot. Переставить в uint128 a; uint128 c; uint256 b; — 2 slot. На деплое разница 20-40k gas, на каждом SLOAD в горячих путях — ощутимо.
Процесс работы
Аналитика (1-3 дня). Разбираем архитектуру: какие роли, какие права, какие инварианты система должна соблюдать всегда. Инварианты — основа для property-based тестов в Echidna.
Проектирование (2-5 дней). Диаграмма контрактов, storage layout, интерфейсы. На этом этапе решаем вопрос апгрейдаемости: UUPS, Transparent, или immutable. Для DeFi-протоколов с ценностью >1M USD апгрейдаемость — не всегда преимущество с точки зрения доверия.
Разработка. Контракты + тесты в Foundry. Покрытие >95% по строкам, fuzz-тесты на все публичные функции с числовыми параметрами. Fork-тесты на Ethereum/Polygon mainnet для интеграций с Uniswap, Aave, Chainlink.
Внутренний аудит. Slither, Mythril, ручной review с чеклистом SWC (Smart Contract Weakness Classification). Не заменяет внешний аудит, но закрывает low/medium severity до его начала.
Деплой. Скрипты через Foundry forge script с верификацией на Etherscan/Polygonscan автоматически. Деплой сначала на testnet (Sepolia, Mumbai), затем mainnet с мультисиг через Gnosis Safe.
Ориентиры по срокам
Простой токен ERC-20 с базовыми функциями — 3-5 дней включая тесты. Стейкинг-контракт с наградами и временными локами — 1-2 недели. Полноценный DeFi-протокол с AMM-логикой или лендингом — от 6 недель. Сроки зависят от сложности логики и требований к покрытию тестами.
Стоимость рассчитывается после анализа технического задания.







