Уменьшение размера контрактов для борьбы с ограничением их размера
Почему существует ограничение?
22 ноября 2016 г. в хардфорке Spurious Dragon (opens in a new tab) было введено EIP-170 (opens in a new tab), которое добавило ограничение на размер смарт-контракта в 24,576 кб. Для вас как для разработчика на Solidity это означает, что по мере добавления все большей функциональности в ваш контракт в какой-то момент вы достигнете предела и при развертывании увидите ошибку:
Предупреждение: Размер кода контракта превышает 24576 байт (ограничение, введенное в Spurious Dragon). Этот контракт может оказаться невозможно развернуть в основной сети. Рассмотрите возможность включения оптимизатора (с низким значением \"runs\"!), отключения строк отката или использования библиотек.
Это ограничение было введено для предотвращения атак типа «отказ в обслуживании» (DoS-атак). Любой вызов контракта относительно дешев с точки зрения затрат газа. Однако влияние вызова контракта на узлы Ethereum непропорционально возрастает в зависимости от размера кода вызываемого контракта (чтение кода с диска, предварительная обработка кода, добавление данных в доказательство Меркла). Всякий раз, когда возникает ситуация, в которой атакующему требуется мало ресурсов, чтобы заставить других проделать большую работу, появляется возможность для 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и просто встроить код, если функция вызывается только один раз.
Избегайте дополнительных переменных
1function get(uint id) returns (address,address) {2 MyStruct memory myStruct = myStructs[id];3 return (myStruct.addr1, myStruct.addr2);4}1function get(uint id) returns (address,address) {2 return (myStructs[id].addr1, myStructs[id].addr2);3}Такое простое изменение дает разницу в 0,28 кб. Скорее всего, вы сможете найти много подобных ситуаций в ваших контрактах, и в сумме они могут дать значительную экономию.
Сократите сообщения об ошибках
Длинные сообщения отката и, в частности, множество различных сообщений отката могут раздувать контракт. Вместо этого используйте короткие коды ошибок и декодируйте их в своем клиенте. Длинное сообщение может стать намного короче:
1require(msg.sender == owner, \"Только владелец этого контракта может вызывать эту функцию\");1require(msg.sender == owner, \"OW1\");Используйте пользовательские ошибки вместо сообщений об ошибках
Пользовательские ошибки были введены в Solidity 0.8.4 (opens in a new tab). Это отличный способ уменьшить размер ваших контрактов, потому что они кодируются в ABI как селекторы (так же, как и функции).
1error Unauthorized();23if (msg.sender != owner) {4 revert Unauthorized();5}Рассмотрите возможность использования низкого значения runs в оптимизаторе
Вы также можете изменить настройки оптимизатора. Значение по умолчанию, равное 200, означает, что он пытается оптимизировать байткод так, как если бы функция вызывалась 200 раз. Если вы измените его на 1, вы, по сути, даете команду оптимизатору оптимизировать код для случая, когда каждая функция выполняется только один раз. Оптимизированная для однократного выполнения функция означает, что она оптимизирована для самого развертывания. Имейте в виду, что это увеличивает стоимость газа за выполнение функций, поэтому, возможно, вы не захотите этого делать.
Небольшое влияние
Избегайте передачи структур в функции
Если вы используете ABIEncoderV2 (opens in a new tab), может помочь отказ от передачи структур в функции. Вместо того чтобы передавать параметр в виде структуры, передавайте необходимые параметры напрямую. В этом примере мы сэкономили еще 0,1 кб.
1function get(uint id) returns (address,address) {2 return _get(myStruct);3}45function _get(MyStruct memory myStruct) private view returns(address,address) {6 return (myStruct.addr1, myStruct.addr2);7}1function get(uint id) returns(address,address) {2 return _get(myStructs[id].addr1, myStructs[id].addr2);3}45function _get(address addr1, address addr2) private view returns(address,address) {6 return (addr1, addr2);7}Объявляйте правильную область видимости для функций и переменных
- Функции или переменные, которые вызываются только извне? Объявляйте их как
external, а неpublic. - Функции или переменные, вызываемые только из самого контракта? Объявляйте их как
privateилиinternalвместоpublic.
Удаление модификаторов
Модификаторы, особенно при интенсивном использовании, могут оказывать значительное влияние на размер контракта. Рассмотрите возможность их удаления и использования вместо них функций.
1modifier checkStuff() {}23function doSomething() checkStuff {}1function checkStuff() private {}23function doSomething() { checkStuff(); }Эти советы должны помочь вам значительно уменьшить размер контракта. Еще раз, не могу не подчеркнуть: всегда стремитесь к разделению контрактов, если это возможно, для достижения наибольшего эффекта.
Последнее обновление страницы: 25 февраля 2026 г.