El ataque a Bybit y el camino que usó
El 21 de febrero de 2025, Bybit perdió alrededor de 1,500 millones de dólares de una wallet fría multifirma de Safe. Es el robo más grande en la historia de la industria, y vale la pena leerlo con cuidado, porque casi todo en él era correcto salvo una cosa.
Qué pasó
La versión corta, según los análisis públicos posteriores al incidente:
- Un atacante comprometió la computadora de un desarrollador de
Safe{Wallet}e inyectó JavaScript malicioso en el bucket de AWS S3 que servía el front-end deSafe{Wallet}. El código entró el 19 de febrero y se activó el 21 de febrero, dirigido específicamente al Safe de Bybit. - Los firmantes de Bybit abrieron la interfaz y revisaron una transacción que se veía normal.
- Lo que en realidad se envió a sus wallets de hardware no era esa
transacción. Era un
delegatecallque sobrescribía elmasterCopydel proxy del Safe (el slot 0) y reemplazaba toda la implementación de la cuenta por la del atacante. - Los firmantes aprobaron. Las firmas eran válidas. El contrato hizo exactamente lo que se le ordenó.
La atribución pública apuntó a actividad vinculada con Corea del Norte (el FBI nombró al grupo TraderTraitor).
Qué no falló
- Los contratos de Safe, no. Ejecutaron una instrucción firmada válidamente. No se explotó ningún bug de Safe.
- La criptografía, no. Todas las firmas eran auténticas.
- Las wallets de hardware, no. Había dispositivos Ledger en el proceso y firmaron
de todos modos, porque una wallet de hardware te muestra lo que le dan, y lo que
le dieron fue la carga maliciosa. Un dispositivo que no puede decodificar un
delegatecallen algo que una persona pueda evaluar protege la llave, no la decisión.
Lo que falló fue el supuesto que está debajo de toda interfaz de wallet: que la pantalla que describe una transacción y los bytes que se firman son lo mismo.
Por qué es el caso general y no un evento raro
La mayoría de las firmas que se producen en una wallet web descansan en ese supuesto. La interfaz arma la carga, la interfaz muestra el resumen, y nada independiente comprueba que uno corresponda con el otro. Si se reemplaza el código que sirve esa interfaz (por un pipeline de compilación comprometido, un CDN secuestrado, una dependencia maliciosa o una credencial de despliegue robada), el resumen pasa a ser lo que el atacante quiera, y tu firma es real.
Ese es el riesgo al que apunta el diseño de firma de Vela. No el phishing. No una llave filtrada. Una pantalla de firma que te está mintiendo.
Qué hace Vela al respecto
Firma legible, hasta el calldata. Cada transacción se decodifica en una
intención legible antes de que la apruebes: monto, destinatario, qué hace en
realidad la llamada (ERC-7730). Una llamada que no
podemos decodificar se marca como no decodificable, no se muestra en silencio
como si estuviera bien. La carga de Bybit era un delegatecall que cambiaba una
dirección de implementación; es justo el tipo de cosa que debería frenar en seco a
un firmante, y esconderla detrás de un resumen amigable es la razón por la que no lo
hizo.
Hay dos cosas que conviene precisar. Una dApp no puede pedirle a Vela un delegatecall directamente (las solicitudes que puede hacer una página producen
llamadas normales), así que la carga de Bybit en sí no podría llegar por ese camino.
Y una página que pide una llamada de tu Safe a sí mismo se rechaza, no solo se
decodifica: enableModule, addOwnerWithThreshold, swapOwner, setFallbackHandler, setGuard y el resto de esa familia se bloquean cuando el
destino es tu propia wallet, también dentro de un lote y dentro de un MultiSend,
igual que cualquier tramo que lleve un delegatecall, sea cual sea su destino, y una
firma de datos tipados SafeTx. La wallet dice qué llamada rechazó y no ofrece nada
que firmar. Cualquiera de ellas, firmada una sola vez, entregaría la cuenta tan
completamente como lo hizo la carga de Bybit: un módulo habilitado puede luego
ejecutar un delegatecall propio. Y si el código de Vela se reemplazara, como pasó
con el de Safe{Wallet}, la decodificación también sería la del atacante; para eso
es el siguiente punto.
Un camino independiente que puede revisar la interfaz. El Trusted Signer de Vela es una página de un solo archivo en sign.getvela.app que decodifica la solicitud y hace la firma WebAuthn por su cuenta: una página que puedes leer de principio a fin, recompilar byte por byte o servir tú mismo. Es una segunda opinión que no comparte la cadena de suministro de la app principal: la app solo le entrega la solicitud, y la página calcula por sí misma lo que se firma. Estado: publicado; las apps de escritorio, iPhone y Android lo usan en un dispositivo donde lo elegiste al crear la wallet o al iniciar sesión. La app de escritorio compara la página con las versiones que trae integradas, pero solo registra una discrepancia en el log, y las apps de celular todavía no hacen esta revisión.
Ningún rol de administrador que podamos perder. Las cuentas de Vela son Safe v1.4.1 sin modificar, y Vela no tiene ningún
rol privilegiado en ellas: ni llave de administrador ni ruta de actualización propia
que nos pudieran obligar o engañar a usar. Hay que ser claros sobre lo que eso no elimina: la primitiva que usaron los atacantes de Bybit (un delegatecall firmado
por un dueño que reescribe la implementación de la cuenta) sigue existiendo en todo
Safe, incluido el de Vela (las transacciones en lote de la propia Vela usan delegatecall hacia el MultiSend de Safe). Requiere una firma válida de una de tus
llaves. Las defensas contra que te convenzan de darla son la decodificación de
arriba y la revisión independiente.
Una verificación nueva para cada firma. Cada firma necesita la confirmación de tu propia llave: Face ID, una huella, un PIN, o un toque y un PIN en una llave de seguridad. No hay una llave de sesión de larga duración, así que no hay ninguna ventana en la que algo pueda firmar por ti sin que estés presente.
El autoalojamiento como último respaldo. Las apps y los servicios de backend son de código abierto. Si no quieres confiar para nada en nuestro pipeline de compilación, compila tú mismo la extensión o una app y opera los servicios que necesites; la guía de autoalojamiento te lleva paso a paso. Eso saca nuestro pipeline de compilación de la cadena de confianza; sigues confiando en el código que compilas, así que léelo.
Lo que Vela no afirma
El front-end de Vela podría quedar comprometido de la misma forma que el de Safe{Wallet}. Nuestro código no está auditado. Decir otra cosa sería justo el tipo
de garantía que este incidente debió haber terminado.
Lo que el diseño intenta es angostar el camino: hacer que la carga sea legible en lugar de opaca, no tener ningún rol de administrador del que se pueda abusar en tu nombre y darte una forma de verificar con algo que no seamos nosotros. El resumen honesto es que esta clase de ataque está mitigada por diseño, no eliminada, y las partes que la reforzarían más están enumeradas, sin terminar, en auditorías y problemas conocidos.