Рефакторинг смарт-контрактов
Контракт написан год назад, работает, деньги не теряет — но каждый новый feature вызывает панику: непонятно, что сломается. Storage layout распух до 30 переменных без логики группировки, функции на 200 строк, ни одного теста на edge cases. Рефакторинг смарт-контракта отличается от рефакторинга обычного кода тем, что цена ошибки — потеря средств пользователей.
Где чаще всего скрывается технический долг
Неоптимизированный storage layout. Solidity упаковывает переменные в 32-байтные слоты. Если переменные объявлены в порядке uint128, uint256, uint128 — это три слота вместо двух. На популярном контракте с тысячами вызовов в день это реальные деньги. Видели контракт, где переупорядочивание 8 переменных под slot packing снизило gas на write-операции на 40%. Это не оптимизация ради оптимизации — это конкретные тысячи долларов экономии пользователей в год.
Unbounded loops как вектор gas griefing. Паттерн for (uint i = 0; i < users.length; i++) в контракте, где users может расти неограниченно — это не просто неэффективность. Злоумышленник добавляет 10 000 адресов, и следующий вызов distribute() улетает за лимит блока (30M gas на mainnet). Функция становится неисполнимой — contract stuck. Рефакторинг на pull-паттерн с пагинацией или enumerable mapping решает это структурно.
Reentrancy без guard на cross-function уровне. ReentrancyGuard от OpenZeppelin защищает одну функцию. Но если withdraw() защищён guard, а claim() нет — и обе они меняют один balance mapping — cross-function reentrancy возможен. Именно так работал Fei Protocol exploit (80M$ в 2022). При рефакторинге аудируем весь граф вызовов, а не только «подозрительные» функции.
Как мы подходим к рефакторингу
Первый шаг — статический анализ через Slither. Он за 2-3 минуты находит:
- reentrancy паттерны (включая cross-function)
- неинициализированные переменные
- tx.origin авторизацию
- неправильный порядок операций (state change после external call)
- shadow переменные
Slither даёт сотни warning-ов на любом реальном контракте — важно отделить критические от информационных. Далее — Mythril для символического выполнения на ключевых функциях.
Coverage аудит. Смотрим, что покрыто тестами, а что нет. Как правило: happy path покрыт, edge cases — нет. Нет теста на «что будет, если owner вызовет эту функцию дважды подряд». Нет теста на «что будет с контрактом после emergency pause». Добавляем тесты через Foundry — его fuzzer за час находит то, что ручные тесты на Hardhat не нашли за месяц.
Структурный рефакторинг. Выносим логику в библиотеки (Library pattern), разделяем storage и logic через Diamond pattern (EIP-2535) если контракт крупный, применяем Check-Effects-Interactions на каждой функции с external call. Переписываем события (Events) — неправильно индексированные параметры делают The Graph запросы неэффективными.
Gas optimization. Конкретные паттерны:
-
storage→memoryдля read-only операций внутри функции -
uint256вместоuint8в локальных переменных (EVM оперирует 256-битными словами, downcast дороже) -
unchecked { i++ }в счётчиках циклов, где overflow невозможен (Solidity 0.8+) -
calldataвместоmemoryдля параметров внешних функций - упаковка событий: не эмитить лишние поля в events
| Паттерн | Экономия gas (примерно) |
|---|---|
| Slot packing переменных | 20-40% на SSTORE |
| memory вместо storage в функции | 15-30% на чтение |
| unchecked increment | 60-80 gas на итерацию |
| calldata вместо memory | 50-100 gas на аргумент |
| Custom errors вместо require strings | 50-200 gas на revert |
Апгрейд Solidity версии
Рефакторинг часто включает миграцию с 0.6/0.7 на 0.8+. Главные изменения:
- Arithmetic overflow/underflow проверяется по умолчанию (можно убрать SafeMath)
- Custom errors через
errorkeyword — дешевле и информативнееrevert("string") - Immutable переменные — экономят gas на константах, которые задаются в конструкторе
Миграция с 0.6 на 0.8 — это не просто замена pragma. ABI encoding изменился, некоторые паттерны с assembly перестали работать, .call.value() заменён на .call{value:}(). Тестируем каждое изменение изолированно.
Процесс работы
День 1. Статический анализ (Slither, Mythril), coverage отчёт, составление реестра проблем с приоритетами.
День 2-3. Рефакторинг по приоритетам: критические security issues → gas optimization → readability. Каждый PR — изолированное изменение с тестами. Никаких «один большой коммит с 50 изменениями».
Финал. Прогон Foundry fuzz tests на рефакторинговых функциях, сравнение gas report до/после через forge snapshot.
Срок — 2-3 дня для контракта до 500 строк. Более сложные системы из нескольких контрактов — до недели.







