Por el Equipo de BlockSec (@BlockSecTeam)

En la última semana, el protocolo Compound tuvo un error que accidentalmente envió una gran cantidad de tokens COMP a los usuarios. La causa de este error (error 2 en este blog) se debe a la corrección incorrecta de otro error (Error 1 en este blog) que fue descubierto previamente.
En este blog, elaboraremos la causa raíz del primer error y la razón por la cual la corrección del primer error provoca el segundo error.
Antecedentes
El protocolo Compound está basado en el Whitepaper de Compound. A través de los contratos cToken, las cuentas en la blockchain suministran capital (Ether o tokens ERC-20) para recibir cTokens o pedir prestados activos del protocolo (manteniendo otros activos como garantía). Los contratos cToken de Compound rastrean estos saldos y establecen algorítmicamente las tasas de interés para los prestatarios.
Para incentivar a los usuarios, aquellos que proporcionan liquidez a Compound (suministrando capital) pueden recibir los intereses. Específicamente, los usuarios proporcionan activos (p. ej., Ether u otros tokens ERC20) a Compound y reciben los cTokens correspondientes. Cuando el cToken es devuelto a Compound, los activos subyacentes (Ether o tokens ERC20) y los intereses serán devueltos al usuario, si el usuario no tiene ninguna deuda en Compound. Por ejemplo, si un usuario tiene 1000 Ether, entonces puede depositar el activo en Compound a través de cEth.mint(1000) para obtener el cToken.

El cToken representa los activos subyacentes que han sido bloqueados en Compound. El usuario puede usar además el cToken como garantía para pedir prestados otros activos. Por ejemplo, un usuario puede depositar 1000 Ether a través de ceth.mint(1000) y luego usar los cTokens obtenidos para pedir prestado x Dai con valor de 75 Ether (sobre-colateralización -- este número depende del factor de garantía) a través de cDai.borrow(x).
La lógica central está implementada en el contrato Comptroller. Mantiene los estados de un usuario, p. ej., cuántos tokens han sido depositados en Compound por el usuario, cuántos tokens ha pedido prestados el usuario, y si el usuario puede pedir prestados más tokens. Las funciones invocadas en este proceso incluyen getHypotheticalAccountLiquidityInternal(), borrowAllowed(), mintAllowed(), entre otras.
Compound también tiene el token de gobernanza llamado COMP. El token COMP puede usarse para votar propuestas. Además, el token COMP puede ser intercambiado en exchanges. Actualmente, el precio del COMP es de alrededor de $300.
Error 1
El 31 de septiembre de 2021, hubo una nueva propuesta (Propuesta 62) en el DAO de Compound, cuyo objetivo era corregir un error en el Comptroller.

El error está relacionado con CompSpeed, que representa el número de tokens COMP que pueden distribuirse a los usuarios en cada bloque.
El Flujo de la Función mint
A continuación, usaremos la función mint para describir la causa de este error. La cadena de invocación de la función mint es: mint → mintInternal → mintFresh.

En la función mintFresh, invoca mintAllowed y luego actualiza el saldo del usuario del cToken.

En la función mintAllowed, primero invoca updateCompSupplyIndex y luego distributeSupplierComp para 1) actualizar el compSupplyState del mercado y 2) distribuir los tokens COMP a los usuarios.
updateCompSupplyIndex

La función updateCompSupplyIndex actualizará el estado de cada mercado, principalmente el compSupplyState[cToken].

En la estructura CompMarketState, registra el número de bloque (block) de esta actualización, y el índice de bonificación (index) que afectará el número de tokens COMP que deben distribuirse a los usuarios (que mantienen el cToken.)
¿Qué es el índice de bonificación (index) para cada token? Este es el valor acumulado con el tiempo (que se muestra en la siguiente fórmula).

Esto muestra el número de COMP que debe distribuirse a los usuarios (por cada cToken que tenga el usuario).
distributeSupplierComp
La otra función distributeSupplierComp es responsable de registrar el número de tokens COMP que deben distribuirse al usuario (proveedor) en compAccrued[supplier].

Específicamente, actualiza el índice de bonificación global en compSupplyState (en la función updateCompSupplyIndex). Luego, en la función distributeSupplierComp, el supplyIndex registra el índice de bonificación actual, y el supplierIndex muestra el último índice de bonificación para el usuario (proveedor). El valor delta (supplyIndex - supplierIndex) * el saldo de cToken del usuario muestra el número de tokens COMP que deben distribuirse al usuario.
La Causa del Error 1
Existe otra función setCompSpeed para ajustar el supplySpeed del mercado (compSpeeds[address[cToken]]).

Esto se debe a que si establecemos el CompSpeed de un mercado en cero, significa que el token COMP no se distribuirá a los usuarios en ese mercado. Por lo tanto, si queremos primero deshabilitar la distribución de COMP para un mercado y luego volver a habilitarla, podemos seguir estos pasos:
- Paso I: Establecer
CompSpeed[cToken]como cero para deshabilitar la distribución de tokens COMP. - Paso II: Invocar la función
setCompSpeedpara establecerCompSpeed[cToken]como un valor distinto de cero.

Paso I: Para los mercados que han sido deshabilitados para la distribución de tokens COMP en el Paso I (supplySpeed == 0), el bloque no es cero, ya que el bloque se actualiza continuamente en updateCompSupplyIndex (else if (deltaBlocks > 0)).

Paso II: Al ejecutar la operación del Paso II, la función setCompSpeedInternal pasará por la sentencia else if (compSpeed != 0) (línea 1083). Luego, en las líneas 1088 a 1093, hay una verificación if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0) para inicializar el index y el block para un nuevo mercado. Sin embargo, dado que estamos re-habilitando la distribución de tokens COMP en un mercado existente (no un mercado nuevo), las sentencias en las líneas 1090 y 1091 no se ejecutarán para inicializar el index y el block (ya que compSupplyState[address(cToken)].block no es cero).
En resumen, para un mercado actualmente deshabilitado, el index es cero. Sin embargo, el block no es cero. Esto significa que cuando volvemos a habilitar el mercado deshabilitado invocando setCompSpeed para establecer CompSpeed[cToken] como un valor distinto de cero, el valor del índice NO será reinicializado a CompInitialIndex (1e36) (las líneas 1090 y 1091 no se ejecutan).
El Impacto del Error 1
Profundizamos en la función distributeSupplierComp que es responsable de distribuir los tokens COMP.

El supplierIndex es compInitialIndex. Sin embargo, el supplyIndex sigue siendo cero debido al error, lo que causará un desbordamiento inferior en Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36).

Error 2: Introducido por la Corrección del Error 1
Para corregir el error, el propietario del proyecto cambia la lógica del código. Específicamente, inicializa inmediatamente el index a compInitialIndex al inicializar un nuevo mercado.

Dado que el índice de bonificación global (index) ha sido inicializado a compInitialIndex, el índice de bonificación del usuario también debería inicializarse a este valor. Veamos la función distributeSupplierComp.

La condición if en la línea 1234 no puede satisfacerse incluso si supplierIndex == 0 ya que el supplyIndex es igual a (no mayor que) compInitialIndex (1e36). Esto causa que el supplierIndex NO sea correctamente inicializado a compInitialIndex (su valor es 0). Luego, el deltaIndex (supplyIndex - supplierIndex) será compInitialIndex, en lugar de cero. El supplierTokens se convertirá en un valor grande si el saldo de cToken del usuario no es cero.
En resumen, si un usuario realiza la operación mint antes de la corrección del error 1, entonces tiene cTokens y el supplierIndex se convertirá en cero (ya que el token COMP ha sido distribuido). Luego, después de la corrección del error 1 (que introduce el error 2), cuando el usuario invoca la función mint nuevamente, puede obtener una gran cantidad de tokens COMP (1e36*ctoken.balanceOf(usuario)).
Mundo Real
Mostramos los mercados afectados a continuación:
0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI
0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD
0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR
0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI
0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE
0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI
Para el usuario (0xa7b95d2a2d10028cc4450e453151181cbcac74fc), el usuario obtiene 4,466.542459954989867175 tokens COMP en esta transacción (0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308).

La depuración adicional de la transacción muestra que, debido al error 2, el deltaIndex es 1e36 y el usuario resulta tener el cToken en ese momento.



La Corrección del Error 2
La corrección del error 2 es sencilla. Cambia la condición if en la función distributeSupplierComp.

Lecciones
- Este es un error causado por la corrección de otro error. Cómo revisar exhaustivamente los cambios de código para proyectos de alto perfil sigue siendo una pregunta abierta.
- El DAO puede eliminar el riesgo de centralización. Sin embargo, también hace que la respuesta a los incidentes de seguridad sea un proceso lento.
- Los proyectos DeFi de alto perfil pueden adoptar buenas prácticas de seguridad de los programas tradicionales, p. ej., implementar un sistema de fuzzing eficiente con un proceso de pruebas continuo.
Acerca de BlockSec
BlockSec es una empresa pionera en seguridad blockchain establecida en 2021 por un grupo de expertos en seguridad de renombre mundial. La empresa está comprometida a mejorar la seguridad y usabilidad para el emergente mundo Web3 con el fin de facilitar su adopción masiva. Con este fin, BlockSec ofrece servicios de auditoría de seguridad de 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 MetaSuites 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 estadounidenses en dos rondas de financiación de inversores preeminentes, incluyendo Matrix Partners, Vitalbridge Capital y Fenbushi Capital.
Sitio web oficial: https://blocksec.com/
Cuenta oficial de Twitter: https://twitter.com/BlockSecTeam



