ABI в Ethereum: как кодируются аргументы и возврат функций смарт-контрактов

ABI (Application Binary Interface) в контексте Ethereum — это спецификация того, как кодируются и декодируются данные при вызове функций смарт-контрактов и при обработке событий. Любой вызов контракта на уровне EVM — это просто байты, упакованные по правилам ABI.

Через ABI договариваются:

  • как из человеческого описания transfer(address,uint256) получить бинарный формат calldata;
  • как читать возвращаемые значения функций;
  • как кодируются события (events), чтобы их можно было декодировать на фронтенде и в аналитике.

ABI в Ethereum: как кодируются аргументы и возврат функций смарт-контрактов

Зачем нужен ABI

Смарт-контракт работает с байтами, а разработчик — с типами и структурами данных. ABI решает задачу «перевода» между этими мирами:

  • описывает сигнатуры функций и событий: имена, типы аргументов, возвращаемые значения;
  • задаёт строгое правило кодирования аргументов в calldata и результатов — в return data;
  • определяет формат логов событий, чтобы их могли парсить кошельки, блок-эксплореры и dApp’ы.

Без ABI:

  • нельзя было бы надёжно вызвать функцию по имени и набору типов;
  • инструменты вроде Web3.js / Ethers.js не смогли бы автоматически формировать транзакции и декодировать ответы.

Как ABI связан с function selector

Для внешних вызовов функций контрактов используется схема:

  • Строка сигнатуры вида "transfer(address,uint256)".
  • Вычисляется keccak256 от этой строки — см. Keccak-256.
  • Первые 4 байта результата — это function selector.

Поле calldata внешнего вызова выглядит так:

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

То есть ABI описывает:

  • какие типы стоят после селектора;
  • как именно каждый тип превращается в байты.

Статические и динамические типы в ABI

ABI делит типы на:

  • Статические — фиксированный размер:
    • uint256, int256, address, bool, bytes32, фиксированные массивы с фиксированным числом элементов;
    • в кодировке каждый такой элемент занимает ровно 32 байта.
  • Динамические — размер зависит от значения:
    • bytes, string, динамические массивы (uint256[], address[]), структуры с динамическими полями;
    • в «голове» (head) хранятся только смещения (offset), а сами данные — в «хвосте» (tail) кодировки.

Общая идея ABI-кодирования:

  • все аргументы сначала превращаются в последовательность 32-байтовых «слов»;
  • для динамических типов в основном блоке записывается offset, который указывает, где в хвосте лежит длина и сами данные;
  • длина динамического массива или строки тоже хранится как 32-байтовое слово перед самими элементами/байтами.

abi.encode и abi.encodePacked

В Solidity поверх базовых правил ABI есть удобные помощники:

  • abi.encode(...)
    • кодирует аргументы в полноценном ABI-формате (как в calldata/return data);
    • каждый аргумент выровнен до 32 байт, динамические типы используют схему head/tail.
  • abi.encodePacked(...)
    • делает плотное, «спакованное» кодирование без выравнивания;
    • может быть удобен для хеширования (keccak256(abi.encodePacked(...))), но при этом возможны коллизии разных наборов аргументов.

Типичные ошибки:

  • использовать abi.encodePacked там, где ожидается обычный ABI-формат (например, внутри низкоуровневого вызова);
  • не учитывать возможные коллизии при хешировании составных параметров.

ABI и вызовы/ответы функций

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

  • фронтенд/библиотека формирует JSON-подобное описание: имя функции, аргументы, типы;
  • по ABI считается селектор, кодируются аргументы → получается calldata;
  • транзакция отправляется в сеть, EVM выполняет байткод;
  • возвращаемое значение кодируется обратно по ABI и попадает в return data.

На уровне EVM:

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

ABI и события (events)

События в Ethereum также используют ABI:

  • строка сигнатуры события ("Transfer(address,address,uint256)") хешируется Keccak-256;
  • получившийся хеш используется как topic0 — идентификатор события;
  • индексация аргументов (indexed/non-indexed) определяет, какие поля попадут в topics, а какие — в data.

ABI:

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

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

  • Фронтенд dApp’ов

JSON-описание ABI контракта подключается к Web3/Ethers.js, и разработчик вызывает contract.transfer(...), не думая о байтах.

  • Кошельки и блок-эксплореры

Интерфейсы читают ABI популярных контрактов и по нему красиво отображают вызовы и события, а не «сырые» hex-значения.

  • Инструменты разработки

Hardhat, Foundry, Truffle, Remix — все используют ABI для генерации интерфейсов и типобезопасных обёрток вокруг контрактов.

См. также

Task Runner