Перейти к основному контенту

Уменьшение размера контрактов для обхода лимита на размер контракта

Solidity
смарт-контракты
хранение
Средний уровень
Маркус Ваас
26 июня 2020 г.
5 минут на чтение

Почему существует лимит?

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 кб. Скорее всего, вы сможете найти много подобных ситуаций в ваших контрактах, и в сумме они могут дать значительный результат.

Сокращайте сообщения об ошибках

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

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), может помочь отказ от передачи структур в функцию. Вместо передачи параметра в виде структуры передавайте необходимые параметры напрямую. В этом примере мы сэкономили еще 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(); }

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