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.
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:
- La función que llamamos, especificada por la función de selector
- 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:
-
transferFrom(address,address,uint256)
es la firma del notorio método ERC20. -
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:
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 binariaa
-
enc
, el codificador actual, como el que mapea los valores ABI a la cadena binaria, tal quelen(enc(a))
depende del valor dea
si, y sólo sia
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)
parak >= 0
y los tiposT1
, ..,TK
: la codificación está hecha de elementos de la “cabeza”K
y elementos de la ”cola”k
, comoenc(X) = head(X(1)) .. head(X(k)) tail(X(1)) .. tail(X(k))
dondeX = (X(1), .. , X(k))
yhead
ytail
son definidos porTi
como:- Para la estática
Ti
:head(X(i)) = enc(X(i))
ytail(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)) ))
ytail(X(i)) = enc(X(i))
(el cual es una forma complicada de decir que, donde tú encontrarías el codificado dex
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 la estática
- Para arrays de tamaño fijo
T[k]
para cualquierT
yk
:enc(X) = enc((X[0], .., X[k-1]))
, es decir, está codificado como una tupla con elementosk
del mismo tipo - Para arrays de tamaño fijo
T[]
dek
:enc(X) = enc(k) enc((X[0], .. , X[k-1]))
, es decir, está codificado una tupla con elementosk
del mismo tipo, ¡prefijados con el número de elementos! -
bytes
de la longitud dek
: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 comobytes
-
uint<M>
:enc(X)
es la codificación big endian dex
, juntado a la izquierda comolen(enc(X)) == 32
-
int<M>
:enc(X)
el big endian es el complemento de los dos codificadores dex
, juntado a la izquierda por0xff
six
es negativo y cero bytes esx
si es no negativo, comolen(enc(X)) == 32
-
bool
: codificado comouint8
donde1
es usado comotrue
y0
como falso -
bytes<M>
:enc(X)
es la secuencia de bytes junto con los ceros finales para quelen(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
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:
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:
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)