반응형

들어가기에 앞서..

설명하지 않아야 할 부분까지 설명하는것은 귀찮으니 필요한 부분이나 빌드업을 위한 내용정도만 작성해보겠다.

이번 편에서는 compound v2에서 핵심 기능을 구현하기 위해 어떠한 내용을 고려했는지, 또 어떻게 구현했는지를 알아보도록 하자.

어떠한 방식으로 compound와 end-user가 소통하는지, 어떠한 수학적 로직이 사용되는지를 먼저 알아보고, 각종 기능들에 대해서는 다음편에서 알아보도록 하자.


Compound v2

compound프로토콜은 2019년에 출간된 Compound whitepaper(https://compound.finance/documents/Compound.Whitepaper.pdf)를 기반으로 하며, 오픈소스, 그리고 커뮤니티에 의해 관리된다. 이번 포스팅에서는 compound의 가장 기본적인 기능을 다루는 compound v2를 다뤄보도록 하겠다. 

제공하는 기능 오버뷰

  • Lending의 기본 기능
    • 담보입금(mint): 담보를 입금하고 이에 대한 cToken을 받는다
    • 담보출금(redeem): cToken을 돌려주고 이에 해당하는 양의 담보(underlying token)을 돌려받는다.
    • 대출(borrow): 담보의 가치(cToken의 가치)에 따라 LTV를 곱한 가치만큼의 자산을 빌릴 수 있다.
    • 상환(repay): 빌린 자산을 상환한다.
    • 청산(liquidation): 사용자가 borrow한 가치가 담보가치보다 커질 경우, 담보를 청산한다.

이외에도 거버넌스 등 기능이 있지만 그건 자세한 설명에서 다뤄도 괜찮을 것 같다.

 

아래 링크를 통해 실제 배포되어있는 각종 컨트랙트 주소들을 확인할 수 있다.

https://github.com/compound-finance/compound-config

 

GitHub - compound-finance/compound-config: Compound Protocol Configuration for Developers

Compound Protocol Configuration for Developers. Contribute to compound-finance/compound-config development by creating an account on GitHub.

github.com

Compound v2 Diagram

프로젝트를 진행하며 그렸던 compound v2의 함수호출 diagram이다.

출처:  https://deephigh.gitbook.io/

  1. 각 엔드유저들은 CErc20, CEther 컨트랙트를 통해 프로토콜과 상호작용하고,
  2. CErc20, CEther는 핵심 기능이 구현되어있는 cToken컨트랙트와 상호작용하여 각종 기능들을 수행한다.
  3. 또한 각 cToken들은 comptroller라고 불리는 프로토콜 관리자 컨트랙트와 상호작용하며 각종 조건검사 등을 수행하게 되는 과정으로 프로토콜이 작동하는 것을 확인해볼 수 있다.

compound v2의 구조에 대한 내용은 바로 다음 포스팅에서 더 자세히 살펴보도록 하자.

이번 포스팅에서는 Compound에서 사용되는 수학적 개념 등 가장 기초가 되는 내용들을 먼저 다루어보겠다.

Protocol Math

compound는 프로토콜에서 사용되는 각종 계산의 정밀도를 위해 ExponentialNoError.sol 라이브러리를 사용한다.

많이들 아는 것과 같이, Solidity에서는 소수점 계산이 부동소수점 방식이 아닌 고정소수점 연산을 이용한다(아무래도 소수계산 자체가 존재하지 않으니..). 따라서 정밀하거나 중요한 연산을 수행할 때 계산 로직이 왜곡될 수 있다는 위험이 존재한다.

아래 예시와 같이 나눗셈 → 곱셈의 순서로 연산을 하거나, 혹은 그렇게 했더라도 나눠지는 수의 스케일과 나누는 수의 스케일이 비슷할 경우 계산 결과의 왜곡이 생길 수 있다.

1200 * 5 / 1000 = 6
1200 / 1000 * 5 = 5

 

따라서, 이러한 위험을 방지하기 위해, 대부분 연산에서 Mantissa를 이용해 1e18을 곱한 수로 연산을 수행한다.

(Mantissa이외에 Double(1e36)이라는 개념도 사용된다.)

cToken, Underlying Token의 Decimals

모든 ERC20, Native Token의 Decimal은 18로 통일되어있는 것이 아니라 모두 다른 decimal을 이용하고 있다. 예를들어 USDC의 decimal은 6이고, WBTC는 8이다. 그리고 ETH는 18이다.

이처럼 모든 토큰별로 매핑되는 각 cToken의 가치와 관련된 계산을 할 때, 모든 Underlying Token의 decimal을 고려하기는 다소 불편한 감이 있다. 따라서 compound에서는 모든 cToken의 decimal을 아래와 같이 6으로 고정해 이용한다.

사진출처: https://docs.compound.finance/v2/#networks

하지만, 이럴 경우 underlying token과 cToken간 decimal 차이가 발생하여 계산과정에서 불편함이 생길 수 있다. compound에서는 이를 다음과 같은 방법으로 계산하여 해소한다.

exchangeRate = (getCash() + totalBorrows() - totalReserves()) / totalSupply()
// getCash: underlying.balanceOf(address(this))
// totalBorrows: User들이 borrow한 총량
// totalReserves: 준비금 총량
// totalSupply: cToken 총 발행량
underlyingTokens = cTokens * exchangeRate / 10^(18+underlyingDecimals-cTokenDecimals)

Calculate Accrual Interest

실제 은행에서도 그렇지만, web3 Defi에서도 이자율이자는 정말 중요하다. 사용자들이 납부해야 할 이자와, 예치자들에게 돌려줄 수익이 보장되고, 정확히 계산되어야 시장이 건전하게 돌아갈 수 있으니 말이다.

그렇다면, 이러한 이자는 어떻게 업데이트해주는걸까? web3에서는 모든 계산이 user가 호출하는 로직에서 수행되고, 매 호출마다 supply, borrow를 한 모든 user의 이자가 업데이트된다면 user들은 프로토콜을 이용하려하지 않을 것이다. (gas fee감당불가) 이를 해결하기 위해, compound에서는 index라는 개념을 통해 이자를 누적시킨다. index가 어떻게 사용되는지, 그리고 어떻게 계산되는지를 알아보자.

계산 로직

function accrueInterset() {
	...
	borrowRate = interestRateModel.getBorrowRate(cash, borrows, reserves)
	...
	simpleInterestFactor = borrowRate * blockDelta // 마지막 업데이트 이후 지난 블록 수 
	borrowIndex = borrowIndex * (simpleInterestFactor + 1)
	...
}

baseRatePerYear: The approximate target base APR, as a mantissa (scaled by BASE) : 연이자율

multiplierPerYear: The rate of increase in interest rate wrt utilization (scaled by BASE) : Utilization 가중치

function getBorrowRate(cash, borrows, reserves) returns (borrowRate){
	ur = borrows * 1e18 / (cash + borrows - reserves)
	borrowRate = ur * multiplierPerBlock / BASE + baseRatePerBlock
}

말로 풀어보자.

우선, borrowRate는 baseRate에 utilizationRate에 multiplier를 곱한 값을 더한 결과이다.

utilizationRate(ur)는 활용률 즉, 프로토콜에 예치된 자산 중 대출에 활용된 자산의 비율을 말하며, 활용이 많이 되고 있다는 것은 해당 자산이 많이 사용된다는 말이므로, 이에 대한 borrowRate가 커지게 된다.

이를 통해 도출된 borrowRate와 마지막 업데이트 이후 지난 블록 수(blockDelta)를 곱해 simpleInterestFactor를 계산하고, 현재 borrowIndex에 이 값을 곱한 후 borrowIndex에 더하여 업데이트한다.

이러한 방식으로 borrowIndex는 처음 세팅 이후, 예치된 자산의 활용율에 따라 점점 커지는 방식이라고 생각할 수 있다.

활용 로직

function borrowBalanceStoredInternal() returns (borrowBalance) {
  ...
  borrowSnapshot = accountBorrows[account]
  borrowBalance = borrowSnapshot.principal * borrowIndex / borrowSnapshot.interestIndex
  ...
}

user가 borrow한 token의 이자를 포함한 총량을 계산하는 과정에서 사용된다.

borrow 원금에 현재 borrowIndex를 곱한 후, borrow했을 때의 index(애매한데, 일단 그렇다 칩시다)로 나눠주면 borrow한 총량을 나타낼 수 있다.

이처럼, 매 호출마다 모든 유저의 이자를 업데이트해주는 것이 아닌, index값만 업데이트해주고, borrow한 총량을 알고싶을 때 index값과, borrow할 때의 index값만 알면 borrowBalance를 알 수 있으니 간단하게 최적화 문제를 해결할 수 있다.

 

ref

https://docs.compound.finance/v2

 

Compound v2 Documentation

Getting Started Introduction to Compound v2 The Compound protocol is based on the Compound Whitepaper (2019); the codebase is open-source, and maintained by the community. The app.compound.finance interface is open-source, and maintained by the community.

docs.compound.finance

https://deephigh.gitbook.io/

 

Welcome | DeepHigh

Welcome

deephigh.gitbook.io

반응형

+ Recent posts