Yul и Yul+: низкоуровневый язык для оптимизации смарт-контрактов под EVM

Yul — это низкоуровневый промежуточный язык (IR) для EVM, который используется компилятором Solidity и подходит для «ручной» оптимизации байткода. Он ближе к ассемблеру, чем высокоуровневые языки (Solidity, Vyper), но при этом более структурирован и удобен для анализа, чем старый inline assembly.

Yul+ — расширение Yul с дополнительными конструкциями (структуры, модификаторы, сахар для циклов и т.п.), которое используют отдельные фреймворки и продвинутые команды.

Yul и Yul+: низкоуровневый язык для оптимизации смарт-контрактов под EVM

Что такое Yul

Yul изначально задумывался как:

  • единый промежуточный язык для разных бэкендов (EVM, eWASM и т.д.);
  • «чистый» ассемблер с минимальным набором конструкций;
  • формат, который удобно оптимизировать и анализировать, прежде чем превратить его в байткод.

Важные черты:

  • низкоуровневый доступ к опкодам EVM;
  • простая структура: блоки кода, функции, переменные через let;
  • минимум синтаксического «сахара» и скрытой магии;
  • удобство для оптимизатора Solidity: многие оптимизации делаются уже на уровне Yul, а не на уровне исходного Solidity-кода.

Yul против inline assembly в Solidity

До появления Yul разработчики часто использовали встроенный блок:

assembly {
    // низкоуровневый код
}

Это legacy-assembly: выразительный, но трудный для анализа и оптимизации.

Yul решает несколько проблем:

  • Более строгая структура

Код Yul представлен в виде имён функций, блоков и выражений. Это даёт возможность компилятору:

  • лучше строить граф потока управления (CFG);
  • делать дэдкод-элиминацию, инлайнинг и другие оптимизации.
  • Одна модель для разных целей

IR-уровень позволяет:

  • сначала скомпилировать Solidity в Yul;
  • прогнать оптимизации;
  • уже потом получить финальный байткод.
  • Единый язык для «ручного» и автоматического кода

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

При этом в Solidity до сих пор можно встретить оба варианта: «старый» assembly { ... } и новый синтаксис с блоком Yul. Современная практика — по возможности использовать именно Yul-стиль.

Основные элементы языка Yul

Yul остаётся языком низкого уровня, но в нём есть базовые высокоуровневые конструкции:

  • Переменные и let

Переменные объявляются через let, например:

  ```text
  let x := add(1, 2)
  ```

Значения — это 256-битные слова, с которыми работает EVM.

  • Функции

Можно объявлять функции, которые компилятор потом развернёт или оставит как подпрограммы:

  ```text
  function addTwo(a, b) -> c {
      c := add(a, b)
  }
  ```
  • Условные конструкции

Простые if и switch без сложной логики типов:

  ```text
  if iszero(x) { 
      // ...
  }
  switch opcode
  case 0 { /* ... */ }
  default { /* ... */ }
  ```
  • Циклы

Есть конструкция for { init } condition { step } { body }, но без «бесконечных» динамических паттернов, опасных с точки зрения газа.

  • Вызов встроенных примитивов

Для работы с памятью, storage и окружением используются встроенные функции-обёртки над опкодами:

  • mload, mstore, sload, sstore;
  • call, delegatecall, staticcall;
  • calldataload, codesize, gas и т.п.

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

Где используется Yul на практике

  • Внутри компилятора Solidity

При включённых оптимизациях (--optimize) компилятор часто:

  • преобразует часть Solidity-кода в Yul IR;
  • прогоняет оптимизации (упрощение выражений, удаление мёртвого кода, инлайнинг функций, оптимизацию работы с памятью и storage);
  • генерирует байткод.

Разработчик может этого не замечать: Yul работает «за кулисами».

  • Ручная оптимизация газа

Продвинутые команды иногда пишут отдельные модули напрямую на Yul:

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

Низкоуровневые компоненты (мосты, precompile-обёртки, кастомные маршрутизаторы) могут выигрывать от полного контроля над байткодом.

  • Yul+ и фреймворки

Yul+ добавляет к базовому Yul более удобный синтаксис и конструкции:

  • структуры;
  • дополнительные циклы;
  • более удобную работу с массивами и storage.

Это не «официальный» стандарт протокола, а практика отдельных проектов и тулчейнов, но влияние Yul+ заметно в экосистеме оптимизированных контрактов.

Преимущества Yul и когда он реально нужен

Использовать Yul имеет смысл, когда:

  • важна максимальная экономия газа

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

  • нужен полный контроль над байткодом

Solidity иногда генерирует обвязку и дополнительные проверки. На Yul можно:

  • точно контролировать последовательность опкодов;
  • управлять layout’ом памяти и storage;
  • реализовать нетипичные паттерны, которые трудно выразить на высокоуровневом языке.
  • строятся продвинутые low-level-конструкции

Например:

  • собственные прокси-шаблоны;
  • кастомные роутеры вызовов;
  • bridge-логика, тесно работающая с calldata, returndata и внешними вызовами.

Для «обычных» контрактов (ERC-20, NFT, простые DAO) типов «как по учебнику» чаще всего достаточно Solidity: выигрыш от Yul не окупит рост сложности.

Риски и сложности при работе с Yul

Yul даёт большую мощь, но требует высокой дисциплины:

  • Вы теряете часть защитных механизмов Solidity

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

  • Сложнее отладка

Отлаживать Yul-код приходится:

  • по трассам исполнения (traces);
  • через анализ опкодов и переменных в дебаггерах;
  • по логам и событиям.

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

  • Выше порог входа для команды и аудиторов

Не каждый Solidity-разработчик комфортно чувствует себя на Yul, а аудит такого кода требует отдельной экспертизы. Ошибки могут быть менее очевидны, чем в привычном Solidity.

  • Меньше документации и примеров

Вокруг Solidity уже сформировался огромный массив гайдов, статей и шаблонов. По Yul и Yul+ информации меньше, многие паттерны живут только в коде конкретных проектов.

Поэтому в большинстве случаев подход таков: сначала пишут и отлаживают протокол на Solidity, а потом *точечно* переписывают самые горячие участки в Yul, если профилирование показывает, что это оправдано.

См. также

Task Runner