Приветствую, кодеры Solidity!

Если вы здесь, то или у вас есть смарт-контракт, который готов к "похудению", или вы просто пытаетесь нарастить свои мышцы в области оптимизации Solidity. Как бы то ни было, сегодня я предлагаю вам навес золота в виде 27 проверенных методов оптимизации. Это ваш новый тренажерный зал для мозга! Всегда приятно иметь на руках свежий чек-лист перед запуском нового проекта.

Прежде всего, позвольте мне представиться. Я Арарат, кодирую на Solidity с 2017 года, принял участие в свыше 100 блокчейн-проектах и не раз был в топ-27 разработчиков Solidity на Upwork. Итак, давайте начнем!

1. Погружайся глубоко в изучение Solidity

Solidity - это относительно простой язык программирования. В отличие от таких языков как C/C++, Java, Python, где для достижения уровня эксперта может потребоваться более 30 лет, в случае с Solidity вы можете достичь этого уровня за пару лет. Настоятельно рекомендую полностью изучить официальную документацию, и после поэкспериментировать с интерактивный справочником по опкодам на сайте evm.codes.

Вполне верно, важно осознать суть того, с чем мы работаем. Эту мысль ярко иллюстрирует известное высказывание Авраама Линкольна:

Дайте мне шесть часов для того, чтобы срубить дерево, и я потрачу первые четыре часа на то, чтобы заточить топор.

2. Изучайте алгоритмы

Важность знания и понимания различных алгоритмов не может быть переоценена, особенно при их использовании в смарт-контрактах. Рассмотрим, например, задачу сортировки массива данных:

Как вы можете наиболее эффективно и экономно обработать такую задачу на Solidity? Здесь знание алгоритмов придет на помощь, позволяя вам выбрать наиболее подходящий метод сортировки.

В контексте данного примера, одним из возможных решений может быть применение алгоритма сортировки "Merge". Если вам не знаком этот алгоритм, вы можете ознакомиться с ним и с другими алгоритмами по ссылке.

3. Организуйте код с учетом приоритетов проверки

Приоритетная проверка — это стратегия, применяемая при использовании операций || или &&. Данный подход подразумевает выполнение наименее затратной операции в первую очередь, что позволяет пропустить более ресурсоемкую операцию, если результат первой оказывается истинным (true).

Аналогичный принцип применим и к использованию конструкции switch։

4. Расположите переменные рационально

Хранилище Ethereum разделено на слоты размером 32 байта, и запись значений в эти слоты может быть дорогой процедурой (до 20 000 газа при использовании "холодной" записи).

Рассмотрим ситуацию, когда у вас есть 3 переменные.

Если вы правильно распределите эти переменные по трём слотам, то сможете экономить один слот (например, переменные a и c могут быть размещены в одном слоте).

В результате, при развертывании используется на один слот меньше, что позволяет экономить 20 000 газа.

Для более подробной информации рекомендую ознакомиться по следующей ссылке.

5. Примените public и external функции с пониманием

Ваши функции могут быть обозначены как public или external, в зависимости от того, как вы планируете их использовать. Обе функции могут быть вызваны извне вашего контракта (например, с помощью кошелька MetaMask). Однако в отличие от external функций, public функции также могут быть вызваны другими функциями внутри вашего контракта.

Рекомендуется начинать с определения функций как external, когда это возможно, а затем переходить на public в случаях, когда это необходимо для архитектуры и логики вашего контракта. Однако при этом всегда следует тщательно оценивать возможные последствия для безопасности и архитектуры.

6. Используйте require для проверок  входных данных и внешних вызовов вместо assert

Assert следует использовать для проверки условий, которые никогда не должны быть ложными в корректно работающем контракте. Использование assert подразумевает, что в случае сбоя в работе контракта все потраченные на его выполнение средства (газ) не возвращаются, что служит защитой от ошибок в коде. Однако, для проверки входных данных и внешних вызовов контракта рекомендуется использовать конструкцию require, поскольку при её использовании в случае неудачи потраченный газ возвращается.

7. Избегайте инициализации переменных значениями по умолчанию

В Solidity, если переменная не установлена или инициализирована, она действительно имеет значение по умолчанию - это может быть 0 для чисел, false для булевых значений и 0x0 для адресов.

8. Используйте компактные сообщении revert и require

Добавление строки причины ошибки к оператору require является часто используемым подходом, однако следует помнить, что эти строки занимают пространство в развернутом байткоде. Из-за системы хранения данных Ethereum, любая строка причины ошибки будет занимать кратное число 32 байт. Поэтому, если ваша строка не соответствует длине 32 байта, она будет использовать больше пространства. Чтобы оптимизировать использование пространства, рекомендуется использовать короткие и ясные строки ошибок, которые соответствуют требованию в 32 байта.

Если строка причины ошибки не требуется для отладки или операционного использования, можно думать о ее удаление для оптимизации кода.

9. Удалите мертвый код

Иногда мы оставляем в коде ненужные части, которые никак не влияют на функциональность контракта:

Бесполезные или "мертвые" строки кода не оказывают влияние на поведение смарт-контракта. Однако, несмотря на это, они всё равно занимают место в байткоде и увеличивают затраты на газ при развертывании и взаимодействии с контрактом.

10. Используйте выражения (a, b) = (b, a) для обмена значений двух переменных

11. Используйте операций пакетирования

Осуществление транзакции на блокчейне требует затрат газа (21000 газа за простую передачу эфира). Если применимо, предоставьте пользователям метод пакетного действия, снижающий общие расходы на газ.

12. Удалите хэш метаданных

В байт-коде смарт-контракта Solidity встроен специфический "хэш" - Swarm-хэш метаданных контракта, включающий ABI, исходный код и информацию о компиляторе.

Учитывая что размер этого хэша составляет 32 байта, его потенциальное отсутствие может сократить затраты на газ. Тем не менее, ручное удаление его достаточно трудоемко.

Стоит обратить внимание, что хотя эта встроенная информация не влияет на затраты газа во время выполнения смарт-контракта, она незначительно увеличивает затраты на газ при развертывании контракта, так как требует записи данных в блокчейн. В runtime же, метаданные хэш не используются, и следовательно, не влияют на стоимость газа при выполнении функций контракта.

Если вам прям очень надо выжать все соки от контракта, рекомендую ознакомится с этим решением тут.

13. Использование операции сдвига влево вместо умножения для увеличения производительности

Сдвиг двоичных данных n раз влево действительно приводит к умножению данных на 2^n. Однако важно заметить, что Solidity автоматически обрабатывает ситуации переполнения (overflow) для всех математических операций, включая операторы сдвига. Тем не менее, следует всегда проявлять осмотрительность при использовании этих операторов, особенно если используемые данные могут превышать размеры типа, с которым они сравниваются.

  1. Cold access и warm access

Если вам необходимо несколько раз обращаться к одной и той же переменной хранения, рассмотрите возможность первоначального чтения этой переменной в память. Это сэкономит газ, поскольку операции чтения и записи в памяти обходятся дешевле, чем аналогичные операции с данными хранения.

15. Используйте ++i вместо i++

На первый взгляд, эта оптимизация может показаться неочевидной, но в контексте Solidity и Ethereum это важно. Дело в том, что i++ создает временную переменную, чтобы хранить старое значение i, что требует дополнительной операции записи в память. Это приводит к увеличению затрат на газ. В свою очередь, ++i просто увеличивает значение, не создавая временной переменной, что экономит газ.

Однако следует помнить, что подобные микрооптимизации, хотя и могут сэкономить некоторое количество газа, обычно не дают значительного улучшения производительности в большом масштабе. Вместо этого фокусируйтесь на более масштабных оптимизациях, таких как минимизация транзакций и эффективное использование памяти.

16. Используйте uint256 вместо uintXYZ

Как я указал выше, данные на хранилище EVM управляются в слотах, каждый из которых имеет размер 32 байта (или 256 бит). Меньшие типы, такие как uint8, фактически занимают полный 256-битный слот, и не могут сэкономить место в хранилище. Однако, когда они используются, EVM должна выполнять дополнительные операции для обеспечения преобразования.

17. Используйте команду selfdestruct() для компенсации газа

Использование функции selfdestruct снижает стоимость транзакции на 24 000 газа, но не более половины стоимости газа транзакции. Это означает, что использование функции selfdestruct имеет смысл только в редких случаях, когда нужно выполнять сложные транзакции и необходимо увеличить запас газа.

Важно отметить, что использование функции selfdestruct должно быть обдуманным и осторожным, так как она удаляет контракт без возможности восстановления. Поэтому ее следует использовать только в тех случаях, когда это действительно необходимо.

18. Оптимизируйте газ, убирая излишние переменные

Ethereum предоставляет возврат газа при удалении переменных. Это стимул для экономии места в блокчейне, который мы используем для снижения стоимости газа наших транзакций. Удаление переменной возвращает 15 000 газа, но не более половины стоимости газа транзакции. Таким образом, удаление переменных может помочь сэкономить газ, но только в рамках текущей транзакции.

19. Совершайте меньше внешних вызовов

Взаимодействие с другими контрактами в сети Ethereum может потребовать значительных затрат газа, поэтому его следует использовать только тогда, когда это действительно необходимо и нет других вариантов.

20. Используйте статические типы фиксированного размера

Статические типы фиксированного размера (например, bool, uint256, bytes5) более эффективны с точки зрения газа, чем динамические типы переменного размера (например, string или bytes).

21. Используйте блок unchecked при выполнение арифметических операций

Начиная с версии Solidity 0.8.0, все арифметические операции по умолчанию отменяются при переполнении и недополнении, что делает использование таких библиотек, как SafeMath, ненужным. Это улучшает безопасность и читаемость кода, даже если это приводит к небольшому увеличению стоимости газа.

Однако, если вы хотите использовать предыдущее поведение, вы можете использовать блок unchecked {...}, чтобы отключить проверки на переполнение и недополнение. Это может иметь смысл в тех случаях, когда вы выполняете много арифметических операций в одном вызове функции (например, в циклах for) и хотите сэкономить на газе.

22. Храните данные в памяти memory вместо storage

Выбор идеального места для размещения данных действительно очень важен. В Solidity есть три места для хранения данных: storage, memory и calldata.

storage - переменная хранится на блокчейне. Это переменная с постоянным состоянием. Для ее определения и изменения требуется газ.

memory - временная переменная, объявленная внутри функции. Не требует газа для объявления, но для изменения переменной памяти требуется газ (меньше, чем для хранения).

calldata - как память, но неизменяемая и доступна только как аргумент внешних функций¹.

Также важно отметить, что если не указано местоположение данных, то по умолчанию это storage.

23. Используйте calldata вместо memoryдля параметров функции

В приведенном выше в 1-ом примере динамический массив arr имеет место хранения memory. Когда функция вызывается извне, значения массива хранятся в calldata и копируются в память во время декодирования ABI (с помощью опкодов calldataload и mstore). А во время цикла forarr[i] получает доступ к значению в памяти с помощью mload. Однако - это неэффективно. Чтобы улучшить эффективность, можно использовать calldataкак на 2-ом примере.

24. Используйте массивы с фиксированным размером вместо динамических

Используйте массивы фиксированного размера, когда известен максимальный размер, но учитывайте, что они требуют предварительного выделения памяти под весь размер массива. В тех случаях, когда требуется большая гибкость или экономия газа, рассмотрите использование динамических массивов.

25. Используйте индексированные событие

Вы можете отметить каждое событие как индексированное, как показано ниже:

Индексированное событие так же позволяет легче искать события.

26. Помните,mapping дешевлеarray

Mapping обычно менее дороги, чем массивы, но вы не можете выполнять над ними итерации. получить длину (.length), добавить элемент в конце (.push()) или удалить (.pop())

27. Используйте Solidity Gas Optimizer

Включайте оптимизатор Solidity для снижения затрат на газ. Однако, настройка значения итераций оптимизатора зависит от ваших целей. Для минимизации затрат на развертывание контракта установите значение на низкое, тогда как для оптимизации расходов на газ во время выполнения (то есть при вызове функций контракта) выберите более высокое значение.

Для настройки оптимизатора Solidity в Hardhat вы можете использовать следующий конфигурационный файл:

Значение runs указывает на количество итераций, которое оптимизатор Solidity будет использовать. Большее значение runs подойдет, если вы ожидаете частые вызовы функций в контракте, в то время как меньшее значение оптимально для случаев, когда важнее минимизировать затраты на газ при развертывании.

Итак, мы рассмотрели 27 проверенных методов оптимизации. Еще больше про Solidity я рассказываю в рамках нового курса Solidity Developer. 13 июля я проведу полностью бесплатный урок на платформе OTUS. Поговорим про введение в смарт-контракты, а именно погрузимся в увлекательную историю смарт-контрактов, иллюстрируя их зарождение и развитие до сегодняшнего дня. Рассмотрим области применения смарт-контрактов, представив реальные примеры их использования в различных сферах, перейдем от теории к практике, создав и задеплоив наш первый смарт-контракт с помощью онлайн-инструмента Remix IDE. Завершим урок обсуждением известных случаев взлома смарт-контрактов, рассмотрим основные уязвимости и способы их устранения. Жду всех.

Регистрируйтесь и приходите. Будет много интересного

Комментарии (9)


  1. Kiel
    04.07.2023 21:07
    +2

    Никогда не используйте операции побитового смещения, если на кону не стоит ваша жизнь. 13 совет абсолютно мимо, пожалуйста, умоляю, никогда так не делайте.

    Как С++ разраб со стажем 3+ лет, скажу, что любят обсуждать ++i или i++, но, на самом деле, вообще всё равно. Компилятор всё перепишет, а вы и не докажете. Лучше забыть о for как о страшном сне и применять его только когда на кону стоит ваша жизнь (foreach наше всё, или for с обходом по ссылкам в плюсах)

    В плюсах, да и шарпе, можно для всего проекта включить checked/unchecked если уж так хочется, но писать это прямо в коде считается варварством

    Скорее всего эти пункты были притянуты для количеством, остальные вроде бы по делу, как тот про "динамические массивы вместе статических" - вот где ад.


    1. tonoyandev Автор
      04.07.2023 21:07
      +1

      Спасибо за комментарий, вот корректировки:

      1. Побитовые операторы сдвига экономят 150 газа, их можно использовать без ограничений.

      2. i++ требует больше газа, чем ++i. Разница незначительна, но есть. Проверить это можно в Remix IDE.

      3. В Solidity доступен только цикл for и while. Они используется практически везде. Главное - контролировать расход газа, чтобы не быть прерванным из-за избытка операций.

      4. Оператор unchecked используется, например, в коде Uniswap V4. Не уверен, где вы увидели в этом варварство. Этот метод экономит газ.

      5. К сожалению, массивы в Solidity работают не так, как в других языках. Есть много ограничений, к которым нужно привыкнуть.


      1. Kiel
        04.07.2023 21:07

        Да, спасибо за развёрнутый ответ. Всё же все эти вещи должен решать компилятор, видимо, пока не решает. Очень грустно. Побитовое смещение сильно портит читабельность и культуру кода (философию скажем). Через неделю вы забудете что написали, или джун придёт или еще что то - все будут сидеть "что за побитовый сдвиг...". Зачем он нужен... Зачем... Почему. Вы же не пишете там в комментах "это для экономии 150 газа". В общем, если с остальным совсем никак не справиться и я понимаю "молодой язык", то вот здесь просто не надо.

        Мне кстати нравится это выражение "газ" ) Какой то кальянный вайб сразу ) Но, честно, не знаю что это за газ, только догадываюсь - в плюсах мы квантами называем неопределенные количества времени для обработки одной операции у процессора, а тут газ, вот теперь чувствую себя старым, а мне блин всего лишь 32...


  1. jobber_man
    04.07.2023 21:07
    +3

    Как старпер с четвертьвековым стажем скажу: никогда не стоит экономить на газе, рано или поздно вы эпично обосретесь.


    1. tonoyandev Автор
      04.07.2023 21:07
      +2

      Если не экономить на газе, то кошелек обосрется еще эпичнее!


  1. Helltraitor
    04.07.2023 21:07

    Solidity - это относительно простой язык программирования. В отличие от таких языков как C/C++, Java, Python, где для достижения уровня эксперта может потребоваться более 30 лет, в случае с Solidity вы можете достичь этого уровня за пару лет

    И вдруг что-то пошло не так (30 советов)

    Почему не использовать Rust? Он, к моему сожалению, довольно популярен для смарт контрактов, быстрый и безопасный (нет, это не значит, что другие языки опасные).

    По моему мнению, он не такой сложный как C/C++ и при этом позволяет развязать сеье руки во многих задачах за счет различных дешевых абстракций.

    Не варюсь в теме блокчейн, но, надеюсь, ничего не напутал


    1. tonoyandev Автор
      04.07.2023 21:07

      Спасибо за вопрос!

      Rust действительно отличный язык программирования, который становится все более популярным в блокчейн-платформах, таких как Polkadot и Solana. Однако в мире Ethereum "королем" остается Solidity.


      Ниже приведу несколько примеров, почему solidity относительно доступный и легкий язык:

      1. Синтаксис: Solidity имеет чистый, прямолинейный синтаксис, схожий с JavaScript, что делает его более доступным для новичков.

      2. Специализация: Solidity специально разработан для смарт-контрактов. Это уменьшает сложность языка, поскольку он не пытается быть универсальным решением для всех задач, в отличие от C++ или JS.

      3. Инструменты: Solidity имеет узкий набор определенных инструментов для работы со смарт-контрактами, что упрощает изучение.

      4. Типизация: Solidity имеет статическую типизацию, что делает код более предсказуемым и безопасным.

      5. Асинхронность: Solidity синхронен, что упрощает понимание потока выполнения кода, в отличие от асинхронного JS.

      6. Параллелизм: Solidity не поддерживает многопоточность или параллелизм, что упрощает логику кода.


  1. decomeron
    04.07.2023 21:07

    Что нужно знать перед тем как начать изучать Soligity?


    1. tonoyandev Автор
      04.07.2023 21:07

      Спасибо за вопрос!

      Знание основ любого языка программированияв и ООП