Back to Blog

Vulnerabilidades de Revest Finance: Más que Re-entrancy

Code Auditing
March 31, 2022
9 min read

El 27 de marzo de 2022, el proyecto DeFi de staking Revest Finance en Ethereum fue atacado debido al mecanismo de call-back de ERC-1155, lo que causó que aproximadamente $2M en tokens (concretamente BLOCKS, ECO, LYXe y RENA) fueran robados. Analizamos el ataque en primera instancia y publicamos en Twitter nuestro análisis esa misma noche (UTC+8).

De hecho, en el momento de redactar el tweet, aún teníamos algunas dudas sobre una función en el contrato TokenVault de Revest. Investigamos el contrato intentando entender su funcionalidad. Más tarde descubrimos que se trata de otra vulnerabilidad crítica de día cero, que puede ser explotada de una manera mucho más sencilla y puede causar las mismas enormes pérdidas (como el ataque que ya ocurrió).

Luego contactamos de inmediato al equipo de Revest Finance, quienes respondieron rápidamente y propusieron una solución temporal para la vulnerabilidad. Tras confirmar que la vulnerabilidad no podía activarse, decidimos publicar este blog.

El resto de este blog consta de tres partes: el mecanismo de Revest Finance, el ataque de re-entrada original y la nueva vulnerabilidad de día cero.

¿Qué es el FNFT de Revest Finance?

El Token No Fungible Financiero (FNFT) de Revest Finance hace posible la transferencia sin confianza de derechos futuros sobre activos bloqueados. El contrato de entrada (contrato Revest) proporciona tres interfaces diferentes para emitir FNFT bloqueando activos subyacentes:

  • mintTimeLock: el activo subyacente se desbloqueará después de un período de tiempo.
  • mintValueLock: el activo subyacente se desbloqueará cuando su valor suba por encima o caiga por debajo de un valor determinado.
  • mintAddressLock: el activo subyacente será desbloqueado por una cuenta determinada.

El contrato Revest conecta los otros tres contratos para bloquear y desbloquear activos subyacentes.

  • FNFTHandler: heredado del token ERC-1155. Crea un nuevo FNFT con el fnftId incremental para cada bloqueo. El bloqueo establece el suministro total del nuevo FNFT en el momento de su creación. El FNFT no puede ser emitido de ninguna otra manera, pero puede ser quemado para desbloquear activos subyacentes.

  • LockManager: registra las condiciones de desbloqueo para cada bloqueo al crearlo y decide si el bloqueo puede desbloquearse al momento de hacerlo.

  • TokenVault: recibe y envía los activos subyacentes y registra los metadatos de cada FNFT, como el valor de un FNFT específico.

Tomamos mintAddressLock como ejemplo para ilustrar el proceso de emisión de FNFTs.

Figura 1
Figura 2

Las dos figuras anteriores describen cómo se crea, emite y quema un FNFT. Específicamente, el usuario A bloquea 100 WETH en Revest Finance, creando el FNFT correspondiente con fnftId igual a 1. Finalmente, emite 100 1-FNFT a los destinatarios especificados con las participaciones indicadas.

Nótese que, una vez que el activo subyacente es desbloqueado, cada 1-FNFT puede ser quemado para recibir un (*1e18) WETH. Como se muestra en la Figura 2, el usuario B retira 25 (* 1e18) WETH quemando 25 1-FNFT.

Además, el contrato Revest proporciona otra interfaz, llamada depositAdditionalToFNFT, que genera dos vulnerabilidades que se discutirán a continuación.

Primero usamos las siguientes dos figuras para describir el uso normal de esta función.

Figura 3
Figura 4

La función depositAdditionalToFNFT bloquea más activos subyacentes en un bloqueo existente (especificado por fnftId). Razonablemente (Figura 3), requiere que la cantidad especificada sea igual al suministro total del FNFT especificado y luego distribuye equitativamente los activos añadidos a cada FNFT especificado.

De lo contrario (Figura 4), crea un nuevo bloqueo con el último fnftId, quema las cantidades especificadas del antiguo FNFT y emite la cantidad especificada del nuevo FNFT, y luego registra el depositAmount del nuevo bloqueo como la suma del depositAmount del antiguo bloqueo y la cantidad especificada, como se muestra en el siguiente código.

// Ahora, transferimos al token vault
if(fnft.asset != address(0)){
    IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}

ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);

emit FNFTAddionalDeposited(_msgSender(), newFNFTId, quantity, amount);

Dado que el depositAmount registrado en el contrato TokenVault indica la cantidad del activo subyacente que un FNFT específico puede retirar, esa operación transfiere el valor de las cantidades especificadas del antiguo FNFT del bloqueo antiguo al nuevo bloqueo.

(Una cantidad especificada mayor que el suministro total revertirá la transacción)

¿Qué es la Vulnerabilidad de Re-entrada?

En esta parte, ilustraremos cómo funciona el ataque de re-entrada y discutiremos la causa raíz y el método de corrección.

Figura 5
Figura 6
Figura 7

Las tres figuras anteriores describen básicamente todo el proceso del ataque de re-entrada. Específicamente, el atacante primero bloquea cero tokens RENA para emitir 2 1-FNFT que no tienen valor. Segundo, el atacante bloquea cero tokens RENA nuevamente pero emite 360,000 2-FNFT que también no tienen valor (por ahora). Durante el último paso, el atacante re-entra en la función depositAdditionalToFNFT del contrato Revest a través del mecanismo de call-back del FNFTHandler heredado del estándar de token ERC-1155, que sobreescribe el depositAmount del bloqueo con fnftId igual a 2 antes de la actualización de fnftId. Como resultado, el atacante obtiene 360,001 2-FNFT con el depositAmount igual a 1e18, lo que significa que puede retirar 360,001 * 1e18 RENA del contrato TokenVault. Además, el único costo es 1e18 RENA.

Método de Corrección

Los códigos de Revest Finance siguen completamente el patrón clásico de re-entrada: usar fnftId -> llamada externa con mecanismo de callback -> actualizar fnftId. Por lo tanto, la forma más directa de solucionar los problemas es romper el patrón. El código corregido se muestra a continuación:

function mint(
    address account, 
    uint id, 
    uint amount, 
    bytes memory data
) external override onlyRevestController {
    require(amount > 0, "Invalid amount");
    require(supply[id] == 0, "Repeated mint for the same FNFT");
    supply[id] += amount;
    fnftsCreated += 1;
    _mint(account, id, amount, data);
}

Primero, mueve la operación de actualización antes de la llamada externa (_mint), lo que puede evitar el ataque. Segundo, dado que el sistema no permite emitir cero FNFT ni emitir repetidamente el mismo FNFT, añade dos verificaciones para asegurar que el sistema funcione como se espera, lo que puede mejorar la seguridad del sistema.

La Nueva Vulnerabilidad de Día Cero

Al analizar el código de Revest Finance, la función handleMultipleDeposits en el contrato TokenVault siempre nos generó confusión, cuyo código se muestra a continuación.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig storage config = fnfts[fnftId];
    config.depositAmount = amount;
    mapFNFTToToken(fnftId, config);
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    }
}

Durante la llamada a la función depositAdditionalToFNFT, la función handleMultipleDeposits cambia el depositAmount del bloqueo antiguo o lo registra en el nuevo. Cuando newFNFTId es cero, no registra el depositAmount del nuevo bloqueo, porque se trata de una operación para añadir activos adicionales al bloqueo existente.

Según el sentido común, cuando newFNFTId no es cero, solo debería registrar el depositAmount del nuevo bloqueo sin cambiar el del antiguo. Sin embargo, el código nos indica que no solo registra el depositAmount del nuevo bloqueo sino que también cambia el del antiguo.

Creemos que se trata de una grave vulnerabilidad lógica de día cero y escribimos un PoC para verificarlo. Las siguientes tres figuras describen cómo funciona el PoC.

Figura 8
Figura 9
Figura 10

Específicamente, el atacante primero bloquea cero RENA para emitir 360,000 1-FNFT. Después de eso, el atacante invoca directamente la función depositAdditionalToFNFT para crear un nuevo bloqueo. Debido al error lógico, el contrato TokenVault cambia incorrectamente el depositAmount del bloqueo antiguo de cero a 1e18. Como resultado, el atacante obtiene 359,999 1-FNFT con un valor de 359,999 RENA. Obviamente, el PoC es mucho más sencillo que el ataque de re-entrada real.

La Solución Temporal para Corregir la Vulnerabilidad

Este es un error lógico, y recomendamos usar el siguiente código para corregirlo.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig memory config = fnfts[fnftId];
    config.depositAmount = amount;
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    } else {
        mapFNFTToToken(fnftId, config);
    }
}

Dado que los dos contratos vulnerables: TokenVault y FNFTHandler almacenan una gran cantidad de estados críticos, el proyecto no puede re-desplegar el contrato TokenVault ni el contrato FNFTHandler sin migrar los estados. Para evitar futuros ataques a esta vulnerabilidad, el proyecto re-desplegó una versión lite del contrato Revest, que deshabilita funciones más complejas para reducir las superficies disponibles para cualquier posible atacante. Tras verificar la solución temporal, creemos que el contrato Revest lite puede mitigar los posibles ataques mencionados en este blog.

Conclusión

Hacer que un proyecto DeFi sea seguro no es una tarea fácil. Además de la auditoría de código, creemos que la comunidad debe adoptar un método proactivo para monitorear el estado del proyecto y bloquear el ataque antes de que ocurra.

Acerca de BlockSec

BlockSec es una empresa pionera en seguridad blockchain establecida en 2021 por un grupo de expertos en seguridad de reconocimiento mundial. La empresa está comprometida a mejorar la seguridad y la usabilidad del emergente mundo Web3 con el fin de facilitar su adopción masiva. Con este fin, BlockSec ofrece servicios de auditoría de seguridad para contratos inteligentes y cadenas EVM, la plataforma Phalcon para el desarrollo de seguridad y el bloqueo proactivo de amenazas, la plataforma MetaSleuth para el rastreo e investigación de fondos, y la extensión MetaDock para que los constructores de web3 naveguen eficientemente en el mundo cripto.

Hasta la fecha, la empresa ha servido a más de 300 distinguidos clientes como MetaMask, Uniswap Foundation, Compound, Forta y PancakeSwap, y ha recibido decenas de millones de dólares en dos rondas de financiación de inversores prominentes, incluyendo Matrix Partners, Vitalbridge Capital y Fenbushi Capital.

Sitio web oficial: https://blocksec.com/

Cuenta oficial de Twitter: https://twitter.com/BlockSecTeam

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit