들어가기에 앞서..
이번 편에서는 compound v2의 전반적인 구조를 알아보고자 한다.
1편과 다소 순서가 뒤바뀐 면이 없지않아 있는 것 같긴 한데, 1편의 내용은 compound에서 사용되는 핵심적인 개념들에 대한 설명이었기 때문에 필수적이었고, 이 또한 전체적인 구조를 파악할 수 있기 때문에 필수적인 것 같다. 그래서 어느 것이 먼저인지는 중요하지 않은 것 같다. 이번 편에서 전체적인 구조를 살펴보고, 다음편 부터 코드레벨까지 뜯어보며 프로토콜 전반에 대한 내용을 살펴보도록 하자.
시작해보자.
Compound v2 구조
위 그림은 업사이드아카데미 1기 - DeepHigh팀으로 활동하면서 만든 Compound v2의 전체적인 호출 flow이다. 위 도식에는 Compound v2를 구성하는
- external entities
- internal storage
- contract 및 function
의 여러 동작flow가 담겨 있다. 모든 flow에서 어떻게 동작하는지에 대한 조금 더 자세한 내용을 살펴보기 위해 일단 end user들과 가장 가까운 파트부터 보는 것이 가장 직관적으로 와닿을 것 같아 그렇게 순서를 구성하였다.
순서
- 전체 구조 Overview
- cToken
- Comptroller
- Governance
- Price Oracle
전체 구조 Overview
Compound v2의 핵심적인 기능들은 아래와 같이 구성되어있다고 볼 수 있다.
- cToken: 사용자가 예치한 자산에 대한 권리 & 예치 자산에 대한 이자 수익 반영하는 토큰.
- CErc20/cEther: cToken의 서로 다른 두 형태
- cToken: 대출, 예치, 청산 등 기능을 수행하는 컨트랙트
- Comptroller: 대출, 예치, 청산 등 기능을 관리하고 담보 비율 및 이자율 모델 등을 조정하는 중앙 관리 역할.
- Unitroller: Comptroller의 로직을 분리하여 업그레이드 가능하게 만든 프록시 컨트랙트
- Comptroller: 본체
- Governance: Comp 토큰을 보유한 사용자들이 프로토콜의 주요 정책(이자율 모델, 담보 요건, 새로운 자산 추가 등)을 propose하고 vote할 수 있도록 하는 역할.
- Comp: 거버넌스 토큰. 보유자는 propose 및 vote에 참여해 프로토콜 운영에 참여 가능.
- Governor: 거버넌스의 핵심 컨트랙트. propose, vote 진행을 담당해 처리.
- Timelock: 승인된 거버넌스 propose가 실행되기 전에 일정 시간 지연을 적용하는 컨트랙트
- Price Feed: compound v2는 chainlink에서 feed data를 받아온다.
이제, 위의 각 구성요소들이 어떠한 역할&기능을 수행하는지 알아보도록 하자.
cToken
프로토콜에 예치(또는 공급)된 자산을 EIP-20표준에 맞춰 표현한 개념으로, Compound v2의 핵심 개념이다.
그래서 이걸로 뭐하는데
- cToken은 사용자가 공급한 자산의 가치를 나타낸다. 이를 통해 이자를 얻을 수 있다(예금)
- cToken을 담보로 대출 등 작업을 수행할 수 있다. (대출)
cToken은 Compound와 user가 상호작용하는 주요 수단이다.
사용자가 담보 입금(mint), 출금(redeem), 대출(borrow), 상환(repay), 청산(liquidate), 또는 전송(transfer)을 할 때 cToken 컨트랙트를 통해 이루어진다.
EIP-20표준을 따른다는 것을 통해 알 수 있듯, 내가 입금한 담보자산을 제3자에게 전송하여 제3자가 이를 이용해 대출 등의 작업을 할 수 있기도 하다.
CErc20/CEther
이들은 각각 cToken컨트랙트를 상속받는 user entry point이다. 컨트랙트의 주요 기능에 대한 Overview를 살펴보자.
functions | visibility | description |
mint | external | 담보 입급하여 cToken발행 |
redeem | external | cToken소각하여 담보 인출(cToken기준) |
redeemUnderlying | external | cToken소각하여 담보 인출(underlying기준) |
borrow | external | cToken을 담보로 대출 |
repayBorrow | external | 대출받은 자산을 상환 |
repayBorrowBehalf | external | A가 대출받은 자산을 B가 상환 |
liquidateBorrow | external | A가 대출받은 자산을 C가 청산(청산보너스 get) |
sweepToken | external | underlying이 아닌 token이 컨트랙트에 존재하면 해당 token을 sweep |
doTransferIn | internal | EIP20 transferFrom |
doTransferOut | internal | EIP20 transfer |
두 컨트랙트 모두 위와 같은 기능들을 제공한다. 차이는 오직 Native ETH를 다루는가, ERC20 token을 다루는가의 차이.
예시
cToken flow
end-user가 어떠한 entry point를 통해 프로토콜과 상호작용하는지 알아봤으니, cToken의 로직들이 어떻게 동작하는지 알아보자.
위의 그림을 보며, 예시로 mint함수를 들어 따라가보도록 하자. (다른 함수들도 다 똑같다.)
- cErc20/CEther를 통해 user가 mint(uint mintAmount)를 호출하면,
- mint는 internal call을 통해 mintInternal(mintAmount)를 호출한다.
- mintInternal는 accrueInterest()를 통해 다음 네 가지 state variable을 업데이트한다.
- borrowIndex: 이자계산에 사용되는 Index 업데이트
- totalBorrows: borrow된 총량 업데이트
- totalReserves: 준비금 업데이트
- accrualBlockNumber: 마지막으로 위의 값들을 업데이트한 block number
- 업데이트 이후, mintFresh를 통해 mint작업에 들어간다.
- comptroller.mintAllowed(address(this), minter, mintAmount)를 통해 minter가 mintAmount만큼 mint할 수 있는지 체크하고, CompSupplyIndex를 업데이트하며, Supplier들에게 comp를 distribute한다.
- 이후, 실제mint와 관련된 로직이 실행된다.
cToken의 다른 기능에 대한 로직(mint, redeem, borrow, repay)또한 위와같은 방식으로 실행된다.
하지만 liquidate에는 약간의 차이가 존재한다. 이 차이를 이해하기 위해서는 청산, 나아가 Compound의 청산에 대한 이해가 조금 필요하니 간단하게 짚고 넘어가보자.
liquidate 핵심만 빠르게
function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral)
liquidate함수의 입력값은 1. 청산의 대상인 borrower, 2. 청산 규모인 repayAmount, 3. 담보자산의 cTokenInterface이 입력된다.
- 청산(liquidate)은 대출자(borrower)의 담보를 “싸게 파는”행위이며, 이를 통해 프로토콜의 재정적 안정성을 유지하고자 하는 기능이다. (처음 들으면 무슨말인지 잘 이해가 안된다. 일단 계속 읽자)
- borrower가 대출(borrow)을 할 때, 담보자산(cToken)들의 총 가치가 borrow하고자 하는 자산의 총 가치보다 커야한다.
- 담보자산의 가치가 빌린 자산의 가치보다 작아지는 현상(shortfall)이 발생하면, 청산의 대상이 된다.
- **청산인(liquidator)**은 이를 발견하고, cToken마켓에 liquidateBorrow를 실행하여 borrower의 **담보자산(cTokenCollateral)**을 청산한다.
- 이 때, repayAmount만큼의 “빌린 자산”을 청산하게 되고, repayAmount만큼의 가치에 할인율(liquidationIncentiveMantissa)을 곱한 수량의 담보자산(cTokenCollateral)을 그 대가로 받아 청산에 대한 보상을 얻게 된다.
그래서 liquidate는 어떤 flow로?
사실 flow자체는 비슷할 수 있는데, 위의 내용들을 토대로 하여, borrower가 빌린 자산과, 청산의 대상이 되는 borrower가 담보로 맡긴 자산 모두에 대해 이자율 업데이트 등 작업을 수행해야하는 것을 알 수 있다. 따라서, 그러한 차이가 존재한다.
실제 liquidate 로직 그리고 cToken 기능들에 대한 전반적인 세부사항은 다음에 올릴 포스팅에서 함께 확인해보도록 하자.
Comptroller
comptroller의 사전적 정의를 살펴보면, 재정 관리자라는 뜻으로 주로 사용되는 것 같다. Compound에서도 마찬가지이다.
Comptroller는 Compound v2의 리스크 관리계층으로, 사용자가 유지해야 할 담보비율을 결정하고, 사용자의 재무건전성을 판단한다. 재무건전성은 청산과 직접적으로 연관되며 사용자가 청산될 수 있는지, 그리고 얼마나 청산될 수 있는지를 판단하는 데 사용된다.
또한 사용자가 cToken과 상호작용할 때 마다 거래가 타당한지를 검토하는 데 사용된다.
Comptroller는 업그레이드 가능한 컨트랙트로, Unitroller라는 별도의 프록시컨트랙트에서 ComptrollerStorage와 Comptroller를 관리한다.
이제, 어떠한 기능들을 수행하는지 전체적으로 살펴보도록 하자.
enter/exitMarket(s)
function enterMarkets(address[] calldata cTokens)
함수 이름 그대로 market(들) 목록에 들어가는 함수이다. 특정 market에 예치한 자산이 담보로 인정되도록 하거나, 특정 market에서 자산을 borrow하기 위해 enterMarket을 해야 한다.
반대로 exitMarket은 이름 그대로 특정 market에서 exit하는 함수이다.
Allowed Functions
cToken컨트랙트에서 각종 Fresh함수들(mintFresh, redeemFresh, ….)은 내부적으로 accrueInterest()를 호출한 이후, comptroller의 ~Allowed 함수(mintAllowed, redeemAllowed, …)를 호출하게 된다. ~Allowed 함수에서는
- 해당 기능에 들어가는 여러 Input value를 검증하고
- updateCompSupplyIndex() 를 통해 보상과 관련된 Index를 업데이트하며
- distributeSupplierComp() 를 통해 supplier에게 comp를 분배한다.
Verify Functions
cToken컨트랙트에서 각종 Fresh함수들(mintFresh, redeemFresh, ….)은 내부적으로 ~Allowed 함수를 호출해 Input값들을 검증한 후, 함수 로직을 수행한 다음, 마지막 단계에서 ~verify함수를 호출한다.
현재 verify함수는 사용되지 않지만, 추후에 다시 사용될 수도 있기에.. 일단 기록한다.
각종 변수 및 유틸리티
- collateralFactor이 값은 Governance에 의해 증/감 할 수 있다.
- 기능이라기보다는 값에 가까운데, 공급한 담보자산의 실제 담보가치(대출 한도)의 비율을 나타내는 값이다. 대형자산(ETH 등), 유동성 높은 자산은 collateralFactor가 크고, 반대는 작다. 0%가 되면 해당 자산은 담보로서 활용할 수 없지만 borrow는 가능하다.
- getAccountLiquidity
- 특정 계정이 사용할 수 있는 유동성의 총 USD가치를 계산한다. 기본적으로 enterMarket을 통해 listing되어있는 담보 및 대출 자산에 대해서만 그 값을 계산한다. 또한, 사용할 수 있는 자산의 총량이 0보다 작아지면 shortfall이 생겨 해당 계정은 청산 가능한 상태에 도달하게 되고, 이에 따라 shortfall값도 리턴해준다.
Governance
Compound는 COMP토큰 보유자가 세 가지 컨트랙트(COMP, Governor Bravo, Timelock)를 컨트롤하며 관리 및 업그레이드된다.
이 컨트랙트들은 커뮤니티가 propose, vote를 통해 cToken, Comptroller의 관리기능을 통해 변경사항을 실행할 수 있도록 한다.
또한 COMP토큰 holder는 자신의 투표권을 다른 주소에 delegate할 수 있어 25,000 COMP이상 delegatae받은 주소는 제안을 propose할 수 있고, 누구든 100 COMP를 lock하여 propose를 생성할 수 있다.
Governance Timeline
- 최초 Propose시, 2일간의 review기간이 주어지며,
- 이후 투표 가중치가 기록되고 투표가 시작된다.
- 투표는 3일간 진행되고,
- 다수이면서 최소 400,000표가 찬성하면 Timelock에 pending상태로 추가되고,
- 해당 propose는 2일 뒤에 실행된다.
결과적으로, 프로토콜의 변경 사항은 최소 일주일이 소요된다.
Price Oracle
프로토콜의 여러 자산 가격 데이터를 Chainlink Price Feed를 통해 제공받는다.
이에 대한 코드베이스: https://github.com/smartcontractkit/open-oracle/blob/master/contracts/PriceOracle/PriceOracle.sol
- multisig를 통해 price feed주소가 업데이트될 수 있음
- getUnderlyingPrice함수를 통해 자산의 USD가격을 반환받을 때, 10^(36 - underlying.decimals())로 스케일 업된다. 예를들어 WBTC는 decimals가 8이므로, 반환값은 1e28이 됨.
여기까지 Compound v2의 기본적인, 그리고 전체적인 구조를 살펴보았다. 다음 글부터는 드디어 코드를 해체분석해보도록 하자.
'web3 > Lending' 카테고리의 다른 글
Defi Lending 해체분석: Compound v2 - helper functions (2) | 2024.11.16 |
---|---|
Defi Lending 해체분석 1편: Compound v2 톺아보기 (1) - Intro (2) | 2024.11.10 |
Defi Lending 해체분석 - 서론 (0) | 2024.11.10 |