Function selector в Ethereum: что такое 4-байтовый селектор функций

Function selector — это 4-байтовый идентификатор функции смарт-контракта в Ethereum. Он стоит в самом начале calldata и говорит EVM, какой именно метод контракта нужно вызвать и как интерпретировать последующие байты.

Selector:

  • рассчитывается из текстовой сигнатуры вида "transfer(address,uint256)" с помощью Keccak-256;
  • состоит из первых 4 байт (32 бит) хеша;
  • используется для маршрутизации вызовов в смарт-контракте и привязан к ABI-описанию — см. ABI.

Function selector в Ethereum: что такое 4-байтовый селектор функций

Что такое сигнатура функции

Чтобы получить селектор, сначала определяют каноническую сигнатуру функции:

  • берётся имя функции, например transfer;
  • в круглых скобках указываются точные типы аргументов через запятую — без пробелов:
    • address, uint256, bool, bytes32, string и т.д.;
  • получается строка вида:
    • "transfer(address,uint256)".

Важно:

  • порядок аргументов имеет значение;
  • типы должны быть записаны в канонической форме (например, uint256, а не uint);
  • пробелы и модификаторы (public, external и т.п.) не включаются в сигнатуру.

Как из сигнатуры получается selector

Алгоритм стандартизирован для экосистемы Ethereum:

  • строка сигнатуры (в ASCII) подаётся на вход функции keccak256 — см. Keccak-256;
  • считается 256-битный (32-байтовый) хеш;
  • первые 4 байта этого хеша и есть function selector.

Пример для transfer(address,uint256):

  • keccak256("transfer(address,uint256)") → 32-байтовый хеш;
  • 0xa9059cbb — первые 4 байта, записанные в hex-формате;
  • именно это значение кошельки и библиотеки подставляют в начало calldata при вызове transfer.

Роль селектора в calldata

Внешний вызов функции контракта выглядит как:

  • 4 байта — function selector;
  • далее — аргументы функции, закодированные по правилам ABI (каждый — 32 байта или offset к динамическим данным).

На уровне EVM опкоды, связанные с вызовами (CALL, DELEGATECALL, STATICCALL — см. опкоды EVM), не знают ничего о функциях и типах. Они просто получают блок байтов:

  • первые 4 байта — селектор;
  • остальное — данные.

Дальше сам контракт, опираясь на свой байткод, определяет:

  • есть ли реализация функции с данным селектором;
  • какую ветку кода выполнить, если селектор не распознан (fallback).

Перегрузка функций и селекторы

В Solidity разрешена перегрузка: несколько функций с одинаковым именем и разными наборами аргументов. Например:

  • foo(uint256 x)
  • foo(address addr)

У каждой перегруженной функции будет:

  • своя сигнатура строки ("foo(uint256)", "foo(address)");
  • свой хеш Keccak-256;
  • свой 4-байтовый selector.

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

Коллизии селекторов и риски

Function selector занимает всего 32 бита, поэтому:

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

Практические последствия:

  • злоумышленник может сконструировать сигнатуру, которая разделяет селектор с уже существующей функцией, а затем пытаться использовать это в контексте слабых инструментов декодирования;
  • при анализе контракта по селектору важно иметь полный ABI, а не пытаться «угадать» сигнатуру только по 4 байтам.

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

Function selector и события

События (events) также используют хеш сигнатуры, но есть важные отличия:

  • для события "Transfer(address,address,uint256)" считается Keccak-256 от строки;
  • полный 32-байтовый хеш используется как topic0 — идентификатор события;
  • сокращения до 4 байт, как у function selector, не происходит.

То есть у событий нет отдельного «4-байтового селектора», но принцип формирования идентификатора из текстовой сигнатуры похож. Детали — в ABI.

Взаимодействие с fallback и receive

Если в контракт приходит вызов:

  • без селектора (пустая calldata) — обычно это означает простой перевод ETH на receive() или fallback();
  • с селектором, которого нет ни у одной функции контракта — будет вызван fallback().

Это даёт разработчику возможность:

  • реализовать «router»-контракты, которые сами сопоставляют селекторы и адреса целевых контрактов;
  • обрабатывать «чужие» селекторы и реализовывать прокси-паттерны.

Практическое использование селекторов

Разработчики и аудиторы часто работают с селекторами напрямую:

  • просмотр вызовов в блок-эксплорерах — по первым 4 байтам calldata можно понять, какой метод вызывался;
  • низкоуровневые вызовы типа address.call(abi.encodeWithSelector(...)) в Solidity;
  • реализация прокси-контрактов и маршрутизаторов, где селектор используется как ключ для делегирования вызова.

Понимание механики function selector помогает:

  • корректно работать с кастомным ABI;
  • избегать ошибок в декодировании данных;
  • лучше анализировать поведение контрактов при аудите.

См. также

Task Runner