La codificación ABI es el formato de datos utilizado para realizar llamadas a funciones en smart contracts. También es la forma en que los smart contracts codifican los datos al realizar llamadas a otros smart contracts.
Esta guía mostrará cómo interpretar datos codificados en ABI, cómo calcular la codificación ABI y enseñará la relación entre la firma de la función (function signature) y la codificación ABI.
Entremos en materia…
abi.encodeWithSignature en Solidity y llamadas de bajo nivel
Si tuviéramos que hacer una llamada de bajo nivel a otro smart contract con una función pública foo(uint256 x) (pasando x = 5 como argumento), haríamos lo siguiente:
otherContractAddr.call(abi.encodeWithSignature("foo(uint256)", (5));
Podemos ver los datos reales devueltos por abi.encodeWithSignature("foo(uint256)", (5)) con el siguiente código:
function seeEncoding() external pure returns (bytes memory) {
return abi.encodeWithSignature("foo(uint256)", (5));
}
y obtendríamos el siguiente resultado (que está codificado en ABI):
0x2fbebd380000000000000000000000000000000000000000000000000000000000000005
Interpretar y entender estos datos de esta manera es el objetivo de este artículo.
Los componentes clave de una llamada a función codificada en ABI
Una llamada a función codificada en ABI es la concatenación del selector de la función (function selector) y los argumentos codificados para la función (si la función toma argumentos).
La firma de la función
La firma de la función es la combinación del nombre de la función y los tipos de sus argumentos sin espacios.
Por ejemplo, la firma de la función para la siguiente función:
function transfer(address _to, uint256 amount) public {
//
}
es transfer(address,uint256). Ten en cuenta que debes usar los tipos de datos completos de los argumentos, como uint256 en lugar de uint. Además, los nombres de las variables como _to y amount no forman parte de la firma de la función. También es importante que no haya espacios en la cadena, como transfer(address, uint256).
Según la documentación de Solidity, hay algunos “casos extremos” (corner cases) que se deben tener en cuenta al calcular la firma de la función:
- Los structs se tratan como tuplas
- Las direcciones payable, las interfaces y los tipos de contrato se tratan como addresses
- Los modificadores “memory” y “calldata” se ignoran
- Un enum es uint8
- Un tipo definido por el usuario se trata como su tipo subyacente
El selector de la función
El selector de la función es simplemente los primeros 4 bytes del hash Keccak-256 de la firma de una función que Solidity usa para identificar una función. Por ejemplo, el hash Keccak-256 de nuestra firma de función mencionada anteriormente transfer(address,uint256) es este valor hexadecimal:
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
Sin embargo, solo los primeros 4 bytes del resultado del hash 0xa9059cbb se utilizan para identificar la función; esos cuatro bytes son el selector de la función.
Puedes usar la biblioteca JavaScript ethers para convertir la firma de la función transfer() en su selector, como se muestra a continuación:
const ethers = require('ethers'); // Ethers v6
const functionSignature = 'transfer(address,uint256)';
const functionSelector = ethers.id(functionSignature).substring(0, 10)
console.log(functionSelector);
El resultado sería como este:

En Solidity, esta función calcula el selector de la función:
function getSelector() public pure returns (bytes4 ret) {
return bytes4(keccak256("transfer(address,uint256)")); // 0xa9059cbb
}
También puedes usar este sitio web de conversión a keccak256 para ver la conversión sin escribir ningún código:

Ahora que tenemos una comprensión clara de qué es el selector de la función, consideremos el siguiente componente de una codificación ABI para llamadas a funciones: las entradas o argumentos de la función.
Entradas o argumentos de la función
Al llamar a una función que no toma argumentos, el selector de la función por sí solo será toda la codificación necesaria para llamar a la función. Por ejemplo, la función play() será identificada por su selector de función 0x93e84cd9 y esos serán todos los datos necesarios.
Sin embargo, se vuelve complejo si la función toma argumentos, como transfer(address to, uint256 amount). En ese caso, los argumentos de la función deben estar codificados en ABI y concatenados al selector de la función.
Usemos transfer(address to, uint256 amount) como ejemplo continuo para ayudarnos a entender cómo se realiza la codificación de los argumentos:
function transfer(address to, uint256 amount) public {
//
}
Estos datos para la llamada a la función no se almacenan de forma permanente dentro de la función o del propio contrato. En cambio, residen en un espacio llamado “calldata”. No puedes modificar los datos en el calldata, ya que son creados por el remitente de la transacción y luego se vuelven de solo lectura.
Puedes ver el calldata de una transacción en Etherscan. A continuación se muestra una captura de pantalla que muestra un ejemplo de calldata de una transacción de transferencia para un token ERC-20 de ejemplo:

Etherscan se refiere al selector de la función como MethodID debajo de la descripción de la función (mira la caja roja en la captura de pantalla a continuación). Por lo tanto, el methodID para transfer() es 0xa9059cbb.

Luego, lo que sigue son dos largos valores hexadecimales etiquetados como [0] y [1]. Esos valores hexadecimales representan los dos argumentos de datos de entrada: el _to, address y el _value, uint256.
Etherscan nos ayuda a separar e interpretar la información del calldata en líneas de 32 bytes (64 caracteres) cada una. Sin embargo, el calldata real se agrupará y se enviará como una larga cadena de texto, y debería verse como el siguiente:
0xa9059cbb000000000000000000000000f89d7b9c864f589bbf53a82105107622b35eaa4000000000000000000000000000000000000000000000028a857425466f800000
También puedes ver el calldata completo y real en su formato original en Etherscan haciendo clic en “View input as” y seleccionando la opción “Original”, como se muestra a continuación:

Para entender mejor qué está sucediendo bajo el capó, desglosemos el calldata y extraigamos la información relevante de la transacción.
Dividiendo el calldata
Consideremos el siguiente calldata e identifiquemos sus componentes:
0xa9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000
Primero, necesitamos conocer la firma de la función; sin ella, no podemos decodificar los datos. Así que, aquí está la firma de la función para el calldata anterior:
transfer(address,uint256)
Luego colocamos la notación hexadecimal (0x) y el selector de la función en su propia línea. El selector de la función siempre es de 4 bytes (8 caracteres hexadecimales). Finalmente, dividimos cada uno de los siguientes 32 bytes en su propia línea. Como veremos más adelante, Solidity codifica los datos en incrementos de 32 bytes.
0x <---------- Hexadecimal notation
a9059cbb <---- Function selector
0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f44
00000000000000000000000000000000000000000000011c9a62d04ed0c80000
Selector de la función
El selector de la función son los primeros 4 bytes del calldata 0xa9059cbb.

Dirección (Address)
El address en transfer(address,uint256) es el siguiente valor de 32 bytes. La dirección real es de 20 bytes, pero se rellena a la izquierda con ceros iniciales para llegar a los 32 bytes.
Dirección (Address):
000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f440

Básicamente, la dirección del receptor será el valor anterior pero sin los rellenos de ceros adicionales: es decir, 0x3F5047BDb647Dc39C88625E17BDBffee905A9F44.
Cantidad (Amount)
Y finalmente, el último elemento en transfer(address,uint256) es la cantidad (amount). La cantidad (0000000000000000000011c9a62d04ed0c80000) se rellena a la izquierda con ceros iniciales para convertirse en 32 bytes, como se muestra a continuación:

Aquí tienes un fragmento de código en Python para ayudarte a convertir rápidamente de hexadecimal a decimal:
>>> int("0x11c9a62d04ed0c80000", 16)
5250000000000000000000
Y también podemos convertir el decimal de nuevo a hexadecimal, como se muestra a continuación:
>>> hex(5250000000000000000000)
0x11c9a62d04ed0c80000
Tipos de datos y relleno (padding)
Hemos establecido que cada elemento en el calldata se codifica como una palabra de 32 bytes y se rellena con ceros si el elemento no ocupa la palabra completa de 32 bytes.
Como regla general, todo tipo de dato de tamaño fijo, como int, bool y uint de todos los tamaños (uint8-uint256), se codificará como una palabra de 32 bytes rellenada a la izquierda con ceros si es necesario.
Por ejemplo, si tienes un uint8 con un valor de 5, se codificará como
0x0000000000000000000000000000000000000000000000000000000000000005.
De manera similar, un bool con un valor true también se rellenaría a la izquierda y se codificaría como 0x0000000000000000000000000000000000000000000000000000000000000001.
Sin embargo, los tipos de datos de tamaño dinámico bytes y string se rellenan a la derecha (right padded). Por ejemplo, los bytes 0x68656c6c6f que representan hello se codificarán como una palabra de 32 bytes rellenada con ceros a la derecha 0x68656c6c6f000000000000000000000000000000000000000000000000000000.
Los tipos de datos de tamaño fijo en Solidity incluyen:
- bool
- uints
- bytes de tamaño fijo (bytesN)
- address
- tuple, struct con datos fijos
- array de tamaño fijo
A continuación se muestran los tipos de datos dinámicos en Solidity:
- bytes
- string
- array dinámico
- un array de tamaño fijo que contiene tipos dinámicos
- un struct que contiene cualquiera de los tipos dinámicos anteriores
Trabajando con calldata dinámico
Hasta ahora, nuestro enfoque ha estado en los tipos de argumentos de calldata estáticos como address y uint256. Aunque los tipos estáticos son bastante sencillos de codificar, la codificación de arrays y strings puede ser un poco complicada debido al tamaño variable de los datos que contienen.
Consideremos una función que toma un array de uints y un único address. Aunque los detalles de implementación de nuestra función no son relevantes aquí, la firma de la función debería verse así:
transfer(uint256[],address)
Ahora, centrémonos en la codificación del array. Supongamos que estamos pasando los siguientes datos a la función de transferencia:
transfer([5769, 14894, 7854], 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1)
Este es el calldata para nuestra función de ejemplo anterior con la firma transfer(uint256[],address). Examinémoslo y veamos cómo se codifica cada parte siguiendo el patrón que describimos anteriormente.
Primero, comenzaremos codificando el “offset” del array uint256[], pero ¿qué es un offset?

Offset
El offset se utiliza para localizar dentro del calldata dónde comienzan o se pueden encontrar datos dinámicos específicos.
Siguiendo con nuestro ejemplo, tenemos un tipo de dato dinámico uint256[] y un tipo estático address. El offset de uint256[] en el calldata anterior es 40 en hexadecimal (64 en decimal) y su codificación ocupa palabras de 32 bytes.
Dado que el array dinámico es el primer argumento de la función, el offset es la primera palabra de 32 bytes en el calldata:

Para explicar más a fondo cómo funciona el offset, la imagen anterior resalta dónde se encuentra el offset del array en el calldata. Cada palabra de bytes está numerada del
- 0-31 (la primera fila de 32 bytes)
- 32-63 (la segunda fila de 32 bytes)
- 64-95 (la tercera fila de 32 bytes)
- etc.
Por lo tanto, 64 (40 en hex) es el byte más a la izquierda (par de caracteres hexadecimales) en la tercera fila donde termina el resaltado verde. Aquí es a donde apunta el offset.
En este ejemplo, el offset es la distancia desde el inicio del primer byte después del selector de la función hasta donde comienzan los datos dinámicos (array). Sin embargo, veremos más adelante que el offset no siempre significa “desplazamiento desde el primer byte después del selector de la función”.
Codificando los datos estáticos: la dirección (address)
La siguiente línea es el address, que es una palabra estática de 32 bytes rellenada con ceros iniciales. Es la misma dirección que ya pasamos, ya que el address ya está en formato hexadecimal.

Codificando la longitud de un dato dinámico: el array
La siguiente línea es la longitud del array, que es la cantidad de elementos en el array. Como puedes ver, tenemos 3 elementos en el array: [5769, 14894, 7854]. La longitud del array es 3, tal como se representa en la imagen a continuación:

Codificación hexadecimal de los elementos del array
Hasta ahora, hemos codificado el tipo estático, el offset y la longitud del array. A continuación, codifiquemos los elementos reales del array. Cada elemento del array se representará como un número hexadecimal como en la imagen a continuación:

Convertimos cada uno de los enteros a su representación hexadecimal y les añadimos ceros iniciales. Por lo tanto, los elementos del array serán los siguientes:

Esto completa nuestra discusión sobre el calldata de esta codificación ABI.
El siguiente video resume todo lo que aprendimos sobre cómo codificar el calldata para transfer(uint[], address):
Codificación ABI de un argumento string
Codificar un string es sencillo, solo necesitas codificar lo siguiente:
- el offset
- la longitud del
string - el contenido del
string(codificado en UTF-8)
Aquí tienes un ejemplo usando una función que contiene un argumento de tipo string:
play(string)
Cuando pasamos un valor:
play("Eze")
el calldata será el texto a continuación:
0x
718e6302
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
457a650000000000000000000000000000000000000000000000000000000000
El offset se representa como 20 en hexadecimal, ya que la posición de la codificación del string está a solo 32 bytes de distancia (32 en decimal es 20 en hexadecimal) del comienzo del calldata después del selector de la función. También podemos ver que la longitud del string (Eze) es 3, ya que solo hay 3 caracteres en el string, un byte cada uno (un byte son dos caracteres hex).

El string “Eze” solo utiliza caracteres ASCII que ocupan un byte cada uno (de ahí una longitud de 3). Sin embargo, caracteres unicode como “好” ocupan 3 bytes. El tamaño máximo que puede tener un carácter utf-8 es de 4 bytes. El string “你好” tiene una longitud de 6 bytes.
Codificación de structs/tuplas en un calldata
Las tuplas (tuples) y los structs se codifican de forma idéntica porque los structs se asignan al tipo de ABI tuple.
Según la especificación de codificación ABI de Solidity, la codificación de un struct es la concatenación de la codificación de sus miembros, con los tipos estáticos rellenados a 32 bytes.
Supongamos que tenemos el siguiente contrato:
contract C {
struct Point {
uint256 x;
uint256 y;
}
function foo(Point memory point) external pure {
//...
}
}
La firma de la función de foo sería foo((uint256, uint256)). Esto no es diferente a si tomara una tupla como entrada. Si tomara un array dinámico de puntos (structs), la firma de la función sería foo((uint256, uint256)[]).
Si todos los elementos de un struct son datos de tamaño fijo, codificaremos todo el struct como un tipo estático y no habrá necesidad de un offset. Sin embargo, la codificación del struct cambiará si tiene un tipo de dato de tamaño dinámico como al menos uno de sus campos.
Por ejemplo, un struct como el que se muestra a continuación:
RareToken {
uint256 n;
}
send(RareToken,address)
se codificará como un tipo estático si le pasamos los siguientes argumentos send( RareToken(1), 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1)) como se demuestra en la siguiente imagen:

Sin embargo, si hay un tipo dinámico en el struct, tendremos que codificar el struct como un tipo dinámico. Tomemos este como ejemplo:
RareToken {
uint256;
string;
}
send(RareToken)
La codificación de la función anterior sería el código en la imagen a continuación si le pasamos los siguientes datos send(RareToken(50,"Eze")) . El struct contiene un tipo dinámico y un tipo estático como se muestra en este diagrama:

Codificación ABI de múltiples argumentos struct
Ahora, supongamos que nuestra función send() tomara 3 structs como argumentos en lugar de uno. El calldata sería el siguiente:

Las primeras tres palabras de 32 bytes son offsets porque la función toma 3 argumentos como entrada, y cada uno de esos argumentos son tipos de datos dinámicos (structs con un campo de tipo dinámico).
La columna de 0x00, 0x20, …, 0x1c0 a la izquierda muestra cómo el offset apunta a la ubicación en el calldata. Ten en cuenta que el offset comienza a contar desde el primer offset, no desde donde está ubicado el offset. Exploraremos más sobre los offsets cuando veamos datos dinámicos anidados.
Codificando un array de tamaño fijo con un tipo estático
La codificación de arrays de tamaño fijo depende del contenido del array; si el array de tamaño fijo contiene tipos dinámicos, entonces el array de tamaño fijo se codificará como un tipo dinámico. Si solo contiene tipos estáticos, se tratará como un tipo estático y se codificará como tal. Esto sigue la misma lógica que un struct que contenía datos dinámicos, como se mostró en la sección anterior.
Comencemos con un array de tamaño fijo de solo tipos estáticos con una longitud de 3:
play(uint256[3])
y le pasamos estos datos:
play([1,2,3])
Y aquí está la codificación del array:

Como puedes ver, no hay offsets en el calldata. Es simplemente la codificación de los elementos del array.
Codificando un array de tamaño fijo que contiene un tipo dinámico
Consideremos un escenario en el que el array de tamaño fijo contiene datos dinámicos. La función a continuación contendrá un array de dos strings.
plays(string[2])
Si le pasamos los siguientes strings:
play(["Eze","Sunday"])
Obtendremos este calldata al codificarse:

Debido a que el array de tamaño fijo era un array de tipos de datos dinámicos, en su totalidad se codificó como un array dinámico, la única diferencia es que la longitud del array no se codificó porque la firma de la función lo definía como un array de longitud fija de 2. Si consideramos la misma función pero con longitud dinámica:
plays(string[])
notaremos que la longitud del array también se codificará:

Múltiples argumentos de array y arrays anidados en calldata
Trabajar con arrays múltiples y anidados en calldata puede ser un poco complejo y engañoso. Sin embargo, el patrón general sigue siendo similar. En esta sección, aprenderemos a codificar y decodificar arrays anidados y a obtener una mejor intuición de cómo funciona el offset.
Usaremos la siguiente firma de función como ejemplo:
transfer(uint256[][],address[])
También pasemos los siguientes datos como argumentos a la función:
transfer([[123, 456], [789]], [0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0x7b38da6a701c568545dcfcb03fcb875f56bedfb3])
Por lo tanto, el calldata para esta función y el argumento será el hexadecimal a continuación:
0x7a63729a
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000140
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000040
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000002
000000000000000000000000000000000000000000000000000000000000007b
000000000000000000000000000000000000000000000000000000000000007b
0000000000000000000000000000000000000000000000000000000000000001
000000000000000000000000000000000000000000000000000000000000007b
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
0000000000000000000000007b38da6a701c568545dcfcb03fcb875f56bedfb3
La nueva característica de nuestra función transfer() es que tiene dos arrays, y uno de esos arrays tiene dos sub-arrays.
A continuación se muestra cómo se construye una estructura de calldata para un array múltiple y anidado a un alto nivel:
- Offsets. Comienza definiendo los offsets para las diferentes ubicaciones de los arrays. Asumiendo que hay múltiples argumentos de array, la codificación de los offsets de esos arrays se definirá primero. Para nuestro ejemplo, habrá dos offsets.
- Offset al primer tipo dinámico
- Offset al segundo tipo dinámico
- Offset al
n-ésimo tipo dinámico (cuando corresponda)
- La longitud del primer parámetro del array (2 en nuestro ejemplo:
[[123, 456], [789]]) viene a continuación y reside donde apunta el primer offset. Esta es la longitud del array completo. Antes de comenzar a trabajar en cada array, comienzas definiendo su longitud. Así que para cada sub-array, definirás su longitud (más adelante en la codificación ABI). - A continuación, se codifican los sub-arrays del primer parámetro del array
- Offset al primer sub-array (
[123, 456]) - Offset al segundo sub-array (
[789])- Longitud del primer sub-array (2 en nuestro ejemplo)
- Primer elemento del primer sub-array (
123) - Segundo elemento del primer sub-array (
456)
- Primer elemento del primer sub-array (
- Longitud del segundo sub-array (1 en nuestro ejemplo)
- Primer elemento del segundo sub-array (
789)
- Primer elemento del segundo sub-array (
- Longitud del primer sub-array (2 en nuestro ejemplo)
- Offset al primer sub-array (
- Una vez que hayas terminado con todos los elementos del primer parámetro del array, comienza a codificar el siguiente parámetro del array
- Longitud del array del segundo parámetro (2 addresses en nuestro ejemplo)
- y los elementos del segundo parámetro (los addresses en nuestro ejemplo), si el parámetro del segundo array tiene sub-arrays, sigues el mismo patrón que se describió anteriormente.
Ahora, visualicemos el calldata de nuestro ejemplo.
Primero, organízalo en 32 bytes (64 caracteres) por línea, aparte del 0x y del selector de la función, para que sea más fácil de leer. La firma de la función y el calldata están en la parte superior de la imagen:

Offset del primer argumento de array
La primera palabra de 32 bytes de la cadena del calldata es el offset, que indica dónde comienzan los datos para el primer parámetro del array. Aquí tienes una representación visual:

Offset del segundo argumento de array
El siguiente paso es la codificación del offset para el segundo argumento del array.
Como vimos con el ejemplo de múltiples structs dinámicos, este offset no “comienza a contar” desde la ubicación del offset, sino desde el primer offset.
Los offsets en general no “comienzan a contar” desde su ubicación actual, sino desde el primer offset que describió ese “nivel” de la estructura de datos anidada. Este concepto quedará más claro a medida que exploremos los sub-arrays.
La imagen a continuación resalta cómo el segundo offset apunta a los datos del segundo argumento (el array de addresses). Ten en cuenta que el offset apunta a un “2”, ya que el segundo argumento es un array de dos addresses.

Dado que cada palabra de bytes está numerada del:
- 0-31
- 32-63
- 64-95
- etc.
320 bytes (140 en hex) es el 0 más a la izquierda en la fila donde termina el resaltado.
Longitud del primer array
A continuación, necesitamos codificar la longitud del primer array. Los elementos de los sub-arrays incluyen [123, 456] y [789], lo que forma [[123, 456], [789]]. Dado que hay dos arrays anidados, la longitud es 2. La longitud se representa en el calldata como se muestra a continuación:

Offsets de los sub-arrays en el primer argumento de array
Después de la longitud del array, tenemos los offsets que muestran dónde se almacena el contenido de esos arrays. Hay dos offsets, ya que hay dos sub-arrays: [123, 456] y [789].
Offset del primer sub-array
El offset del primer sub-array ([123,456]) es el 40 al que apunta la caja de la derecha. Ambos “comienzan a contar” desde la primera palabra después de la longitud que define el array. Nuevamente, ten en cuenta que está apuntando a una palabra que contiene un 2 porque [123,456] es de longitud 2.

Offset del segundo sub-array
El offset del segundo sub-array ([789]) no comienza desde el principio del calldata. Está ubicado en a0 (el último resaltado rojo en la imagen a continuación), que está a 160 bytes (en decimal) desde donde se declara el offset del primer sub-array.
Recuerda que los offsets en general no “comienzan a contar” desde su ubicación actual, sino desde el primer offset que describió ese “nivel” de la estructura de datos anidada. Ahora estamos a un nivel de profundidad en el array anidado, por lo que nuestro primer offset es el 40 resaltado en púrpura a continuación. Este offset no comienza a contar desde su propia ubicación:

La longitud del primer sub-array
A continuación viene la longitud del primer sub-array. El primer sub-array contiene 2 elementos y se identifica por el resaltado amarillo a continuación:

Los elementos en el primer sub-array
Las siguientes dos palabras son las representaciones hexadecimales de los dos elementos del primer sub-array, como se muestra en la imagen a continuación:

La longitud del segundo sub-array
Luego pasamos a la longitud del segundo sub-array, que solo contiene un elemento:

El elemento en el segundo sub-array
El segundo array tiene solo un elemento, como puedes ver por su longitud. También representamos el valor del único elemento en el array (315), que es el valor hex de 789, en los siguientes 32 bytes, como se muestra a continuación.

Longitud del segundo array
Finalmente, llegamos a representar la longitud del segundo parámetro, el array de addresses. Tenemos 2 addresses, por lo que la longitud es 2; y los dos parámetros son los addresses representados en este diagrama.

Los elementos address en el segundo array
Y así es como convertimos la función transfer([[123, 123], [123]], [0x5b38da6a701c568545dcfcb03fcb875f56beddc4,0x7b38da6a701c568545dcfcb03fcb875f56bedfb3]) a su representación hexadecimal para la EVM.

El siguiente video resume el calldata del ejemplo de esta sección:
Una animación de array anidado triple
A continuación mostramos un video que demuestra cómo es la codificación ABI de un array 3D de uint: f(uint[][][] memory data):
Longitud del calldata y costo de gas
Como desarrollador de Solidity, una de tus principales preocupaciones es ahorrar gas. Aún más, trabajar con calldata conlleva un costo adicional: cada byte en el calldata cuesta gas.
Para determinar el costo de un calldata, primero necesitamos averiguar la longitud del calldata contando los bytes. Usemos nuestra cadena de calldata anterior como caso de estudio:
0xa9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000
Primero eliminaremos el 0x, ya que es solo un prefijo para que entendamos que es un hexadecimal relacionado con Ethereum. Ahora, nos quedaremos con esto:
a9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000
Esta cadena tiene 136 dígitos hexadecimales de longitud, lo que representa 68 bytes. Cada byte está representado por dos caracteres (dígitos hexadecimales) en la cadena del calldata. Por lo tanto, podemos calcular la longitud dividiendo 136 entre 2 = 68.
Cada byte distinto de cero en calldata cuesta 16 de gas, mientras que los bytes cero cuestan 4 de gas. Así que, necesitamos separarlos para continuar con nuestro cálculo.
Tenemos:
Costo total de gas para el calldata .
Dado que los bytes cero son más baratos, algunos desarrolladores minan para obtener addresses o direcciones de smart contracts con varios bytes cero a la izquierda, ya que esto reduce el costo de gas al pasar esa dirección como argumento.
Conclusión
A lo largo de esta guía, hemos aprendido los conceptos básicos de la codificación ABI para llamadas a funciones, los componentes clave de una llamada a función codificada en ABI y hemos obtenido una comprensión más detallada de calldata. También hemos explorado cómo calcular el costo de gas del calldata e incluso fuimos más allá al explorar ejercicios más complejos de decodificación y codificación de calldata para ayudar a solidificar el conocimiento; espero que te haya resultado útil. Para reforzar aún más lo que has aprendido en este artículo, recomiendo leer más sobre la especificación de codificación ABI de Ethereum y practicar los problemas en la siguiente sección.
¡Feliz codificación!
Problemas de práctica
- ¿Cuántos bytes hay en el calldata para una llamada a
foo(uint16 x)? - ¿Cuál es la codificación ABI para
foo(uint256 x, uint256[])cuando se pasa(2, [5, 9])? - ¿Cuál es la codificación ABI para
foo(S[] memory s)dondeSes un struct con los camposuint256 x; uint256[] a;? (Crédito a este tweet por la inspiración).
Ejercicios Capture the Flag (CTF)
RareSkills Solidity Riddles: Forwarder
DamnVulnerableDeFi: ABI Smuggling
Autoría
Este artículo fue escrito por Eze Sunday en colaboración con RareSkills.
Publicado originalmente el 29 de mayo