El calldata de una transferencia ERC-20 suele empezar por 0xa9059cbb, seguido de dos palabras de 32 bytes: la primera es la dirección del receptor y la segunda, el importe. En la cadena no hay ningún campo que indique que esa secuencia de bytes corresponde a transfer(address,uint256), y la EVM (Ethereum Virtual Machine, máquina virtual de Ethereum) tampoco conoce los nombres de las funciones. Solo sigue la lógica de despacho compilada en el bytecode durante el despliegue: compara los primeros 4 bytes del calldata y salta al segmento de código correspondiente.
De dónde vienen esos 4 bytes, por qué algunos parámetros se colocan directamente al principio y otros hay que ir a buscarlos más atrás, y por qué, con un calldata en la mano pero sin definición de interfaz, no hay forma de decodificarlo, son tres cuestiones clave para entender la interacción con contratos. Este artículo explica primero las reglas de codificación de la ABI (Application Binary Interface, interfaz binaria de aplicación); después recorre a mano la decodificación siguiendo el calldata de ejemplo que da el documento de la especificación y, por último, aclara por qué el registro de eventos reparte los parámetros entre topics y data.
El selector es un truncamiento del hash de la firma canónica, no el nombre de la función
La especificación de la ABI de Solidity define el selector de función (function selector) en muy pocas palabras: los primeros 4 bytes del calldata, tomados de los 4 bytes más altos del hash Keccak-256 de la firma de la función. La firma aquí es la forma canónica y no equivale a cómo se escribe en el código fuente: el nombre de la función seguido de la lista de tipos de los parámetros entre paréntesis, con un solo tipo separado por comas, sin espacios, sin nombres de parámetros y sin modificadores de ubicación de datos como memory, calldata o storage.
La forma canónica también normaliza los tipos. uint e int deben escribirse como uint256 e int256; address payable y los tipos de contrato se escriben como address; un enumerado (enum) se escribe como uint8 y un tipo de valor definido por el usuario, como su tipo subyacente. Un struct se escribe como los tipos de sus componentes entre paréntesis, es decir, una tupla: si S tiene dos campos uint256, la firma de function f(S memory s) es f((uint256,uint256)) y el selector se toma de los primeros 4 bytes de keccak256("f((uint256,uint256))"). El valor de retorno no participa en la firma, lo que concuerda con la resolución de sobrecargas de Solidity: quien llama distingue la función destino solo por los parámetros, y una diferencia en el valor de retorno no sirve para desambiguar. Cada carácter de la firma influye en el resultado; con un espacio de más o escribiendo uint en lugar de uint256, el selector cambia por completo.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SelectorDemo {
function selectorOfTransfer() external pure returns (bytes4) {
// la firma canónica va entre comillas: sin espacios, sin nombres de parámetros, uint normalizado a uint256
return bytes4(keccak256("transfer(address,uint256)"));
}
}
Una consecuencia directa del espacio de 32 bits es que las colisiones no son una hipótesis teórica. A septiembre de 2026, el selector 0xa9059cbb figura en 4byte.directory con 6 firmas candidatas: además de transfer(address,uint256), aparecen many_msg_babbage(bytes1), workMyDirefulOwner(uint256,uint256) y nombres de relleno como func_2093253501(bytes); las entradas de ese repositorio se pueden enviar libremente y su número y contenido cambian con el tiempo. Cuando dos funciones distintas comparten selector, el prefijo del calldata por sí solo no permite distinguir la intención de la llamada, y hay que recurrir al código del contrato destino o a la definición de la interfaz.
La documentación oficial de Solidity no dedica un apartado propio a esta limitación, pero solc sí comprueba en la implementación la repetición de selectores dentro de un mismo contrato (incluida la cadena de herencia): si dos funciones comparten selector, la compilación falla con Function signature hash collision (se puede reproducir con un contrato mínimo). Entre contratos y entre versiones no existe esa comprobación. Esto muestra también que no es fiable limitarse a buscar el nombre de la función en bases de datos tipo 4byte: la base de datos ofrece un conjunto de candidatas y, en caso de colisión, no puede determinar una respuesta única.
Los tipos estáticos ocupan una palabra entera
La codificación empieza en el quinto byte. Las reglas dividen primero los parámetros en estáticos y dinámicos: los tipos estáticos se codifican en su sitio y se escriben directamente en el flujo de bytes de parámetros; los dinámicos colocan su contenido real más adelante y dejan en su lugar únicamente un desplazamiento.
La EVM tiene palabras de 32 bytes, así que todo tipo estático ocupa una palabra entera. uint<M> e int<M> se escriben en big-endian con relleno en los bits altos: ceros para los números sin signo y 0xff o 0x00 por extensión de signo para los que llevan signo. bool equivale a uint8: true se codifica como 1 y false como 0. address equivale a uint160, de modo que una dirección de 20 bytes cae en los bytes bajos de la palabra de 32 y delante quedan 12 bytes a cero. Los tipos de bytes de longitud fija bytes<M> se rellenan en la dirección contraria: el valor se alinea a la izquierda y se rellena con ceros por la derecha hasta 32 bytes. Aunque ambos sean estáticos, uint32(1) y bytes4(0x00000001) caen en la misma palabra pero en posiciones distintas: el primero en los últimos 4 bytes y el segundo en los 4 primeros; es el punto donde más fácilmente se confunde uno al leer un volcado hexadecimal.
Los arrays de longitud fija <type>[M] también se consideran estáticos cuando el tipo de sus elementos es estático; al codificar, se despliegan como una tupla: los M elementos se colocan uno tras otro, sin escribir la longitud. Los structs y las tuplas se despliegan igual, de forma recursiva. La especificación define los tipos estáticos como «todos los tipos salvo bytes, string, T[], T[k] con elementos de tipo dinámico y las tuplas con miembros dinámicos»; esta definición es el único criterio para decidir si se lee el valor directamente o se sigue un puntero.
El propio calldata también tiene coste de gas, y no concuerda con la alineación de 32 bytes de la ABI. EIP-2028 bajó el coste de los bytes distintos de cero en los datos de transacción de 68 gas a 16 gas, mientras que los bytes cero siguen costando 4 gas; este coste forma parte del gas intrínseco de la transacción. Pasar un uint256 con un valor muy pequeño sigue ocupando 32 bytes, en su mayoría ceros, con un coste bajo pero no nulo; a la inversa, empaquetar varios enteros pequeños de forma compacta como bytes32 ahorra gas a cambio de perder en la cadena los límites legibles entre parámetros, y quien decodifica debe conocer ese diseño personalizado.
Los tipos dinámicos se direccionan en dos niveles: desplazamiento y prefijo de longitud
bytes, string, T[], y también los arrays de longitud fija y las tuplas con elementos de tipo dinámico, son tipos dinámicos. Su longitud depende del valor en tiempo de ejecución y no puede fijarse en tiempo de compilación, así que no pueden ocupar un ancho fijo en el flujo de bytes de parámetros.
La ABI usa un diseño de cabeza y cola (head/tail). Primero se colocan en orden las «cabezas» de todos los parámetros: la de un tipo estático es su propia codificación; la de un tipo dinámico, un desplazamiento de 32 bytes. La base del desplazamiento es el inicio del bloque de parámetros, es decir, el primer byte después del selector, no el inicio de todo el calldata; esta es la fuente más común de desalineación al decodificar a mano. La longitud de las cabezas depende solo de los tipos de los parámetros, no de sus valores. Tras las cabezas se colocan en orden las «colas» de los parámetros dinámicos; el primer elemento de la cola es la longitud, cuya unidad depende del tipo: bytes para bytes y string, número de elementos para los arrays; el contenido viene después de la longitud.
La longitud de un string es el número de bytes tras la codificación UTF-8, no el número de caracteres, por lo que el chino y los emoji ocupan varios bytes. Tras el contenido se rellena con ceros hasta un múltiplo de 32 bytes. Los elementos de un array aplican recursivamente las mismas reglas; en arrays anidados (como uint256[][]) cada nivel tiene su propia longitud y su propio desplazamiento, y el desplazamiento es siempre relativo al inicio del bloque de codificación de su nivel. La especificación deja muy claro el objetivo de este diseño: en el peor caso, el número de lecturas para acceder a un valor no supera su profundidad de anidamiento en la estructura de parámetros, y los datos de cualquier elemento se pueden trasladar en bloque sin invalidarse, porque solo dependen de direcciones relativas.
Un detalle que se pasa por alto con facilidad es que los desplazamientos no tienen que ser mínimos ni las regiones de datos tienen que ser disjuntas. El modo de codificación estricta (strict encoding mode) definido en la especificación exige cabezas compactas y sin huecos; el codificador de Solidity siempre emite el modo estricto, pero el decodificador no lo comprueba de forma obligatoria. Dicho de otro modo, una misma llamada semánticamente idéntica puede tener varias disposiciones de bytes, y la implementación de decodificación debe aceptar desplazamientos con huecos o rechazarlos explícitamente; las dos opciones producen diferencias al comparar clientes.
Decodificar a mano un calldata
El ejemplo de decodificación está tomado directamente del apartado Examples de «Contract ABI Specification»: la llamada sam(bytes,bool,uint256[]), con los argumentos "dave", true y [1,2,3], ocupa 292 bytes y, agrupada de 32 en 32 bytes, queda así.
0xa5643bf2
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000004
6461766500000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
Los pasos de decodificación se pueden reproducir estrictamente según la definición de la especificación:
- Los primeros 4 bytes
0xa5643bf2son el selector y corresponden a la firmasam(bytes,bool,uint256[]). Conviene notar que eluintde la firma debe escribirse comouint256. - El bloque de parámetros tiene 3 cabezas de 32 bytes cada una. La primera palabra es
0x60, es decir, 96, el desplazamiento del primer parámetrobytes; la segunda es0x01, conbooligual atrue; la tercera es0xa0, es decir, 160, que apunta al tercer parámetrouint256[]. - Las cabezas suman 96 bytes, así que la cola del primer parámetro empieza en el byte 96 del bloque de parámetros, y
0x60es coherente. La primera palabra de la cola es0x04, que indica que elbytestiene 4 bytes; los 4 bytes siguientes son64617665, que en ASCII se leedave; después se rellena con ceros hasta 32 bytes. - La cola del primer parámetro ocupa dos palabras, 64 bytes, de 96 a 160, de modo que el desplazamiento del segundo parámetro dinámico es
0xa0. Allí la primera palabra es0x03, que indica que el array tiene 3 elementos; las 3 palabras siguientes son 1, 2 y 3.
El mismo proceso se puede escribir como un decodificador mínimo. Este fragmento de Python solo maneja la firma anterior y sirve para mostrar que la unidad del desplazamiento es el byte y dónde se sitúa el prefijo de longitud.
# la entrada es el bloque de parámetros posterior al selector, dividido en palabras de 32 bytes
def word(data, i):
return int.from_bytes(data[i * 32:(i + 1) * 32], "big")
def decode_sam(args: bytes):
bytes_off = word(args, 0) # 1.ª palabra: desplazamiento en bytes del parámetro bytes respecto al inicio del bloque
flag = word(args, 1) != 0 # 2.ª palabra: bool
arr_off = word(args, 2) # 3.ª palabra: desplazamiento de uint256[]
n = word(args, bytes_off // 32) # tras convertir el desplazamiento en índice de palabra, se lee el prefijo de longitud
s = args[bytes_off + 32: bytes_off + 32 + n].decode()
k = word(args, arr_off // 32) # el array también lee primero el número de elementos
items = [word(args, arr_off // 32 + 1 + j) for j in range(k)]
return s, flag, items # ("dave", True, [1, 2, 3])
La ABI no se autodescribe: el schema es imprescindible fuera de la cadena
El propio comienzo de la especificación lo dice: esta codificación no se autodescribe y decodificar exige un schema. El peso ingenieril de esta frase es mayor de lo que parece al leerla.
A la EVM solo le llegan bytes; los nombres de funciones, los nombres de parámetros y la información de tipos desaparecen por completo tras la compilación. El despacho consiste en comparar los 4 bytes del selector y el destino del salto es un switch generado por el compilador; los datos de retorno también son solo un flujo de bytes, y quien llama, para leer un valor de retorno de longitud dinámica, necesita operaciones como RETURNDATASIZE y RETURNDATACOPY (propuestas en EIP-211 y activadas en la actualización Byzantium). En la cadena no hay ningún lugar donde guardar el JSON de la ABI; solo puede publicarse junto al código fuente como producto de compilación. De ahí se derivan varias restricciones concretas.
Para que un explorador de bloques muestre «qué función se llamó y con qué parámetros», primero debe obtener el código fuente verificado y la ABI. De un contrato sin verificar solo se puede mostrar el calldata en bruto o recurrir a la búsqueda inversa en bases de datos tipo 4byte, que devuelve un conjunto de firmas candidatas y no permite decidir en caso de colisión. Los indexadores y las tuberías de datos deben conocer la ABI en el momento del despliegue; de lo contrario no pueden indexar eventos ni llamadas. La evolución de la interfaz tampoco tiene un mecanismo de compatibilidad como la numeración de campos: añadir, eliminar o reordenar parámetros de una función cambia el selector y rompe por completo la interfaz para quien llama, así que las actualizaciones suelen limitarse a añadir funciones nuevas en lugar de modificar las antiguas. Un contrato proxy puede añadir funciones en la capa de implementación, pero el selector debe mantenerse estable: cualquier «refactorización» que cambie la firma hará que las llamadas existentes caigan en la rama fallback, y el fallback solo recibe el calldata en bruto, sin poder restituir la intención de quien llamó.
Otros dos puntos se malinterpretan a menudo. Que dos selectores coincidan no significa que las codificaciones sean intercambiables: transfer(address,uint256) y many_msg_babbage(bytes1) comparten 0xa9059cbb, pero la longitud y la semántica de sus parámetros son por completo distintas, y las herramientas deben combinar el código del contrato o la longitud del calldata para decidir en caso de colisión. El selector de un error personalizado (custom error) se toma igualmente de los primeros 4 bytes de la firma del error, y cualquier contrato puede devolver datos que coincidan con la firma de un error; por eso la especificación advierte de forma explícita a quien llama que no trate los datos de error como información de origen fiable, sino solo como una pista.
Los eventos ponen la capacidad de búsqueda en topics y dejan la legibilidad en data
Las reglas de codificación de los eventos tienen el mismo origen que las de las llamadas a funciones, pero el resultado es distinto. Un registro está formado por la dirección del contrato, como máximo 4 temas (topic) y una cantidad arbitraria de datos. En los eventos no anónimos, topics[0] es siempre el hash Keccak-256 de la firma del evento, sin truncar a 4 bytes; por eso al filtrar eventos con eth_getLogs la condición se escribe con el hash de la firma y no con el nombre del evento. La firma del evento también usa la forma canónica, y uint se normaliza a uint256.
El resto de los parámetros se reparte en dos vías según lleven o no indexed. Los parámetros de tipo valor con indexed se codifican directamente en topics[1] a topics[3]: los enteros con relleno en los bits altos, los bytes de longitud fija con relleno a la derecha y las direcciones con sus 20 bytes bajos. Un evento no anónimo admite como máximo 3 parámetros indexed, que junto con el tema de la firma llenan exactamente los 4; un evento declarado anonymous no escribe el tema de la firma y admite hasta 4 parámetros indexed, a cambio de perder la capacidad de filtrar por firma. Los parámetros sin indexed se codifican en data según las reglas de la ABI; dentro de esos datos también hay estructura de cabeza y cola, y los parámetros dinámicos se siguen direccionando mediante desplazamientos.
Las diferencias se concentran en los tipos complejos marcados como indexed. Cuando un array, un string, un bytes o un struct se marca como indexed, lo que se escribe en el topic es el hash Keccak-256 de esa codificación. Un hash es irreversible, así que este tipo de parámetros se puede buscar con exactitud: calculando de antemano el hash del valor buscado se puede usar como condición de filtro para encontrar registros, pero no se puede restituir el valor original a partir del registro, solo confirmar si hay coincidencia o no. La solución que da la especificación es declarar dos parámetros que guarden el mismo valor: uno indexed para buscar y otro sin indexed para leer, a cambio de aumentar el tamaño del registro y el gas. La propia regla de codificación del hash deja ambigüedad: cuando un struct contiene varios arrays dinámicos, la secuencia de bytes resultante de la concatenación puede no ser única, y la especificación advierte de que no se debe concluir el significado de un evento basándose solo en el resultado de búsqueda de parámetros indexed.
Por último, hay que distinguir «buscable» de «decodificable». Un parámetro indexed de tipo valor se puede leer directamente; un parámetro indexed de tipo dinámico solo se puede localizar, no leer. Los eventos separan topics y data como un equilibrio entre el tamaño del registro, la capacidad de búsqueda y la capacidad de decodificación: ninguna categoría de parámetro puede obtener todas las propiedades a la vez.
Fuentes
- Documentación de Solidity, «Contract ABI Specification», selector de función, diseño de cabeza y cola, modo de codificación estricta, reglas de codificación de eventos y parámetros indexed, ejemplo de decodificación
sam: https://docs.soliditylang.org/en/latest/abi-spec.html - Lista de firmas candidatas para
0xa9059cbben 4byte.directory (consultada en septiembre de 2026; las entradas se pueden enviar libremente): https://www.4byte.directory/signatures/?bytes4_signature=0xa9059cbb - EIP-609, «Hardfork Meta: Byzantium», metapropuesta que incluye EIP-211 y EIP-214 en la lista de contenidos de Byzantium: https://eips.ethereum.org/EIPS/eip-609
- EIP-2028, «Transaction data gas cost reduction», los bytes de calldata distintos de cero bajan de 68 gas a 16 gas: https://eips.ethereum.org/EIPS/eip-2028
- EIP-211, «New opcodes: RETURNDATASIZE and RETURNDATACOPY», lectura de datos de retorno dinámicos y
BYZANTIUM_FORK_BLKNUM: https://eips.ethereum.org/EIPS/eip-211 - Ethereum Yellow Paper, en el apéndice G de la tabla de tarifas,
G_txdatazero4 gas yG_txdatanonzero16 gas: https://ethereum.github.io/yellowpaper/paper.pdf - Referencia de opcodes de la EVM de ethereum.org y tabla dinámica de gas de wolflo/evm-opcodes, precio del byte cero y distinto de cero del gas intrínseco de transacción: https://ethereum.org/en/developers/docs/evm/opcodes/ , https://github.com/wolflo/evm-opcodes/blob/main/gas.md
Lecturas relacionadas
Para la relación entre las instrucciones de llamada y el contexto, véase el 0.17 «Comparativa de la familia CALL: CALL, CALLCODE, DELEGATECALL y STATICCALL».