El 29 de octubre de 2026, a las 20:20:59 UTC, la hot wallet de Binance conocida como Binance 14 dejó de ser una cuenta normal de Ethereum y pasó a ejecutar el código de otro. Tres minutos y veinticuatro segundos después, ese código sacó de la cuenta 167.464 ETH y 1.615 millones de USDT en una sola transacción. La cuenta siguió recibiendo depósitos durante casi cuatro horas, y cada vez que entraba algo se volvía a barrer. Binance recuperó el control a las 00:10:35 del día 30.

He pasado el día siguiente reconstruyendo lo que ocurrió con la cadena como única fuente: Blockscout, un nodo RPC público y un desensamblador. Lo que cuento aquí separa en todo momento lo que he comprobado de lo que dicen terceros, porque en las primeras 24 horas de un incidente así circula de todo, y parte de lo que circula es falso.

Qué he comprobado y qué no

Todo lo que lleva hash o dirección en este artículo lo he verificado contra la red principal de Ethereum entre las 12:00 y las 18:00 UTC del 30 de octubre, con tres herramientas públicas y sin clave de API:

  • La API v2 de Blockscout (https://eth.blockscout.com/api/v2/) para transacciones, listas de autorizaciones y transferencias.
  • El RPC público https://ethereum-rpc.publicnode.com para eth_getCode, eth_call, eth_getBlockByNumber y eth_getLogs. No es un nodo de archivo, así que no sirve para preguntar por el estado de una cuenta en un bloque antiguo.
  • pyevmasm para desensamblar el contrato. Con Foundry, cast disassemble hace lo mismo.

Lo que no he podido verificar va atribuido a quien lo dice. Las cifras totales del robo (se han publicado 5.300, 6.300 y 6.400 millones de dólares), lo ocurrido en las otras nueve cadenas que se mencionan, la vía de entrada (Binance habla del portátil de un ingeniero de DevOps) y la atribución a Lazarus (la hacen TRM Labs y Chainalysis). Nada de eso está en la cadena de Ethereum, y yo solo he mirado la cadena de Ethereum.

Los precios en dólares son los de Blockscout a la hora de escribir esto (ETH a 2.225 $). Ayer, cuando salieron los fondos, valían más.

Las direcciones

Papel Dirección
Víctima: Binance 14 0x28C6c06298d514Db089934071355E5743bf21d60
Operador 0x7C8601d4645b93cea45F71e118A729332e6B5CA0
Sweeper (contrato delegado) 0x63e842B054Bd624a465454D533C33F573e9c6fAe
Receptor 1 0x5227f603C1eE34CE3C06eFe6477003BDfCCB2f8c
Receptor 2 0xA97bF7836F8A08F9Ed91D41453D371493FAE8d10

Etiquetas como “Binance 14” son de servicios de explorador, no de la propia cuenta. Las uso porque son las que vas a ver en Etherscan, pero conviene recordar que no vienen firmadas por nadie.

EIP-7702 en cinco minutos

Hasta mayo de 2025, en Ethereum había dos tipos de cuenta que no se mezclaban. Las cuentas externas (EOA) tienen clave privada y no tienen código; los contratos tienen código y no tienen clave. Si tu clave firmaba algo, ese algo era una transacción, y la transacción hacía una cosa y se acababa.

EIP-7702, activado en la red principal con el fork Pectra el 7 de mayo de 2025, rompe esa separación a propósito. Una EOA puede firmar una autorización que dice: cuando alguien llame a mi dirección, ejecuta el código del contrato X como si fuera mío. El mecanismo tiene cuatro piezas:

  1. Un nuevo tipo de transacción, el 0x04, con un campo extra: authorization_list.
  2. Cada autorización es una tupla [chain_id, address, nonce, y_parity, r, s]. La firma es de la EOA que delega, no de quien envía la transacción. Quien paga el gas y quien delega pueden ser dos cuentas distintas. Aquí lo fueron.
  3. Al procesarla, el nodo escribe en la cuenta de la EOA un designador de delegación: los bytes 0xef0100 seguidos de la dirección del contrato. Desde ese momento, cualquier llamada a la EOA ejecuta ese código con el almacenamiento, el saldo y la identidad de la EOA. Dentro del contrato, msg.sender es la propia EOA.
  4. La delegación no caduca. Se queda hasta que la EOA firme otra autorización que apunte a otra dirección, o a la dirección cero para revocarla.

El punto 4 es el que convierte una firma en una puerta permanente. Una transacción firmada por error se ejecuta una vez. Una autorización 7702 firmada por error le da a quien controle el contrato delegado una llave de la cuenta que ya no depende de volver a usar la clave privada. Mientras la delegación siga puesta, el barrido se repite con transacciones normales que paga cualquiera.

La función se diseñó para cosas buenas: que una wallet de usuario pueda pagar su propio gas en tokens, agrupar varias acciones en una, o poner un límite de gasto, sin migrar a una smart account. Aquí se usó para lo contrario.

La transacción de tipo 4: 50 firmas, una válida

La delegación entró en el bloque 26.300.753, a las 20:20:59 UTC, en la transacción 0xad99264d5532ae0dd04b2004eebda05f25657e70dacb764f7ac8d4ccd051298b. La envió el operador 0x7C86…5CA0, que pagó el gas, y lleva 50 autorizaciones. Las 50 delegan en el mismo sweeper 0x63e842…6fAe, y las 50 están firmadas por la misma cuenta: Binance 14.

La estructura, por campos, es esta (una EOA delegando en un contrato tiene esta forma):

Transacción tipo 0x04 (EIP-7702)
├── chain_id, nonce, max_priority_fee, max_fee, gas_limit
├── destination = 0x28C6…1d60   (la propia Binance 14)
├── value = 0, data = 0x
└── authorization_list = [
      { chain_id: 1, address: 0x63e842…6fAe, nonce: 18914775, y_parity, r, s },
      { chain_id: 1, address: 0x63e842…6fAe, nonce: 18914776, ... },
      ...  48 más, nonces consecutivos ...
      { chain_id: 1, address: 0x63e842…6fAe, nonce: 18914824, ... }
    ]

Los 50 nonces van seguidos, del 18.914.775 al 18.914.824. Al recuperar el firmante de cada firma, Blockscout devuelve el campo authority y en las 50 es la misma: 0x28C6…1d60. Esto confirma dos cosas que se habían publicado como rumor y que ahora puedo dar por comprobadas: eran 50 autorizaciones y los nonces eran esos.

El truco de meter 50 está en cómo valida una autorización el protocolo. El nonce de cada tupla tiene que coincidir exactamente con el nonce actual de la cuenta en el momento de procesarla. Si no coincide, esa autorización se descarta sin abortar la transacción: el resto se siguen intentando. El atacante no sabía con certeza en qué nonce estaría Binance 14 cuando su transacción minara, porque es una hot wallet con tráfico constante y el nonce sube solo. Así que firmó una ventana de 50 nonces alrededor del valor esperado y mandó las 50 juntas. La que acertara, entraba.

Acertó la del nonce 18.914.809, la número 35 de la lista. En el detalle de la transacción se ve el estado por autorización: la 18.914.809 sale ok y las otras 49 salen invalid_nonce. Una basta: la cuenta quedó delegada.

El coste en gas lo confirma. La transacción consumió 1.258.562 de gas. El intrínseco de 50 autorizaciones es 21.000 + 50 × 25.000 = 1.271.000, y cada autorización que resulta ser de una cuenta ya existente devuelve un reembolso de 12.500. Es decir, 1.271.000 − 12.500 = 1.258.500, casi clavado a lo observado. El gas dice, él solo, que se procesaron 50 autorizaciones y que 49 eran de nonce repetido.

Que las 50 firmas salgan de la clave de Binance 14 es el hecho duro. Lo que ese hecho implica, no. La cadena no distingue una firma hecha por un atacante que controla la clave de una hecha por la propia Binance; solo ve que la firma es válida. Que fuera un robo lo dice Binance, no la cadena. Si lo fue, 50 firmas consecutivas con nonces correlativos apuntan a que alguien tuvo acceso a la clave o al flujo de firma que la usa, no a un fallo del contrato ni de EIP-7702. El protocolo hizo exactamente lo que define: ejecutar una delegación firmada por su dueña.

El ensayo general estuvo a la vista horas antes. A las 13:51 y 14:19 el operador hizo dos delegaciones de tipo 4 de una sola autorización sobre dos cuentas de prueba y las barrió. A las 16:20 repitió sobre una tercera, probando esta vez también withdrawETH. El sweeper definitivo lo desplegó a las 15:59, en la transacción 0x97408e75…. El gas del operador salió del pool de 0,1 ETH de Tornado Cash (0x12D66f87…, etiquetado TornadoCash_Eth_01), en tres retiradas de 0,098 ETH los días 28 y 29.

El sweeper, desde el bytecode

El contrato 0x63e842…6fAe tiene 1.674 bytes de runtime. Lo saqué con eth_getCode y lo desensamblé con pyevmasm. No hace falta escribir un desensamblador: la librería existe y resuelve esto en diez líneas.

from pyevmasm import disassemble_all
code = bytes.fromhex(open("sweeper.hex").read()[2:])
for ins in disassemble_all(code):
    print(f"{ins.pc:04x}: {ins}")

El dispatcher de arranque compara los cuatro primeros bytes del calldata (el selector) con cinco funciones:

001c: CALLDATALOAD
001d: PUSH1 0xe0
001f: SHR                      ; selector = calldata[0:4]
0021: PUSH4 0x01e33667  EQ ... ; withdrawToken(address,address,uint256)
002c: PUSH4 0x043ff9d2  EQ ... ; callExecute(address,address,uint256,bytes)
0037: PUSH4 0x4782f779  EQ ... ; withdrawETH(address,uint256)
0042: PUSH4 0x8a179be4  EQ ... ; sweep(address,address[])
004c: PUSH4 0x97dc97cb  SUB... ; authorizedCaller()

Los cinco selectores resuelven en 4byte y openchain sin ambigüedad. Uno de ellos, 0x043ff9d2, no está en 4byte pero sí en openchain: callExecute(address,address,uint256,bytes).

El control de acceso es lo primero que hace cada función con efectos. Esta secuencia aparece idéntica al entrar en sweep, withdrawETH, withdrawToken y callExecute:

0088: CALLVALUE  PUSH2 0x83  JUMPI     ; si envías ETH con la llamada, revert
...
00e2: PUSH20 0x7c8601d4645b93cea45f71e118a729332e6b5ca0
00f7: CALLER                           ; msg.sender
00f8: SUB                              ; authorizedCaller - msg.sender
00f9: PUSH2 0x2e7  JUMPI               ; si no es cero, salta a revert

La dirección del operador 0x7C86…5CA0 está incrustada en el bytecode como constante. El único que puede llamar a cualquier función con efectos es esa cuenta. Es el onlyAuthorized de toda la vida, pero con la dirección quemada en el código en lugar de en una variable de almacenamiento, así que no se puede cambiar. La vista authorizedCaller() (selector 0x97dc97cb) devuelve esa misma constante desde PUSH20 … MSTORE RETURN, sin pasar por el control de acceso.

Hay receive (acepta ETH sin datos, el STOP en 0x0019 tras comprobar que el calldata está vacío) y no hay fallback con lógica: cualquier selector que no sea de los cinco cae en INVALID. No hay SELFDESTRUCT, no hay DELEGATECALL, no hay almacenamiento: todo son constantes y calldata. Es un contrato de usar y tirar, sin estado.

sweep(address to, address[] tokens) es el corazón. Recorre la lista de tokens:

  • Si el token es la dirección cero, trata el elemento como ETH: lee SELFBALANCE, y si pasa de 1 ether (0xde0b6b3a7640000) envía el saldo menos 1 ether con un CALL. Deja siempre 1 ETH en la cuenta; el bytecode no dice para qué.
  • Si es un token normal, hace staticcall a balanceOf(address(this)) (selector 0x70a08231), y si el saldo es mayor que cero llama a transfer(to, saldo) (selector 0xa9059cbb).
  • Si una transferencia falla, no aborta el barrido: emite un evento con un código (vi LOG2 con el mismo topic 0x394e1e4b… en las ramas de fallo) y sigue con el siguiente token. Por eso un solo sweep puede mover 65 activos aunque tres de ellos reviertan.

Que el barrido tolere fallos es lo que lo hace peligroso: le pasas una lista larga de tokens, barre todos los que puede y no se atasca con los que no. La reconstruí como pseudo-Solidity para leerla de un vistazo:

function sweep(address payable to, address[] calldata tokens) external onlyAuthorized {
    for (uint256 i = 0; i < tokens.length; i++) {
        address token = tokens[i];
        if (token == address(0)) {
            uint256 bal = address(this).balance;
            if (bal > 1 ether) { (bool ok,) = to.call{value: bal - 1 ether}(""); /* si falla, emite evento y sigue */ }
        } else {
            (bool s, bytes memory r) = token.staticcall(abi.encodeCall(IERC20.balanceOf, (address(this))));
            if (!s || r.length < 32) continue;                 // token raro: lo salta
            uint256 bal = abi.decode(r, (uint256));
            if (bal > 0) token.call(abi.encodeCall(IERC20.transfer, (to, bal)));  // si falla, sigue
        }
    }
}

callExecute es el otro mecanismo, y el que explica los dos movimientos de USDC. Comprueba que el destino tenga código (EXTCODESIZE, si es cero revierte con NoCodeAtExecutor) y le reenvía una llamada execute(address,uint256,bytes) (selector 0xb61d27f6). Es el patrón de ejecución de las smart accounts estilo ERC-4337. Binance 14, ya con el código del sweeper puesto, lo usó para ordenar a una smart account suya que soltara el USDC. Volveré a eso enseguida.

Un detalle sobre la “fuente” del contrato. Blockscout muestra un Target.sol verificado, pero el propio fichero avisa en su cabecera de que es una reconstrucción hecha por IA a partir del bytecode a las 00:30 UTC del 30 de octubre, no el código original. El runtime compila byte a byte igual, así que la lógica es fiable; los nombres de contrato y parámetros son suposiciones. Lo que cuento arriba sale del desensamblado, no de esa fuente.

Cómo se vaciaban las recargas

Binance 14 es una hot wallet de recarga: recibe depósitos de clientes a todas horas. El atacante no la vació una vez, montó un bucle de barrido. Mirando el ritmo de salidas de la cuenta por franjas de diez minutos:

20:20  38 salidas   (empieza la delegación, primer sweep)
20:30  11
20:40   1
...
22:50  18
23:00  28
00:00   1
00:10   1           (revocación a las 00:10:35)
00:20 157           (ya controlada por Binance, salidas legítimas de recarga)

Entre el primer barrido grande y la revocación, el operador lanzó más sweep según iba entrando dinero: a las 21:04, 21:44, 22:39 y 23:56, y cinco barridos en vacío a partir de las 23:36 cuando ya no quedaba nada que sacar (23.100 de gas cada uno, el coste de recorrer una lista y no transferir nada).

El caso más claro del bucle es la transferencia de 200 millones de USDT. Entre el barrido de las 21:04 y el de las 21:44 entraron a Binance 14, vía logs Transfer de USDT, 200,4 M USDT en 95 transferencias, 200 M de ellos en una sola desde 0x22132139bf7f3921b1cadeab931f4fbf7bf2bc9e. A las 21:44 el sweep 0x55ba4dab… se los llevó. Es decir: dinero que llegó a la cuenta de Binance ya comprometida, y que salió por la puerta trasera minutos después de entrar.

El flujo de fondos

Agregando las transferencias de los siete barridos con botín (a precios de hoy):

Activo Cantidad Valor aprox.
USDT 1.819 M ~1.818 M $
USDC 823 M ~823 M $
ETH (barridos) 172.624 ETH ~384 M $
WBETH 29.934 ~73,6 M $
RLUSD 62,6 M ~62,6 M $
XAUT (oro tokenizado) 11.374 ~44,4 M $
UNI 6,2 M ~43,2 M $
ENA, PEPE, USD1, LINK, AAVE… varios decenas de M $ cada uno

Solo lo que he podido sumar en Ethereum, a la cotización de hoy, pasa de 3.200 millones de dólares, y eso dejando fuera buena parte de los 122 tokens menores del último barrido. Con los precios de ayer la cifra sube. No llego ni valido los 5.300-6.400 M que se publican como total multi-cadena: esos incluirían las otras nueve redes, que no he mirado.

El reparto por destino:

  • Receptor 1 (0x5227f6…2f8c): el grueso. USDT, USDC, los 167.464 ETH del primer barrido, y los 823 M USDC que salieron por callExecute.
  • Receptor 2 (0xA97bF7…8d10): los 122 tokens menores y los últimos 2.962 ETH.

Los dos receptores son EOA sin código (eth_getCode devuelve 0x en ambos). Desde ahí el dinero no se quedó quieto:

  • El ETH se consolidó. El receptor 1 mandó 195.460 ETH a 0x8532FA14… a las 20:38, 2.000 ETH a esa misma dirección a la 01:14 y 17.260 ETH a 0x03b0eE5d…. El receptor 2 mandó 3.000 ETH a 0xc2096a12… y 7.647 ETH a 0x54401f80…. Esas direcciones acumulan el ETH y hoy siguen con saldo: 197.460, 40.600, 17.260 ETH. No se ha movido a ningún mezclador ni puente desde ahí, al menos en lo que he mirado.
  • Los 28.005 ETH de Binance 16 entran en la foto. A las 20:24:47, cuatro minutos después del primer barrido, la hot wallet Binance 16 (0xDFd5293D…) mandó 28.005,6 ETH al receptor 1 en una transacción normal de tipo 2, firmada con su propia clave, nonce 15.703.491. No es una delegación 7702, es una transferencia corriente. Que una segunda hot wallet de Binance mande ETH a la cuenta del atacante con una transacción normal encaja con que el compromiso no fuera solo de Binance 14, sino del sistema que firma por varias cuentas. Pero eso ya es inferencia: la cadena solo prueba que la transferencia salió de Binance 16 firmada por Binance 16.
  • Los stablecoins empezaron a convertirse. El receptor 1 cambió tokens por ETH en Uniswap (selector 0x791ac947, el swapExactTokensForETHSupportingFeeOnTransferTokens del router V2 0x7a250d56…) en cientos de operaciones, y movió USDT en bloques de 400-500 M a otras EOA. El receptor 2 hizo lo mismo con el router V3.

Y aquí aparece el límite de este tipo de ataque cuando lo que robas son stablecoins centralizadas. Al consultar las listas negras de los emisores, el receptor 1 ya está bloqueado en USDT y en USDC, y el receptor 2 en USDC:

# isBlackListed(address) en USDT  -> selector 0xe47d6060
# isBlacklisted(address) en USDC  -> selector 0xfe575a87
curl -s -X POST https://ethereum-rpc.publicnode.com \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
       "to":"0xdAC17F958D2ee523a2206206994597C13D831ec7",
       "data":"0xe47d6060000000000000000000000000'\
'5227f603c1ee34ce3c06efe6477003bdfccb2f8c"},"latest"]}'
# -> 0x...01  (bloqueado)

Tether y Circle congelan a golpe de eth_call: una vez una dirección está en la lista negra, sus USDT o USDC quedan inmóviles, no se pueden transferir. Eso explica la prisa por cambiar a ETH en Uniswap durante las horas siguientes. El ETH, DAI y los tokens descentralizados no se pueden congelar; los 823 M de USDC y buena parte del USDT que no llegaron a convertirse a tiempo, sí. Una parte del botín, en la práctica, ya es papel mojado para el atacante.

La revocación

A las 00:10:35 del 30 de octubre, Binance 14 se envió a sí misma la transacción de tipo 4 0xafbf94c9…: una sola autorización que delega en 0x0000000000000000000000000000000000000000. Delegar en la dirección cero es la forma canónica de EIP-7702 de borrar la delegación. Diez minutos después, las salidas de la cuenta vuelven a dispararse (157 en la franja de las 00:20), que son ya retiradas legítimas de recarga. Hoy eth_getCode sobre Binance 14 devuelve 0x: la cuenta ha vuelto a ser una EOA sin código.

Que Binance pudiera revocar significa que para las 00:10 ya controlaba de nuevo la clave de Binance 14 (la revocación la firma la propia cuenta). El agujero estuvo abierto 3 horas y 50 minutos. Eso es mucho tiempo para un incidente de este tamaño, y la pregunta de por qué tardó casi cuatro horas en cerrarse es, seguramente, la más incómoda para Binance.

Lo que no se sabe

Con la cadena delante, estas preguntas se quedan sin respuesta y conviene no inventarlas:

  • Cómo llegó el atacante a firmar con la clave. La cadena prueba que las 50 firmas son de Binance 14, no cómo se consiguieron. Binance apunta a un portátil de DevOps comprometido. Es su versión, y es plausible, pero no la puedo verificar on-chain.
  • Si Binance 16 estaba comprometida igual o solo se usó como paso. La transferencia de 28.005 ETH es normal y firmada por Binance 16, nada más.
  • El total real y las otras cadenas. Mis 3.200 M son solo Ethereum y solo lo que pude sumar. Las cifras de 5.300 a 6.400 M vienen de firmas de análisis y de la propia Binance.
  • La atribución. Lazarus la dicen TRM Labs y Chainalysis. El uso de Tornado Cash para financiar el gas y el patrón de consolidación son compatibles con grupos estatales, pero “compatible con” no es “es”. El uso de 7702 con autorizaciones en bloque, en concreto, se ha visto este año en drenadores automáticos de wallets de a pie, no solo en operaciones grandes.

Lecciones para exchanges y custodios

El protocolo funcionó como está escrito. El fallo está aguas arriba, en cómo una organización firma y en qué vigila. Lo que este caso deja claro:

EIP-7702 convierte una firma robada en acceso persistente. Antes, robar una firma te daba una transacción. Ahora, robar una autorización 7702 te da la cuenta hasta que alguien la revoque. El modelo de amenaza de cualquiera que custodie claves cambió el 7 de mayo de 2025, lo supiera o no.

La política de firma tiene que rechazar el tipo 4 por defecto. Una cuenta de recarga no tiene ningún motivo legítimo para firmar una autorización de delegación. El HSM o el servicio de firma debe negarse a firmar transacciones de tipo 0x04 y tuplas de autorización 7702 salvo en una lista blanca explícita y revisada. Si el flujo de firma de Binance hubiera rechazado el tipo 4 para sus hot wallets, estas 50 firmas no habrían existido.

Hay que monitorizar el código de las EOA. Una hot wallet que de repente tiene código (eth_getCode deja de devolver 0x) es una alarma de máxima prioridad. Es barato: un cron que consulte eth_getCode de cada cuenta caliente cada pocos segundos y avise si cambia. Esto es lo que recorta las 3 horas y 50 minutos a minutos. Una regla de detección mínima:

import json, subprocess

RPC = "https://ethereum-rpc.publicnode.com"
HOT_WALLETS = ["0x28C6c06298d514Db089934071355E5743bf21d60", ...]

def code_of(addr):
    body = {"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":[addr,"latest"]}
    out = subprocess.run(["curl","-s","-X","POST",RPC,
                          "-H","content-type: application/json",
                          "-d",json.dumps(body)], capture_output=True, text=True).stdout
    return json.loads(out)["result"]

for w in HOT_WALLETS:
    code = code_of(w)
    # EIP-7702: una EOA delegada empieza por el designador 0xef0100
    if code != "0x":
        delegate = "0x" + code[8:] if code.startswith("0xef0100") else None
        alert(f"{w} tiene código: {code[:20]}… delegado en {delegate}")

Quien quiera algo más fino puede escuchar con eth_getLogs el evento que Pectra no emite (no hay log nativo de delegación) y reconstruirlo mirando las transacciones de tipo 4 cuyo authority sea una de sus cuentas; pero el eth_getCode periódico ya caza el 100 % de los casos, porque la delegación deja huella en el estado.

El tiempo de respuesta importa tanto como la prevención. El atacante montó un bucle de barrido precisamente porque sabía que la cuenta seguiría recibiendo depósitos. Cada minuto que la delegación estuvo puesta, llegó y salió dinero nuevo. Un corte de recargas automático al detectar código en una hot wallet habría parado la sangría aunque no se revocara al instante.

Los stablecoins centralizados son el punto débil del atacante, no el tuyo. Que Tether y Circle hayan congelado los receptores en horas es la mejor noticia del incidente. Si tu exchange tiene relación con los emisores, la ventana para pedir un congelado se mide en minutos, y cada minuto cuenta antes de que el atacante cambie a ETH en un DEX.

Cómo reproducir las comprobaciones

Todo esto se verifica sin clave de API. La delegación:

curl -s https://eth.blockscout.com/api/v2/transactions/\
0xad99264d5532ae0dd04b2004eebda05f25657e70dacb764f7ac8d4ccd051298b \
  | python3 -c 'import json,sys; t=json.load(sys.stdin); al=t["authorization_list"]; \
print("autorizaciones:",len(al)); \
print("ok:",[a["nonce"] for a in al if a["status"]=="ok"])'
# autorizaciones: 50
# ok: [18914809]

El bytecode del sweeper y su desensamblado:

curl -s -X POST https://ethereum-rpc.publicnode.com \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
       "params":["0x63e842B054Bd624a465454D533C33F573e9c6fAe","latest"]}' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"])' > sweeper.hex
python3 -c 'from pyevmasm import disassemble_all; \
print("\n".join(str(i) for i in disassemble_all(bytes.fromhex(open("sweeper.hex").read()[3:]))))'

Y que Binance 14 ya no está delegada:

curl -s -X POST https://ethereum-rpc.publicnode.com \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
       "params":["0x28C6c06298d514Db089934071355E5743bf21d60","latest"]}'
# {"result":"0x"}

Conclusión

No hubo ningún fallo en Ethereum, ni en EIP-7702, ni siquiera en el contrato del atacante, que es de una sencillez casi insultante: 1.674 bytes, cinco funciones, cero estado. Lo que falló fue el eslabón de siempre, la custodia de la clave, pero con una diferencia nueva que cambia el cálculo. Desde Pectra, una sola firma arrancada a una hot wallet ya no vale para una transacción, vale para toda la cuenta y durante el tiempo que tarden en darse cuenta.

El truco de las 50 autorizaciones es elegante y barato: cuesta 25.000 de gas por intento, y solo hace falta que una acierte el nonce. Cualquiera que custodie claves en Ethereum tiene que asumir que firmar una transacción de tipo 4 es, de facto, entregar la cuenta, y tratar esa firma con el cuidado que eso merece: rechazarla por defecto, vigilar el código de sus EOA y tener un botón de corte que no dependa de recuperar la clave primero.

La parte buena es que el dinero deja un rastro público que cualquiera puede seguir con curl, y que los stablecoins centralizados se congelan. La parte mala es que las 3 horas y 50 minutos que la puerta estuvo abierta las pagó todo el que depositó en esa cuenta durante la madrugada del 29 al 30 de octubre.