En este capítulo examinaremos los siguientes temas sobre Compound V3:
- valoración del colateral
- absorción de préstamos con colateral insuficiente (liquidaciones)
- venta del colateral absorbido
- qué son las reservas
- y cómo las reservas afectan a las liquidaciones.
Son muchos temas para tratar en un solo artículo, pero todos están fuertemente entrelazados, por lo que es mejor abordarlos en conjunto.
Requisitos previos
El lector ya debería estar familiarizado con cómo Compound V3 define el principal y el valor presente y con las liquidaciones y el colateral en DeFi.
Estructura de almacenamiento UserBasic
Repasemos la estructura UserBasic en CometStorage.sol.

Si el principal (recuadro azul) es negativo, significa que el usuario es un prestatario, y el valor negativo será el valor principal de su deuda.
assetsIn (recuadro rojo) es un mapa de bits (bitmap) para indicar si han depositado un determinado activo colateral o no. Al momento de escribir este artículo, el mapa de bits se distribuye de la siguiente manera:

Las variables baseTrackingIndex y baseTrackingAccrued son para llevar el registro contable de la distribución de recompensas, y se discutirán en un artículo separado. La variable _reserved no se utiliza.
Ten en cuenta que esta estructura no nos dice cuánto colateral tiene el usuario. Eso se mantiene en la variable balance dentro de la estructura UserCollateral, la cual se almacena en el mapeo anidado userCollateral. La variable _reserved no se utiliza.

Para listar el colateral que un usuario ha suministrado, iteramos a través de los 0…numAssets que almacena Compound y comprobamos si ese bit está en uno para ese usuario. Si lo está, obtenemos la dirección del token asociada a ese bit y verificamos el saldo del usuario en userCollateral[user][collateralAsset] para ver cuánto de ese colateral posee el usuario.
Al multiplicar el balance por el precio del oráculo, conocemos el valor en dólares del colateral del usuario. La siguiente tabla da un ejemplo de cómo sumar el valor total del colateral de un usuario.

AssetInfo
La dirección del oráculo de donde Compound obtiene el precio del colateral se almacena en la estructura AssetInfo (recuadro azul).

Observa que la estructura AssetInfo anterior tiene un tamaño de 432 bits: requiere 2 espacios para almacenarse. Volveremos a esto en una sección posterior.
Mostrando AssetInfo en los Mercados de Compound Finance
Comparemos el contenido de la estructura AssetInfo mostrada arriba con la interfaz de usuario del Mercado de Compound Finance. Aquí vemos la mayor parte de la información mostrada.
Los documentos y el código no nos dicen para qué se usa la variable scale, pero contiene el número 1e18, por lo que presuntamente es para permitir al consumidor saber cómo escalar los porcentajes.
El significado de estas variables fue explicado en el artículo sobre liquidaciones y colateral.

Cuando consultamos los valores actuales para el token UNI (assetId 3) (https://etherscan.io/address/0xc3d688B66703497DAA19211EEdff47f25384cdc3#readProxyContract#F16), podemos comparar los valores con los que se muestran en el mercado. La relación debería estar clara. Cabe destacar: la penalización por liquidación es 1 - liquidation factor. El liquidateCollateralFactor es el LTV en el que se liquida el préstamo. El liquidationFactor codifica la penalización por liquidación. El hecho de que el liquidationFactor en la estructura no signifique lo mismo que el liquidationFactor en la interfaz de usuario es confuso.
La imagen superior es la captura de pantalla, y los valores de abajo son una captura de pantalla de Etherscan consultando getAssetInfo() para el token UNI.
A continuación, mostramos la relación entre los parámetros del colateral de UNI en la interfaz de usuario de Compound y en Etherscan al consultar getAssetInfo() para el token UNI.

La siguiente pregunta obvia es: “¿dónde almacena Compound las estructuras AssetInfo?”
Las “estructuras” AssetInfo se guardan en variables inmutables
La información de cada activo está empaquetada en variables inmutables: no se mantiene en el almacenamiento por cuestiones de eficiencia de gas. Dado que toma dos palabras de 32 bytes para almacenar la estructura AssetInfo, Comet enumera las palabras uint256 con assetXX_a, assetXX_b. Las XX aquí indican el índice del activo. Así que asset00_a y asset00_b mantienen colectivamente la estructura AssetInfo para el activo 0. Recuerda, se necesitan dos variables de 256 bits para almacenar AssetInfo, que tiene un tamaño de 432 bits.

Ahora podemos mostrar la implementación de getAssetInfo() de Comet.sol:280-356. Simplemente desempaqueta la variable inmutable en la estructura AccountInfo y la devuelve. El desplazamiento de bits y el empaquetado que utiliza son directos, por lo que no los explicaremos aquí.

Debido a que estas variables son inmutables, la gobernanza debe desplegar una nueva implementación y actualizar el proxy si desea agregar otro activo colateral o cambiar los parámetros de uno de los activos. Se debe tener cuidado de solo adjuntar activos y no interferir con las definiciones previas.
Comprobando si un prestatario puede ser liquidado
La función isLiquidatable() de Comet.sol suma los activos colaterales mantenidos por un usuario multiplicados por su liquidationFactor. Si esta suma es menor que el valor presente de su deuda (que es un número negativo), entonces el usuario es liquidable.

Esto significa que un prestatario podría tener un activo por debajo del umbral de liquidación, pero si otros activos colaterales equilibran el déficit, entonces el usuario no es liquidable.
El valor total del colateral no “cuenta” para el saldo del colateral del usuario: se reduce por el factor de liquidación.
Aquí está el mismo ejemplo de antes mostrando el valor real de los activos colaterales del usuario.

En el ejemplo anterior, el prestatario hipotético será liquidado si el saldo de su préstamo excede los $8,360.
Liquidando a un prestatario (absorb)
Si la función isLiquidatable() devuelve verdadero, entonces el colateral del prestatario puede ser absorbido hacia el protocolo. Lo que algunos protocolos llaman “liquidación”, Compound V3 lo llama “absorber” (absorb).
Las absorciones son a todo o nada: no existe la opción de liquidar parte del colateral en Compound V3. La totalidad de los saldos de los activos del prestatario se establece en cero.
Ejemplo de absorción (absorb)
Supongamos que Bob depositó $1000 en ETH en Compound V3 y pidió prestados $800 en USDC. Esto satisface el ratio de colateralización del 80%. El valor de ETH cae a $880, causando que el LTV alcance el 90.9%, lo que desencadena el umbral de liquidación del 90%.
Un liquidador llama a absorb() en la cuenta de Bob y los $880 de colateral en ETH son absorbidos por el protocolo.
Digamos que la penalización por liquidación es del 5%.
Dado que el valor del colateral es actualmente de $880 en ETH, el 5% de eso es $44 en ETH.
El protocolo deducirá $44 del colateral de Bob como penalización, dejando $836. Como Bob pidió prestados $800 en USDC, hay un excedente de $36. Es decir, el protocolo toma $800 para cubrir la deuda dejando un remanente de $36. Esto se acredita a Bob, quien ahora se convierte en un prestamista con un depósito de $36 en USDC.
Bob ya ha retirado los $800 en USDC cuando obtuvo el préstamo, por lo que sus tenencias totales ahora son de $836.
Ten en cuenta que nada en la interacción de absorb() recompensó directamente al liquidador.
Ten en cuenta que cuando un prestatario es liquidado, se convertirá en prestamista si el colateral es suficiente para cubrir la deuda.
Si el colateral no es suficiente para cubrir la deuda, entonces el protocolo asume implícitamente una pérdida de sus reservas, las cuales discutiremos a continuación.
Reservas
Los intereses pagados por los prestatarios que superan lo que los prestamistas ganaron se denominan “reservas” en Compound V3.
Ejemplo de Reservas
Alice presta al protocolo 100 USDC y gana un 5% de interés. Bob pide prestados 100 USDC del protocolo y paga un 10% de interés. En aras de la simplicidad, asumamos que Alice y Bob son los únicos actores en el sistema. Habrá un 5% de interés extra que Bob pagó que Alice no ganó. Esta cantidad extra es la reserva.
No importa si Bob ha pagado el préstamo o no (es decir, si ya ha transferido 110 USDC al protocolo o no). Él debe al protocolo 110 USDC y el protocolo le debe a Alice 105 USDC. Por lo tanto, hay 5 USDC en las reservas.
Supongamos que Bob paga el préstamo. Ahora el protocolo tiene un saldo de 110 USDC, de los cuales 105 se le deben a Alice. Todavía hay 5 USDC en las reservas: nada cambió.
La función getReserves() devuelve este valor. Los USDC que el protocolo “posee” son la suma de
-
el saldo de USDC que mantiene Compound, es decir,
ERC20(baseToken).balanceOf(address(this))y -
el valor presente del
totalBorrow, -
menos la cantidad que el protocolo debe a los prestamistas, es decir, el valor presente del
totalSupply.
En otras palabras, es usdc_balance + totalBorrow - totalSupply.
Como puedes ver en la función a continuación, a totalSupply se le asigna un signo negativo porque eso es lo que Compound le debe a los prestamistas. Los factores positivos —la cantidad de USDC retenida y la cantidad neta de USDC que los prestatarios deben a Compound— son la cantidad de USDC “poseída” por Compound.

Si observamos la función getReserves() en Etherscan, veremos que las reservas al momento de escribir este artículo son de 3.47 millones de USD (6 decimales).

Cuando miramos el mercado USDC / Mainnet de Compound, también vemos que la interfaz frontal muestra las reservas actuales como 3.47 millones.

Si llamamos a getReserves() antes y después de un absorb(), notaríamos que las reservas disminuyeron. Esto sucede por dos razones:
-
El protocolo pagó el préstamo (por lo que se le debe menos). Esto salió de las reservas, así que, naturalmente, hay menos reservas.
-
El prestatario se convierte en prestamista con un pequeño depósito. Este depósito es adeudado por Compound al prestamista, lo que disminuye aún más las reservas.
withdrawReserves()
El exceso de reservas es para uso de la gobernanza y puede retirarse usando la función a continuación.

Reservas Objetivo
Compound tiene una variable pública inmutable targetReserves definida en Comet.sol

Cuando miramos targetReserves en Etherscan, vemos que es de 5 millones de USDC.

Las reservas objetivo (target reserves) tienen solo un uso en el protocolo: determinar si el protocolo tiene suficiente “margen de seguridad” para no vender el colateral absorbido. Ten en cuenta que la gobernanza podría cambiar este valor desplegando una nueva instancia de Comet.
Es decir, si Compound V3 tiene suficiente “efectivo excedente”, prefieren retener el colateral con el propósito de especular que aumentará de valor.
Examinemos la única función donde se usa esta variable.
buyCollateral()
El colateral todavía está dentro del protocolo después de un absorb. Todo lo que sucedió fue que el saldo de colateral del usuario se redujo a cero; sin embargo, el colateral no fue transferido a ninguna parte. Este colateral todavía está “dentro” de Compound V3.
Para incentivar a los liquidadores, el colateral en poder de Compound se vende con un descuento a través de la función buyCollateral().
Hay dos partes cruciales en la lógica de negocio de esta función:
-
Si la cantidad de reservas es mayor que las reservas objetivo ($5 millones), esta función se revertirá, no permitiendo que los liquidadores compren colateral. (recuadro amarillo en el código a continuación). Como se mencionó anteriormente, Compound desea especular con el colateral. Como ya se encuentra en una posición con mucho efectivo, no desean acumular más efectivo.
-
El tipo de cambio al que el protocolo vende el colateral está determinado por la función
quoteCollateral()(recuadro rojo en el código a continuación).
El resto del código debería ser autoexplicativo.

Bot de liquidación
Para liquidar a un prestatario, el liquidador llama a absorb() con la cuenta del prestatario como argumento, y luego llama a buyCollateral() en la misma transacción. El liquidador debería comprobar que las reservas no superen a las reservas objetivo (targetReserves), y que la cuenta sea liquidable mediante isLiquidateable(). Compound V3 proporciona un bot liquidador de referencia. Ten en cuenta que esta es una implementación de referencia: intentar obtener liquidaciones antes que otros es altamente competitivo, por lo que tu código necesitará estar extremadamente optimizado en consumo de gas para poder liquidar de forma rentable una posición antes de que otros lo hagan.
Resumen
La lista de activos que Compound V3 acepta como colateral —así como sus parámetros como el ratio de colateralización, ratio de liquidación, dirección del oráculo, etc.— están “empaquetados” en variables inmutables. Para cambiar esto, es necesaria una actualización del proxy a Compound V3.
El saldo de colateral de un usuario se rastrea a través de la combinación de un mapa de bits que indica si tienen un saldo distinto de cero para un determinado activo, y luego un mapeo anidado de prestatario ⇒ activo ⇒ assetBalance. Su saldo total de colateral es la suma de cada activo multiplicado por el precio del oráculo.
Las liquidaciones son a todo o nada. Cuando un usuario es liquidado, perderá 1 - liquidationRatio de su colateral y el remanente será utilizado para pagar la deuda y asignarle al usuario un saldo positivo.
El protocolo ahora retiene el exceso de colateral y lo pone a disposición para su venta con un descuento de acuerdo con la función quoteCollateral(). Sin embargo, no lo venderá si las reservas son más altas que las targetReserves.
Las reservas son simplemente el dinero que se le debe al protocolo más el saldo de USDC del protocolo menos la cantidad que el protocolo debe a los prestamistas. Este dinero puede ser retirado por la gobernanza.
Aprende Más con RareSkills
Consulta nuestro bootcamp de blockchain para cursos técnicos avanzados de web3.
Publicado originalmente el 8 de enero de 2024