Фаззинг-тестирование смарт-контрактов

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Фаззинг-тестирование смарт-контрактов
Сложная
~2-3 рабочих дня
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Аудит обновляемых смарт-контрактов

Обновляемые (upgradeable) контракты — компромисс. С одной стороны, возможность исправить баги и добавить функциональность без миграции. С другой — полный отказ от immutability гарантий, которые делают blockchain безопасным. Любой апгрейд — это потенциальная точка атаки: если злоумышленник получил контроль над upgrade механизмом, он может заменить логику на произвольный код и вывести все средства. Именно поэтому аудит upgradeable контрактов сложнее и длиннее аудита обычных.

Compound, Aave, Uniswap V3 используют тот или иной вариант upgradeability. Но они же вложили сотни тысяч долларов в аудит именно upgrade механизмов.

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

Transparent Proxy (EIP-1967)

Самый распространённый паттерн от OpenZeppelin. Proxy хранит адрес implementation в deterministic storage slot. Вызовы делегируются в implementation через delegatecall.

User → Proxy (storage) → delegatecall → Implementation (logic)

Критическая уязвимость: storage collision. Proxy использует слот 0x360894... для адреса implementation. Если implementation случайно использует тот же слот (или слот admin, или initializer flag) — перезапись при upgrade может сломать весь контракт.

// Опасный паттерн: переменные в Implementation
contract VulnerableImpl {
    // Слот 0 в storage
    address public owner;        // КОЛЛИЗИЯ с proxy admin слотом!

    // Если Proxy хранит admin в слоте 0 — owner в implementation
    // будет читать/писать адрес admin proxy
}

// Правильный паттерн: использовать namespaced storage (EIP-7201)
// или убедиться что impl начинается с правильного смещения
contract SafeImpl {
    // При использовании OpenZeppelin TransparentUpgradeableProxy
    // implementation НЕ должна иметь переменных в слотах,
    // зарезервированных proxy (0x360894..., 0xb53127...)
    // OpenZeppelin Upgrades Hardhat plugin проверяет это автоматически
}

UUPS (Universal Upgradeable Proxy Standard, EIP-1822)

Upgrade логика находится в implementation, не в proxy. Proxy проще и дешевле в деплое.

Уязвимость UUPS: selfdestruct в uninitialized implementation. Если implementation контракт задеплоен и не инициализирован, злоумышленник может вызвать initialize напрямую на implementation (не через proxy), стать owner-ом, и вызвать upgradeTo(maliciousContract) с selfdestruct. После selfdestruct implementation — proxy становится нефункциональным навсегда.

Именно это произошло с несколькими протоколами, пока OpenZeppelin не добавил _disableInitializers() в конструктор implementation:

contract MyContractV1 is UUPSUpgradeable, OwnableUpgradeable {
    constructor() {
        _disableInitializers(); // ОБЯЗАТЕЛЬНО в каждом implementation
    }

    function initialize(address initialOwner) public initializer {
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
    }

    function _authorizeUpgrade(address newImplementation)
        internal override onlyOwner {}
}

Beacon Proxy

Один Beacon хранит адрес implementation, множество Proxy смотрят на этот Beacon. Один upgrade обновляет все proxy одновременно. Используется когда нужно задеплоить сотни одинаковых контрактов (например, vault per user).

Уязвимость: centralization Beacon. Кто контролирует Beacon — контролирует все proxy. Если Beacon owner — EOA или слабо защищённый мультисиг, это катастрофический single point of failure.

Что проверяем при аудите

Инициализация

Самая частая ошибка upgradeable контрактов — неправильная инициализация:

// НЕПРАВИЛЬНО: конструктор в upgradeable контракте не работает
contract BrokenUpgradeable {
    address public owner;

    constructor() {
        owner = msg.sender; // НЕ будет вызван при деплое через proxy!
    }
}

// ПРАВИЛЬНО: initializer функция
contract CorrectUpgradeable is Initializable, OwnableUpgradeable {
    function initialize() public initializer {
        __Ownable_init(msg.sender);
    }
}

Checklist инициализации:

  • Все parent contracts инициализированы через __Parent_init() паттерн
  • initializer modifier присутствует (предотвращает повторный вызов)
  • _disableInitializers() в конструкторе
  • Все state переменные, которые должны быть инициализированы при деплое, покрыты

Storage layout compatibility

При upgrade новая implementation должна иметь точно такой же storage layout начиная со слота 0. Добавление новых переменных — только в конец. Переупорядочивание, удаление, изменение типов — storage collision и corruption данных.

// V1
contract MyContractV1 {
    uint256 public value;      // слот 0
    address public owner;      // слот 1
}

// V2 ПРАВИЛЬНО: добавляем в конец
contract MyContractV2 {
    uint256 public value;      // слот 0 — не изменён
    address public owner;      // слот 1 — не изменён
    uint256 public newValue;   // слот 2 — новый
}

// V2 НЕПРАВИЛЬНО: вставка в середину
contract MyContractV2_BROKEN {
    uint256 public newValue;   // слот 0 — КОЛЛИЗИЯ с value!
    uint256 public value;      // слот 1 — был в слоте 0!
    address public owner;      // слот 2 — был в слоте 1!
}

Инструменты: @openzeppelin/upgrades-core проверяет совместимость storage layout автоматически. В аудите — ручная сверка storage slots через forge inspect ContractName storage-layout.

Access control upgrade функций

Кто может вызвать upgrade? Это центральный вопрос аудита:

  • EOA: недопустимо для production. Один скомпрометированный ключ = потеря всего.
  • Gnosis Safe мультисиг: приемлемо, требует M-of-N подписей.
  • Timelock: лучшая практика. Upgrade в очереди 48-72 часа, сообщество может отреагировать.
  • Governor + Timelock: максимальная защита через on-chain голосование.
// Аудит проверяет: есть ли timelock перед upgrade?
function _authorizeUpgrade(address newImplementation) 
    internal override 
{
    // ПЛОХО: только onlyOwner без timelock
    // require(msg.sender == owner, "Not owner");
    
    // ХОРОШО: только через timelock (= через governance голосование)
    require(msg.sender == address(timelock), "Only timelock");
}

Invariant preservation

После каждого upgrade проверяем: не нарушены ли инварианты контракта? Если до upgrade totalSupply == sum(balances), это должно быть истиной после upgrade.

// Тест совместимости V1 → V2
contract UpgradeTest is Test {
    function testUpgradePreservesState() public {
        // Деплоим V1 через proxy, устанавливаем state
        MyContractV1 v1 = deployV1();
        v1.setValue(42);
        v1.deposit{value: 1 ether}();

        // Делаем upgrade до V2
        upgradeTo(address(new MyContractV2()));

        // Проверяем что state сохранён
        MyContractV2 v2 = MyContractV2(address(proxy));
        assertEq(v2.value(), 42);
        assertEq(address(proxy).balance, 1 ether);

        // Проверяем новую функциональность V2
        v2.newFunction();
    }
}

Функциональные изменения при upgrade

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

Пример: V1 имеет limit на withdraw (не более 10 ETH за транзакцию). V2 убирает этот limit — возможно намеренно, но аудитор должен явно это подтвердить.

Специфические векторы атак

Upgrade + reentrancy

Во время выполнения upgrade транзакции (особенно если в upgrade логике есть внешние вызовы) возможна reentrancy. Контракт в промежуточном состоянии (между старой и новой логикой) уязвим.

Delegatecall injection

Если implementation содержит функцию с произвольным delegatecall к внешнему адресу — злоумышленник может использовать это для выполнения кода в контексте proxy (и его storage).

// КРИТИЧЕСКИ ОПАСНО в upgradeable контракте
function execute(address target, bytes calldata data) external onlyOwner {
    target.delegatecall(data); // Атакующий owner = полный контроль над proxy storage
}

Selfdestruct в implementation (EIP-6780 частично смягчает)

EIP-6780 (Dencun upgrade, март 2024) изменил поведение selfdestruct: теперь selfdestruct в той же транзакции что и CREATE работает как раньше, но вызванный в отдельной транзакции — только переводит ETH, не удаляет код. Для UUPS уязвимости это частичное смягчение, но не полное решение.

Инструменты аудита upgradeable контрактов

Инструмент Назначение
@openzeppelin/hardhat-upgrades Автоматическая проверка storage layout
forge inspect ... storage-layout Ручной анализ слотов
Slither Статический анализ, включая upgrade паттерны
Echidna / Medusa Fuzzing на invariant preservation
Foundry fork tests Тесты upgrade на mainnet fork
Tenderly Симуляция upgrade транзакций

Процесс аудита

Фаза 1: Статический анализ (3-5 дней). Slither + ручной анализ proxy паттерна. Карта storage слотов V1, проверка _disableInitializers, access control upgrade функций.

Фаза 2: Storage compatibility (2-3 дня). Сравнение storage layouts V1 и V2. Для каждой переменной: слот, тип, offset. Особое внимание на mapping и dynamic arrays (их layout нетривиален).

Фаза 3: Функциональные изменения (3-5 дней). Построение diff между V1 и V2 на уровне функций. Для каждого изменения: намеренно ли, не нарушает ли инварианты, нет ли новых векторов атак.

Фаза 4: Upgrade механизм (2-3 дня). Полный lifecycle upgrade: кто инициирует → timelock → execution → rollback возможность (есть ли она?).

Фаза 5: Тестирование (5-7 дней). Fork tests на Foundry. Fuzzing критичных функций. Ручные тесты edge cases.

Отчёт и ремедиация (3-5 дней). Документирование findings, severity classification (Critical/High/Medium/Low/Informational), рекомендации.

Полный аудит upgradeable контракта среднего размера: 3-5 недель. Стоимость — после оценки объёма кода и сложности upgrade механизма.