WEB3DEV Español

Cover image for Profundización de la Codificación ABI
Hector
Hector

Posted on

Profundización de la Codificación ABI

Este artículo es una traducción de ljmanini, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_eshttps://twitter.com/web3dev_es en Twitter.

Buenos días a todos, bienvenidos a mi primer post para el medium.

Anoche vi el siguiente tweet de z0age y me percaté que no sé lo suficiente sobre cómo funciona la codificación ABI así que, aquí está mi resumen personal luego de leer los documentos de Solidity.

Image description

Como sabrás, en Solidity el ABI es usado para codificar las funciones de llamadas y las estructuras de datos para la comunicación cuando hay interacciones con contratos inteligentes. El ABI especifica cómo se codifica y decodifica los argumentos de función, error y evento.

Cuando invocamos una función de un contrato inteligente, hay dos cosas importantes que necesitamos especificar:

  1. La función que llamamos, especificada por la función de selector
  2. Los argumentos que elegimos pasar con esa llamada

En este artículo, hecharemos un vistazo a la primera y nos enfocaremos en la segunda.

Función de Selector

Los primeros cuatro bytes de calldata para la función de invocación, específica que la función sea invocada: estos son los primeros 4 bytes de los keccack256 de la firma de dicha función.

La firma es definida para la cadena que contiene el nombre de la función, seguida por el tipo de lista de parámetros que están entre paréntesis, divididos por comas. Por ejemplo:

  1. transferFrom(address,address,uint256) es la firma del notorio método ERC20.
  2. addressProcessBundle((uint256[2],address[],(uint256,(uint256,address,bytes)[])[])) es un ejemplo de una firma usando una estructura compuesta por un array de tamaño fijo, un array de tamaño dinámico y un array de tamaño dinámico de una estructura que contiene un número y un array dinámico de otra estructura que contiene un número, una dirección y un array de bytes de tamaño dinámico.

Hasheando las cadenas de arriba y tomando los primeros 4 bytes de la izquierda, dará lugar a su selector:

Image description

Argumento de Codificación

Comenzado desde el quinto byte del calldata, los siguientes argumentos codificados siguen. Una distinción importante se hace entre los tipos estáticos y dinámicos: los tipos estáticos están codificados en el lugar, mientras que los tipos dinámicos están codificados en una posición lejana respecto al argumento de los “bloques” actuales.

Los tipos dinámicos son: bytes , string , T[] , T[k] para cualquier dinámico y T y k >= 0 y (T1, .., Tk) y para algunos 1 <= i <= k es dinámico. ¡Todos los demás son estáticos!

Antes de profundizar en la especificación formal de la codificación, definimos:

  • len(a) como el número de bytes en una cadena binaria a
  • enc, el codificador actual, como el que mapea los valores ABI a la cadena binaria, tal que len(enc(a)) depende del valor de a si, y sólo si a es dinámico.

Observa que, la definición de enc la tendremos si a es del tipo estático, ¡su codificación no depende de su valor (y viceversa)!

Especificación Formal de la Codificación

Respira hondo, vamos a lo profundo.

Para cualquier valor ABI x, enc(x) es definido recursivamente, basado en su tipo.

  • Para las estructuras (T1, .., Tk) para k >= 0 y los tipos T1, .., TK: la codificación está hecha de elementos de la “cabeza” K y elementos de la ”cola” k, como enc(X) = head(X(1)) .. head(X(k)) tail(X(1)) .. tail(X(k)) donde X = (X(1), .. , X(k)) y head y tail son definidos por Ticomo:
    • Para la estática Ti: head(X(i)) = enc(X(i)) y tail(X(i)) = "" (los tipos estáticos están codificados en su lugar, ¿te acuerdas?)
    • Para el Ti dinámico: head(X(i)) = enc(len( head(X(1)) .. head(X(k)) tail(X(1)) .. tail(X(i-1)) )) y tail(X(i)) = enc(X(i)) (el cual es una forma complicada de decir que, donde tú encontrarías el codificado de x si fuera estático, encontrarás un offset desde la base de la estructura, donde encontrarás la codificación.) Volveremos a esto con un ejemplo, al final
  • Para arrays de tamaño fijo T[k] para cualquier T y k: enc(X) = enc((X[0], .., X[k-1])), es decir, está codificado como una tupla con elementos k del mismo tipo
  • Para arrays de tamaño fijo T[] de k: enc(X) = enc(k) enc((X[0], .. , X[k-1])), es decir, está codificado una tupla con elementos k del mismo tipo, ¡prefijados con el número de elementos!
  • bytes de la longitud de k: enc(X) = enc(k) pad_right(X), es decir, está codificado como el número de bytes seguido por la secuencia real de bytes, juntado a su derecha para que su longitud sea un múltiplo de 32
  • string: enc(X) = enc(enc_utf8(X)), es decir, x es UTF-8 y interpretado como bytes
  • uint<M>: enc(X) es la codificación big endian de x, juntado a la izquierda como len(enc(X)) == 32
  • int<M>: enc(X) el big endian es el complemento de los dos codificadores de x, juntado a la izquierda por 0xff si x es negativo y cero bytes es x si es no negativo, como len(enc(X)) == 32
  • bool: codificado como uint8 donde 1 es usado como true y 0 como falso
  • bytes<M>: enc(X) es la secuencia de bytes junto con los ceros finales para que len(enc(X)) == 32

También podría mostrarte los tipos de codificadores como fixed y ufixed pero no lo haré, ya que aún no tienen apoyo total en Solidity v0.8.19.

Un ejemplo práctico

Ahora, me gustaría guiarte a través de un ejemplo de cómo puedes leer datos de calldata brutos y decodificarlos manualmente (si te gustaría tener un reto, intenta codificarlos manualmente).

Vamos a tomar el calldata de la función de la Bóveda de Balance, en particular, la de una llamada a su función swap ya que, toma 2 estructuras y 2 uint256s como argumentos. Aquí puedes encontrar el tx que estaremos diseccionando.

Primero que nada, agarraremos el calldata mostrado por Etherscan (el cual hace un muy buen trabajo de darnos el calldata unidos en ranuras de 32 bytes):

Function: swap((bytes32,uint8,address,address,uint256,bytes), (address,bool,address,bool), uint256, uint256)

MethodID: 0x52bbbe29                                               Offset from start of argument encoding block
 00000000000000000000000000000000000000000000000000000000000000e0 0x0000
 0000000000000000000000008d7e58c0ebf988dbb31a993696286106964dd4f4 0x0020
 0000000000000000000000000000000000000000000000000000000000000000 0x0040
 0000000000000000000000008d7e58c0ebf988dbb31a993696286106964dd4f4 0x0060
 0000000000000000000000000000000000000000000000000000000000000000 0x0080
 0000000000000000000000000000000000000000000b3a7f984c82f6ffa3d428 0x00a0
 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 0x00c0
 929a9b6d40e4723f690db77a7ebb65d3254be1e00002000000000000000004d0 0x00e0
 0000000000000000000000000000000000000000000000000000000000000000 0x0100
 0000000000000000000000000000000000000000000000000000000000000000 0x0120
 000000000000000000000000677d4fbbcdd9093d725b0042081ab0b67c63d121 0x0140
 00000000000000000000000000000000000000000000000006f05b59d3b20000 0x0160
 00000000000000000000000000000000000000000000000000000000000000c0 0x0180
 0000000000000000000000000000000000000000000000000000000000000000 0x01a0
Enter fullscreen mode Exit fullscreen mode

Primero, vamos a ejecutar a través de cada palabra de los 32 bytes, desde arriba hacia abajo:

  • De los bytes 0x00 a 0x1f encontramos 0xe0, donde deberíamos encontrar la cabeza del primer argumento, una estructura. ¿Recuerdas lo que esto significa? ¡Significa que al menos, un campo de la estructura es dinámico! De hecho, la primera estructura tiene miembros bytes.

  • En bytes, desde 0x20 a 0x3f, donde deberías encontrar la cabeza de la segunda estructura, encontraremos lo que parece un address. Esto es, de hecho, el primer miembro de la segunda estructura: en las siguientes posiciones, hasta 0x9f, puedes ver todos los otros miembros.

  • En bytes, desde 0xa0 a 0xbs, encontraremos el número hexadecimal 0x0b3a7f984c82f6ffa3d428 el cual es 13574434982555110814766120 en su base decimal: la tercera función del parámetro.

  • En bytes, desde 0xc0 a 0xdf, todos los bytes 0xff: esto quiere decir que el cuarto parámetro era configurar type(uint256).max

Aquí está lo que sabemos hasta ahora:

Image description

Ahora que hemos encontrado 3 de los 4 argumentos, no hay muchos lugares donde el último pueda estar escondido: leyendo el primero de la estructura de la cabeza como un offset, nos conduce a la octava palabra desde el tope, el cual es el primer miembro de la primera estructura, un elemento de bytes32.

Luego de esto, en cada palabra, encontraremos todos los miembros subsecuentes de la estructura, hasta que encontramos un 0xc0 donde el miembro final bytes, debería estar.

A primera vista, esto puede parecer que no tiene mucho sentido, ya que en la palabra, comenzando desde 0xc0 el segundo uint256 es colocado. ¿Cómo?

Lo que resuelve esta confusión, es entender que este offset no debe ser interpretado desde el byte 0x00 del argumento codificado, en cambio, es un offset basado desde donde el miembro de la primera estructura están enumerados así que 0xe0.

Así que, ¿dónde está el miembro bytes? ¡En la palabra comenzando en 0xe0 + 0xc0 = 0x01a0! Ya que es un array de bytes vacío, esta ranura codifica 0 y ninguno de los datos subsecuentes están enumerados.

Aquí está la imágen completa:

Image description

Conclusión

Espero que esto haya sido una lectura interesante para ti, y que hayas aprendido algo nuevo, como yo.

Si quieres seguir e intentar más combinaciones exóticas de tipos (por ejemplo, array de tamaño dinámico de la estructura, los cuales tienen arrays de estructuras que mantienen a los miembros de los bytes), te recomiendo que agarres cast desde la fundición de la cadena de herramientas: invéntate alguna firma azarosa con este tipo de cosas locas y pásalas a través de cast abi-encode y cualquier dato que te guste e intenta completar el ejercicio que hicimos hoy.

¡Chao, nos vemos la próxima!

Discussion (0)