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

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Фаззинг-тестування смарт-контрактів
Складний
~2-3 дні
Часті запитання

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

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1217
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    919
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1146
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    609
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    884

Аудит оновлюваних смарт-контрактів

Оновлювані контракти — це компромісс. З одного боку, можливість виправити баги та додати функціональність без міграції. З іншого — повна відмова від гарантій незмінності, які роблять blockchain безпечним. Будь-який апгрейд — це потенційна точка атаки: якщо атакувальник отримає контроль над механізмом апгрейду, він може замінити логіку на довільний код і вивести всі кошти. Саме тому аудит оновлюваних контрактів складніший і довший, ніж аудит звичайних.

Compound, Aave, Uniswap V3 використовують деяку форму оновлюваності. Але вони інвестували сотні тисяч доларів в аудит саме механізмів апгрейду.

Паттерни оновлюваності та їхні вразливості

Transparent Proxy (EIP-1967)

Найпоширеніший паттерн від OpenZeppelin. Прокси зберігає адресу реалізації у детермінованому слоті storage. Дзвінки делегуються реалізації через delegatecall.

Користувач → Прокси (storage) → delegatecall → Реалізація (логіка)

Критична вразливість: колізія storage. Прокси використовує слот 0x360894... для адреси реалізації. Якщо реалізація випадково використовує той же слот (або слот admin, або initializer flag) — перезапис під час апгрейду може зламати весь контракт.

// Небезпечний паттерн: змінні у Implementation
contract VulnerableImpl {
    // Слот 0 у storage
    address public owner;        // КОЛІЗІЯ з прокси admin слотом!

    // Якщо Proxy зберігає admin у слоті 0 — owner у реалізації
    // буде читати/писати адресу proxy admin
}

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

UUPS (Universal Upgradeable Proxy Standard, EIP-1822)

Логіка апгрейду знаходиться у реалізації, не у прокси. Прокси простіший та дешевший для розгортання.

UUPS вразливість: selfdestruct у неініціалізованій реалізації. Якщо контракт реалізації розгорнутий і не ініціалізований, атакувальник може викликати initialize безпосередньо на реалізації (не через прокси), стати власником і викликати upgradeTo(maliciousContract) з selfdestruct. Після selfdestruct реалізації — прокси стає непрацездатним назавжди.

Це трапилося з кількома протоколами, поки OpenZeppelin не додав _disableInitializers() у конструктор реалізації:

contract MyContractV1 is UUPSUpgradeable, OwnableUpgradeable {
    constructor() {
        _disableInitializers(); // ОБОВ'ЯЗКОВО у кожній реалізації
    }

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

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

Beacon Proxy

Один Beacon зберігає адресу реалізації, множина Proxies дивляться на цей Beacon. Один апгрейд оновлює всі proxies одночасно. Використовується коли потрібно розгорнути сотні однакових контрактів (наприклад, vault на користувача).

Beacon вразливість: централізація. Той, хто контролює Beacon — контролює всі proxies. Якщо Beacon owner — EOA або слабо захищений multisig — катастрофічна єдина точка відмови.

Що ми перевіряємо під час аудиту

Ініціалізація

Найчастіша помилка в оновлюваних контрактах — неправильна ініціалізація:

// НЕПРАВИЛЬНО: конструктор у оновлюваному контракті не працює
contract BrokenUpgradeable {
    address public owner;

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

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

Контрольний список ініціалізації:

  • Усі батьківські контракти ініціалізовані через паттерн __Parent_init()
  • Присутній модифікатор initializer (запобігає повторному дзвінку)
  • _disableInitializers() у конструкторі
  • Усі змінні стану, які мають бути ініціалізовані при розгортанні, охоплені

Сумісність layout storage

При апгрейді нова реалізація повинна мати точно такий же layout storage починаючи зі слота 0. Додавання нових змінних — тільки в кінці. Переупорядкування, видалення, зміна типів — колізія storage та корупція даних.

// 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 автоматично перевіряє сумісність layout storage. У аудиті — ручна верифікація через forge inspect ContractName storage-layout.

Контроль доступу до функцій апгрейду

Хто може викликати апгрейд? Це центральне питання аудиту:

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

Збереження інваріантів

Після кожного апгрейду перевіряємо: чи не порушені інваріанти контракту? Якщо до апгрейду totalSupply == sum(balances), це має бути істинно після апгрейду.

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

        // Апгрейдуємо до V2
        upgradeTo(address(new MyContractV2()));

        // Перевіряємо що стан збережено
        MyContractV2 v2 = MyContractV2(address(proxy));
        assertEq(v2.value(), 42);
        assertEq(address(proxy).balance, 1 ether);

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

Функціональні зміни під час апгрейду

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

Приклад: V1 має ліміт на withdrawal (не більше 10 ETH за транзакцію). V2 видаляє цей ліміт — можливо навмисно, але аудитор повинен це явно підтвердити.

Специфічні вектори атак

Апгрейд + Reentrancy

Під час виконання апгрейду транзакції (особливо якщо логіка апгрейду має зовнішні дзвінки), можлива reentrancy. Контракт у проміжному стані (між старою та новою логікою) вразливий.

Інекція Delegatecall

Якщо реалізація містить функцію з довільним delegatecall до зовнішної адреси — атакувальник може використовувати це для виконання коду у контексті прокси (та його storage).

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

Selfdestruct у Implementation (EIP-6780 Частково Пом'якшує)

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

Інструменти аудиту

Інструмент Призначення
@openzeppelin/hardhat-upgrades Автоматична перевірка layout storage
forge inspect ... storage-layout Ручний аналіз слотів
Slither Статичний аналіз, включаючи паттерни апгрейду
Echidna / Medusa Фаззинг на збереженню інваріантів
Foundry fork tests Тести апгрейду на fork mainnet
Tenderly Симуляція транзакцій апгрейду

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

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

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

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

Фаза 4: Механізм апгрейду (2-3 дні). Повний життєвий цикл апгрейду: хто ініціює → timelock → виконання → можливість rollback?

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

Звіт та виправлення (3-5 днів). Документування результатів, класифікація критичності (Critical/High/Medium/Low/Informational), рекомендації.

Повний аудит оновлюваного контракту середнього розміру: 3-5 тижнів. Вартість — після оцінки обсягу коду та складності механізму апгрейду.