들어가기에 앞서…
Compound v2를 구성하는 주요 기능에는 mint, redeem, borrow, repay, liquidate가 있다. 이러한 기능들이 어떻게 작동하는지, 더 나아가 어떠한 방식으로 구현되어있는지 등을 살펴보려 한다.
이번 편에서는 Compound에서 자금을 관리하는 cToken을 주요 기능을 살펴보려 한다. 중간중간 등장하는 추가적인 개념들은 이 글에서 가볍게 다루고, 추후에 더 자세히 살펴보는 것이 좋을 것 같다.
각 기능에서는 CErc20/CEther 컨트랙트에서 상속받는 CToken 컨트랙트의 CToken::~Internal() 함수를 호출하고, 이는 CToken::~Fresh() 함수를 호출하며, 또 이는, Comptroller::~Allowed() 함수 및 Comptroller::~Verify()->Optional함수를 호출하는 흐름을 이해하고 있으면 더욱 편할 것 같다.
Mint(uint mintAmount)
이전 포스팅에서도 언급했던 것과 같이, mint(uint mintAmount)함수는 CErc20/CEther 컨트랙트에 구현되어있으며, cToken 컨트랙트의 mintInternal(uint mintAmount) 함수를 호출하여 실제 로직을 실행한다.
1. CErc20 & CEther::mint()
mint를 하기 위한 entry point인 CErc20과 CEther를 살펴보자.
function mint() external payable {
mintInternal(msg.value);
}
function mint(uint mintAmount) override external returns (uint) {
mintInternal(mintAmount);
return NO_ERROR;
}
이러한 구현적 차이 이외에는 두 컨트랙트에서 별도의 차이가 존재하지 않는다.
CEther는 네이티브 Token을 처리하기 위한, CErc20은 ERC20 Token을 처리하기 위한 컨트랙트라는 정도만 이해하고 넘어가도 된다.
Logic
각각의 mint 함수에서는 상속받는 컨트랙트인 CToken의 mintInternal(mintAmount)를 호출한다.
2. CToken::mintInternal()
function mintInternal(uint mintAmount) internal nonReentrant {
accrueInterest();
mintFresh(msg.sender, mintAmount);
}
이 함수에서는 보이는 것과 같이 두 개의 주요 로직을 실행한다.
accrueInterest()
accrueInterest함수는 프로토콜이 현재 가지고 있는 token 수량, 사용자들의 borrow 총량, 준비금 등을 바탕으로 새로운 index 값을 계산, borrow 총량을 업데이트, 준비금을 업데이트 하는 로직을 수행한다. 이에 대한 자세한 로직은 다른 글을 통해 설명하였다.
accrueInterest() 실행 이후에 mintFresh() 함수를 실행하게 되는데,
mint의 주요 로직이 구현되어있는 함수이다. 자세히 들어가보도록 하자.
3. CToken::mintFresh()
function mintFresh(address minter, uint mintAmount) internal;
mint와 관련된 주요 로직이 실행되는 mintFresh()함수이다.
차례대로 로직을 살펴보자.
mintFresh() - 1
uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);
if (allowed != 0) {
revert MintComptrollerRejection(allowed);
}
해당 cToken에 대해 minter가 mintAmount만큼 mint를 할 수 있는지 여부를 리턴해주는 mintAllowed 함수를 호출한다. (이러한 목적인 것 같기는 한데, 실제로 이렇지는 않다.)
3-1. CToken::mintAllowed
function mintAllowed(address cToken, address minter, uint mintAmount) override external returns (uint) {
require(!mintGuardianPaused[cToken], "mint is paused");
// Shh - currently unused
minter;
mintAmount;
if (!markets[cToken].isListed) {
return uint(Error.MARKET_NOT_LISTED);
}
updateCompSupplyIndex(cToken);
distributeSupplierComp(cToken, minter);
return uint(Error.NO_ERROR);
}
- guardian에 의해 pause되어있는지를 검사한다.해당 조건문은 이를 통해 pause되어있는지 검사하고, pause되어있다면 revert를 발생시킨다.
compound에는 governance에 의해 임명된 guardian이라는 주체(wallet)이 있는데, 이들은 프로토콜상에 문제, 해킹, 또는 어떠한 사정이 있을 경우에 특정 cToken market의 기능을 pause할 수 있는 권한을 가지고 있다. - compound만의 특징이라고 할 수 있을 것 같은데, ~Allowed 함수 내에 로직이 비어있는 것이 보인다. 추후 업데이트 시에 인터페이스가 변하지 않게 하려고 minter와 mintAmount를 함수 arg에 넣기만 하고 이용하지는 않은 것으로 보인다.
- 인자로 넣은 cToken의 주소가 market에 listing되어있는지 검사하여 입력값 검증을 실행하고 있는 것을 확인할 수 있다.
- updateCompSupplyIndex(cToken), distributeSupplierComp(cToken, minter) 을 통해 supplier에게 돌아가는 보상을 업데이트해준다. 상세한 내용은 보상 관련 포스팅을 추후 업로드 하도록 하겠다.
mintFresh() - 2
if (accrualBlockNumber != getBlockNumber()) {
revert MintFreshnessCheck();
}
accrualBlockNumber 즉, accrueInterest()함수에서 이자율 및 관련 state변수들을 업데이트한 block number가 현재의 block.number와 같지 않다면 revert를 실행한다. 즉, accrueInterest함수를 실행하지 않고는 mintFresh()를 실행할 수 없다는 것.
mintFresh() - 3
Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});
exchangeRateStoredInternal() 함수를 통해 exchangeRate를 불러온다.
function exchangeRateStoredInternal() virtual internal view returns (uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
return initialExchangeRateMantissa;
} else {
uint totalCash = getCashPrior();
uint cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
uint exchangeRate = cashPlusBorrowsMinusReserves * expScale / _totalSupply;
return exchangeRate;
}
}
cToken의 totalSupply, totalCash, totalBorrows, totalResserves를 바탕으로 exchangeRate즉, cToken과 underlying token의 교환비율을 반환해주는 함수이다.
사용처: underlying amount = cToken amount * exchangeRate
- totalSupply가 0보다 클 때
totalSupply가 0보다 크면, cToken이 발행된 적이 있는 즉, mint수량이 있다는 말이다. 이 경우 exchangeRate는 $\frac{totalCash + totalBorrows-totalReserves}{totalSupply}$가 된다.
즉, 프로토콜의 전체 자산(보유중인 token + borrow된 token - 준비금)을 총 cToken발행량으로 나눈 값이다. - totalSupply가 0일 때
totalSupply가 0이라는 것은, cToken 발행량이 0이라는 것이다. 즉, market에 유동성이 존재하지 않는 것이기 때문에, initialExchangeRateMantissa를 반환해준다. cETH의 intialExchangeRateMantissa값은 $2 \times 10^18$ 이다.
mintFresh() - 4
uint actualMintAmount = doTransferIn(minter, mintAmount);
uint mintTokens = div_(actualMintAmount, exchangeRate);
이제, doTransferIn(safeTransferFrom을 실행한다)을 통해 minter로부터 mintAmount만큼의 underlying token을 송금받고, 송금받은 token 수량인 actualMintAmount 을 exchangeRate로 나누어 mint 즉, 공급해야 할 cToken수량을 계산한다.
mintFresh() - 5
totalSupply = totalSupply + mintTokens;
accountTokens[minter] = accountTokens[minter] + mintTokens;
mint를 하는 절차.
totalSupply를 증가시켜주고, minter주소의 accountTokens를 증가시킨다.
mintFresh() - 6
emit Mint(minter, actualMintAmount, mintTokens);
emit Transfer(address(this), minter, mintTokens);
이벤트 로그를 발생시키고 함수가 종료된다.
mint 요약
- mint(mintAmount)
- mintInternal(mintAmount)
- accrueInterest()
- mintFresh()
- mintAllowed()
- mint logic
- mintAmount만큼 doTransferIn()
- mintAmount / exchangeRate 만큼 cToken mint
계속 추가할 예정