Function selector — это 4-байтовый идентификатор функции смарт-контракта в Ethereum. Он стоит в самом начале calldata и говорит EVM, какой именно метод контракта нужно вызвать и как интерпретировать последующие байты.
Selector:
- рассчитывается из текстовой сигнатуры вида "transfer(address,uint256)" с помощью Keccak-256;
- состоит из первых 4 байт (32 бит) хеша;
- используется для маршрутизации вызовов в смарт-контракте и привязан к ABI-описанию — см. ABI.
Что такое сигнатура функции
Чтобы получить селектор, сначала определяют каноническую сигнатуру функции:
- берётся имя функции, например 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;
- избегать ошибок в декодировании данных;
- лучше анализировать поведение контрактов при аудите.
