Перейти до основного вмісту

Зменшення розміру контрактів для боротьби з лімітом розміру контракту

solidity
смарт-контракти
зберігання
Середній рівень
Маркус Ваас
26 червня 2020 р.
5 хвилин на читання
Редагувати сторінку (opens in a new tab)

Чому існує ліміт?

22 листопада 2016 року (opens in a new tab) хард-форк Spurious Dragon запровадив EIP-170 (opens in a new tab), який додав ліміт розміру смарт-контракту в 24,576 кб. Для вас як розробника на Solidity це означає, що коли ви додаєте все більше і більше функціональності до свого контракту, в певний момент ви досягнете ліміту і під час розгортання побачите помилку:

Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.

Цей ліміт було запроваджено для запобігання атакам типу «відмова в обслуговуванні» (DOS). Будь-який виклик контракту є відносно дешевим з точки зору газу. Однак вплив виклику контракту на вузли Етеріуму непропорційно зростає залежно від розміру коду викликаного контракту (читання коду з диска, попередня обробка коду, додавання даних до доказу Меркла). Щоразу, коли виникає ситуація, коли зловмиснику потрібно небагато ресурсів, щоб спричинити багато роботи для інших, з'являється потенціал для DOS-атак.

Спочатку це було меншою проблемою, оскільки одним із природних лімітів розміру контракту є ліміт газу блоку. Очевидно, що контракт має бути розгорнутий у межах транзакції, яка містить увесь байт-код контракту. Якщо ви включите в блок лише цю одну транзакцію, ви зможете використати весь цей газ, але він не є нескінченним. Починаючи з оновлення London, ліміт газу блоку може варіюватися від 15 до 30 мільйонів одиниць залежно від попиту в мережі.

Далі ми розглянемо деякі методи, впорядковані за їхнім потенційним впливом. Подумайте про це з точки зору схуднення. Найкраща стратегія для досягнення цільової ваги (у нашому випадку 24 кб) — це зосередитися спочатку на методах із найбільшим впливом. У більшості випадків достатньо просто налагодити дієту, але іноді потрібно трохи більше. Тоді ви можете додати фізичні вправи (середній вплив) або навіть добавки (незначний вплив).

Значний вплив

Розділіть ваші контракти

Це завжди має бути вашим першим кроком. Як можна розділити контракт на кілька менших? Зазвичай це змушує вас продумати хорошу архітектуру для ваших контрактів. Менші контракти завжди кращі з точки зору читабельності коду. Для розділення контрактів запитайте себе:

  • Які функції пов'язані між собою? Кожен набір функцій, можливо, краще виділити в окремий контракт.
  • Які функції не потребують читання стану контракту або потребують лише певної його частини?
  • Чи можете ви розділити сховище та функціональність?

Бібліотеки

Один із простих способів відокремити код функціональності від сховища — це використання бібліотеки (opens in a new tab). Не оголошуйте функції бібліотеки як внутрішні (internal), оскільки вони будуть додані до контракту (opens in a new tab) безпосередньо під час компіляції. Але якщо ви використовуєте публічні (public) функції, то вони фактично знаходитимуться в окремому контракті бібліотеки. Розгляньте можливість застосування using for (opens in a new tab), щоб зробити використання бібліотек зручнішим.

Проксі

Більш просунутою стратегією буде система проксі. Бібліотеки використовують DELEGATECALL під капотом, що просто виконує функцію іншого контракту зі станом контракту, який її викликає. Перегляньте цю публікацію в блозі (opens in a new tab), щоб дізнатися більше про системи проксі. Вони надають більше функціональності, наприклад, можливість оновлення, але також додають багато складності. Я б не додавав їх лише для зменшення розміру контрактів, хіба що це ваш єдиний варіант з якихось причин.

Середній вплив

Видалення функцій

Це має бути очевидним. Функції значно збільшують розмір контракту.

  • Зовнішні (External): Часто ми додаємо багато функцій перегляду (view) для зручності. Це цілком нормально, поки ви не досягнете ліміту розміру. Тоді вам варто серйозно подумати про видалення всіх функцій, окрім абсолютно необхідних.
  • Внутрішні (Internal): Ви також можете видалити внутрішні/приватні (internal/private) функції та просто вбудувати їхній код (inline), якщо функція викликається лише один раз.

Уникайте додаткових змінних

function get(uint id) returns (address,address) {
    MyStruct memory myStruct = myStructs[id];
    return (myStruct.addr1, myStruct.addr2);
}
function get(uint id) returns (address,address) {
    return (myStructs[id].addr1, myStructs[id].addr2);
}

Така проста зміна дає різницю в 0,28 кб. Швидше за все, ви зможете знайти багато подібних ситуацій у своїх контрактах, і вони дійсно можуть скластися в значні обсяги.

Скорочення повідомлень про помилки

Довгі повідомлення про скасування (revert) і, зокрема, велика кількість різних повідомлень про скасування можуть роздути контракт. Замість цього використовуйте короткі коди помилок і розшифровуйте їх у своєму контракті. Довге повідомлення може стати набагато коротшим:

require(msg.sender == owner, "Only the owner of this contract can call this function");
require(msg.sender == owner, "OW1");

Використовуйте власні помилки замість повідомлень про помилки

Власні помилки (custom errors) були запроваджені в Solidity 0.8.4 (opens in a new tab). Це чудовий спосіб зменшити розмір ваших контрактів, оскільки вони кодуються в ABI як селектори (так само, як і функції).

error Unauthorized();

if (msg.sender != owner) {
    revert Unauthorized();
}

Розгляньте низьке значення запусків (runs) в оптимізаторі

Ви також можете змінити налаштування оптимізатора. Значення за замовчуванням 200 означає, що він намагається оптимізувати байт-код так, ніби функція викликається 200 разів. Якщо ви зміните його на 1, ви фактично скажете оптимізатору оптимізувати для випадку виконання кожної функції лише один раз. Функція, оптимізована для виконання лише один раз, означає, що вона оптимізована для самого розгортання. Майте на увазі, що це збільшує витрати газу на виконання функцій, тому ви можете не захотіти цього робити.

Незначний вплив

Уникайте передачі структур у функції

Якщо ви використовуєте ABIEncoderV2 (opens in a new tab), може допомогти відмова від передачі структур (structs) у функцію. Замість того, щоб передавати параметр як структуру, передавайте необхідні параметри безпосередньо. У цьому прикладі ми заощадили ще 0,1 кб.

function get(uint id) returns (address,address) {
    return _get(myStruct);
}

function _get(MyStruct memory myStruct) private view returns(address,address) {
    return (myStruct.addr1, myStruct.addr2);
}
function get(uint id) returns(address,address) {
    return _get(myStructs[id].addr1, myStructs[id].addr2);
}

function _get(address addr1, address addr2) private view returns(address,address) {
    return (addr1, addr2);
}

Оголошуйте правильну видимість для функцій та змінних

  • Функції або змінні, які викликаються лише ззовні? Оголошуйте їх як external замість public.
  • Функції або змінні, які викликаються лише зсередини контракту? Оголошуйте їх як private або internal замість public.

Видалення модифікаторів

Модифікатори, особливо при інтенсивному використанні, можуть мати значний вплив на розмір контракту. Розгляньте можливість їх видалення та використання функцій замість них.

modifier checkStuff() {}

function doSomething() checkStuff {}
function checkStuff() private {}

function doSomething() { checkStuff(); }

Ці поради мають допомогти вам значно зменшити розмір контракту. Ще раз хочу наголосити: завжди зосереджуйтесь на розділенні контрактів, якщо це можливо, для досягнення найбільшого ефекту.

Останнє оновлення сторінки: 3 квітня 2026 р.