Как связать кредиторов и заемщиков с помощью смарт-контрактов? Обзор кредитного протокола Compound V2

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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 можно в официальном репозитории. Смотреть необходимо контракты: CTokenStorage, CErc20Storage.

Детально разберем функции смарт-контракта 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 factor, close factor, liquidation 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);
    }

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

Источник: https://habr.com/ru/companies/metalamp/articles/811373/


Интересные статьи

Интересные статьи

На многих веб-сайтах есть форма для загрузки файла. Для некоторых сайтов, таких как OneDrive или Google Диск, загрузка файлов является основной функцией. С течением времени веб-страницы становятся все...
Всем привет, я Света, тимлид команды тестирования мобильных приложений в Emerging Travel Group :) В своей предыдущей статье я рассказала про любимый и родной Charles, а в этой я решила немного выйти и...
Привет, Хабр! Меня зовут Андрей Соловьёв, я DS в Сбере. Вероятно, практически каждый читатель этой статьи сталкивался с проблемными дорогами, если вы автомобилист, или тротуарами, если вы пешеход. Пло...
Kalm — бесплатное приложение с открытым исходным кодом. Представляет собой стандартный контроллер Kubernetes, который можно установить в любой кластер (версии v1.15 и выше), включая Amazon EKS и Googl...
Работать с Data Science в Jupyter, конечно, очень приятно, но если вы хотите пойти дальше и развернуть свой проект или модель на облачном сервере, то здесь есть много отличных решений — с...