Lending протокол или протокол кредитования. Это следующий важный раздел в сфере DeFi после DEX (Decentralized exchange). Lending протокол дает возможность пользователям брать в займы активы.

Другими словами, lending протокол является агрегатором ликвидности. Работает это следующим образом, протокол набирает потенциальных кредиторов, которые готовы предоставить свои активы для займа. Эта активы предоставляется заемщикам . Получается, что сам протокол выступает в роли посредника между заемщиком и кредитором. Он следит за честностью сделки и позволяет кредитору получить свои проценты, а заёмщику выдать кредитные активы. Для кредиторов это может быть своего рода пассивным доходом.

Когда появилась идея криптовалютного кредитования? Некоторые протоколы DeFi, такие как MakerDAO, находились в разработке еще в 2014 году. В сентябре 2018 года Compound Labs представила первую версию протокола, который позволял брать в займы пять активов: ETH, TUSD, ZRX, BAT и REP. В 2020 экономика резко остановилась из-за пандемии. Это привело к снижению процентных ставок и резкому сокращению кредитования в классических финансовых институтах. В это время для стимулирования рынка Compound выпустил токен COMP. Это привело к начало взрывного роста кредитных протоколов. Примерно в это же время появился протокол Aave.

Терминология

Borrower - заемщик.

Lender - кредитор.

Loan (займ) - основной долг или сумма кредитных средств. Обычно заемщик должен погасить основной долг + выплатить комиссию за использование средств.

Interest (процент) - комиссия, которая выплачивается lending протоколу за использование заемных средств или начисляется кредитору предоставившему свои активы для займа.

Collateral (залог) - это актив, который заемщик предоставляет для кредитора в качестве гарантии на случай невозможности погасить основной долг. Если это происходит, кредитор может конфисковать и реализовать залог. Кредитор использует залог в качестве механизма снижения риска, который гарантирует, что заемщик погасит заем.

Liquidation (ликвидация) - это "ликвидация позиции" или продажа залога для выплаты кредита. Если стоимость залога уменьшается и падает ниже порогового значения или если стоимость кредита увеличивается, то залог будет «ликвидирован», чтобы не понести экономические потери.

Принцип работы

Кредитный протокол отличается быстротой займа и ориентированностью на конечного пользователя. Большинство протоколов имеют гибкую систему процентов, целью которой является распределение выгоды для всех: протокола, заёмщика и кредитора.

Кредитование. Пользователи могут вкладывать активы в кредитный пул (смарт-контракт). Взамен они получат LP токены (токены кредитного пула), которые представляют их долю в общей стоимости кредитного пула. Обычно свои активы можно забрать в любое время с начисленными процентами.

Заем. Из-за большой волатильности активов в DeFi, заем требует сверх-обеспечения для избежания финансовых потерь в случае, когда заемщик не может выплатить кредит. Поэтому пользователь должен внести залог, стоимость которого превышает стоимость кредита. И тогда в случае, если стоимость залога по отношению к займу значительно упадет, то залога будет достаточно для покрытия основного долга. Заемщик может погасить основной долг + проценты за использование займа в любое время и забрать залог.

Важно! Процентные ставки для кредитования и займа постоянно меняются в зависимости от общего количества активов в кредитном пуле и общей суммы займов. Соотношение активов к займам называют коэффициентом использования (где-то этот показатель может называться иначе, но суть его одна). Если коэффициент использования возрастает, процентная ставка также повышается с целью привлечения дополнительных депозитов и сдерживания активности заемщиков, в противном случае действует обратный механизм.

Ликвидация. Стоимость залога может уменьшится или стоимость активов в займе может вырасти и тогда залога становится недостаточно для обеспечения займа. Если заемщик не погасит долг, то он может быть ликвидирован. Заемщик должен следить за тем, чтобы стоимость залога не падала ниже порога ликвидации. Если это произойдет, то специальные участники протокола - ликвидаторы могут автоматически ликвидировать залог заемщика для покрытия займа.

Важно! Подобной ситуации можно избежать путем внесения большего залога или погашения кредита полностью или частично.

Зачастую на ликвидацию смотрят в негативном ключе и это полностью ошибочное мнение. Если бы механизм ликвидации отсутствовал, то протоколы кредитования не могли бы защитить свою экономику и кредиторов. Получается, что механизм ликвидации помогает поддерживать баланс сил в экосистеме lending протоколов и является жизненно важным элементом.

Compound V2

Compound - один из самых первых lending протоколов в DeFi. Используется для кредитования и заимствования без посредника в виде центрального органа (например, банк). Compound связывает кредиторов и заемщиков, используя группу смарт-контрактов в EVM совместимых сетях. Вознаграждения выплачиваются в криптовалюте.

В протоколе существует два основных вида пользователя:

  • Кредиторы (Lenders) — поставляют активы на Compound. Отправляют активы на адрес смарт-контракта, который контролируется Compound. Стимулом является получение вознаграждения за предоставления средств.

  • Заемщики (Borrowers) — берут в займы активы, предоставленные для Compound кредиторами. Необходимо внести залог в качестве обеспечения для получения возможности взять займ. В момент возврата займа удерживается процент за использование заемных средств.

Важно! Дальше я буду говорить про вторую версию Compound!

Контракты

Протокол предоставляет два основных контракта:

  • СToken.sol. Олицетворяет каждый отдельный актив с которым работает протокол Compound. Такой актив в рамках cToken называется underlying (базовым).

  • Comptroller.sol. Выступает в роли главного контракта, осуществляет управление рисками, определяет количество обеспечения, которое пользователь должен поддерживать для своего займа, контролирует процесс ликвидации.

В большинстве случаев пользователь взаимодействует с протоколом через контракт CToken.

Подробнее про cToken

В настоящее время существует два типа cToken:

  1. CErc20. Является оберткой для базового актива ERC-20.

  2. CEther. Является оберткой для нативной валюты ETH.

Контракты CErc20 и CEther расширяют функционал CToken для работы с ERC-20 стандартом и с нативной валютой блокчейна соответственно. Для нативной валюты CToken будет называться сETH, для USDT - cUSDT и так далее.

С точки зрения пользователя cToken — это основной путь взаимодействия с Compound протоколом. Для осуществления операций (предоставление ликвидности или займ актива) пользователю необходимо взаимодействовать с этим контрактом. В ответ пользователю выдается эквивалент самого cToken в качестве подтверждения операции. Каждый cToken можно передавать или продавать без ограничений. В любой момент времени cToken можно обменять только на базовый актив, который изначально заблокирован в протоколе.

Для учета количества активов, протокол вводит следующие переменные:

  • totalBorrows (общее количество средств в займе),

  • totalReserves (общее количество доступных средств для займа),

  • initialExchangeRate (первоначальный курс обмена cToken на базовый актив и обратно) и другие.

Подробнее ознакомиться с переменными, которые описывают Storage контракта cToken можно в официальном репозитории. Смотреть необходимо контракты: CTokenStorageCErc20Storage.

Детально разберем функции смарт-контракта cToken. Некоторые функции представлены в контрактах CErc20 и CEther, которые расширяют функционал cToken или объявлены публичными переменными в интерфейсах CTokenInterfaces.

  • mint() - позволяет передать актив в протокол. В обмен пользователь получит количество cToken равное базовому токену деленному на exchangeRate (текущий обменный курс). На вложенный актив может накапливаться процент.

  • redeem() - функция погашения. Конвертирует указанное количество cToken в базовый актив и возвращает его пользователю.

  • redeemUnderlying() - функция погашения. Конвертирует cToken в указанное количество базового актива и возвращает его пользователю.

  • borrow() - функция займа актива. Передает актив из протокола пользователю и начинает накапливать проценты долга на основе процентной ставки актива. Требует предоставление залога, через вызов функции mint().

  • repayBorrow() - функция погашения займа. Передает актив в протокол, уменьшая долг пользователя.

  • repayBorrowBehalf() - функция погашения займа. Передает актив в протокол, уменьшая долг указанного пользователя. Это функция позволяет погасить долг за место другого заемщика.

  • liquidateBorrow() - функция ликвидации необеспеченного займа. Не может быть вызвана самим заемщикам, а только другими участниками (ликвидаторами) протокола.

  • getCashPrior() - функция получения баланса базового актива cToken.

  • totalBorrows() - функция получения суммы непогашенных базовых активов, предоставленных рынком в займы в настоящее время.

  • borrowBalanceCurrent() - функция получения суммы непогашенных базовых активов для указанного пользователя.

  • balanceOf() - функция получения баланса cToken пользователя, вложившего активы в протокол.

  • balanceOfUnderlying() - функция получения баланса базового актива пользователя, который был вложен в протокол. Равен балансу cToken пользователя, умноженному на обменный курс.

  • supplyRatePerBlock() - функция получения текущей процентной ставки для вложения базового актива в протокол за текущий блок.

  • borrowRatePerBlock() - функция получения текущей процентной ставки для займа базового актива у протокола за текущий блок.

  • totalReserves() - функция получения резервов базового актива. Небольшая часть процентов заемщика накапливается в протоколе, определяемом резервным коэффициентом (reserve factor).

  • reserveFactorMantissa() - функция получения доли процентов, в настоящее время отложенных на резервы.

Подробнее про comptroller

В Compound работает несколько сToken, которые представляют собой независимые друг от друга контракты. При этом Compound позволяет положить в залог один актив и взять в заем другой.

Это звучит логично, потому что залог и займ в одном и том же активе не имеет смысла. А значит над всеми контрактами cToken должно осуществляться управление.

Comptroller выступает в роли связующего звена для различных cToken. Он оперирует такими понятиями, как: "operation allowed", collateral factorclose factorliquidation incentive.

Каждый раз, когда пользователь взаимодействует с cToken, под капотом cToken обращается к comptroller за валидацией транзакции. Comptroller имеет полное право отклонить транзакцию при невыполнение условий.

Пример!

  • Когда пользователь хочет взять заем в Compound, сначала он обращается к соответствующему cToken контракту, чтобы предоставить залог. Например, для вложения ETH в качестве залога нужно обращаться на контракт cETH.

  • cToken контракт (в нашем случае cETH) взаимодействует с comptroller контрактом для проверки соответствия залога условиям. Например, достаточен ли размер залога. Если условия соблюдены, comptroller разрешает транзакцию, и пользователь может занять другие активы в соответствии с его залогом.

  • Comptroller также обновляет состояние аккаунта пользователя, отслеживая, сколько он занял и какой у него текущий залог.

Эта схема является упрощенной и краткой. В процессе взаимодействия comptroller и cToken вызывается множество дополнительных функций. Например, comptroller может распределять COMP токены пользователям.

В своем смарт-контракте comptroller имеет параметры:

  • borrowCaps. Параметр отвечает за ограничение займа для каждого пользователя. По умолчанию он равен 0, что говорит об отсутствии ограничения.

  • collateralFactorMantissa. Показатель, который представляет максимальную сумму для займа относительно суммарного залога. Например, значение 0.9 говорит о том, что можно занимать 90% от залоговой стоимости. Залог равен 100 DAI. Заем может быть эквивалентным стоимости не больше 90 DAI.

  • liquidationIncentiveMantissa. Параметр, который обозначает бонус, который получит ликвидатор.

Эти параметры находятся в контракте ComptrollerStorage.sol от которого наследуется контракт Comptroller.sol и используются при займах и выплатах.

Рабочая связка сToken и Comptroller организует процесс кредитования и займа для пользователей в рамках протокола. Для взаимодействия пользователя с конкретным активом вводится понятие market.

Market

Market или рынок - это полноценная мини экосистема для отдельного актива протокола. Каждый такой market обслуживается через свой собственный контракт cToken, который представляет право пользователя на базовый актив. Comptroller выступает в качестве вспомогательного контракта для cToken и регулирует доступные типы активов, в которых можно предоставлять залог, а также занимается подсчетом всей залоговой суммы пользователя, которую он внес через другие контракты cToken.

Основные характеристики market в Compound:

  1. cToken. Каждый рынок имеет свой собственный cToken (например, cDAI для DAI, cETH для ETH). Пользователи получают cToken, когда предоставляют ликвидность или залог на платформе.

  2. Процентные ставки. Каждый рынок имеет свою ставку для займа и ставку для предоставления активов. Ставки динамически адаптируются в зависимости от спроса и предложения на этом рынке.

  3. Показатели. Отображает количество актива, доступного для заимствования, общее количество заработка и другие данные.

  4. Уровень обеспеченности. Определяет, какой процент от займа должен быть обеспечен залогом, чтобы участвовать в данном рынке.

  5. Ликвидация. Регулирует процесс ликвидации и следит за процентом, который ликвидаторы могут получить в качестве вознаграждения.

Перенесемся ненадолго в приложение Compound и посмотрим, как выглядят различные markets.

Внизу скриншота представлена таблица, где за первый столбец отвечает название market. Рынок для актива "Ether" в сети Ethereum выделен красной линией. Список остальных столбцов таблицы:

  1. Utilization. Коэффициент использования или процент взятых в займ активов относительно общего количества актива доступного для займа. Про него мы поговорим подробнее позже.

  2. Earn APR. Earn Annual Percentage Rate. Годовой процент заработка для кредиторов.

  3. Borrow APR. Borrow Annual Percentage Rate. Годовой процент для заемщиков.

  4. Total Earning. Общий доход этого market.

  5. Total Borrowing. Общая сумма займа.

  6. Total Collateral. Общая сумма внесенного в протокол залога в этом активе.

  7. Collateral assets. Доступные типы актива для предоставления в качестве залога.

Всю информацию по markets можно увидеть по legacy ссылке. Смотри скриншот ниже.

Важно! Процентные ставки, доступные для залога, активы и другие важные параметры market настраиваются администратором протокола.

Получается, что множество markets - это список токенов которые доступных для кредитования или займа. Для того, чтобы Comptroller знал с какими cToken ассоциировать пользователя, необходимо ему об этом "сказать". Для этого, внутри контракта Comptroller, реализована функция enterMarkets(), которая ассоциирует пользователя с несколькими markets. Вызвать функцию enterMarkets() или "выйти на рынок" необходимо в одном случае: Для займа других активов. После "выхода на рынок" и предоставления залога можно занимать другие активы.

Supplying

Поговорим о том, с чего начинается взаимодействие пользователя с протоколом. В первую очередь, пользователь должен передать собственный актив протоколу. Переданный актив будет выступать в качестве ликвидности или в качестве залога для займа. Взамен переданных активов, пользователь получает cToken соответствующего актива.

Важно! Пользователи, не являющиеся техническими специалистами, могут взаимодействовать с протоколом при помощи интерфейса.

Предоставление ликвидности под капотом работает следующим образом:

  1. Начинается предоставление ликвидности с вызова функции mint() на соответствующем контракте CToken. Вызов в нашем собственном контракте Adapter может выглядеть следующим образом:

    contract CompoundAdapter {
        /// Добавление tokenA в качестве ликвидности в Compound
        function addCollateral(uint256 amount) external {
          /// Перевод средств с кошелька пользователя на контракт.
          /// Подразумеваем, что владелец кошелька заранее дал апрув на списание tokenA
          tokenA.transferFrom(msg.sender, address(this), amount);
          /// Старт взаимодействия с контрактом Compound
          cTokenA.mint(amount);
        }
    }
  2. Под капотом функция mint() находится в CErc20.sol или CEther.sol. Дальше вызов уходит в контракт СToken.sol через вызов mintInternal().

    function mint(uint mintAmount) override external returns (uint) {
        mintInternal(mintAmount);
        return NO_ERROR;
    }
  3. В mintInternal() мы видим последовательный вызов двух функций accrueInterest() и mintFresh(). Первая производит начисление накопленных процентов за последний расчетный период, вторая производит непосредственный трансфер ликвидности внутрь протокола и выдачу CToken взамен.

    function mintInternal(uint mintAmount) internal nonReentrant {
        accrueInterest();
        mintFresh(msg.sender, mintAmount);
    }
  4. Разберем функцию accrueInterest()

    function accrueInterest() virtual override public returns (uint) {
        /// Получаем текущий номер блока и последний номер блока в котором было начисление процентов
        uint currentBlockNumber = getBlockNumber();
        uint accrualBlockNumberPrior = accrualBlockNumber;
    
        /// Выходим из функции без ошибки, если в текущем блоке начисление процентов уже производилось
        if (accrualBlockNumberPrior == currentBlockNumber) {
            return NO_ERROR;
        }
    
        uint cashPrior = getCashPrior();
        uint borrowsPrior = totalBorrows;
        uint reservesPrior = totalReserves;
        uint borrowIndexPrior = borrowIndex;
    
        /// Расчет коэффициента текущего процента по займам
        uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
        require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high");
    
        /// Высчитываем разницу между последним блоком, когда был произведен расчет и текущим
        uint blockDelta = currentBlockNumber - accrualBlockNumberPrior;
    
        /// Расчет процентов начисленных на займы, резервы
        Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta);
        uint interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior);
        uint totalBorrowsNew = interestAccumulated + borrowsPrior;
        uint totalReservesNew = mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);
        uint borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);
    
        /// Устанавливаем текущий блок, как блок в котором произведено начисление процентов
        accrualBlockNumber = currentBlockNumber;
    
        /// Обновляем коэффициент для займа
        borrowIndex = borrowIndexNew;
        /// Обновляем показатели общего количества займов и общего количества резервов
        totalBorrows = totalBorrowsNew;
        totalReserves = totalReservesNew;
    
        emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew);
    
        return NO_ERROR;
    }
  5. Разберем функцию mintFresh()

    function mintFresh(address minter, uint mintAmount) internal {
        /// Проверка в comptroller на возможность вызывать mint() функцию
        uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);
        if (allowed != 0) {
            revert MintComptrollerRejection(allowed);
        }
    
        /// Проверяем, что в этом блоке уже были начислены накопленные проценты
        if (accrualBlockNumber != getBlockNumber()) {
            revert MintFreshnessCheck();
        }
    
        /// Получаем текущую стоимость СToken относительно базового актива
        /// Внутри функции exchangeRateStoredInternal() происходит расчет стоимости на базе totalCash, totalBorrows, totalReserves, totalSupply
        Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});
    
        /// Трансфер ликвидности на контракт CToken.
        /// Функция отличается для CErc-20 и CEther, так как нативная валюта передается с вызовом функции mint()
        uint actualMintAmount = doTransferIn(minter, mintAmount);
    
        /// Расчет количества cToken на базе стоимости cToken
        uint mintTokens = div_(actualMintAmount, exchangeRate);
    
        totalSupply = totalSupply + mintTokens;
        /// Начисление сToken взамен предоставленной ликвидности
        accountTokens[minter] = accountTokens[minter] + mintTokens;
    
        emit Mint(minter, actualMintAmount, mintTokens);
        emit Transfer(address(this), minter, mintTokens);
    }

Схема ниже в помощь! ?

Borrowing

Поговорим о том, что происходит внутри протокола Compound при займе. Предполагаем, что залог (ликвидность или обеспечение) в некотором активе уже внесен в протокол согласно описанному в разделе supplying.

Важно! Здесь нужно отметить, что заем требует сверх обеспечения, поэтому заемщику необходимо первым делом оставить достаточно залога.

  1. Начинается заем с "выхода на рынок". Для этого необходимо вызвать функцию enterMarkets() на контракте comptroller и передать в нее список адресов CToken, которые планируется использовать. Наш потенциальный контракт Adapter будет выглядеть следующим образом.

    contract CompoundAdapter {
        constructor(Comptroller comptroller, address[] cTokens) {
            /// Совершаем "выход на рынок"
            uint256[] memory errors = comptroller.enterMarkets(cTokens);
            if (errors[0] != 0) {
                revert EnterMarketsFailed();
            }
        }
    
        function borrow(uint256 amount) {
            cTokenB.borrow(amount);
            tokenB.transfer(
                msg.sender,
                tokenB.balanceOf(address(this))
            );
        }
    }
  2. Вызов enterMarkets() обрабатывает список полученных cToken и вызывает функцию addToMarketInternal()

    function addToMarketInternal(CToken cToken, address borrower) internal returns (Error) {
        Market storage marketToJoin = markets[address(cToken)];
    
        /// Возврат из функции, если токен не разрешен протоколом
        if (!marketToJoin.isListed) {
            return Error.MARKET_NOT_LISTED;
        }
    
        /// Преждевременный возврат, если пользователь уже присоединялся к рынку
        if (marketToJoin.accountMembership[borrower] == true) {
            return Error.NO_ERROR;
        }
    
        /// Записываем в state контракта, что пользователь присоединился к рынку
        marketToJoin.accountMembership[borrower] = true;
        accountAssets[borrower].push(cToken);
    
        emit MarketEntered(cToken, borrower);
    
        return Error.NO_ERROR;
    }
  3. Дальше необходимо вызвать функцию borrow() на контракте cToken. После этого кредиторы получают процент за предоставленную ликвидность, а у заемщиков увеличивается количество средств. Под капотом borrow находится в контрактах CErc20 и CEther соответственно и вызывает функцию borrowInternal().

    function borrow(uint borrowAmount) override external returns (uint) {
        borrowInternal(borrowAmount);
        return NO_ERROR;
    }
    
  4. В borrowInternal() на контракте CToken по аналогии с mintInternal() вызывается две функции: accrueInterest() и borrowFresh().

    function borrowInternal(uint borrowAmount) internal nonReentrant {
        accrueInterest();
        borrowFresh(payable(msg.sender), borrowAmount);
    }
  5. accrueInterest() мы уже частично рассмотрели в supplying. Посмотрим на borrowFresh()

    function borrowFresh(address payable borrower, uint borrowAmount) internal {
        /// Проверка возможности займа на контракте comptroller
        uint allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount);
        if (allowed != 0) {
            revert BorrowComptrollerRejection(allowed);
        }
    
        /// Отмена транзакции, если в текущем блоке не производилось начисление процентов
        if (accrualBlockNumber != getBlockNumber()) {
            revert BorrowFreshnessCheck();
        }
    
        /// Отмена транзакции, если в протоколе недостаточно свободных для займа средств
        if (getCashPrior() < borrowAmount) {
            revert BorrowCashNotAvailable();
        }
    
        /// Получаем уже существующую сумму долга заемщика
        uint accountBorrowsPrev = borrowBalanceStoredInternal(borrower);
        /// Получаем новую сумму с учетом нового займа
        uint accountBorrowsNew = accountBorrowsPrev + borrowAmount;
        /// Получаем общую сумму займа по протоколу
        uint totalBorrowsNew = totalBorrows + borrowAmount;
    
        /// Обновляем информацию по займам в state контракта
        accountBorrows[borrower].principal = accountBorrowsNew;
        accountBorrows[borrower].interestIndex = borrowIndex;
        totalBorrows = totalBorrowsNew;
    
        /// Трансфер занятых активов с протокола до заемщика.
        /// Функция отличается для CErc-20 и CEther контрактов
        doTransferOut(borrower, borrowAmount);
    
        emit Borrow(borrower, borrowAmount, accountBorrowsNew, totalBorrowsNew);
    }

Interest rates

Процентные ставки обновляются в каждом блоке и зависят от изменения соотношение свободных активов к занятым активам. Есть три типа процентных ставок:

  1. Supply rate. Это процентная ставка, которую зарабатывают поставщики ликвидности за предоставление своих активов протоколу.

  2. Exchange rate. Это курс обмена внутри протокола cToken (cDAI, cETH и так далее) к базовому активу (DAI, ETH )

  3. Borrow rate. Это процентная ставка, которую необходимо выплатить заемщику за использование заемных средств.

Значения ставок рассчитываются в отдельном смарт-контракте InterestRateModel.

Важно! Для каждого контракта CToken можно установить собственную модель расчета процентных ставок. Для этого админам доступна для вызова функция _setInterestRateModel(). Она находится в контракте cToken.

Сам контракт InterestRateModel.sol является абстракцией, которая требует реализации двух функций getBorrowRate() и getSupplyRate(). Предназначение этих функций высчитывать процентные ставки на базе: общего количества свободных активов (cash), общего количества активов в займе (borrows), количества резервных средств.

Узнать, какая модель начисления процентов достаточно просто. Необходимо вызвать публичную функция чтения interestRateModel() на контракте cToken. Ниже на скриншоте я сделал это для cDAI в сети Ethereum.

Результатом вызова функции стал адрес 0xFB564da37B41b2F6B6EDcc3e56FbF523bD9F2012. Проверим его на etherscan.

Полный код контракта JumpRateModelV2 можно посмотреть тут же в etherscan или найти в репозитории.

Supply rate

В любой момент времени для каждого cToken можно запросить процентную ставку, начисляемую поставщикам ликвидности за блок, вызвав публичную функцию supplyRatePerBlock().

function supplyRatePerBlock() override external view returns (uint) {
    return interestRateModel.getSupplyRate(getCashPrior(), totalBorrows, totalReserves, reserveFactorMantissa);
}

Интересно, что вызов interestRateModel.getSupplyRate() используется только для получения информации снаружи контракта. Для расчетов в момент снятия ликвидности используется exchangeRate.

В каждом блоке информация об процентная ставке может быть разной. При этом мы видели, что непосредственно начисление процентов происходит с вызовом функции accrueInterest().

Гениальное всегда работает просто! Поставщики ликвидности предоставляют активы, которые заемщики могут брать в займы. Заемщики выплачивают проценты (платятся в базовом активе) за использование активов. Таким образом количество базового актива постоянно растет. Все собранные проценты (выросшее количество базового актива) равномерно распределяются для всех поставщиков ликвидности в рамках market актива.

Exchange rate

Эта процентная ставка служит для отображения курса обмена базового актива на cToken и обратно. Когда предоставляем ликвидность, получаем в обмен cToken согласно обменному курсу (exchange rate).

Обменный курс между cToken и базовым активом высчитывается следующим образом:

exchangeRate = \frac{getCash() + totalBorrows() - totalReserves()}{totalSupply()}

Получить значение текущего обменного курса можно вызвав функцию exchangeRateCurrent() на контракте cToken.

Borrow rate

В любой момент времени для каждого cToken можно запросить процентную ставку, начисляемую заемщикам за блок.

function borrowRatePerBlock() override external view returns (uint) {
    return interestRateModel.getBorrowRate(getCashPrior(), totalBorrows, totalReserves);
}

Именно эту ставку заемщик должен заплатить за использование активов, взятых у протокола. В отличие от supplyRatePerBlock()borrowRatePerBlock() реализует логику interestRateModel.getBorrowRate(), которая принимает участие в расчетах начисленных процентов в функции accrueInterest() контракта cToken.

Utilization rate

Теперь пора углубиться в контракт InterestRateModel. Он является абстракцией для реализации модели процентных ставок. Сама реализация находится в контракте JumpRateModelV2.

Если мы посмотрим на JumpRateModelV2, то увидим, что он еще в свою очередь наследуется не только от InterestRateModel, но и от BaseJumpRateModelV2.

Здесь нужно остановится, так как для getSupplyRate() используется вызов функции utilizationRate().

function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual override public view returns (uint) {
    uint oneMinusReserveFactor = BASE - reserveFactorMantissa;
    uint borrowRate = getBorrowRateInternal(cash, borrows, reserves);
    uint rateToPool = borrowRate * oneMinusReserveFactor / BASE;
    return utilizationRate(cash, borrows, reserves) * rateToPool / BASE;
}

Utilization rate - коэффициент использования или процент взятых в заем активов относительно общего количества актива доступного для займа.

Количество актива в протоколе ограничено, так как любой актив предоставляется кредиторами в качестве ликвидности. Поэтому, жизненно необходимо соблюдать баланс между количеством доступного актива для займа и уже выданными активами в качестве займа.

Если свободных активов будет много, то их может быть недостаточно для генерации прибыли кредиторам. И наоборот, если свободных активов мало, то заемщикам придется искать другие lending протоколы.

Представить вычисление коэффициента использования можно в следующем виде:

utilizationRate = \frac{totalBorrows }{totalSupply}\text{, где:}
  • totalBorrows - Общее количество активов в займе

  • totalSupply - Общее количество актива

Однако totalSupply состоит из следующих компонентов

totalSupply = cash + borrows - reserves\text{, где:}
  • cash - количество актива доступное для займа. Сумма актива, предоставленная кредиторами.

  • borrow - количество актива в займе.

  • reserves - количество актива, удерживаемого протоколом в качестве прибыли

Пример!

Alice выступает кредитором и предоставляет для займа 750$

Bob выступает кредитором и предоставляет для займа 300$

Charlie взял в заем 150$

Резерв протокола составляет 10$

Тогда utilizationRate будет вычисляться следующим образом:

cash = 750 + 300 - 150 = 900 (отнимаем 150, так так Charlie уже занял эти средства и они больше недоступны для займа)

borrows = 150

reserves = 10

Получаем utilizationRate = totalBorrows / cash + borrows - reserves = 150 / (900 + 150 - 10) ≈ 0.144 ≈ 14.4%

Важно! UtilizationRate равный 14.4% означает, что только 14.4% активов протокола использовано в качестве займов. Это означает, что большая часть активов свободна, то есть протокол имеет свободную ликвидность. Заемщики могут использовать эти активы.

Закономерность! При повышении коэффициента использования - ставки на предоставление ликвидности будут расти для мотивации поставщиков, чтобы увеличить количество свободных для займа активов. При понижении коэффициента использования - ставки на заем будут снижаться, чтобы заем был более привлекателен для заемщиков.

Язык Solidity, который используется для написания смарт-контрактов для Ethereum совместимых сетей не умеет работать с дробными числами. Поэтому на смарт-контрактах это решается путем добавления в формулу точности BASE.

utilizationRate = \frac{borrows * BASE}{cash + borrows - reserves}

Для получения utilizationRate off-chain необходимо поделить utilizationRate на точность BASE.

Kink

Что произойдет, если коэффициент использования станет слишком большим и будет приближаться к 100%? Это означает, что соотношение баланса свободных активов для займа и заимствованных активов нарушено. То есть количество используемых активов в займах будет приближаться к общему количеству активов. Если коэффициент использования будет равен 100%, то это по сути остановит работу протокола. При таком состоянии, протокол не имеет свободной ликвидности для новых займов, а так как все активы уже находятся в займе, кредиторы не смогут вернуть ее обратно. В таком состоянии протокол будет пока не появятся новые поставщики ликвидности или не будет возвращена часть займов. И это ненормально. ?

Для предотвращения подобной ситуации используется специальный показатель kink. Он описывает значение utilizationRate после пересечения которого процентные ставки увеличиваются для кредиторов и заемщиков.

При utilizationRate < kink процентные ставки изменяются линейно. При utilizationRate > kink процентные ставки изменяются экспоненциально. Кредиторы начинают получать значительно больший процент за предоставление активов. Заемщикам становится дороже брать кредиты.

Перегиб кривой процентных ставок регулирует ликвидность протокола, благодаря нему событие безостановочного роста коэффициента использования становится маловероятным, так как высокие процентные ставки для кредиторов быстро привлекают большое количество ликвидности, что понижает коэффициент использования.

Важно! Уровень kink установленный в протоколе Compound равен 80%. Таким образом, после того, как utilizationRate > 80%, процентные ставки для поставщиков и заемщиков будут резко возрастать. Можно самостоятельно проверить вызвав функцию kink() в etherscan контракта JumpRateModelV2.

Borrow rate в InterestRateModelV2

Теперь можно посмотреть вычисление interest borrow rate. Я рассказал про kink и utilizationRate. Отсюда делаем вывод, что есть два варианта расчета:

  1. При utilizationRate <= kink. Формула:

    borrowRate = \frac{utilizationRate * multiplierPerBlock}{BASE} + baseRatePerBlock
    • baseRatePerBlock - Базовая процентная ставка, определяет ставку при utilizationRate = 0%.

    • multiplierPerBlock - Коэффициент процентной ставки, который моделирует ее увеличение при увеличении utilizationRate. То есть определяет наклон графика роста процентных ставок пропорционально utilizationRate. 

    • BASE - точность вычисления, так как в Solidity не поддерживается работа с дробными числами.

      Таким образом, мы получаем линейный рост процентной ставки вплоть до преодоления уровня kink.

  2. При utilizationRate > kink. Формула:

    borrowRate = \frac{excessUtil * jumpMultiplierPerBlock}{BASE} + normalRate
    • normalRate = (kink * multiplierPerBlock / BASE) + baseRatePerBlock. Как при utilizationRate <= kink

    • excessUtil = utilization rate - kink. Разница превышение utilizationRate уровня kink.

    • jumpMultiplierPerBlock. Для вычисления процентных ставок после преодоления уровня kink

В коде BaseJumpRateModelV2 это выглядит гораздо лаконичнее:

function getBorrowRateInternal(uint cash, uint borrows, uint reserves) internal view returns (uint) {
    uint util = utilizationRate(cash, borrows, reserves);

    if (util <= kink) {
        return ((util * multiplierPerBlock) / BASE) + baseRatePerBlock;
    } else {
        uint normalRate = ((kink * multiplierPerBlock) / BASE) + baseRatePerBlock;
        uint excessUtil = util - kink;
        return ((excessUtil * jumpMultiplierPerBlock) / BASE) + normalRate;
    }
}

Redemption

Ты, как мой читатель должен был уже смекнуть, что для операций supply() - предоставление ликвидности и borrow() - заем активов, должны быть обратные операции. На примерах и описаниях выше ты можешь самостоятельно поковыряться в коде протокола Compound.

Я помогу тебе в этом и укажу точки входа:

  1. Вывод ликвидности. Контракт cToken, функция redeemInternal().

  2. Погашение займа. Контракт cToken, функция repayBorrowInternal()

Liquidation

Со временем обеспечения займа может перестать хватать в силу волатильности криптовалют. Тогда займ пользователя попадает под механизм ликвидации.

Возможность ликвидации займа

Для того, чтобы понять, выполняются ли условия неприкосновенности залога и позиции займа в Compound сравнивается общая стоимость залога и общая стоимость заемных активов. За это отвечает отдельная функция getAccountLiquidity(address user) в контракте Comptroller.

В этот раз я предлагаю пропустить цепочку внутренних вызовов и сразу переместиться в функцию getHypotheticalAccountLiquidityInternal(), где заложена основная логика. Это все еще контракт Comptroller.

function getHypotheticalAccountLiquidityInternal(
    address account,
    CToken cTokenModify,
    uint redeemTokens,
    uint borrowAmount
) internal view returns (Error, uint, uint) {
    /// Сюда записываются промежуточные результаты и вычисления
    AccountLiquidityLocalVars memory vars;

    CToken[] memory assets = accountAssets[account];
    /// Организуем цикл для перебора всех вложенных активов пользователя с адресом account
    for (uint i = 0; i < assets.length; i++) {
        CToken asset = assets[i];

        /// Подгружаем информацию по каждому активу
        (oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account);
        if (oErr != 0) {
            return (Error.SNAPSHOT_ERROR, 0, 0);
        }

        /// Добавляем точность согласно контракту "./ExponentialNoError.sol"
        vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa});
        vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa});

        /// Получаем прайс из оракула для базового актива
        vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset);
        if (vars.oraclePriceMantissa == 0) {
            return (Error.PRICE_ERROR, 0, 0);
        }
        vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa});

        /// Рассчитываем стоимость одного cToken
        vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);

        /// sumCollateral += tokensToDenom(стоимость одно cToken) * cTokenBalance(количество cToken у аккаунта)
        vars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral);

        /// sumBorrowPlusEffects += oraclePrice(стоимость актива согласно оракулу) * borrowBalance(баланс заемных средств)
        vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);

        /// Для актива который пользователь брал в займ и позиция которого будет меняться
        if (asset == cTokenModify) {
            // sumBorrowPlusEffects += tokensToDenom(прайс одного cToken) * redeemTokens(количество cToken для закрытия позиции)
            vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);

            // sumBorrowPlusEffects += oraclePrice(стоимость актива) * borrowAmount (сумма средств в займе)
            vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects);
        }
    }

    /// Защита от переполнения
    if (vars.sumCollateral > vars.sumBorrowPlusEffects) {
        return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0);
    } else {
        return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral);
    }
}

На верхнем уровне это можно представить, как на схеме.

Внутри функции getHypotheticalAccountLiquidityInternal() протокол рассчитает в цикле залоговую стоимость каждого market в котором участвует пользователь. Для этого необходимо рассчитать сумму и получить данные пользователя: cTokenBalanceborrowBalanceexchangeRate. Это основная информация о borrow позиции пользователя. Для collateralFactor и стоимости collateral актива будет использован оракул.

На основании стоимости актива, курса обмена сTokens на актив и collateralFactor будет вычислена залоговая стоимость одного сToken:

tokensToDenom = collateralFactor * exchangeRate * oraclePrice

Затем запишем полученное значение в переменную, которая будет хранить суммарную стоимость по всем market.

sumCollateral += tokensToDenom * cTokenBalance

Так же вычисляется стоимость заемных средств в market с сохранением в переменную суммарного займа для всех market:

sumBorrowPlusEffects += oraclePrice * borrowBalance

Схематично проверку каждого market можно изобразить следующим образом.

Отлично, на этом цикл вычислений для каждого отдельного рынка завершается. После циклического использования данного алгоритма для каждого рынка, в котором участвует пользователь мы получаем финальные значения суммирующих переменных sumCollateral и sumBorrowPlusEffects.

В конце проверяется! Если sumCollateral > sumBorrowPlusEffects, то залога достаточно для дальнейшего обеспечения займа. Если sumCollateral < sumBorrowPlusEffects, то залога недостаточно для дальнейшего обеспечения займа. Это значит, что заем пользователя может подлежать ликвидации.

Когда залога недостаточно, пользователи подлежат ликвидации другими участниками протокола (ликвидаторами). При этом они не могут снимать или брать активы, пока залога не хватает. Получается, пользователю нужно исправить нехватку обеспечения (т.е. чтобы somCollateral > sumBorrowPlusEffects).

Почему может произойти ситуация с нехваткой обеспечения?

  • Проценты по заемным средствам накапливаются со временем, что приводит к понижению ликвидности пользователя.

  • Стоимость залога резко падает вниз (залог дешевеет).

  • Стоимость заемного актива резко взлетает вверх (заем дорожает и залога больше не хватает).

Процесс ликвидации

Когда ликвидация становится возможна, ликвидатор может погасить часть или весь заем пользователя. Взамен он получает вознаграждение в виде части залоговых средств пользователя.

Возможный процент погашения ликвидатором определяется процентом в диапазоне от 0% до 100% займа, который может быть погашен за одну транзакцию. Однако в протоколе есть возможность установить минимально и максимально допустимую сумму погашения. За это отвечает переменная closeFactorMinMantissacloseFactorMaxMantissa в контракте Comptroller.

  1. Начинается процесс ликвидации с вызова функции liquidateBorrow() на контрактах CErc20.sol или CEther.sol

        function liquidateBorrow(address borrower, CToken cTokenCollateral) external payable {
            liquidateBorrowInternal(borrower, msg.value, cTokenCollateral);
        }
  2. Дальше вызов уходит внутрь функции liquidateBorrowInternal() контракта CToken.

    function liquidateBorrowInternal(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal nonReentrant {
        /// Начисление накопленных процентов за последний расчетный период
        accrueInterest();
    
        /// Вызов ликвидации
        liquidateBorrowFresh(msg.sender, borrower, repayAmount, cTokenCollateral);
    }

    По сути уже знакомая нам схема и по процессу supplying и borrowing.

  3. Дальше вызов уходит в функцию liquidateBorrowFresh() контракта CToken.

    function liquidateBorrowFresh(address liquidator, address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal {
        /// Проверяется разрешение на ликвидацию у контракта Comptroller
        uint allowed = comptroller.liquidateBorrowAllowed(address(this), address(cTokenCollateral), liquidator, borrower, repayAmount);
        if (allowed != 0) {
            revert LiquidateComptrollerRejection(allowed);
        }
    
        /// Проверяется, что начисление процентов было именно в текущем блоке
        if (accrualBlockNumber != getBlockNumber()) {
            revert LiquidateFreshnessCheck();
        }
    
        /// Дополнительно проверяется, что для нужного CToken начисление процентов было в текущем блоке
        if (cTokenCollateral.accrualBlockNumber() != getBlockNumber()) {
            revert LiquidateCollateralFreshnessCheck();
        }
    
        /// Запрещается заемщику ликвидировать собственный заем
        if (borrower == liquidator) {
            revert LiquidateLiquidatorIsBorrower();
        }
    
        /// Сумма погашения не должна быть нулевой
        if (repayAmount == 0) {
            revert LiquidateCloseAmountIsZero();
        }
    
        /// Сумма погашения не должна быть отрицательной, за пределами uint
        if (repayAmount == type(uint).max) {
            revert LiquidateCloseAmountIsUintMax();
        }
    
        /// Вызов функции погашения займа
        uint actualRepayAmount = repayBorrowFresh(liquidator, borrower, repayAmount);
    
        /// Рассчитывается количество cToken, которое должно списаться с ликвидатора для погашения займа
        (uint amountSeizeError, uint seizeTokens) = comptroller.liquidateCalculateSeizeTokens(address(this), address(cTokenCollateral), actualRepayAmount);
        require(amountSeizeError == NO_ERROR, "LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED");
    
        /// Проверяется, что у ликвидатора достаточно cToken для списания в счет ликвидации займа пользователя
        require(cTokenCollateral.balanceOf(borrower) >= seizeTokens, "LIQUIDATE_SEIZE_TOO_MUCH");
    
        /// Если адрес контракта вызова и адрес указанного cToken совпадает, то
        /// уменьшить позицию заемщика и начислить вознаграждение ликвидатору
        if (address(cTokenCollateral) == address(this)) {
            seizeInternal(address(this), liquidator, borrower, seizeTokens);
        } else {
            require(cTokenCollateral.seize(liquidator, borrower, seizeTokens) == NO_ERROR, "token seizure failed");
        }
    
        emit LiquidateBorrow(liquidator, borrower, actualRepayAmount, address(cTokenCollateral), seizeTokens);
    }

Остается посмотреть 4 функции, которые вызываются по ходу исполнения:

  1. Что происходит в функции проверки возможности ликвидации займа liquidateBorrowAllowed() на контракте Comptroller.

    function liquidateBorrowAllowed(
        address cTokenBorrowed,
        address cTokenCollateral,
        address liquidator,
        address borrower,
        uint repayAmount
    ) override external returns (uint) {
        liquidator;
    
        /// Проверка, что токены доступны в протоколе compound
        if (!markets[cTokenBorrowed].isListed || !markets[cTokenCollateral].isListed) {
            return uint(Error.MARKET_NOT_LISTED);
        }
    
        /// Получаем сумму займа для заемщика
        uint borrowBalance = CToken(cTokenBorrowed).borrowBalanceStored(borrower);
    
        /// Проверка, что market для cToken не был запрещен
        if (isDeprecated(CToken(cTokenBorrowed))) {
            require(borrowBalance >= repayAmount, "Can not repay more than the total borrow");
        } else {
            /// Проверяем, что позиция пользователя доступна для ликвидации
            (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower);
            if (err != Error.NO_ERROR) {
                return uint(err);
            }
    
            if (shortfall == 0) {
                return uint(Error.INSUFFICIENT_SHORTFALL);
            }
    
            /// Проверка, что будет закрыта позиция на сумму не больше максимально разрешенной протоколом
            uint maxClose = mul_ScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance);
            if (repayAmount > maxClose) {
                return uint(Error.TOO_MUCH_REPAY);
            }
        }
        return uint(Error.NO_ERROR);
    }
  2. Что происходит при вызове функции repayBorrowFresh(). По сути отрабатывает погашение займа, только от имени ликвидатора. Ты уже должен был самостоятельно пройти процесс погашения займа, поэтому не буду приводить функцию целиком. Если ты этого еще не сделал или подзабыл, то можно посмотреть в контракт cToken.

  3. Что происходит при вызове функции liquidateCalculateSeizeTokens() непосредственно для расчета количества залогового токена в контракте cToken.

    function liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint actualRepayAmount) override external view returns (uint, uint) {
        /// Получение стоимости залоговых и заемных средств из оракула
        uint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed));
        uint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral));
        if (priceBorrowedMantissa == 0 || priceCollateralMantissa == 0) {
            return (uint(Error.PRICE_ERROR), 0);
        }
    
        /// Получение exchangeRate для обмена CToken
        uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error
        uint seizeTokens;
        Exp memory numerator;
        Exp memory denominator;
        Exp memory ratio;
    
        /// Расчет количества залогового токена, которое будет ликвидировано
        numerator = mul_(Exp({mantissa: liquidationIncentiveMantissa}), Exp({mantissa: priceBorrowedMantissa}));
        denominator = mul_(Exp({mantissa: priceCollateralMantissa}), Exp({mantissa: exchangeRateMantissa}));
        ratio = div_(numerator, denominator);
    
        seizeTokens = mul_ScalarTruncate(ratio, actualRepayAmount);
    
        return (uint(Error.NO_ERROR), seizeTokens);
    }
  4. Что происходит при вызове функции seizeInternal() для непосредственной ликвидации cToken пользователя и начисления вознаграждения для ликвидатора.

    function seizeInternal(address seizerToken, address liquidator, address borrower, uint seizeTokens) internal {
        /// Проверяется возможность вызова функции на контракте Comptroller
        uint allowed = comptroller.seizeAllowed(address(this), seizerToken, liquidator, borrower, seizeTokens);
        if (allowed != 0) {
            revert LiquidateSeizeComptrollerRejection(allowed);
        }
    
        /// Запрещается заемщику ликвидировать собственный заем
        if (borrower == liquidator) {
            revert LiquidateSeizeLiquidatorIsBorrower();
        }
    
        /*
         * Рассчитываем новые балансы для заемщика и ликвидатора
         *  borrowerTokensNew = accountTokens[borrower] - seizeTokens
         *  liquidatorTokensNew = accountTokens[liquidator] + seizeTokens
         */
        uint protocolSeizeTokens = mul_(seizeTokens, Exp({mantissa: protocolSeizeShareMantissa}));
        uint liquidatorSeizeTokens = seizeTokens - protocolSeizeTokens;
        Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});
        uint protocolSeizeAmount = mul_ScalarTruncate(exchangeRate, protocolSeizeTokens);
        uint totalReservesNew = totalReserves + protocolSeizeAmount;
    
        /// Непосредственно обновляем значения балансов cToken для заемщика и ликвидатора
        totalReserves = totalReservesNew;
        totalSupply = totalSupply - protocolSeizeTokens;
        accountTokens[borrower] = accountTokens[borrower] - seizeTokens;
        accountTokens[liquidator] = accountTokens[liquidator] + liquidatorSeizeTokens;
    
        emit Transfer(borrower, liquidator, liquidatorSeizeTokens);
        emit Transfer(borrower, address(this), protocolSeizeTokens);
        emit ReservesAdded(address(this), protocolSeizeAmount, totalReservesNew);
    }

Схематично порядок вызова ликвидации, который я описал выше, можно представить следующим образом.

Cascading liquidations

Опр! Каскадные ликвидации - это последовательность ликвидаций, которые вызывают друг друга. Например, когда значение залога резко и системно падает, открывается возможность ликвидации по нескольким позициям. Ликвидаторы честно выполняют свою обязанность, но получая залоговый актив в качестве обеспечения стараются поскорее от него "избавиться" (например,продать, пока еще выгодно), что еще больше роняет стоимость актива и открывает возможность для новых ликвидаций вызывая цепную реакцию. Это происходит потому что для расчета стоимости залога используется оракул, который ориентируется на рыночную стоимость актива.

В протоколе Compound предусмотрены некоторые меры для снижения риска каскадных ликвидаций:

  1. Оценка обеспечения. Compound использует надежные ценовые оракулы и проводит частые переоценки стоимости активов на рынке. Это помогает правильно оценить стоимость залога и предотвратить ненужные или преждевременные ликвидации.

  2. Требования к обеспечению. Требуется сверх обеспечение займа. Это создает "буфер" для колебаний стоимости активов.

  3. Ограничения на максимальную сумму заимствования. Протокол устанавливает лимиты для различных активов. Это помогает предотвратить чрезмерное заимствование и снижает вероятность массовых ликвидаций.

  4. Сглаживание цен. Вместо того чтобы реагировать на краткосрочные колебания рыночных цен, Compound может использовать усредненные или сглаженные цены на протяжении определенного времени для определения стоимости залога.

  5. Администрирование. В случае экстремальной ситуации на рынке у администраторов протокола есть возможность вмешаться в систему, чтобы предотвратить каскадные ликвидации или другие проблемы. Это может включать в себя изменение параметров системы или даже временную приостановку работы протокола.

  6. Коммуникация с сообществом. Compound активно взаимодействует со своим сообществом через различные каналы, включая форумы и социальные сети. Это позволяет оперативно информировать пользователей о рыночных рисках, предоставляя рекомендации по управлению своими позициями.

Эти меры, в совокупности, помогают уменьшить риски каскадных ликвидаций, но не могут полностью исключать подобный исход. Поэтому в заемщикам все-таки необходимо быть осведомленными о рисках и управлять своими позициями в займах соответствующим образом.

Вывод

На этой хорошей ноте стоит остановится. Я рассказал все самые основные моменты работы протокола. От предоставления ликвидности до займа и от расчетов начисления процентов до ликвидаций. На сегодняшний день протокол Compound выпустил третью версию. В этой версии вводится новое понятие "Comet" и обновленная архитектура, но это уже совсем другая история!

Спасибо, что прочитали статью до конца! Буду рад вашей обратной связи в комментариях:)

Комментарии (0)