들어가기에 앞서 ...
Compound v2에서 사용되는 여러 helper function들의 로직을 정리한 포스팅이다. 사실 Lending 프로토콜에서 가장 중요한 것은 사용자들이 직접 호출하는 entry point라고도 할 수 있겠지만, 실제로 사용자들의 자금을 관리, 이자를 업데이트, 프로토콜에 대한 관리 등을 수행하는 것은 이러한 helper function들이 있기에 가능한 것이기 때문에 따로 글을 빼 포스팅을 작성하였다.
목차 (Helper functions)
- CToken::accrueInterest()
- … (계속해서 추가해나갈 예정이다)
CToken::accrueInterest()
function accrueInterest() virtual public returns (uint);
역할
cToken의 borrow 및 deposit 자산에 대해 시간이 지남에 따라 발생하는 이자를 계산하고, 이를 기반으로 잔액, 이자율 및 관련 지표(borrowIndex, totalBorrows)를 최신 상태로 업데이트하는 역할을 한다.
주요 기능들에서 로직을 실행하기 이전에 accrueInterest 함수를 실행하면서 시작한다.
변수 Update
- accrualBlockNumber
- borrowIndex
- totalBorrows
- totalReserves
로직 deep dive
accrueInterest() - 1
function accrueInterest() public returns (uint) {
uint currentBlockNumber = getBlockNumber();
uint accrualBlockNumberPrior = accrualBlockNumber;
if (accrualBlockNumberPrior == currentBlockNumber) {
return NO_ERROR;
}
- ethereum mainnet의 compound v2는 block.timestamp가 아닌, block.number를 기반으로 이자 등 관련 지표를 관리한다. 따라서 이전 업데이트 이후 발생한 이자를 계산하기 위해 현재 block.number값을 currentBlockNumber에 받아온다.
- 전역으로 저장된 가장 최근에 Index가 업데이트된 block.number를 accrualBlockNumberPrior변수에 저장한다.
- currentBlockNumber == accrualBlockNumberPrior 를 비교하여 같다면 return을 시켜준다.
block number를 기반으로 state 업그레이드가 발생하기 때문에 같은 block에서 이미 update가 되었다면 여러 index들을 새로 업데이트해줄 필요가 없기 때문.
문득, (3)의 내용이 gas를 얼마나 줄여주는지 궁금해져 foundry testcode를 작성해 테스트를 진행해보았다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
contract CounterTest is Test {
testCompoundV2Gas target;
function setUp() public {
vm.createSelectFork("eth_mainnet");
target = new testCompoundV2Gas();
}
function test_compoundV2GasOp() public {
console.log(target.getGasAmount());
console.log(target.getGasAmount());
}
}
contract testCompoundV2Gas {
// cETH address
address cETH= 0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5;
function getGasAmount() public returns (uint gasUsed) {
gasUsed = gasleft();
(bool suc, ) = cETH.call(abi.encodeWithSignature("totalBorrowsCurrent()"));
require(suc, "function call failed");
gasUsed = gasUsed - gasleft();
}
}
test 결과를 통해 알 수 있듯, 약 40000 wei 즉, $0.7884 (24.11.16 기준)의 gas 최적화가 되는 것을 알 수 있다.
계속 해보자.
accrueInterest() - 2
uint cashPrior = getCashPrior(); // cToken컨트랙트의 underlying balance
uint borrowsPrior = totalBorrows; // underlying token을 user들이 borrow한 총량
uint reservesPrior = totalReserves; // 총 준비금
uint borrowIndexPrior = borrowIndex; // 현재 borrowIndex
이와 같은 네 개의 memory변수에 state값들을 저장하는 것을 확인해볼 수 있다. 자세한 내용은 주석 참고
function getCashPrior() internal view returns (uint) {
EIP20Interface token = EIP20Interface(underlying);
return token.balanceOf(address(this));
}
getCashPrior는 이와 같이 cToken 컨트랙트의 underlying token 잔액(balance)를 불러온다.
accrueInterest() - 3
uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high");
여러 변수들을 업데이트 해주기 위해 compound v2는 현재의 underlying balance, 빌린 금액, 준비금 총액을 이용해 borrowRate를 계산해주게 된다.
interestRateModel::borrowRate()함수를 살펴보자.
function getBorrowRate(uint cash, uint borrows, uint reserves) public view returns (uint) {
uint ur = utilizationRate(cash, borrows, reserves);
return (ur * multiplierPerBlock / BASE) + baseRatePerBlock;
}
utilizationRate 즉, 활용율을 계산한 뒤 여러 가중치를 반영한 borrowRate를 리턴해주는 함수이다.
활용율은 전체 예치(deposit)된 자산 중, borrow에 쓰인 비율을 나타내는 비율이라고 알면 된다. 아래 코드를 보면 바로 이해가 될 것이다.
function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
// Utilization rate is 0 when there are no borrows
if (borrows == 0) {
return 0;
}
return borrows * BASE / (cash + borrows - reserves);
}
이러한 활용율에 multiplierPerBlock(자산 이용율에 이자율의 증가 속도를 제어하는 상태변수) 를 곱한 후, 최소 이자율인 baseRatePerBlock을 더해주어 최종적으로 borrowRate를 반환해준다.
이렇게 얻은 borrowRate는 borrowRateMax값보다 작아야함을 검사하며 조건검사를 끝낸다.
accrueInterest() - 4
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);
다소 복잡하고 어질어질해보이는데, 각 internal function들에 대해 가볍게 짚고 넘어가보는걸로 해보자.
피연산자(A, B, C)
- mul_(A, B): A * B
- mul_ScalarTruncate(A, B): A * B / BASE (BASE스케일만큼 스케일링)
- mul_ScalarTruncateAddUInt(A, B, C): A * B / BASE + C
이를 이용해 위 계산식들을 수식으로 표현해보면,
uint blockDelta = currentBlockNumber - accrualBlockNumberPrior;
Exp memory simpleInterestFactor = borrowRateMantissa * blockDelta
uint interestAccumulated = simpleInterestFactor * borrowsPrior / BASE
uint totalBorrowsNew = interestAccumulated + borrowsPrior;
uint totalReservesNew = reserveFactorMantissa * interestAccumulated + reservesPrior
uint borrowIndexNew = (simpleInterestFactor + 1) * borrowIndexPrior
이와 같이 나타낼 수 있다. 이제 로직을 살펴보자.
- blockDelta: 이전 accrueInterest시점으로부터 지금까지 지난 block 수를 계산한다.
- simpleInterestFactor: borrowRate에 blockDelta를 곱해 UtilizationRate에 따른 이자율에 시간 스케일을 반영한 값을 계산한다.
- interestAccumulated: simpleInterestFactor에 borrow된 총량을 곱해주어 현재까지 대출된 총액에 추가로 발생한 이자를 반영해준다. 이를 통해 누적된 이자를 계산한다.
- totalBorrowsNew: interestAccumulated에 borrowsPrior를 더해 총 부채를 업데이트해준다.
- totalReservesNew: 준비금을 업데이트해준다. 여기서 사용된 reserveFactorMantissa값은 거버넌스에 의해 설정된 값이다.
- borrowIndexNew: 마지막으로, borrowIndex를 simpleInterestFactor만큼의 증가분을 반영하여 업데이트 해준다.
accrueInterest() - 5
accrualBlockNumber = currentBlockNumber;
borrowIndex = borrowIndexNew;
totalBorrows = totalBorrowsNew;
totalReserves = totalReservesNew;
emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew);
return NO_ERROR;
마지막으로, 계산된 memory변수들을 state에 반영한다.
event로그를 발생시키면서, NO_ERROR를 반환하여 로직을 종료한다.
아래는 전체 코드이다.
accrueInterest()
function accrueInterest() virtual override public returns (uint) {
/* Remember the initial block number */
uint currentBlockNumber = getBlockNumber();
uint accrualBlockNumberPrior = accrualBlockNumber;
/* Short-circuit accumulating 0 interest */
if (accrualBlockNumberPrior == currentBlockNumber) {
return NO_ERROR;
}
/* Read the previous values out of storage */
uint cashPrior = getCashPrior();
uint borrowsPrior = totalBorrows;
uint reservesPrior = totalReserves;
uint borrowIndexPrior = borrowIndex;
/* Calculate the current borrow interest rate */
uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high");
/* Calculate the number of blocks elapsed since the last accrual */
uint blockDelta = currentBlockNumber - accrualBlockNumberPrior;
/*
* Calculate the interest accumulated into borrows and reserves and the new index:
* simpleInterestFactor = borrowRate * blockDelta
* interestAccumulated = simpleInterestFactor * totalBorrows
* totalBorrowsNew = interestAccumulated + totalBorrows
* totalReservesNew = interestAccumulated * reserveFactor + totalReserves
* borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex
*/
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);
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/* We write the previously calculated values into storage */
accrualBlockNumber = currentBlockNumber;
borrowIndex = borrowIndexNew;
totalBorrows = totalBorrowsNew;
totalReserves = totalReservesNew;
/* We emit an AccrueInterest event */
emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew);
return NO_ERROR;
}
이후에 계속 helper function 들을 추가해야겠다.
'web3 > Lending' 카테고리의 다른 글
Defi Lending 해체분석 1편: Compound v2 톺아보기 (2) - 구조 (0) | 2024.11.11 |
---|---|
Defi Lending 해체분석 1편: Compound v2 톺아보기 (1) - Intro (2) | 2024.11.10 |
Defi Lending 해체분석 - 서론 (0) | 2024.11.10 |