[Compound series] 2. 컴파운드 로직 분석 — 예금, 출금

Tariz
Decipher Media |디사이퍼 미디어
21 min readJun 17, 2021

시작하며

본 게시글에서는 앞 게시글 [Compound series] 1. 컴파운드 톺아보기에 이어 Compound의 로직을 컨트랙트 코드를 기반으로 분석합니다. Compound는 암호화폐 과담보 대출 서비스로 특정 암호화폐를 예금하고, 이를 담보로 원하는 암호화폐를 대출할 수 있는 서비스입니다. 오늘 게시글에서는 예금과 출금의 동작 방법에 대하여 구체적인 내용을 정리하며, 이를 위해 각 로직에 대한 Compound의 컨트랙트 코드를 분석합니다.

[Compound series]

  1. 컴파운드 톺아보기
  2. 컴파운드 로직 분석 (예금, 출금)
  3. 컴파운드 로직 분석 (대출, 상환)
  4. 컴파운드 로직 분석 (청산)
  5. 컴파운드 이자율 계산 분석
  6. 컴파운드의 미래 — Compound Gateway

Authors

부현식, 정현 of Decipher DeFi Research Team DeFinite
Seoul Nat’l Univ. Blockchain Academy Decipher(@decipher-media)

용어 정리

  • 사용자 : Compound 프로토콜에서 자산을 공급, 차입하는 모든 참여자. 공급자가 될 수도 있고 차입자가 될 수도 있다.
  • 공급자 : Compound에 자산을 예치만 한 사용자. ‘대출’ 이자를 수령할 수 있다.
  • 차입자 : Compound를 통해 자산을 예치 후 다른 자산을 빌려 가는 사용자. ‘차입’ 이자를 지불해야 한다.
  • 대출 서비스 : Compound 프로토콜 자체를 일컫는 말. Compound에서 제공되는 서비스를 일컬을 때는 ‘차입’ 서비스가 아닌 ‘대출’ 서비스라고 한다.
  • 대출금 : 공급자 입장에서 대출 서비스의 대상이 되는 자산. 예) 대출금 회수
  • 차입금 : 차입자 입장에서 대출 서비스의 대상이 되는 자산. 예) 차입금 상환
  • 기초 암호화폐(underlying cryptocurrency): Compound에서 cToken으로 변환되기 전 암호화폐를 의미

cToken

[Compound series] 1. 컴파운드 톺아보기 편에서 살펴본 것처럼 사용자는 Compound에서 원하는 암호화폐를 예치하고 이를 담보로 암호화폐를 차입할 수 있습니다. Compound의 운용 로직은 총 5가지로 나눌 수 있으며 예금, 출금, 대출, 상환, 청산이 이에 해당됩니다. Compound는 이 과정에서 사용되는 모든 자산을 cToken으로 변환하여 운용합니다.

Compound는 과담보 대출을 지원하는 모든 기초 암호화폐에 대한 cToken 컨트랙트를 가지고 있습니다. 즉, ETH에 대한 cETH 컨트랙트, DAI에 대한 cDAI 컨트랙트가 각각 존재합니다. 모든 자산의 컨트랙트는 Compound의 5가지 운용 로직을 모두 포함하고 있으며, 사용자의 거래 자산에 따라 선택되어 동작합니다. 쉽게 말해, 사용자가 ETH를 예치한다면 cETH 컨트랙트의 예치 기능이 동작하고, DAI를 예치한다면 cDAI 컨트랙트의 예치 기능이 동작하게 됩니다.

각 상황에 따른 cToken 컨트랙트의 동작 방법을 간단히 살펴봄으로써 cToken의 역할을 이해해보도록 하겠습니다.

상황 1. 공급자가 ETH를 Compound에 예치하는 경우

[Mint 함수 실행]1. 공급자의 월렛에서 ETH를 Compound의 cETH 컨트랙트에 전송2. Compound가 특정 교환 비율을 사용하여 전송한 ETH를 cToken인 cETH로 변환 후 사용자의 월렛에 전송

기초 암호화폐인 ETH에 대해 교환되는 cToken인 cETH의 개수는 Compound의 시장 상황에 따라 결정되는 ‘교환 비율’에 의해 결정됩니다.

상황 2. 공급자가 예치한 ETH를 Compound에서 출금하는 경우

[Redeem 함수 실행]1. ETH 출금 요청2. Compound가 특정 교환 비율을 사용하여 사용자가 요청한 ETH를 받기 위해 지급해야 할 cETH의 개수 계산3. 사용자가 계산된 cETH를 Compound의 cETH 컨트랙트에 전송4. Compound가 1번에서 요청한 ETH를 공급자의 월렛에 전송

*교환 비율에 대한 자세한 내용은 아래에서 설명합니다.

상황 3. 차입자가 예치한 ETH를 담보로 Compound에서 DAI를 차입하는 경우

[Borrow 함수 실행]1. 사용자가 DAI 차입 요청2. Compound가 사용자가 요청한 DAI가 해당 사용자에게 대출 가능한지 확인3. Compound가 사용자가 요청한 DAI를 사용자 월렛에 전송

상황 4. 차입자가 차입한 DAI를 Compound에 상환하는 경우

[Repay 함수 실행]1. 차입자가 DAI 상환 요청2. Compound가 차입자의 DAI 상환 가능 여부 확인3. 차입자가 Compound의 cDAI 컨트랙트에 차입한 DAI를 전송

예금 [Mint]

요약: 공급자가 예치한 기초 암호화폐를 cToken으로 변환하여 공급자 월렛에 전송

mint는 Compound에 예치한 암호화폐를 프로토콜에서 관리하기 위해 cToken을 발행하는 기능입니다. 아래 컨트랙트에서 mint 함수의 기능을 자세하게 살펴보겠습니다.

  1. Compound 시장의 대출량과 이자율 검사
mintInternal

mintInternal 함수mintFresh 함수의 실행 전 간단한 에러를 확인하는 기능입니다. 위 함수에서 확인하는 에러는 accrueInterest 함수로 Compound 시장의 암호화폐의 대출량과 이자율을 계산하는 로직에 이상이 없다는 것입니다. 에러 이후 mintInternal 함수는 mintFresh 함수를 실행합니다.

한편, accrueInterest 함수는 예금을 위해 실행되는 mint 함수뿐만 아니라 출금, 대출, 상환을 위해 실행되는 모든 함수에서 실행됩니다. 이 함수의 핵심 역할은 각 함수의 실행마다 이자를 계산하는 것입니다.

accrueInterest 함수에 대한 자세한 내용은 다음 게시글인 [Compound series] 3. 컴파운드 이자율 분석에서 다룰 예정입니다.

다음으로 소개되는 Compound의 모든 로직(예금, 출금, 대출, 상환)에는 accrueInterest 함수 실행과 에러 검사를 위한 로직을 모두 포함하고 있지만, 위 기능과 동일하므로 설명에서는 제외됩니다.

2. 공급자에게 제공해야 할 cToken 발행 여부 확인과 COMP 토큰 계산

mintFresh
minter: 공급자의 월렛 주소mintAmount: 공급자가 예치할 기초 암호화폐의 개수

mint 기능의 핵심 로직은 mintFresh 함수를 통해 실행됩니다. 본격적인 mint 기능 실행에 앞서 Compound는 comptroller 컨트랙트의 mintAllowed 함수를 통해 다음 두 가지를 확인합니다.

mintAllowed

1) 공급자에게 제공해야 할 cToken의 발행 가능 여부 확인

Compound는 예치 기능을 운용하는 데 있어서 공급자의 기초 암호화폐를 모두 cToken으로 변환하며, 이를 공급자의 예치 증서로 공급자에게 제공합니다. 즉, Compound 입장에서는 (1) 공급자의 예치 자산이 Compound 시장에서 이미 운용되고 있는 자산이어야 하며, (2) 해당 기초 암호화폐의 cToken이 발행 가능한 상태여야 합니다. 위 과정에서는 이 두 가지를 검사한 후 다음 동작을 실행합니다.

2) distributeSupplierComp 함수를 통해 공급자에게 제공해야 할 COMP 토큰의 개수 계산

distributeSupplierComp

Compound는 모든 사용자(공급자, 차입자)에게 서비스 사용에 대한 특수한 리워드인 Compound의 거버넌스 토큰인 COMP를 제공합니다. COMP 토큰의 제공량은 블록마다 정해져 있으며, 제공 비율은 각 Compound 내의 토큰 마켓의 상황에 따라 변화합니다. COMP 토큰의 자세한 내용은 본 Compound의 docs에서 확인할 수 있습니다.

3. 이자율을 가장 최신 블록 시점으로 업데이트했는지 확인

accrualBlockNumber
accrualBlockNumber: 마지막으로 이자를 갱신한 시점의 블록 넘버

다시 mintFresh 함수로 돌아오면, mintAllowed 함수에 에러가 없는 경우 이자율 업데이트 여부를 확인합니다. Compound의 이자율은 블록마다 변경될 수 있지만, 연산의 효율을 위해 ‘블록마다’ 계산하여 업데이트하지 않고 특정 기능이 호출될 때 밀린 블록만큼의 연산을 누적하여 한 번에 실시합니다. mintFresh 함수는 mint 함수를 호출한 시점의 블록 넘버에서 이자율이 다른 사용자의 거래에 의해 업데이트되었는지 확인하는 과정을 거칩니다.

Compound의 모든 기능은 이자율을 통한 연산이 필요하기 때문에 호출된 시점에 이자율이 업데이트되었는지 확인합니다. mintInternal 함수와 마찬가지로, 이후 다루는 모든 기능에는 위 로직(accrualBlockNumber)이 포함되어 있지만, 기능이 중복되므로 설명에서는 제외됩니다.

4. 현재 교환 비율에 따른 cToken 발행량 계산

minFresh — vars.actualMintAmount
actualMintAmount: 공급자가 예치할 기초 암호화폐의 개수exchangeRate: 기초 암호화폐에 대응하여 발행되는 cToken의 개수를 결정하는 교환 비율mintTokens: 사용자가 예금한 기초 암호화폐의 개수에 따라 받는 cToken의 개수

사용자가 예금한 기초 암호화폐의 개수에 대해 받을 수 있는 cToken의 개수는 현재 블록 시점에서 계산된 특정 교환 비율(exchageRate)에 따라 결정됩니다.

이 교환 비율을 결정하는 식은 아래와 같습니다.

exchangeRate = 
(underlyingBalance + totalBorrowBalance − reserve)/cTokenSupply
underlyingBalance: cToken 컨트랙트에 남아있는 기초 암호화폐 개수totalBorrowBalance: cToken 컨트랙트에서 대출한 토큰의 개수(대출 이자 포함)reserve: 대출 이자 중에서 cToken 컨트랙트가 보험으로 남겨놓은 개수cTokenSupply: cToken 컨트랙트가 예금한 사용자에게 공급한 cToken의 개수

위와 같이 계산된 교환 비율은 시간이 지남에 따라 증가하게 됩니다. 그 이유는 분자에 해당하는 totalBorrowBalance에 차입 이자가 포함되기 때문입니다. 이 교환 비율이 증가하게 되면 사용자는 동일한 개수의 cToken에 대해서 더 많은 기초 암호화폐를 받을 수 있습니다. 교환 비율을 통해 cToken의 개수가 정해지는 수식은 다음과 같습니다.

mintToken = actualMintAmount/exchangeRate

위 식을 통해, 교환 비율이 0.02일 때 10 ETH를 예금한다면, 사용자는 500 cETH를 받는 것을 알 수 있습니다.

예금 이자에 대한 자세한 내용은 이후 게시글인 [Compound series] 3. 컴파운드 이자율 분석에서 다룰 예정입니다.

5. 사용자의 cToken 보유량과 컨트랙트의 cToken 발행량 업데이트

mintFresh — addUInt
totalSupply: 컨트랙트가 사용자에게 공급한 cToken의 개수accountToken: 사용자 계정이 보유한 cToken의 개수mintTokens: 사용자가 예금한 기초 암호화폐의 개수에 따라 받는 cToken의 개수

이제 mintFresh함수는 위에서 계산된 mintTokens의 개수를 통해 사용자의 cToken 보유량과 컨트랙트의 cToken 발행량을 새롭게 계산합니다. 먼저 컨트랙트가 사용자들에게 공급한 cToken의 개수를 조정합니다. 컨트랙트가 예금 기능을 통해 사용자에게 cToken을 제공하면 totalSupply의 개수도 늘어나도록 해야 하기 때문에 새롭게 업데이트되는 totalSupplyNew는 다음과 같이 계산됩니다.

totalSupplyNew = totalSupply + mintTokens

두 번째로 사용자의 cToken 보유량을 계산합니다. 사용자는 예금 기능을 통해 컨트랙트를 통해 위에서 계산된 cToken을 지급 받기 때문에 새롭게 업데이트 되는 accountTokensNew는 다음과 같이 계산됩니다.

accountTokensNew = accountTokens[minter] + mintTokens

6. 토큰 양 업데이트

mintFresh — totalSupply
totalSupply: cToken 컨트랙트가 예금한 사용자에게 공급한 cToken의 개수accountToken: 사용자 계정이 보유한 cToken의 개수

이제 Compound 프로토콜은 5번 과정에서 계산한 값인 전체 cToken 발행량과 사용자의 cToken 보유량을 업데이트 합니다.

7. 전송 결과 확인

위 과정이 모두 정상적으로 동작했다면 컨트랙트는 사용자가 기초 암호화폐를 전송하도록 한 후(Mint), 특정 교환 비율을 통해 결정된 cToken을 사용자의 지갑으로 전송합니다(Transfer).

위 사진을 통해 2번 이벤트에서 0.158 ETH의 기초 암호화폐 예금되고, 3번 이벤트에서 6.479개의 cETH가 컨트랙트에서 사용자에게 전송되는 것을 알 수 있습니다.

출금 [Redeem]

요약: 사용자가 출금 요청한 기초 암호화폐를 전송하기 위해 사용자가 전송해야 하는 cToken의 개수를 결정한 후, 사용자는 cToken을 컨트랙트는 기초 암호화폐를 전송

redeem은 사용자가 Compound에 예치한 자산의 출금을 요청할 때 실행되는 함수입니다. 이 함수는 사용자의 과거 예금으로 인해 발행되어 월렛에 보유하고 있던 cToken을 특정 교환 비율에 따라 기초 암호화폐로 변환하여 전송하는 기능입니다. 사용자는 두 가지 방법으로 출금을 요청할 수 있으며 각 방법에 따라 실행되는 첫 번째 함수가 달라집니다.

  1. 사용자가 요청한 트랜잭션의 입력값 확인

1) 사용자가 출금을 위해 전송할 cToken 개수를 입력

redeemInternal
redeemTokens: 사용자가 Compound에 전송하는 cToken의 개수
  • 사용자는 자신이 가지고 있는 cToken 중에서 환전하고 싶은 양을 선택하여 요청할 수 있습니다.
  • 예) 100ETH 예금으로 받은 500cETH 중에서 200 cETH에 해당하는 ETH 출금 요청

2) 사용자가 출금하고 싶은 암호화폐의 개수를 입력

redeemUnderlyingInternal
redeemAmount: 사용자가 돌려받는 기초 암호화폐의 개수
  • 사용자는 자신의 예치 자산 중에서 출금하고 싶은 양을 선택하여 요청할 수 있습니다.
  • 예) 전체 예치 자산인 100ETH 중에서 80ETH 출금 요청

실제로 Compound에서는 2번 방법을 앱에서 지원하고 있습니다. 따라서 이후 로직 설명도 2번의 상황을 가정하여 살펴봅니다. 앞서 mint에서 살펴본 accureInterest 함수 실행에 에러가 없다면 redeemFresh 함수를 실행합니다. redeemFresh 함수의 입력값에서 주목할 것은 두 번째 값이 0이라는 것입니다.

accureInterest, accrualBlockNumber에 대한 설명은 mint 함수와 중복되므로 설명에서 제외됩니다.

2. 공급자가 지정한 기초 암호화폐에 대한 cToken 개수 계산

redeemFresh
redeemTokens: 사용자가 Compound에 전송하는 cToken의 개수redeemAmount: 사용자가 돌려받는 기초 암호화폐의 개수

1단계에서 살펴본 입력값을 통해 redeemFresh 함수를 실행합니다. redeemFresh 함수의 첫 번째 실행은 redeemTokens의 값이 0인지 아닌지 확인하는 것입니다. redeemTokens이 0이 아닌 경우는 1번 단계에서 사용자가 출금 시 전송할 cToken의 개수를 지정했다는 것을 의미하고, 0인 경우는 출금 시 받을 기초 암호화폐의 개수를 지정했다는 것을 의미합니다. redeemTokens 값이 0 이상이라면, cToken 컨트랙트는 전송된 cToken의 값을 통해 사용자에게 지불할 기초 암호화폐의 개수를 계산해야 합니다.

redeemTokens의 값이 0이라면, cToken 컨트랙트는 사용자가 입력한 기초 암호화폐(redeemAmount)를 전송하기 위해 사용자가 전송해야 하는 cToken의 개수(redeemTokens)를 계산해야합니다.

본 게시글에서는 1단계의 2번 상황인 redeemTokens의 개수가 0인 경우를 가정하여 로직을 살펴봅니다.

공급자가 받길 원하는 기초 암호화폐 개수(redeemAmount)에 대해 전송해야하는 cToken의 개수는 다음과 같은 수식에 의해 결정됩니다.

redeemTokens = redeemAmountIn/exchangeRateCurrentredeemAmountIn = redeemAmountsexchageRateCurrent = exchageRate

수식에서 사용되는 교환 비율(exchangRate)은 앞서 mint에서 살펴본 교환 비율과 동일한 방법으로 계산됩니다. 위 교환 비율은 시간이 지남에 따라 차입자의 차입 이자 지급으로 인해 상승합니다. 위 식에서 증가한 교환 비율을 통해 사용자는 출금 시 자신이 예금 시 받았던 cToken을 지불하여 예금 시보다 더 많은 개수의 기초 암호화폐를 돌려받게 되며, 여기에 포함된 순수익이 예금에 대한 이자로 적용됩니다. 이와 마찬가지로 동일한 기초 암호화폐의 개수에 대해 더 적은 cToken을 지불하도록 계산됩니다.

앞선 예시로 돌아가 공급자가 0.02의 교환 비율을 통해 사용자가 10 ETH를 예치하여 500 cETH를 받았다고 가정해봅시다. 동일한 공급자가 출금을 요청하는 시점의 교환 비율이 0.04로 증가한다면, 동일한 10ETH를 출금하기 위해 지급해야하는 cETH는 250개에 불과합니다.

3. redeemAllowed 함수 실행

redeemAllowed

위 과정에서 입력값이 정해지면 comptroller 컨트랙트에서 redeemAllowed 함수를 실행합니다. 이 함수에는 핵심적인 로직 중 하나인 ‘출금시 사용자의 계좌 유동성 확인’을 위한 과정이 포함되어 있습니다.

출금 시 사용자의 계좌 유동성 확인을 위한 과정은 링크된 게시글을 확인해주세요.

가상 계좌를 통해 사용자의 계좌에 위 금액을 출금할 만큼 유동성이 충분하다는 것을 확인하면, mintAllowed 함수의 로직과 동일하게 다음 두 가지를 확인합니다. 1) cToken이 compound 마켓에 리스팅 되어있는 토큰인지 확인하고 2) distributeSupplierComp 함수를 통해 공급자에게 제공해야 할 COMP 토큰의 개수 계산합니다.

redeemAllowedInternal
comptroller — redeemAllowed

4. 사용자의 cToken 보유량과 컨트랙트의 cToken 발행량 업데이트

subUInt
totalSupply: 컨트랙트가 사용자에게 공급한 cToken의 개수accountToken: 사용자 계정이 보유한 cToken의 개수redeemTokens: 사용자가 Compound에 전송하는 cToken의 개수

이제 redeemFresh 함수는 위에서 계산된 redeemTokens의 개수를 통해 사용자가 보유한 cToken과 컨트랙트가 공급한 cToken 컨트랙트를 새롭게 계산합니다. 먼저 컨트랙트가 사용자들에게 공급한 cToken의 개수를 조정합니다. 컨트랙트가 출금 기능을 통해 사용자에게 cToken을 돌려받으면 totalSupply의 개수도 줄어들도록 해야하기 때문에 새롭게 업데이트되는 totalSupplyNew는 다음과 같이 계산합니다.

totalSupplyNew = totalSupply — redeemTokens

두 번째로 사용자가 위에서 계산된 cToken을 컨트랙트에 전송해야 하기 때문에 그만큼의 cToken을 보유하고 있어야 합니다. 이를 확인하고 계정의 cToken 값을 업데이트하기 위해 사용자의 새로운 cToken 보유량을 다음과 같이 계산합니다.

accountTokensNew = accountTokens[redeemer] — redeemTokens

4. Compound 내의 유동성(cash) 확인

getCashPrior

redeem 함수는 궁극적으로 공급자가 전송한 cToken을 기초 암호화폐로 변환하여 공급자에게 전송하는 기능입니다. 공급자가 받게 되는 암호화폐의 개수는 교환하고자 하는 cToken의 개수와 현재 교환 환율을 곱한 값입니다. 그리고 이렇게 계산된 기초 암호화폐의 개수는 시장이 유동할 수 있는 기초 암호화폐 개수보다 적어야 합니다.

getCashPrior: 시장이 유동할 수 있는 기초 암호화폐의 개수

getCashPrior 함수를 통해 가져오는 값은 cToken 컨트랙트가 보유한 전체 기초 암호화폐 중에서 유동할 수 있는 기초 암호화폐의 개수입니다. 즉, Compound는 공급자가 요청한 기초 암호화폐를 전송하기 전에 지급 개수가 cToken 컨트랙트가 유동할 수 있는 기초 암호화폐 개수보다 작은지 확인하고, 큰 경우 에러를 반환합니다.

5. 토큰 양 업데이트

redeemFresh — calculated
totalSupply: cToken 컨트랙트가 예금한 사용자에게 공급한 cToken의 개수accountToken: 사용자 계정이 보유한 cToken의 개수

이제 Compound 프로토콜은 3번 과정에서 계산한 값인 전체 cToken 발행량과 사용자의 cToken 보유량을 업데이트합니다.

6. 전송 결과 확인

위 과정이 모두 정상적으로 동작했다면 컨트랙트는 사용자가 cToken을 전송하도록 한 후, 1번 과정에서 사용자가 입력한 기초 암호화폐를 사용자의 지갑으로 전송합니다.

위 사진을 통해 2번 이벤트에서 사용자가 미리 입력한 0.52개의 ETH에 대해 현재 교환 비율로 계산된 2.18개의 cETH가 cETH 컨트랙트에 전송된 후, 3번 이벤트에서 0.52개의 ETH가 컨트랙트에서 사용자에게 전송되는 것을 알 수 있습니다.

마치며

[Compound series] 2. 컴파운드 로직 분석은 Compound 서비스의 주요 로직중 예치, 출금, 차입, 상환 시 실행되는 함수의 기능과 동작을 cToken 컨트랙트를 기반으로 자세하게 분석하였습니다.

Compound는 다양한 암호화폐들을 예치하고, 이를 담보로 다른 암호화폐를 차입할 수 있는 과담보 대출 서비스를 제공합니다. 그 과정에서 예치한 암호화폐들은 Compound에서 운용되는 자산인 cToken으로 변환되며 Compound에서 지정된 로직에 의해 그 양과 금리가 결정됩니다.

본 게시글에서는 예치와 출금의 로직을 살펴보았으며, 대출과 상환 컨트랙트의 자세한 내용은 [Compound series] 2–2. 컴파운드 로직 분석 (대출, 상환)에서 이어 설명합니다.

디사이퍼 — DeFinite팀 소개

이병헌 — Decipher Senior Researcher, President of Decipher

부현식 — Decipher Mentor

이우진 — Decipher Junior Researcher

정재영 — Decipher Senior Researcher

정현 — Decipher mentor

--

--