Как написать оптимизированный (затратный на газ) смарт-контракт?

Как мы все знаем, есть много факторов, которые определяют хороший смарт-контракт, например:

  • Безопасность : он имеет минимальную/нулевую уязвимость, поэтому противник не может им воспользоваться. Иммунитет к атакам.

  • Стоимость : сколько всего

      (a) smart contract deployment costs, 
    
      (b) running/invoking each function of it costs. 
    
  • Правильность : выполняется так, как было запланировано.


В этом вопросе я хотел бы сосредоточиться на стоимости смарт-контракта.

Вопрос 1: Как сделать рентабельный смарт-контракт?

Вопрос 2: Как не использовать некоторые дорогостоящие функции и какие есть альтернативы? Другими словами, есть ли известные функции, которые дороже других операций, и можем ли мы заменить их более качественными?

Вопрос 3: В целом, какова хорошая практика написания смарт-контракта с минимальными затратами?

Вкратце: Как мы можем сэкономить газ : эфир : деньги?

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

Ответы (3)

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

Operation         Gas           Description

ADD/SUB           3             Arithmetic operation
MUL/DIV           5             Arithmetic operation
ADDMOD/MULMOD     8             Arithmetic operation
AND/OR/XOR        3             Bitwise logic operation
LT/GT/SLT/SGT/EQ  3             Comparison operation
POP               2             Stack operation 
PUSH/DUP/SWAP     3             Stack operation
MLOAD/MSTORE      3             Memory operation
JUMP              8             Unconditional jump
JUMPI             10            Conditional jump
SLOAD             200           Storage operation
SSTORE            5,000/20,000  Storage operation
BALANCE           400           Get balance of an account
CREATE            32,000        Create a new account using CREATE
CALL              25,000        Create a new account using CALL

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

Сокращение расхода газа по контракту важно в двух ситуациях:

  1. Стоимость развертывания контракта
  2. Стоимость вызова контрактных функций

Стоимость развертывания контракта

Для этого большая часть оптимизации выполняется во время компиляции, как описано в документации-faqs ,

Включены ли комментарии в развернутые контракты и увеличиваются ли они при развертывании?

Нет, все, что не нужно для выполнения, удаляется при компиляции. Сюда входят, среди прочего, комментарии, имена переменных и имена типов.

А подробности об оптимизаторе можно посмотреть здесь .

Другой способ уменьшить размер — удалить бесполезный код. Например:

1 function p1 ( uint x ){ 
2    if ( x > 5)
3     if ( x*x < 20)
4        XXX }

В приведенном выше коде строки 3 и 4 никогда не будут выполняться, и этого типа бесполезного кода можно избежать, тщательно просматривая логику контракта, что уменьшит размер смарт-контракта.

Стоимость вызова контрактных функций

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

  1. Сокращение дорогостоящих операций

Дорогие операции — это коды операций, которые имеют больше значений газа, таких как SSTORE. Ниже приведены некоторые методы сокращения дорогостоящих операций.

A) Использование правил короткого замыкания

Операторы || и && применяют общие правила короткого замыкания. Это означает, что в выражении f(x) || g(y), если f(x) оценивается как true, g(y) не будет оцениваться, даже если это может иметь побочные эффекты.

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

Если f(x) недорогая, а g(y) дорогая, организация логических операций

  • ИЛИ ЖЕ :f(x) || g(y)
  • А ТАКЖЕ :f(x) && g(y)

сэкономит больше газа в случае короткого замыкания.

If f(x)имеет значительно более высокую вероятность возврата false по сравнению с g(y), организация операций AND f(x) && g(y)может привести к экономии большего количества газа при выполнении за счет короткого замыкания.

Если f(x)имеет значительно более высокую вероятность возврата true по сравнению с g(y), организация операций ИЛИ f(x) || g(y)может привести к экономии большего количества газа при выполнении за счет короткого замыкания.

Б) дорогие операции в цикле

например:

 uint sum = 0;
 function p3 ( uint x ){
     for ( uint i = 0 ; i < x ; i++)
         sum += i; }

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

 uint sum = 0;
 function p3 ( uint x ){
     uint temp = 0;
     for ( uint i = 0 ; i < x ; i++)
         temp += i; }
     sum += temp;
  1. Другие узоры, связанные с циклом

петля, объединяющая

function p5 ( uint x ){
    uint m = 0;
    uint v = 0;
    for ( uint i = 0 ; i < x ; i++) //loop-1
        m += i;
    for ( uint j = 0 ; j < x ; j++) //loop-2
        v -= j; }

петлю-1 и петлю-2 можно комбинировать и экономить газ,

 function p5 ( uint x ){
    uint m = 0;
    uint v = 0;
    for ( uint i = 0 ; i < x ; i++) //loop-1
       m += i;
       v -= j; }

и еще несколько шаблонов петель можно найти здесь .

  1. Использование массивов байтов фиксированного размера

Из Документов ,

Можно использовать массив байтов как byte[], но это тратит много места, 31 байт на каждый элемент, если быть точным, при передаче вызовов. Лучше использовать байты.

а также

Как правило, используйте байты для необработанных байтовых данных произвольной длины и строку для строковых данных произвольной длины (UTF-8). Если вы можете ограничить длину определенным количеством байтов, всегда используйте один из байтов от 1 до байт32, потому что они намного дешевле.

наличие фиксированной длины всегда экономит газ. Обратитесь и к этому вопросу .

  1. Удаление бесполезного кода, как объяснялось ранее при развертывании по контракту, сэкономит газ даже при выполнении функций, если это можно сделать внутри функций.

  2. Неиспользование библиотек при реализации функциональности дешевле для простых применений.

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

  1. Использование видимости externalдля функций, доступ к которым осуществляется только извне, заставляет использовать их calldataв качестве местоположения параметра, и это экономит газ при выполнении функции.

  2. Использование memoryпеременных внутри функций локально, когда это возможно, экономит газ при доступе к файлу storage.

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

В соответствии с 1-A, если g() является дорогостоящим, он должен быть справа в обоих примерах. LHS всегда будет работать, несмотря ни на что. Единственная причина, по которой его следует переместить в левое положение &&, заключается в том, что g() с большей вероятностью вернет false, что с большей вероятностью приведет к короткому замыканию &&, но не ||.
да, спасибо за указание на ошибку g(y) должно быть справа :)

Ваш код Solidity будет скомпилирован в байт-код, состоящий из инструкций виртуальной машины Ethereum (EVM). Стоимость газа вызова функции представляет собой сумму стоимости газа всех выполненных инструкций EVM. Удобную для чтения таблицу точной стоимости газа для каждой из этих инструкций можно найти здесь:

Если вам нужны все подробности, вы должны взглянуть на приложение H желтой бумаги:

Как видите, инструкции вроде ADDи MULстоят очень дешево, только на 3газу 5.

Инструкции вроде BALANCEи CREATEстоят намного дороже у 400и 32000газ.

Чтение из памяти стоит 200за SLOADинструкцию, а запись в память стоит 5000газа, но чтение из памяти и запись в память стоит только 3газа.

Внутренние транзакции (отправка эфира) будут стоить вам 21000каждый раз.

Исходя из этого, вот несколько советов по снижению стоимости газа:

  • Записывайте в хранилище только после завершения вычислений, используйте локальные переменные для запоминания вашего прогресса (если применимо).

  • Избегайте слишком большого количества операций записи и чтения из хранилища

  • Избегайте создания слишком большого количества новых контрактов

  • Избегайте слишком большого чтения глобального состояния (например, получения баланса адреса).

  • Избегайте слишком большого количества транзакций

  • Используйте bytes32вместо stringдля небольших строк

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

Я считаю, что написание функций в смарт-контракте с использованием встроенных функций сборки делает его более экономичным, чем любые изменения, которые вы пытаетесь внести в свой контракт. простая функция цикла for в Solidity, принимающая x количество wei. где та же функция, написанная на встроенной сборке Solidity, требует всего 0,7 (x) количества wei.

Интересно! Знаете ли вы какие-либо хорошие ресурсы для этого? Спасибо! 🙏