К основному содержимому

Введите слово из вопроса или названия статьи.

3 · Под капотом

ABI: как вызов функции превращается в байты - селекторы, calldata и события

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

4 мин чтенияОбновлено:
В этой статье

Контракт - это байты, и вызывать его приходится байтами. ABI (Application Binary Interface) - договорённость о том, как превратить «вызови функцию transfer с такими-то аргументами» в последовательность байтов, понятную и интерфейсу, и контракту. Знание ABI превращает загадочную строку 0xa9059cbb000… в читаемый вызов.

Селектор функции

Контракт получает на вход один блок байтов (calldata). Как он понимает, какую функцию вызвали? По первым четырём байтам - селектору.

Селектор - первые 4 байта хеша keccak256 от сигнатуры функции: имя и типы аргументов без пробелов.

text
сигнатура:  transfer(address,uint256)
keccak256:  a9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
селектор:   0xa9059cbb

Это один из самых известных селекторов: именно с него начинается каждый перевод токена ERC-20. Имена аргументов в сигнатуре не участвуют - только типы.

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

ФункцияСелектор
confirmDelivery(uint256)0xfd84cb97
acceptOffer(uint256,bytes32)0x72dfe66b
createOffer((bool,address,address,uint16,uint16,address,uint256,uint8,bytes32))0xd96ef60e

Заметьте: вызов createOffer принимает структуру - в сигнатуре она записывается как кортеж в скобках.

Кодирование аргументов

После селектора идут аргументы. Правило для простых типов: каждый занимает 32 байта, то есть выравнивается до слова.

Вызов transfer(0x6462…90aD, 1000000) - это 4 + 32 + 32 = 68 байт:

text
0xa9059cbb                                                         // селектор
0000000000000000000000006462dddc0a3d893a7c17f94410e2d27162a390ad   // адрес, дополненный нулями слева
00000000000000000000000000000000000000000000000000000000000f4240   // 1000000 в шестнадцатеричной

Адрес - 20 байт, поэтому слева дополняется 12 нулевыми байтами. Число дополняется нулями слева, потому что хранится как uint256 в формате big-endian. Видите f4240? Это 1 000 000: для USDT с 6 знаками - ровно один токен.

Структуры и кортежи

Структура, состоящая из статических типов (числа, адреса, bool, bytes32), кодируется «в линию»: поля идут друг за другом по 32 байта. Параметры createOffer - девять полей, значит, 9 × 32 = 288 байт после селектора:

text
asBuyer, counterparty, arbiter, arbiterBaseFeeBps, arbiterDisputeFeeBps,
token, amount, durationPreset, termsHash

Каждое поле дополнено до слова: bool - это 32 байта, где последний равен 0 или 1; uint16 со значением 100 - это 31 нулевой байт и байт 0x64.

Динамические типы

Строки, bytes и массивы имеют переменную длину, поэтому кодируются иначе: в «голове» хранится смещение (offset) до данных, а сами данные лежат в «хвосте»: сначала длина, потом содержимое, дополненное до границы слова.

Для функции f(uint256 a, string s) вызов выглядит так: селектор, затем a, затем смещение до s (0x40 - сразу после двух слов головы), и далее в хвосте длина строки и её байты. Декодировать такое вручную неудобно, поэтому есть библиотеки (viem, ethers), которые берут ABI в формате JSON и делают всё сами.

Файл ABI

ABI в формате JSON - это описание всех функций, событий и ошибок контракта: имена, типы, входы, выходы, «изменяемость» (view, payable). Именно он позволяет интерфейсу превратить confirmDelivery(7) в нужные байты, а ответы - обратно в числа. Если ABI не совпадает с контрактом, вызов либо не пройдёт, либо сделает не то. Поэтому в проекте ABI сгенерирован из скомпилированного контракта и не пишется вручную.

Ошибки

Пользовательские ошибки (error TokenNotAllowed(address token);) тоже кодируются: 4 байта селектора ошибки плюс аргументы. Для TokenNotAllowed(address) это 0x51c9316d. Когда транзакция откатывается, узел возвращает эти байты, и интерфейс по селектору находит человеческий текст: «Эта валюта не разрешена в сети».

Так интерфейс превращает технический revert в понятную подсказку, а не в «execution reverted».

События и топики

Событие записывается в журнал транзакции как лог. Его устройство: topic0 - это keccak256 от сигнатуры события; следующие топики - значения параметров, помеченных как indexed; остальное - в поле data.

Для нашего события о завершении сделки:

text
DealSettled(uint256 indexed dealId, uint8 indexed outcome,
            uint256 buyerAmount, uint256 sellerAmount,
            uint256 arbiterFee, uint256 platformFee)

topic0 = 0xd716fe621ea710daf4a2c1145a1bc9d712441578dcb8c686ed242ea2590e7789
topic1 = dealId           (индексирован)
topic2 = outcome          (индексирован)
data   = buyerAmount ‖ sellerAmount ‖ arbiterFee ‖ platformFee

Индексированные параметры можно искать: «покажи все события DealSettled для сделки 7» - это запрос по топику. Неиндексированные данные искать нельзя, зато они дешевле. Такой компромисс разработчик продумывает заранее: индексируют то, по чему будут искать. Как это используется для построения истории сделки - в статье про события и логи.

Инструменты для проверки

  • cast sig "transfer(address,uint256)" - селектор по сигнатуре (инструмент Foundry).
  • cast 4byte 0xa9059cbb - поиск имени по селектору в публичной базе.
  • Decode Input Data в обозревателе - готовая расшифровка.
  • viem в TypeScript: encodeFunctionData, decodeFunctionData, toFunctionSelector.

Почему это важно и для безопасности

Селекторы - 4 байта, поэтому возможны коллизии: разные функции с одинаковым селектором. Для обычного контракта это не проблема (компилятор сообщит), но в прокси-архитектурах такие совпадения использовались для атак. Другой вывод проще: подписывая транзакцию в кошельке, вы подписываете эти самые байты. Если кошелёк умеет расшифровывать ABI - читайте расшифровку, если не умеет - это повод насторожиться.

Что почитать дальше

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

Упоминается в статьях

Готовы попробовать?

Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.