Operaciones entre cadenas, DeFi y contratos inteligentes · Lección 1 de 6
Decodificación de interacciones con contratos inteligentes
9 min de lectura
Toda interacción con un contrato inteligente llega en forma de bytes: una cadena de calldata en la transacción y temas y datos en los registros. Convertir esos bytes en «quién llamó a qué, con qué argumentos y qué ocurrió» es la habilidad central del análisis del modelo de cuentas. Depende de obtener la ABI correcta, para el código correcto, en el bloque correcto, y de saber de dónde provienen los hashes que identifican las funciones y los eventos.
La ABI como clave de decodificación
La interfaz binaria de aplicaciones (ABI) es una descripción en JSON de las funciones, los eventos y los errores de un contrato: sus nombres, los tipos de parámetros, qué parámetros de eventos están indexados y los tipos de retorno. Los compiladores la generan a partir del código fuente, y los exploradores la publican para los contratos verificados. Con la ABI, un decodificador sabe que un prefijo determinado de 4 bytes significa transfer, que los siguientes 32 bytes son una dirección y que los 32 bytes posteriores son una cantidad.
La decodificación utiliza la firma canónica: el nombre seguido de los tipos de parámetros entre paréntesis, separados por comas, sin espacios ni nombres de parámetros. Los alias se expanden (uint se convierte en uint256) y las estructuras se escriben como tuplas, por ejemplo, aggregate3((address,bool,bytes)[]). Un solo espacio adicional o un alias sin expandir produce un hash completamente diferente, lo que constituye la razón más común por la que un decodificador creado manualmente no encuentra nada.
Selectores de función
Un selector de función son los primeros 4 bytes del hash keccak-256 de la firma canónica de la función. keccak256("transfer(address,uint256)") comienza con 0xa9059cbb, por lo que toda llamada a un transfer de ERC-20 comienza con esos 4 bytes. Después del selector, los argumentos se codifican según la ABI en palabras de 32 bytes: los tipos estáticos (address, uint256, bool) ocupan su posición, con relleno a la izquierda; los tipos dinámicos (bytes, string, arrays) dejan un desplazamiento en su posición y colocan su longitud y contenido al final.
a9059cbb selector: transfer(address,uint256)
000000000000000000000000b0b5e6f7…41aa arg 1: dirección del destinatario
0000000000000000000000000000000000000000000000000000000077359400 arg 2: 2.000.000.000 unidades base
Estos datos de calldata envían 2.000.000.000 unidades base, 2.000 tokens si el token tiene 6 decimales, al destinatario. Tenga en cuenta que el destinatario no es el campo to de la transacción, que corresponde al contrato del token.
Cuatro bytes permiten aproximadamente 4.300 millones de valores, y existen colisiones de selectores: tanto burn(uint256) como collate_propagate_storage(bytes16) producen el hash 0x42966c68. Las bases de datos abiertas de firmas (como el directorio 4byte y la búsqueda de firmas de Sourcify/OpenChain) relacionan los selectores con sus firmas textuales, pero pueden devolver varios candidatos para un selector, y cualquiera puede enviar entradas. Dé preferencia a la ABI verificada del contrato que está decodificando; utilice las bases de datos para contratos no verificados y elija entre los candidatos comprobando que los argumentos se decodifiquen correctamente. Un selector que el contrato no reconoce se dirige a su función de respaldo, por lo que las llamadas con un «selector desconocido» pueden aun así realizar alguna acción.
El mismo esquema identifica los datos de reversión, que explican por qué falló una transacción. Un mensaje simple de require se codifica como Error(string), con el selector 0x08c379a0, seguido del texto codificado según la ABI; las comprobaciones insertadas por el compilador, como las de desbordamiento aritmético, utilizan Panic(uint256), con el selector 0x4e487b71 y un código numérico; y los contratos pueden definir errores personalizados, cuyos selectores se derivan exactamente igual que los selectores de función y aparecen en la ABI. Decodificar los motivos de reversión de los intentos fallidos de un atacante antes de una explotación exitosa suele mostrar qué estaba probando.
Firmas de eventos y temas
Los eventos utilizan el hash completo de 32 bytes de la firma canónica del evento como topic0. Un evento no anónimo puede tener hasta tres parámetros indexados en topic1 a topic3. Los eventos anónimos omiten topic0, lo que libera una cuarta posición indexada, pero los hace invisibles para los filtros que buscan por firma. Los parámetros indexados de tipo dinámico (string, bytes, arrays) se almacenan como el hash keccak-256 del valor, por lo que el valor original no puede recuperarse a partir del tema.
Un conjunto de referencia de valores de topic0, verificados mediante el cálculo de los hashes de las firmas:
Transfer(address,address,uint256) 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Approval(address,address,uint256) 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
ApprovalForAll(address,address,bool) 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31
TransferSingle(address,address,address,uint256,uint256) 0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62
TransferBatch(address,address,address,uint256[],uint256[]) 0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb
Swap(address,uint256,uint256,uint256,uint256,address) 0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822
Swap(address,address,int256,int256,uint160,uint128,int24) 0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67
Upgraded(address) 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b
Las dos líneas de Swap son los eventos del par de Uniswap v2 y del fondo de liquidez de Uniswap v3 (consulte la lección Swaps en DEX, exchanges instantáneos y flujos de liquidez). De la tabla se desprenden dos enseñanzas. En primer lugar, topic0 indica el tipo de evento y el campo address del registro indica qué contrato lo emitió; un filtro que solo utiliza topic0 devuelve eventos de todos los contratos que usan la firma, incluidos los falsos. En segundo lugar, los eventos con los mismos tipos, pero con una indexación diferente, comparten un topic0, como ocurre con Transfer de ERC-20 y ERC-721, por lo que debe comprobar el número de temas antes de decodificar.
Contratos proxy y búsqueda de la implementación
Muchos contratos son proxies: un contrato pequeño que mantiene el almacenamiento y los saldos y reenvía cada llamada, mediante delegatecall, a un contrato de implementación que contiene la lógica. Actualizar significa dirigir el proxy a una nueva lógica. Para un analista, esto tiene tres consecuencias:
- La ABI que necesita es la de la implementación, no la del proxy.
- Los registros se emiten con la dirección del proxy, porque el código se ejecuta en el contexto del proxy. La identidad de un token sigue siendo la dirección del proxy a través de las actualizaciones.
- La implementación puede cambiar con el tiempo, por lo que debe decodificar con la implementación que estaba activa en el bloque de la transacción.
EIP-1967 estandarizó dónde almacenan los proxies estos punteros, para que las herramientas puedan encontrarlos. Cada posición de almacenamiento es el keccak-256 de una cadena fija menos 1:
| Propósito | Cadena a la que se aplica el hash | Posición de almacenamiento |
|---|---|---|
| Implementación | eip1967.proxy.implementation | 0x3608…2bbc |
| Administrador | eip1967.proxy.admin | 0xb531…6103 |
| Beacon | eip1967.proxy.beacon | 0xa3f0…3d50 |
Leer la posición de almacenamiento de la implementación en un bloque determinado requiere una llamada JSON-RPC:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getStorageAt",
"params": [
"0x9c4e…1d27",
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"0x1406f40"
]
}
La respuesta es una palabra de 32 bytes, por ejemplo, 0x000000000000000000000000e5a1…7c30; los últimos 20 bytes son la dirección de la implementación en el bloque 21.000.000. Las lecturas históricas requieren un nodo de archivo (consulte la lección Bloques, cabeceras y API de nodos). El proxy también emite Upgraded(address indexed implementation) en cada cambio, por lo que obtener esos registros proporciona la cronología completa de actualizaciones:
{
"address": "0x9c4e…1d27",
"topics": [
"0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b",
"0x000000000000000000000000e5a1…7c30"
],
"data": "0x",
"blockNumber": "0x13d6200",
"decoded": {"event": "Upgraded", "proxy": "0x9c4e…1d27", "implementation": "0xe5a1…7c30", "block": 20800000}
}
El campo address del registro es el proxy, y su único parámetro indexado es la nueva implementación. Aquí, la implementación leída en el bloque 21.000.000 se instaló en el bloque 20.800.000; las transacciones anteriores a ese bloque deben decodificarse con la ABI de la implementación anterior.
Otros patrones requieren otras búsquedas:
- Los proxies transparentes y UUPS (del estilo EIP-1822, según la implementación de OpenZeppelin) utilizan ambos la posición de almacenamiento de implementación de EIP-1967; se diferencian en dónde reside la función de actualización.
- Los proxies beacon almacenan una dirección de beacon; llame a
implementation()en el beacon. - Los proxies mínimos (clones EIP-1167) incorporan de forma fija la dirección de la implementación en su bytecode de ejecución de 45 bytes, entre el prefijo
363d3d373d3d3d363d73y el sufijo5af43d82803e903d91602b57fd5bf3. - Los proxies multifirma de Safe mantienen su implementación (el singleton, históricamente llamado
masterCopy) en la posición de almacenamiento 0, no en una posición de EIP-1967. El robo a Bybit de febrero de 2025 sobrescribió exactamente esa posición (consulte la lección Análisis de exploits y hackeos). - Los diamantes (EIP-2535) dirigen cada selector a un contrato de faceta diferente; necesita la tabla de correspondencias entre selectores y facetas en ese bloque.
Código fuente verificado
Un explorador o el repositorio abierto Sourcify marca un contrato como verificado cuando la compilación del código fuente publicado con la configuración indicada reproduce el bytecode desplegado. Sourcify distingue una coincidencia completa (exacta), en la que incluso coincide el hash de los metadatos incorporados, de una coincidencia parcial, en la que solo coincide el código ejecutable. La verificación demuestra que el código fuente corresponde al bytecode. No demuestra que el código sea seguro, esté auditado o sea honesto, y el nombre del contrato en el código fuente verificado lo elige quien lo despliega. Para los contratos no verificados, los analistas combinan búsquedas de selectores, resultados de descompiladores y comportamiento observado en las trazas, y reducen su nivel de confianza en consecuencia.
Multicall y llamadas por lotes
El procesamiento por lotes oculta acciones detrás de una única llamada externa. Formas comunes:
- Contratos Multicall, como
aggregate3((address,bool,bytes)[])de Multicall3 (selector 0x82ad56cb), que ejecuta una lista de llamadas. multicall(bytes[])(0xac9650d8) ymulticall(uint256,bytes[])(0x5ae401dc, con una fecha límite) de un enrutador, donde cada elemento contiene calldata para otra función del mismo enrutador.- Lotes multifirma (MultiSend de Safe) y lotes de cuentas inteligentes (
handleOpsde ERC-4337, cuentas delegadas de EIP-7702).
El selector externo solo indica «lote». Decodifique de forma recursiva: decodifique la llamada externa, tome cada elemento bytes interno, lea su selector y decodifíquelo con la ABI del destino. Después, contraste los resultados con la traza, el árbol de llamadas que muestra el destino, el valor y el éxito de cada llamada interna, porque un lote puede permitir que fallen llamadas individuales. Los registros de todas las llamadas internas aparecen en un único recibo, ordenados por logIndex, lo que los vincula con los pasos. La lección Consulta de datos UTXO y registros y trazas de EVM muestra cómo hacerlo a escala.
Enfoque del examen: firmas canónicas (sin espacios, uint256, tuplas); derivación de un selector de función (primeros 4 bytes de keccak-256, por ejemplo, 0xa9059cbb para transfer) y lectura de palabras de calldata; colisiones de selectores y cómo elegir entre candidatos de bases de datos; topic0 como el hash completo de la firma del evento, límites de parámetros indexados, eventos anónimos y temas dinámicos almacenados como hashes; proxies: los registros llevan la dirección del proxy, la posición de almacenamiento de implementación de EIP-1967 (keccak-256 de "eip1967.proxy.implementation" menos 1), eth_getStorageAt en un bloque, eventos Upgraded, patrones beacon, mínimos y de Safe con posición de almacenamiento 0; qué demuestra y qué no demuestra la verificación; decodificación recursiva de lotes multicall y confirmación con trazas.