
Вступ
Щоб подолати ці обмеження, було розроблено кілька рішень рівня 2, таких як Base, які контролюються масштабованістю та зменшеними витратами на газ. Хоча L2 мають значне зниження комісій порівняно з основною мережею Ethereum, розробники смарт-контрактів не можуть ігнорувати оптимізацію газу під час їх розробки.
Цей підручник буде спрямований на консолідацію досвіду користувачів та створення більш конкурентоспроможних, децентралізованих додатків за допомогою передових стратегій, що знижують вартість газу на смарт-контрактах, які будуть протестовані. Наведені приклади моделюються за допомогою простих контрактів і в основному враховують ціни на газ під час виконання, оскільки витрати на розгортання значно відрізняються залежно від розміру контракту.
Оптимізація газу має велике значення для розробників, користувачів та довгострокового успіху проекту. Економічне використання газу смарт-контрактів може забезпечити більшу економічну ефективність та масштабованість протоколів, а також зменшити їх вразливість до загроз безпеки, таких як атаки типу «відмова в обслуговуванні».
Практичне застосування смарт-контрактів вимагає всебічного та систематичного аудиту кожного смарт-контракту.
Значення оптимізації газу Solidity
Оптимізація газу дозволяє смарт-контрактам, протоколам і проектам працювати в умовах перевантажених мереж і робить їх дешевими та ефективними. Завдяки вдосконаленню коду контракту з'являється можливість виявити можливі вразливості, а протоколи та користувачі отримують більшу впевненість.
Оптимізація газу також є важливим напрямком розвитку. Це не просто приємна додаткова функція, а запорука довгострокового успіху та безпеки смарт-контрактів. Той факт, що можна будувати на L2 з порівняно низькими комісіями, не означає, що комісії за газ не будуть значно вищими, ніж у конкурентів.
Усі тести використовують Foundry та Solidity версії 0.8.13, локальний блокчейн-вузол Anvil, команди forge test та 100 оптимізаційних запусків.
Професійні аудиторські фірми повинні брати участь у повних перевірках безпеки, цей посібник повинен бути допоміжним.
Зменшення обсягу даних в ланцюжку
Незамінні токени стали популярними в 2021 році і привернули увагу до повністю ончейн-NFT. Ончейн-NFT, на відміну від традиційних NFT, які використовують офчейн-дані, включаючи метадані та посилання на зображення, розміщують всю інформацію безпосередньо в блокчейні.
Ці токени відомі своєю високою вартістю, тому гібридні рішення є стандартними, коли користувачі страждають від комісій. Незалежно від того, чи ви створюєте NFT, ігри або протоколи DeFi, ви завжди повинні думати про те, які дані насправді потрібно зберігати в блокчейні, і про компроміси між обома альтернативами.
Значно зменшіть використання газу, зберігаючи інформацію поза ланцюгом, що вимагає менше місця для зберігання змінних. Одним із конкретних методів є використання подій для зберігання даних поза ланцюгом, а не в фактичному сховищі в ланцюзі.
Витрати на газ транзакції збільшуються за рахунок додаткових функцій emit, що виникають внаслідок подій; але інформація не зберігається в ланцюжку, тому економія зазвичай перевищує витрати.
Розглянемо смарт-контракт, який дозволяє користувачам голосувати «так» або «ні». Перший варіант — це контракт, який зберігає голоси користувачів у структурах ланцюжка. Демонстрація тесту функції голосування за допомогою Foundry з понад 100 ітераціями показує певні витрати на газ.
Порівняйте зі смарт-контрактом, який не зберігає інформацію в ланцюжку, але генерує подію при виклику функції голосування. Тест, що передбачає понад 100 разів використання Foundry в новій функції голосування, дає результати.
Зменшення обсягу даних в ланцюжку дозволило знизити середню вартість газу на 90,34%.
Для доступу до позаланцюгових даних у ланцюзі доступні функції Chainlink, рішення підтримки, які можна інтегрувати з найпопулярнішими мережами L2, такими як Base.
Використовуйте відображення над масивами
У Solidity є дві основні структури даних: масиви та маппінги.
- •Масиви використовуються для зберігання наборів елементів, які приписані певним індексам
- •Відповідності ключ-значення — це структури даних, які підтримують прямий доступ до даних за допомогою унікальних ключів
Хоча масиви можуть бути корисними для зберігання векторів та інших подібних даних, зазвичай перевагу надають маппінгам через їхню газову ефективність. Вони особливо оптимально підходять для ситуацій, коли потрібні дані на запит, наприклад, за іменем, адресою гаманця або балансом рахунку.
Щоб зрозуміти використання газу при використанні масивів або маппінгів, можливо, доведеться звернутися до газу, що використовується пов'язаними оперативними кодами EVM. Оперативні коди — це низькорівневі інструкції, які віртуальна машина Ethereum виконує під час виконання смарт-контрактів, причому кожен оперативний код має вартість газу.
Щоб отримати доступ до значень масиву, необхідно сплатити за кожну одиницю газу, використану операторами EVM. Зберігання адрес користувачів та відповідних балансів у вигляді масиву вимагає циклічного перегляду всіх елементів масиву, перевірки відповідності userAddress та наданого аргументу та повернення балансу у разі відповідності.
Перехід безпосередньо до конкретного балансу користувача дозволив би уникнути необхідності переглядати всі елементи масиву. Після заміни масивів на мапи витрати газу на доступ до даних зменшилися на 89 відсотків.
Порівняння використання газу: масиви проти відображень
| Метод | Перед оптимізацією | Після оптимізації | Економія палива |
|---|---|---|---|
| Доступ до даних | 30 586 | 3 081 | 89% |
| Додавання даних | - | - | 93% |
Використовуйте константи та незмінні величини для зменшення витрат газу на смарт-контракти
Ще одна порада щодо оптимізації — використовувати константи та незмінні змінні. При визначенні змінних як незмінних або константних їхні значення надаються тільки під час створення контракту і більше ніколи.
Вони не займають місце в пам'яті EVM, на відміну від інших змінних. Можна безпосередньо кодувати значення в байт-коді смарт-контракту, що означає, що вартість зберігання даних зменшується завдяки тому, що змінні, такі як maxSupply та owner, не мають ключових слів constant або immutable.
Коли тести виконуються 100 разів, середня вартість газу становить 112 222 одиниці. Оскільки maxSupply та owner є відомими значеннями, які не передбачається змінювати, найкращим вибором є оголошення максимального запасу та власника, які не використовують простір для зберігання.
Тести з постійними або незмінними змінними дають середню економію 35,89% (від 112 222 до 71 940 одиниць газу).
Оптимізуйте невикористані змінні
Очевидним порадою щодо оптимізації газу є оптимізація змінних у смарт-контрактах. Однак у більшості випадків некорисні змінні зберігаються під час виконання контракту, що призводить до непотрібного використання газу.
Видалення окремих невикористаних змінних може бути використано для економії витрат на газ, в середньому на 18%, якщо враховувати контракти з невикористаними змінними, визначеними та обробленими у функціях, але ніколи не викликаними в інших місцях.
Результати оптимізації невикористаних змінних
| Етап оптимізації | Використання газу | Заощадження |
|---|---|---|
| Перед оптимізацією | 32 513 | - |
| Після оптимізації | 27 429 | 18% |
Повернення газу Solidity: видалення невикористаних змінних
Видалення невикористаних змінних — це спроба присвоїти змінним значення за замовчуванням після їх повного обчислення без збереження даних у пам'яті. Наприклад, значення за замовчуванням для змінних типу uint дорівнює 0.
Не руйнуючи змінні функції після її завершення, середні витрати газу становлять 100 300 одиниць для присвоєння змінних даним. В середньому 19% економії газу при використанні клавіші delete для видалення змінних даних (для встановлення змінних на 0).
Використовуйте масиви фіксованого розміру замість динамічних масивів, щоб мінімізувати витрати на газ у смарт-контрактах
Використовуйте мапування, де це можливо, щоб зменшити витрати газу на смарт-контракти. Проте, якщо потрібні масиви, масиви фіксованого розміру є більш економічними з точки зору газу, ніж масиви динамічного розміру, оскільки масиви динамічного розміру можуть розширюватися нескінченно, що призводить до вищих витрат газу.
Динамічні масиви можуть розширюватися, тому EVM повинен контролювати їх довжину і оновлювати їх при додаванні нових елементів.
Розгляньте код, який визначає масиви динамічного розміру та оновлює їх за допомогою функцій updateArray. Оператори Require гарантують, що надані індекси знаходяться в межах масиву фіксованого розміру. Після 100 повторень тесту середня кількість використаного газу становить 12 541.
Зміна масивів на фіксований розмір 5 створює масиви фіксованого розміру довжиною 5, типу uint256. EVM знає, що fixedArray зі змінною розміром 5 має 5 слотів, і довжина не зберігається в пам'яті. Заміна динамічних масивів на фіксовані економить 17,99% газу.
Оптимізуйте свої смарт-контракти вже сьогодні
Почніть застосовувати ці методи оптимізації газу, щоб зменшити витрати на Base та інших ланцюгах L2 на 90%.
Використання uint8 як заміни uint256
Це менш ефективно і може бути дорожчим у використанні, ніж uint256, через роботу EVM. EVM має 256-бітні розміри слів. Операції з 256-бітними цілими числами (uint256) зазвичай є найефективнішими, оскільки вони відповідають розміру слова EVM, тоді як менші типи (такі як uint8) вимагають від компілятора Solidity створення додаткових операцій, щоб вмістити менші типи в один 256-бітний слот пам'яті.
Загалом, хоча невеликі типи, такі як uint8, можуть бути корисними з точки зору зберігання (кілька невеликих типів можна упакувати в одну операцію з 256-бітним слотом пам'яті), було помічено, що менші типи допомагають лише зі зберіганням. Економія місця на диску може бути зведена нанівець через перетворення в uint256 і назад для виконання обчислень.
Об'єднання змінних розміром менше 256 біт
Об'єднання змінних розміром менше 256 біт зазвичай не є таким ефективним, як 256-бітні змінні. Однак ситуації, що не підтримують взаємодію, змушують використовувати менш потужні типи в менших розмірах, наприклад, булеві значення, які споживають 1 байт або 8 біт для зберігання.
При використанні менш потужних типів можна оголошувати змінні стану з урахуванням місця для зберігання, і Solidity може упакувати їх разом і зберігати, використовуючи один і той же слот пам'яті. Перевага упаковки змінних зазвичай розглядається в операціях зберігання, а не в операціях пам'яті або стеку.
Оскільки розмір двосторонніх комбінацій становить 16 біт, або на 240 біт менше, ніж ємність для зберігання одного слота пам'яті, Solidity можна використовувати для упаковки змінних в один слот, щоб мінімізувати використання газу для розгортання, не вимагаючи багато слотів для зберігання змінних стану.
Перегрупування оголошень змінних дозволяє оптимізувати в середньому 13% газу.
Результати пакування змінних
| Етап | Використання газу | Заощадження |
|---|---|---|
| Попередня оптимізація | 1 678 | - |
| Оптимізація після публікації | 1 447 | 13% |
Виберіть зовнішній модифікатор видимості
Щоб максимально збільшити газ смарт-контракту, доцільно вибрати правильну видимість функцій. Зовнішні модифікатори видимості можуть бути більш ефективними у використанні газу, ніж публічні, через спосіб, яким публічна функція управляє своїми аргументами, та спосіб, у який дані передаються до функцій.
Зовнішні функції можуть отримати доступ до calldata, що є тимчасовим простором EVM, доступним тільки для читання, який містить параметри виклику функції. Можна викликати публічні функції:
- •Зовні (коли вони викликаються зовні транзакцією за допомогою calldata)
- •Внутрішньо (коли вони викликаються в контракті за допомогою calldata)
Оскільки функції є публічними, вони повинні отримувати масиви в пам'яті, що може бути дорого в плані витрат газу, якщо масиви великі. Переключення функцій на зовнішні дозволяє отримувати масиви в calldata, що є більш економічно вигідним у ситуаціях, коли масиви великі, заощаджуючи в середньому 0,3 відсотка одиниць газу на кожен виклик.
Зберігання для повторного читання того самого значення
Хороший спосіб заощадити газ — не читати одне й те саме значення в пам'яті багато разів. Читання з пам'яті коштує дорожче, ніж читання з оперативної пам'яті. Багаторазове перечитування одних і тих самих змінних у пам'яті та записування в пам'ять, коли це не потрібно, може бути марною тратою газу в смарт-контрактах.
Цього можна уникнути, визначивши змінні, які знаходяться в пам'яті на початку функцій, і присвоївши їм значення числових змінних.
Це економить 17% газу, що споживається функціями sumNumbers, але чим більший масив, тим більшими будуть номери ітерацій і споживання газу.
Зберігання результатів змінних у кеші
| Етап | Використання газу | Заощадження |
|---|---|---|
| Перед оптимізацією | 3 527 | - |
| Після оптимізації | 2 905 | 17% |
Не ініціалізуйте змінні до значень за замовчуванням
При оголошенні змінних стану без їх ініціалізації (тобто без присвоєння їм початкового значення) ці змінні автоматично ініціалізуються значеннями за замовчуванням:
- •uint: 0
- •bool: false
- •адреса: адреса(0)
Останнє є дешевшим, ніж оголошення значень за замовчуванням і їх просте оновлення під час взаємодії користувача з системою, що не є ефективним з точки зору споживання газу. Без ініціалізації змінних оптимізація забезпечує 4% середньої економії газу.
Підтримка оптимізації компілятора Solidity
Solidity включає компілятори з налаштуваннями, які легко змінювати для оптимізації скомпільованого коду. Оптимізована збірка запускається кілька сотень разів, оптимізуючи код і перетворюючи його в більш економічні форми, які вимагають менше газу для виконання.
Компілятори можна налаштувати для досягнення відповідного компромісу між витратами на розгортання та виконання. Визначення приблизної кількості контрактів, які потрібно виконати, за допомогою команд виконання:
- •Більш високі рівні є оптимальними з точки зору низьких цін на газ при виконанні контракту
- •Менша кількість чисел є оптимальною з точки зору зниження витрат газу при розгортанні контракту
Оптимізуйте прапорці та значення run=200, щоб компілятори оптимізували код для економії газу під час 200-кратного виконання функцій incrementCount. Класифікуйте такі налаштування відповідно до унікальних вимог програми.
Бонус Оптимізація Solidity Gas: Асемблер
При написанні смарт-контрактів у Solidity вони компілюються в байт-коди, послідовність операційних кодів EVM. За допомогою асемблера ви можете просто писати код, що виконується на рівнях, більш сумісних з операційними кодами, а в деяких випадках можливість ручної оптимізації операційних кодів дає йому перевагу над байт-кодом Solidity.
Хоча писати код на такому низькому рівні не найпростіше завдання, воно має перевагу в тому, що дозволяє оптимізувати операційні коди вручну, і тому загалом є кращим порівняно з байт-кодом Solidity. Такий рівень оптимізації забезпечує більшу ефективність і результативність виконання контракту.
Навіть у простих випадках, коли дві функції повинні додавати два числа, прості версії Solidity та Assembly матимуть деякі відмінності, але версії Assembly є дешевшими.
Реалізація проектів на L2, таких як Base, означатиме, що користувачі матимуть менші витрати на використання протоколів, але обов'язком розробників є забезпечення впровадження технік оптимізації газу. Ці підказки можуть значно заощадити витрати на транзакції, додати масштабованість та підвищити ефективність контракту в цілому.
Assembly може використовуватися для оптимізації виконання смарт-контрактів за допомогою газу, але в деяких випадках версії Assembly можуть призвести до створення небезпечного коду. Існують обґрунтовані пропозиції залучити експертів з безпеки смарт-контрактів до перегляду контрактів перед їх розгортанням.


