Flujos de tokens, NFT y contratos inteligentes · Lección 1 de 6
Lectura de transferencias de tokens
9 min de lectura
En las cadenas basadas en cuentas, la mayor parte del valor que sigue un analista se mueve como tokens y no como moneda nativa. El saldo de un token es solo un número dentro de un contrato, y el único registro práctico de cómo cambió es el registro de eventos que el contrato decidió emitir. Leer esos registros correctamente y saber cuándo no cuentan toda la historia marca la diferencia entre un análisis de flujos defendible y uno que se equivoca por un factor de un billón.
Los tokens son estado del contrato; las transferencias son eventos
Un token ERC-20 (EIP-20) es un contrato que mantiene una asignación de direcciones a saldos. Cuando alguien «envía USDC», la transacción no se dirige al destinatario: su campo to es el contrato del token (o un enrutador que lo llama), su value nativo suele ser 0, y el destinatario aparece únicamente dentro de los datos de llamada y los registros. Por eso, las pestañas «transacciones» y «transferencias de tokens» del explorador pueden verse completamente distintas para una misma dirección.
El estándar exige que el contrato emita un evento cada vez que se mueven saldos:
event Transfer(address indexed _from, address indexed _to, uint256 _value)
Cada registro contiene la address del contrato que lo emite, hasta cuatro temas y un campo data. El primer tema, topic0, es el hash keccak-256 de la firma canónica del evento Transfer(address,address,uint256): sin nombres de parámetros, sin espacios y sin la palabra clave indexed. Por lo tanto, cada Transfer ERC-20 en todas las cadenas EVM tiene el mismo topic0. Los parámetros marcados como indexed se convierten en los siguientes temas, cada uno rellenado por la izquierda hasta alcanzar 32 bytes; el resto se codifica según ABI en data. Este es un registro sin procesar tal como lo devuelve eth_getLogs (identificadores truncados):
{
"address": "0x7d1f…c3a0",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000a1b2c3d4…9f0e",
"0x000000000000000000000000b0b5e6f7…41aa"
],
"data": "0x0000000000000000000000000000000000000000000000000000000077359400",
"blockNumber": "0x1406f40",
"transactionHash": "0x4f3a…c21e",
"logIndex": "0x57"
}
Decodificación paso a paso: topic0 identifica el evento; topic1 es _from (elimina los 24 caracteres cero iniciales para obtener la dirección de 20 bytes); topic2 es _to; la palabra de datos es _value, 0x77359400 = 2.000.000.000 de unidades base. El bloque es el 21.000.000 y este es el registro número 87 de ese bloque. El registro decodificado se ve así:
{
"event": "Transfer",
"token_contract": "0x7d1f…c3a0",
"from": "0xa1b2c3d4…9f0e",
"to": "0xb0b5e6f7…41aa",
"value_raw": "2000000000",
"decimals": 6,
"value": "2000.000000",
"block_number": 21000000,
"tx_hash": "0x4f3a…c21e",
"log_index": 87
}
Dos precauciones. Primero, la identidad del token es la address del registro, nunca su nombre o símbolo: cualquiera puede desplegar un contrato llamado «USDC» que emita eventos Transfer perfectamente formados. Segundo, una transferencia de NFT ERC-721 tiene el mismo topic0 porque su firma también es Transfer(address,address,uint256); la diferencia es que el ID del token está indexado, por lo que el registro tiene cuatro temas y datos vacíos (consulta la lección NFT y estándares multitoken). Filtra siempre tanto por el número de temas como por topic0.
Los eventos los escribe el propio código del contrato. Un contrato malicioso puede emitir eventos Transfer que no correspondan a ningún cambio de saldo, y las campañas de envenenamiento de direcciones se aprovechan de esto: generan transferencias de valor cero o falsas «desde» una víctima hacia una dirección parecida, para que aparezcan en el historial de la víctima y se copien por error. Las transferencias de valor cero y las transferencias de tokens no incluidos en la lista deben filtrarse o marcarse antes del análisis.
Decimales y unidades
El registro almacena un entero en unidades base. El valor de decimales que lo convierte en unidades legibles para las personas no está en el evento; se obtiene de la función de vista decimals() del contrato, que debes consultar una vez por contrato y guardar junto con su fuente.
| Token (red principal de Ethereum) | Decimales | Valor sin procesar para 1 unidad |
|---|---|---|
| USDC, USDT | 6 | 1.000.000 |
| WBTC | 8 | 100.000.000 |
| DAI, WETH, la mayoría de los tokens | 18 | 1.000.000.000.000.000.000 |
Aplicar 18 decimales a un token con 6 decimales subestima las cantidades por un factor de 10^12; aplicarlos al revés las sobreestima por el mismo factor. Además, el mismo activo puede usar distintos decimales en distintas cadenas, porque las versiones puenteadas y reemitidas son contratos separados. Los valores sin procesar son enteros de 256 bits (hasta 78 dígitos), muy por encima de los enteros de 64 bits y los números de coma flotante de doble precisión, así que guárdalos como cadenas o decimales de alta precisión y divídelos solo en el paso de presentación (consulta la lección Calidad, normalización y procedencia de los datos).
Acuñación, quema y suministro
EIP-20 indica que un contrato que crea tokens debe emitir un Transfer con _from establecido en la dirección cero. La mayoría de las implementaciones sigue la convención inversa para las quemas y emite un Transfer con _to establecido en la dirección cero. Enviar tokens a una dirección «muerta», como 0x…dEaD, no es una quema: el saldo sigue existiendo y el suministro total no cambia, aunque nadie tenga la clave.
No todos los tokens siguen la convención. Algunos emiten eventos Mint y Burn personalizados en lugar de Transfer o además de este. WETH (el contrato WETH9 original) emite Deposit cuando se envuelve ETH y Withdrawal cuando se desenvuelve, sin emitir un evento Transfer, por lo que reconstruir un saldo de WETH solo a partir de los registros Transfer arroja un resultado incorrecto para cualquier dirección que envuelva o desenvuelva tokens. Una conciliación sencilla del suministro permite detectar estos casos:
-- Suministro neto según los eventos Transfer frente a totalSupply() consultado en el mismo bloque
SELECT
SUM(CASE WHEN from_address = '0x0000000000000000000000000000000000000000'
THEN value_raw ELSE 0 END)
- SUM(CASE WHEN to_address = '0x0000000000000000000000000000000000000000'
THEN value_raw ELSE 0 END) AS supply_from_events
FROM token_transfers
WHERE token_contract = '0x7d1f…c3a0'
AND block_number <= 21000000;
La consulta suma las acuñaciones menos las quemas hasta el bloque 21.000.000 en una tabla token_transfers decodificada. Compara el resultado con totalSupply() consultado en ese bloque; una discrepancia significa que el contrato acuña o quema mediante eventos que tu decodificador no lee.
Aprobaciones y transferFrom
ERC-20 permite que un propietario autorice a un gastador con approve(spender, amount), lo que emite Approval(address indexed owner, address indexed spender, uint256 value). Más tarde, el gastador —normalmente un enrutador de DEX o un fondo de préstamos— llama a transferFrom(owner, recipient, amount). En el evento Transfer resultante, from es el propietario cuyos tokens se movieron, no la cuenta que envió la transacción. Los analistas que equiparan «remitente de la transacción» con «origen de los fondos» se equivocan en todas las operaciones mediadas por un enrutador.
Las aprobaciones son importantes en las investigaciones:
- Aprobaciones ilimitadas establecen el valor en 2^256 − 1 (los 64 dígitos hexadecimales
f). Un gastador con una aprobación de este tipo puede mover todo el saldo del propietario en cualquier momento. - Los drenadores de aprobaciones engañan a una víctima para que apruebe a un gastador controlado por un atacante. Con permit (EIP-2612), la víctima solo firma un mensaje fuera de la cadena; el atacante lo envía, paga el gas y el evento Approval aparece en una transacción que la víctima nunca envió.
- Las aprobaciones vinculan direcciones, lo cual resulta útil para agruparlas (consulta la lección Heurísticas del modelo de cuentas).
No siempre es posible reconstruir las autorizaciones a partir de los eventos. Gastar una autorización mediante transferFrom la reduce, pero no todas las implementaciones emiten un nuevo Approval cuando esto ocurre; la biblioteca de OpenZeppelin, ampliamente copiada, dejó de hacerlo en la versión 5.0. Para conocer la autorización actual, llama a allowance(owner, spender) en el bloque de interés.
Tokens que rompen el modelo simple
La regla ordenada «saldo = suma de los valores Transfer entrantes menos los salientes» no se cumple con varios diseños comunes.
| Diseño | Qué ocurre | Qué hacer |
|---|---|---|
| Comisión por transferencia | Una transferencia de 100 entrega, por ejemplo, 98; 2 van a una billetera de comisiones o se queman, a menudo mediante un segundo evento Transfer | Usa la cantidad recibida en el Transfer al destinatario, no la cantidad de los datos de llamada |
| Rebase | Los saldos cambian sin ningún evento Transfer (el suministro se expande o contrae, o se acumulan recompensas) | Consulta balanceOf en distintas alturas de bloque; modela el rebase |
| Basado en participaciones | El contrato lleva el registro de participaciones; los saldos mostrados se derivan de ellas y se redondean hacia abajo por unas pocas unidades base | Lleva el registro de los eventos de participaciones cuando existan |
| Congelable o pausible | El emisor puede bloquear una dirección o quemar su saldo mediante funciones personalizadas | Decodifica también los eventos administrativos del emisor |
El stETH de Lido es un ejemplo conocido de rebase: los saldos de los titulares crecen cuando el oráculo del protocolo informa de las recompensas, sin que se emita ningún Transfer, y las transferencias pueden dejar 1–2 wei debido al redondeo de las participaciones. stETH también emite un evento TransferShares junto a cada Transfer. Su versión envuelta sin rebase, wstETH, se comporta como un ERC-20 común. Recuerda también que los movimientos de ETH nativo dentro de llamadas a contratos no emiten ningún registro; solo aparecen en las trazas (consulta la lección Transacciones, recibos y registros sin procesar de Ethereum).
Rutina de conciliación
Antes de informar sobre los flujos de tokens de una dirección o un contrato, realiza una breve comprobación:
- Identifica el token por la dirección del contrato y el ID de cadena, y registra
decimals(). - Obtén los registros Transfer con el topic0 correcto y tres temas, desde el bloque de despliegue del contrato hasta el bloque de corte.
- Reconstruye los saldos de las direcciones pertinentes.
- Compáralos con
balanceOfconsultado en el bloque de corte, y compara el suministro total contotalSupply(). - Explica cada diferencia (rebase, comisiones, eventos de acuñación personalizados, depósitos al estilo WETH) o indícala como una limitación.
La lección Reconstrucción de saldos e historiales trata con más profundidad las instantáneas de saldos; el punto aquí es que los eventos de tokens son afirmaciones hechas por el código del contrato, y las consultas de estado son la forma de verificarlas.
Enfoque del examen: la firma del evento Transfer y su topic0 (0xddf252ad…); qué parámetros son temas y cuáles son datos; por qué la dirección del contrato en el registro, y no el símbolo, identifica un token; conversión de valores sin procesar con decimales (6 frente a 18) y almacenamiento seguro de enteros de 256 bits; convenciones de acuñación y quema (dirección cero) y por qué Deposit/Withdrawal de WETH y las direcciones muertas las incumplen; approve, transferFrom y Approval, incluidas las aprobaciones ilimitadas y permit; distinción entre Transfer ERC-20 y ERC-721 según el número de temas; tokens con comisión por transferencia y con rebase, y conciliación con balanceOf y totalSupply.