Introducción
A medida que el ecosistema DeFi se desarrolla rápidamente, Compound Finance V2, como uno de los pioneros en este campo, ha atraído a una gran cantidad de usuarios con su innovador modelo de préstamo. Sin embargo, cualquier aplicación distribuida compleja enfrenta amenazas de seguridad potenciales, especialmente cuando se trata de flujos de fondos que involucran millones o incluso miles de millones de dólares. Por lo tanto, es particularmente importante realizar una auditoría de seguridad completa y detallada de Compound Finance V2 y sus proyectos Fork. Este manual tiene como objetivo proporcionar a desarrolladores, investigadores de seguridad y entusiastas de DeFi una guía exhaustiva de auditoría de seguridad para ayudar a identificar y prevenir riesgos potenciales de manera más efectiva.
1. Resumen del contexto del proyecto
Compound Finance V2 es una plataforma de préstamo abierta construida sobre la cadena de bloques de Ethereum, que permite a los usuarios depositar varios tokens subyacentes ERC-20 y ganar intereses por ellos, y también permite pedir prestados tokens del mercado bajo la condición de pagar intereses. A través de la introducción del concepto de "mercado de tasas de interés", se logra una gestión descentralizada del fondo y un mecanismo automatizado de ajuste de tasas de interés.
2. Análisis de la arquitectura del proyecto
Los componentes arquitectónicos centrales de Compound Finance V2 incluyen:
Comptroller: controla la lógica del sistema completo, como el cálculo de tasas de interés, mantenimiento del estado de cuentas, etc.
cToken: un token personalizado que implementa el estándar ERC-20, representando los derechos del usuario en el sistema.
InterestRateModel: un modelo que calcula las tasas de interés de depósitos y préstamos.
PriceOracle: proporciona oráculos de precios de activos.
Gobernanza: responsable de las funciones relacionadas con la gobernanza de la comunidad.
2.1 Comptroller
El contrato Comptroller es el sistema nervioso central de Compound Finance V2, que coordina el comportamiento de las distintas instancias de cToken. Sus principales responsabilidades son:
Gestionar la lista de mercados, determinando cuáles mercados están activos.
Ejecutar diversas verificaciones para operaciones intermercado, como la verificación de la salud de las posiciones de los usuarios, etc.
Configurar y actualizar parámetros globales, como límites de préstamo, factores de colateral, umbrales de liquidación, etc.
2.2 cToken
Cada token ERC-20 compatible tiene una instancia de cToken correspondiente (es decir, contratos CErc20 / CEther), que maneja todas las operaciones de interacción de ese token con el proyecto. Cada cToken, además de implementar la funcionalidad básica de transferencia de tokens, también añade algunas funciones específicas de Compound, como préstamos, acumulación de intereses y distribución de recompensas. Por lo tanto, podemos considerar el cToken como un certificado de los activos depositados por el usuario en Compound y como una entrada para realizar operaciones de préstamo.
Cuando los usuarios depositan los activos subyacentes en el contrato, pueden acuñar los tokens cToken correspondientes, la tasa de cambio entre cToken y el activo subyacente se calcula de la siguiente manera:
Nota: borrows representa el monto del préstamo, cash representa el saldo del fondo, reservas representa los fondos de reserva. La tasa de interés del préstamo está determinada por la tasa de utilización, la tasa de interés del depósito está determinada por la tasa de interés del préstamo.
Los usuarios generalmente interactúan con diferentes contratos cToken para realizar operaciones de préstamo de tokens en diferentes mercados:
2.3 InterestRateModel
El contrato InterestRateModel define el método para calcular las tasas de interés. Diferentes mercados pueden utilizar diferentes tipos de modelos de tasas de interés para adaptarse a sus propias preferencias de riesgo y necesidades de liquidez.
Los modelos de tasas de interés utilizados en el mercado de Compound V2 son principalmente dos, uno es lineal y el otro es de punto de inflexión.
La fórmula para calcular la tasa de interés del modelo lineal es la siguiente:
La fórmula para calcular la tasa de utilización de fondos es la siguiente:
La tasa de interés de los depósitos varía linealmente con la tasa de interés de los préstamos:
Un aumento gradual en la tasa de utilización significa que el dinero en el fondo está disminuyendo gradualmente, y al alcanzar un cierto pico, puede provocar que los usuarios no puedan depositar y pedir prestado normalmente. Para evitar esta situación, Compound ha introducido un segundo modelo de tasa de interés: el modelo de punto de inflexión.
La fórmula para calcular la tasa de interés del modelo de punto de inflexión es la siguiente:
Cuando la tasa de utilización alcanza un cierto pico, las tasas de interés de préstamo y de depósito aumentan drásticamente en un instante, incentivando a los usuarios a depositar más y pedir prestado menos, manteniendo así la tasa de utilización dentro de un rango adecuado, este pico también se conoce como punto de inflexión (generalmente cuando la tasa de utilización alcanza el 80%).
2.4 PriceOracle
El contrato PriceOracle es responsable de obtener información de precios del mercado externo y convertirla en valores utilizados internamente en el sistema, lo cual es crucial para calcular con precisión el valor de las posiciones de los usuarios.
2.5 Mecanismo de gobernanza y modelo de incentivos
Compound ha introducido un mecanismo de gobernanza único que permite a los usuarios que poseen el token de gobernanza (COMP) participar en votaciones sobre decisiones importantes, como cambiar ciertos parámetros o agregar nuevos tipos de activos. Al emitir el token de gobernanza (COMP), se incentiva a los usuarios a participar activamente en las actividades de la plataforma y se ofrecen recompensas a los contribuyentes. Para más detalles, consulte la documentación oficial de Compound y el repositorio de código. (https://docs.compound.finance/v2/; https://github.com/compound-finance/compound-protocol)
3. Proceso de interacción
A continuación, ilustramos el proceso general de interacción de los usuarios en Compound Finance V2 con un ejemplo simple:
3.1 Proceso de depósito y canje
Si el usuario Alice necesita depositar 1 WBTC en Compound, llamará a la función mint del contrato cWBTC para realizar el depósito. Este contrato hereda del contrato cToken, primero llamará a la función mintInternal para invocar la función accrueInterest y actualizar las tasas de interés de préstamos y depósitos, y luego llamará a mintFresh para realizar la operación de acuñación específica.
La función mintFresh llamará externamente a la función mintAllowed del contrato Comptroller para verificar si el mercado actual permite depósitos, luego transferirá 1 WBTC del usuario al contrato a través de la función doTransferIn, y según la tasa de cambio más reciente, acuñará la cantidad correspondiente de tokens cToken para el usuario (supongamos que la tasa de cambio más reciente es 0.1, entonces Alice recibirá 10 tokens cWBTC).
Si Alice decide en el futuro canjear su depósito, puede llamar a la función redeem para intercambiar cWBTC por WBTC, la tasa de intercambio puede haber cambiado (supongamos que es 0.15), lo que significa que Alice podrá canjear 1.5 WBTC, de los cuales 0.5 WBTC son ingresos por intereses.
3.2 Proceso de préstamo y reembolso
Alice primero necesita llamar a la función enterMarkets del contrato Comptroller para establecer su cWBTC como colateral, y solo después podrá realizar un préstamo.
Supongamos que Alice elige prestar 70 USDC, dado que el factor de colateral para WBTC es 0.75, Alice puede pedir prestado hasta el 75% del valor de los activos de WBTC, por lo que esto no excederá su límite máximo de préstamo.
Nota: Para evitar el riesgo de liquidación, Alice debe mantener un cierto espacio de amortiguamiento en lugar de agotar completamente su límite de préstamo.
Alice llama a la función borrow del contrato cUSDC, la cual primero llama a la función interna borrowInternal para invocar la función accrueInterest y actualizar las tasas de interés de los préstamos y depósitos, y luego llama a borrowFresh para realizar la operación de préstamo específica.
Después de realizar la verificación del valor de la posición del usuario mediante la función borrowAllowed del contrato Comptroller, primero se registrarán los datos del préstamo y luego se transferirán los tokens al usuario mediante la función doTransferOut.
Si Alice necesita reembolsar, puede llamar a la función repayBorrow del contrato cUSDC para reembolsar por sí misma, o permitir que otra persona llame a la función repayBorrowBehalf para reembolsar por ella.
3.3 Proceso de liquidación
Si el precio de WBTC cae drásticamente, haciendo que el valor del colateral de Alice caiga por debajo del 75% de su monto del préstamo, la posición de préstamo de Alice estará en estado de liquidación.
Los liquidadores externos (por ejemplo, Bob) pueden llamar a la función de liquidación liquidateBorrow en el contrato cUSDC para ayudar a Alice a pagar parte de su deuda. Primero, actualizará simultáneamente las tasas de interés de cUSDC y el colateral cToken utilizado para el reembolso a través de la función liquidateBorrowInternal, y luego llamará a liquidateBorrowFresh para realizar la operación de liquidación específica.
Después de realizar la verificación de si la liquidación está permitida mediante la función liquidateBorrowAllowed del contrato Comptroller, se llamará a la función repayBorrowFresh para transferir USDC al contrato para el reembolso y actualizar los datos de préstamo del prestatario liquidado. Luego, se llamará a la función liquidateCalculateSeizeTokens del contrato Comptroller para calcular la cantidad de colateral que Bob puede obtener de Alice según el valor de la liquidación, y finalmente se utilizará la función seize del contrato cToken del mercado de colateral especificado (por ejemplo, cWBTC) para transferir cToken a Bob y Alice.
Abre este enlace para ver la versión de alta definición de la imagen anterior, hacer clic en leer el artículo también te llevará directamente: https://www.figma.com/board/POkJlvKlWWc7jSccYMddet/Compound-V2?node-id=0-1&node-type=canvas.
Bob paga parte del préstamo de Alice (por ejemplo, 20 USDC) y a cambio obtiene el colateral de Alice de valor correspondiente (como WBTC), además Bob también puede recibir un incentivo de liquidación adicional (supongamos que es del 5%). El resultado final es que Bob recibe WBTC por un valor de 21 USDC (20 USDC del préstamo + 1 USDC del incentivo de liquidación).
4. Lista de verificación de vulnerabilidades de seguridad
4.1 Vulnerabilidades de redondeo causadas por mercados vacíos
Si el cToken es el caso de un mercado vacío (es decir, no hay usuarios realizando préstamos en el mercado), como el valor de exchangeRate en la función exchangeRateStoredInternal depende de la cantidad de tokens de activos subyacentes en el contrato, se puede manipular el precio del cToken al transferir grandes cantidades de tokens de activos subyacentes al contrato cToken.
Por lo tanto, se pueden usar pequeñas cantidades de cToken para pedir prestados grandes cantidades de otros tokens, y luego llamar a la función redeemUnderlying del cToken para extraer los tokens subyacentes. Al calcular el canje, la cantidad de cToken que se debe deducir será considerablemente menor de lo esperado (casi solo la mitad) debido al redondeo hacia abajo en la división.
Supongamos que en este momento la cantidad de cToken que posee es 2 (también es el total totalSupply), y la exchangeRate ha sido manipulada y aumentada a 25,015,031,908,500,000,000,000,000,000, la cantidad de tokens subyacentes que se necesitan canjear es 50,030,063,815. Entonces, se espera que la cantidad de cToken que se debe deducir sea:
Sin embargo, la cantidad de cToken calculada realmente es:
Por lo tanto, al final solo se necesita liquidar una pequeña cantidad de cToken para obtener una gran cantidad de tokens de activos prestados de otros mercados.
Se puede consultar la transacción de hackeo del proyecto Fork de Compound Hundred Finance debido a esta vulnerabilidad: https://optimistic.etherscan.io/tx/0x6e9ebcdebbabda04fa9f2e3bc21ea8b2e4fb4bf4f4670cb8483e2f0b2604f451
Puntos clave de auditoría: Durante la auditoría, se debe prestar atención a si el método de cálculo de la tasa de cambio es fácilmente manipulable y si el método de redondeo es apropiado, además se puede sugerir al equipo del proyecto que emita inmediatamente una pequeña cantidad de cToken después de crear un nuevo mercado, para evitar que el mercado quede vacío y sea manipulado.
4.2 Vulnerabilidades de reingreso causadas por tokens ERC677 / ERC777
ERC677 / ERC777 es una extensión del contrato ERC20, que es compatible con el estándar de protocolo de tokens ERC20. Estos tokens permiten que durante el proceso de transferencia, si la dirección receptora es un contrato, se desencadene la función de callback de la dirección receptora (como transferAndCall o tokensReceived).
En la versión anterior del código de Compound Finance V2, cuando un usuario pedía prestado en el mercado de cToken, primero transfería los tokens prestados y luego registraba los datos del préstamo.
Si los tokens prestados son tokens ERC677 / ERC777 que tienen funcionalidad de callback, se puede construir un contrato malicioso para recibir los tokens a través de la función de callback y reingresar a la función borrow para pedir prestados tokens nuevamente, dado que los datos del préstamo del usuario aún no se han registrado, se puede pasar la verificación de la salud de la cuenta para pedir prestados tokens nuevamente.
Se puede consultar la transacción de hackeo del proyecto Fork de Compound Hundred Finance debido a esta vulnerabilidad: https://blockscout.com/xdai/mainnet/tx/0x534b84f657883ddc1b66a314e8b392feb35024afdec61dfe8e7c510cfac1a098
Puntos clave de auditoría: La última versión del código de Compound V2 ya ha corregido la lógica de préstamo, cambiándola para primero registrar los datos del préstamo y luego transferir los tokens prestados. Durante la auditoría, se debe prestar atención a si el código relacionado con la funcionalidad de préstamo y crédito cumple con las especificaciones CEI (Checks-Effects-Interactions) y considerar el impacto de los tokens que tienen funcionalidad de callback.
4.3 Riesgos de manipulación de precios causados por mecanismos de oráculo inapropiados
Debido a que Compound Finance utiliza un modelo de préstamo sobrecolateralizado, la cantidad de tokens que un usuario puede pedir prestado depende de si el valor del colateral es suficiente.
Por lo tanto, si el mecanismo de fijación de precios del oráculo utilizado para calcular el valor del colateral es fácilmente manipulable, es muy probable que se puedan pedir prestados tokens más allá de lo esperado.
Por ejemplo, en el evento del hackeo del proyecto Fork de Compound Lodestar Finance, el oráculo obtiene el precio del token de colateral plvGLP dividiendo la cantidad de tokens plsGLP en el contrato plvGLP (totalAssets) por la oferta total de plvGLP (totalSupply) para calcular la tasa de cambio, y luego multiplica esa tasa de cambio por el precio del token GLP para calcular el precio del token plvGLP.
Y plvGLP tiene una función de donación que permite a los usuarios donar sGLP para acuñar tokens plsGLP correspondientes para el contrato plvGLP.
Así, el atacante puede primero utilizar un préstamo relámpago para crear una gran cantidad de posiciones de colateral plvGLP en el mercado de Lodestar Finance, luego utilizar un préstamo relámpago en GMX para emitir una gran cantidad de sGLP, y luego a través de la función donate emitir tokens plsGLP para el contrato plvGLP aumentando el valor de totalAssets. A medida que aumenta el total de activos, la tasa de cambio de plvGLP se incrementará, provocando un aumento repentino en el precio de los tokens plvGLP, permitiendo así pedir prestados otros tokens más allá de lo esperado en el mercado.
Se puede consultar la transacción del hackeo de Lodestar Finance: https://arbiscan.io/tx/0xc523c6307b025ebd9aef155ba792d1ba18d5d83f97c7a846f267d3d9a3004e8c
Además, es importante tener en cuenta que Compound Finance o sus proyectos Fork también pueden utilizar oráculos fuera de la cadena como ChainLink o CoinBase para obtener el precio del colateral. Si se producen condiciones de mercado volátiles, esto podría provocar discrepancias entre los precios fuera de la cadena y en la cadena, poniendo en riesgo la seguridad de los fondos del proyecto.
Por ejemplo, el precio del token LUNA puede caer drásticamente debido a razones del mercado, y los protocolos Fork de Compound Finance como Venus Protocol y Blizz Finance utilizan oráculos de Chainlink como fuentes de fijación de precios para calcular el valor del colateral, donde el precio mínimo (minAnswer) del token LUNA está codificado de forma fija en 0.10 dólares.
Cuando el precio del token LUNA cae por debajo de 0.1 dólares (por ejemplo, 0.001 dólares), cualquiera puede comprar grandes cantidades de LUNA a precio de mercado y usarlas como colateral (valor 0.10 dólares) para pedir prestados otros activos de la plataforma.
Puntos clave de auditoría: Durante la auditoría, se debe prestar atención a si el mecanismo de fijación de precios del oráculo utilizado para calcular el valor del colateral puede ser fácilmente manipulado externamente, se puede sugerir al equipo del proyecto utilizar múltiples fuentes de precios para una evaluación integral y así evitar el riesgo de una fuente de precios única.
4.4 Riesgos de manipulación de tasas de cambio causados por tokens de múltiples puntos de entrada
En el código de Compound hay una función llamada sweepToken, que permite a los usuarios que accidentalmente transfirieron tokens al contrato retirarlos. El código de la versión anterior es el siguiente, y esta función tiene una verificación de seguridad importante: el parámetro token pasado no puede ser el token de activo subyacente del contrato.
Sin embargo, si en el mercado de algún cToken los tokens de activos subyacentes tienen múltiples contratos de entrada (es decir, es posible acceder al mismo saldo subyacente a través de múltiples direcciones de contrato, lo que hace que las interacciones externas afecten el saldo de todos los puntos de entrada, este es un patrón similar a un proxy en etapas tempranas), el atacante podría llamar a la función sweepToken, pasando un contrato de entrada diferente al de underlying, lo que permitiría transferir los tokens de activos subyacentes del contrato.
A continuación, tomaremos TUSD como ejemplo, que tiene dos contratos de entrada. El contrato de entrada auxiliar 0x8dd5fbce reenvía cualquier llamada (como transfer o balanceOf) al contrato principal, lo que significa que interactuar con cualquiera de los dos contratos afectará los datos de saldo en ambos contratos (es decir, los dos contratos comparten los mismos datos de saldo).
En este momento, supongamos que la dirección del token de activo subyacente configurada en el mercado es la dirección del contrato principal de TUSD, entonces podemos usar la dirección del contrato de entrada auxiliar 0x8dd5fbce como el parámetro token al llamar a la función sweepToken, lo que permitirá transferir todos los tokens de activos subyacentes TUSD al administrador.
Y la tasa de cambio de TUSD / cTUSD se verá afectada por la cantidad de tokens de activos subyacentes TUSD en el contrato cTUSD, cuando TUSD se transfiera completamente a la dirección del administrador, la tasa de cambio TUSD / cTUSD caerá drásticamente. En este momento, el atacante puede liquidar a otros usuarios a una tasa de cambio muy baja o devolver una cantidad de tokens menor a la esperada después de pedir prestado para obtener ganancias.
Cabe mencionar que en la última versión del código de Compound V2 se ha agregado una verificación de permisos a la función sweepToken, asegurando que solo el rol de administrador pueda llamar a ese contrato, y se han eliminado todos los mercados que tienen tokens de múltiples puntos de entrada.
Puntos clave de auditoría: Durante la auditoría, para la funcionalidad de transferencia de tokens dentro del contrato, se debe considerar el impacto que puede tener la existencia de tokens de múltiples puntos de entrada en el proyecto, se puede sugerir al equipo del proyecto no usar tokens de múltiples puntos de entrada o verificar si la cantidad de tokens de activos subyacentes en el contrato cambia antes y después de la transferencia de tokens, y llevar a cabo una verificación de permisos adecuada para las funciones relacionadas.
4.5 Problemas de compatibilidad entre las versiones de los contratos
Si en el proyecto Fork de Compound Finance, el código de un contrato central se bifurca de la nueva versión del código de Compound Finance V2, mientras que algún otro contrato que interactúa con él utiliza el código de la versión anterior, puede surgir un problema de compatibilidad.
Por ejemplo, en la versión anterior del contrato cToken, el valor de retorno de la función getBorrowRate del contrato InterestRateModel era dos valores de tipo uint, mientras que en la nueva versión del contrato InterestRateModel, la función getBorrowRate solo devolverá un valor de tipo uint.
Sin embargo, en el proyecto Fork de Compound Finance V2 Percent Finance, el equipo del proyecto utiliza el código de contrato cToken de la versión anterior, mientras que el contrato InterestRateModel utiliza la nueva versión, lo que provoca que la función accrueInterest dentro del cToken falle al llamar a la función getBorrowRate. Como accrueInterest se utiliza tanto en retiros como en préstamos, esto resulta en que ambas funcionalidades de retiro y préstamo no pueden funcionar correctamente, bloqueando completamente los fondos en el contrato.
Puntos clave de auditoría: Durante la auditoría, se debe prestar atención a los cambios en las interfaces de contrato, variables de estado, firmas de funciones y eventos en el código actualizado, para asegurarse de que no se interrumpa el funcionamiento normal del sistema existente, garantizando la consistencia de las actualizaciones de versiones de todos los códigos de contrato o asegurando que el código actualizado sea compatible con las versiones anteriores.
4.6 Problemas de codificación fija causados por el despliegue en múltiples cadenas
En el código de Compound Finance V2, la constante blocksPerYear representa la cantidad estimada de bloques producidos por año, su valor está codificado de forma fija en el contrato de modelo de tasas de interés como 2102400, porque el tiempo promedio de bloque de Ethereum es de 15 segundos.
Sin embargo, los tiempos de bloque en diferentes cadenas no siempre son los mismos, y la cantidad aproximada de bloques producidos durante todo el año también puede no ser la misma. Si un proyecto Fork de Compound se despliega en otras cadenas, pero no modifica los valores codificados de acuerdo con la situación de cada cadena, esto podría resultar en un cálculo final de la tasa de interés que exceda lo esperado. Esto se debe a que el valor de blocksPerYear influye en los valores de baseRatePerBlock y multiplierPerBlock, que a su vez afectan la tasa de interés del préstamo.
Por ejemplo, si el tiempo de bloque en la cadena BSC es de 3 segundos, entonces la cantidad estimada de bloques producidos en un año (blocksPerYear) debería ser 10512000. Si no se modifica el valor de blocksPerYear antes del despliegue, esto resultará en que la tasa de interés calculada al final sea cinco veces más alta de lo esperado.
Puntos clave de auditoría: Durante la auditoría, se debe prestar atención a si las constantes o variables codificadas en el contrato del proyecto pueden dar lugar a resultados no deseados bajo las características de diferentes cadenas, se sugiere al equipo del proyecto modificar correctamente sus valores de acuerdo con las condiciones de cada cadena.
Otros
Además de los problemas principales mencionados anteriormente, los proyectos Fork de Compound V2 generalmente modifican parte de la lógica comercial según el diseño del equipo del proyecto, como la adición de código para interactuar con protocolos externos de terceros. Esto necesita ser evaluado durante la auditoría en función de su lógica comercial específica y requisitos de diseño para determinar si afectará el modelo de préstamo central de Compound Finance V2 y el proyecto.
Escrito al final
Esperamos que este manual de auditoría de seguridad de Compound Finance V2 y sus proyectos Fork ayude a todos a comprender y evaluar mejor la seguridad de este tipo de sistemas complejos durante la auditoría, a medida que la tecnología evoluciona, este manual también se actualizará y mejorará.
Referencia:
[1] https://github.com/YAcademy-Residents/defi-fork-bugs
[2] https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2
[3] https://github.com/code-423n4/2023-05-venus-findings/issues/559
[4] https://learnblockchain.cn/article/2593
[5] https://github.com/compound-finance/compound-protocol
Autor | 九九
Editor | Liz