일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 머신러닝기초
- 컨트렉트 배포 자동화
- nest.js설명
- 스마트컨트렉트 함수이름 중복 호출
- 티스토리챌린지
- chainlink 설명
- 컨트렉트 동일한 함수이름 호출
- 프록시배포구조
- ethers v6
- ethers type
- git rebase
- 스마트 컨트렉트 함수이름 중복
- ambiguous function description
- 러스트 기초
- 스마트컨트렉트테스트
- 러스트기초
- ethers typescript
- ethers websocket
- 스마트컨트렉트프록시
- vue기초
- 스마트컨트렉트 예약어 함수이름 중복
- 오블완
- 체인의정석
- Vue.js
- 러스트 기초 학습
- rust 기초
- multicall
- SBT표준
- Vue
- ethers
- Today
- Total
체인의정석
컴파운드 분석 3편 - CToken & Comptroller (transfer, 유동성체크, 이자율 체크, 청산) 본문
branch version 2.8 기준
1. 초기값 설정해주기
function initialize(ComptrollerInterface comptroller_,
InterestRateModel interestRateModel_,
uint initialExchangeRateMantissa_,
string memory name_,
string memory symbol_,
uint8 decimals_) public {
require(msg.sender == admin, "only admin may initialize the market");
require(accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once");
// Set initial exchange rate
//초기 교환 비율 설정
initialExchangeRateMantissa = initialExchangeRateMantissa_;
require(initialExchangeRateMantissa > 0, "initial exchange rate must be greater than zero.");
// Set the comptroller
//Comptroller 설정
uint err = _setComptroller(comptroller_);
require(err == uint(Error.NO_ERROR), "setting comptroller failed");
// Initialize block number and borrow index (block number mocks depend on comptroller being set)
// 블록 번호 초기 설정 및 borrow index 설정
accrualBlockNumber = getBlockNumber();
borrowIndex = mantissaOne;
// Set the interest rate model (depends on block number / borrow index)
// 이자율 모델 설정 (이자율 모델의 경우 컨트렉트 형태로 존재)
err = _setInterestRateModelFresh(interestRateModel_);
require(err == uint(Error.NO_ERROR), "setting interest rate model failed");
name = name_;
symbol = symbol_;
decimals = decimals_;
// The counter starts true to prevent changing it from zero to non-zero (i.e. smaller cost/refund)
//0에서 0이 아닌 값이 되는 것을 막기 위하여 카운터 설정
_notEntered = true;
}
2. 토큰을 보내는 함수
/**
* @notice Transfer `tokens` tokens from `src` to `dst` by `spender`
* @dev Called by both `transfer` and `transferFrom` internally
* @param spender The address of the account performing the transfer
* @param src The address of the source account
* @param dst The address of the destination account
* @param tokens The number of tokens to transfer
* @return Whether or not the transfer succeeded
*/
function transferTokens(address spender, address src, address dst, uint tokens) internal returns (uint) {
/* Fail if transfer not allowed */
//transfer가 허용된 경우에만 실행
uint allowed = comptroller.transferAllowed(address(this), src, dst, tokens);
if (allowed != 0) {
return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.TRANSFER_COMPTROLLER_REJECTION, allowed);
}
/* Do not allow self-transfers */
if (src == dst) {
return fail(Error.BAD_INPUT, FailureInfo.TRANSFER_NOT_ALLOWED);
}
/* Get the allowance, infinite for the account owner */
uint startingAllowance = 0;
if (spender == src) {
startingAllowance = uint(-1);
} else {
startingAllowance = transferAllowances[src][spender];
}
/* Do the calculations, checking for {under,over}flow */
MathError mathErr;
uint allowanceNew;
uint srcTokensNew;
uint dstTokensNew;
(mathErr, allowanceNew) = subUInt(startingAllowance, tokens);
if (mathErr != MathError.NO_ERROR) {
return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_NOT_ALLOWED);
}
(mathErr, srcTokensNew) = subUInt(accountTokens[src], tokens);
if (mathErr != MathError.NO_ERROR) {
return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_NOT_ENOUGH);
}
(mathErr, dstTokensNew) = addUInt(accountTokens[dst], tokens);
if (mathErr != MathError.NO_ERROR) {
return fail(Error.MATH_ERROR, FailureInfo.TRANSFER_TOO_MUCH);
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
accountTokens[src] = srcTokensNew;
accountTokens[dst] = dstTokensNew;
/* Eat some of the allowance (if necessary) */
if (startingAllowance != uint(-1)) {
transferAllowances[src][spender] = allowanceNew;
}
/* We emit a Transfer event */
emit Transfer(src, dst, tokens);
comptroller.transferVerify(address(this), src, dst, tokens);
return uint(Error.NO_ERROR);
}
내부적으로 호출하는 comptroller 부분
/**
* @notice Checks if the account should be allowed to transfer tokens in the given market
* @param cToken The market to verify the transfer against
* @param src The account which sources the tokens
* @param dst The account which receives the tokens
* @param transferTokens The number of cTokens to transfer
* @return 0 if the transfer is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol)
*/
function transferAllowed(address cToken, address src, address dst, uint transferTokens) external returns (uint) {
// Pausing is a very serious situation - we revert to sound the alarms
require(!transferGuardianPaused, "transfer is paused");
// Currently the only consideration is whether or not
// the src is allowed to redeem this many tokens
uint allowed = redeemAllowedInternal(cToken, src, transferTokens);
if (allowed != uint(Error.NO_ERROR)) {
return allowed;
}
// Keep the flywheel moving
updateCompSupplyIndex(cToken);
distributeSupplierComp(cToken, src, false);
distributeSupplierComp(cToken, dst, false);
return uint(Error.NO_ERROR);
}
Pause가 걸려있는지 먼저 체크
그 후 redeem이 가능한지 본다.
마켓에서 리스팅이 되어 있는 토큰인지 체크하고
만약 인출자가 어카운트 정보에 등록이 안되어 있다면 유동성 체크를 할 필요가 없고
등록이 된 계정이라면 전송후에 유동성이 부족한지 여부를 확인한다.
function redeemAllowedInternal(address cToken, address redeemer, uint redeemTokens) internal view returns (uint) {
if (!markets[cToken].isListed) {
return uint(Error.MARKET_NOT_LISTED);
}
/* If the redeemer is not 'in' the market, then we can bypass the liquidity check */
if (!markets[cToken].accountMembership[redeemer]) {
return uint(Error.NO_ERROR);
}
/* Otherwise, perform a hypothetical liquidity check to guard against shortfall */
(Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(redeemer, CToken(cToken), redeemTokens, 0);
if (err != Error.NO_ERROR) {
return uint(err);
}
if (shortfall > 0) {
return uint(Error.INSUFFICIENT_LIQUIDITY);
}
return uint(Error.NO_ERROR);
}
3. 유동성 체크 : 대출/ 상황 전 미리 청산가격에 대한 자산 시나리오 돌려보는 로직 분석
/**
* @dev Local vars for avoiding stack-depth limits in calculating account liquidity.
* Note that `cTokenBalance` is the number of cTokens the account owns in the market,
* whereas `borrowBalance` is the amount of underlying that the account has borrowed.
*/
struct AccountLiquidityLocalVars {
uint sumCollateral;
uint sumBorrowPlusEffects;
uint cTokenBalance;
uint borrowBalance;
uint exchangeRateMantissa;
uint oraclePriceMantissa;
Exp collateralFactor;
Exp exchangeRate;
Exp oraclePrice;
Exp tokensToDenom;
}
stack depth 제한을 피하기 위하여서 변수 지정한 후 변화가 일어날 경우를 가정하여 각각 계산
function getHypotheticalAccountLiquidityInternal(
address account,
CToken cTokenModify,
uint redeemTokens,
uint borrowAmount) internal view returns (Error, uint, uint) {
AccountLiquidityLocalVars memory vars; // Holds all our calculation results
uint oErr;
MathError mErr;
// For each asset the account is in
CToken[] memory assets = accountAssets[account];
for (uint i = 0; i < assets.length; i++) {
CToken asset = assets[i];
// Read the balances and exchange rate from the cToken
(oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account);
if (oErr != 0) { // semi-opaque error code, we assume NO_ERROR == 0 is invariant between upgrades
return (Error.SNAPSHOT_ERROR, 0, 0);
}
vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa});
vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa});
// Get the normalized price of the asset
// 오라클로 가져온 자산의 가격 체크
vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset);
if (vars.oraclePriceMantissa == 0) {
return (Error.PRICE_ERROR, 0, 0);
}
vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa});
// Pre-compute a conversion factor from tokens -> ether (normalized price value)
// 토큰을 이더리움으로 바꿨을 때의 가격 미리 계산
(mErr, vars.tokensToDenom) = mulExp3(vars.collateralFactor, vars.exchangeRate, vars.oraclePrice);
if (mErr != MathError.NO_ERROR) {
return (Error.MATH_ERROR, 0, 0);
}
// sumCollateral += tokensToDenom * cTokenBalance
// 총 cToken의 예치량 + (차감할 토큰 * 토큰의 잔고) = 새로운 cToken의 예치량
(mErr, vars.sumCollateral) = mulScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral);
if (mErr != MathError.NO_ERROR) {
return (Error.MATH_ERROR, 0, 0);
}
// sumBorrowPlusEffects += oraclePrice * borrowBalance
// 빌리는 잔고량에 의한 추가적인 영향 계산
(mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);
if (mErr != MathError.NO_ERROR) {
return (Error.MATH_ERROR, 0, 0);
}
// Calculate effects of interacting with cTokenModify
// cToken의 변화로 인한 영향 계산
if (asset == cTokenModify) {
// redeem effect
// sumBorrowPlusEffects += tokensToDenom * redeemTokens
(mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);
if (mErr != MathError.NO_ERROR) {
return (Error.MATH_ERROR, 0, 0);
}
// borrow effect
// sumBorrowPlusEffects += oraclePrice * borrowAmount
(mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects);
if (mErr != MathError.NO_ERROR) {
return (Error.MATH_ERROR, 0, 0);
}
}
}
// These are safe, as the underflow condition is checked first
if (vars.sumCollateral > vars.sumBorrowPlusEffects) {
return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0);
} else {
return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral);
}
}
getHypotheticalAccountLiquidityInternal :주어진 금액을 상환/차입할 경우 계정 유동성이 얼마나 되는지 결정합니다.
실제로는 청산시 청산 여부를 체크하기 위하여 사용됩니다.
3가지 인자 값은
1. account : 유동성을 판단할 지갑 주소
2. redeemToken : 측정할 상환할 토큰의 수
3. borrowAmount : 측정할 차입시 빌릴 자산의 수
리턴값의 첫번째 값은
liquidity이며 : 담보 조건을 초과하는 자산 요건
두번째 값은
shortfall이다. : 담보 조건에부족한 자산요건
하는 행위의 경우
1번 : cToken의 잔고와 교환 비율을 가져옵니다.
(oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account);
2번 : 해당 자산의 오라클 자산을 체크합니다.
vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa});
3번 : 토큰에서 변환 계수 사전 계산 -> ether(정규화된 가격 값)
(mErr, vars.tokensToDenom) = mulExp3(vars.collateralFactor, vars.exchangeRate, vars.oraclePrice);
4번 : 총 담보자산 + (변환된 계수 * 현재 cToken의 잔고) = 새로운 총 담보자산
(mErr, vars.sumCollateral) = mulScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral);
5번 : 대출 자산 추가의 영향력 + (오라클 가격 * 대출 잔고) = 새로운 대출자산 추가의 영향력
(mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);
6번 : cToken의 변화로 인한 영향 계산
6-1 : 상환 시 일어나는 행위
대출 자산 추가 영향 + (이더로 표준화된 토큰 가치 * 상환할 토큰) = 새로운 대출 자산 추가 영향
(mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);
6-2 : 대출 시 일어나는 행위
대출 자산 추가 영향 + (오라클 가격 * 대출량) = 새로운 대출 자산 추가 영향
(mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects);
7 : 현재 청산에 대한 유저의 자산 현황 리턴
총 담보 자산이 sumBorrowPlusEffects 보다 클 경우
여유분의 유동성을 리턴하며
반대의 경우 부족한 양을 리턴한다.
// These are safe, as the underflow condition is checked first
if (vars.sumCollateral > vars.sumBorrowPlusEffects) {
return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0);
} else {
return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral);
4. 현재 BorrowRate 조회해오기
/**
* @notice Accrue interest to updated borrowIndex and then calculate account's borrow balance using the updated borrowIndex
* @param account The address whose balance should be calculated after updating borrowIndex
* @return The calculated balance
*/
function borrowBalanceCurrent(address account) external nonReentrant returns (uint) {
require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed");
return borrowBalanceStored(account);
}
/**
* @notice Return the borrow balance of account based on stored data
* @param account The address whose balance should be calculated
* @return The calculated balance
*/
function borrowBalanceStored(address account) public view returns (uint) {
(MathError err, uint result) = borrowBalanceStoredInternal(account);
require(err == MathError.NO_ERROR, "borrowBalanceStored: borrowBalanceStoredInternal failed");
return result;
}
/**
* @notice Return the borrow balance of account based on stored data
* @param account The address whose balance should be calculated
* @return (error code, the calculated balance or 0 if error code is non-zero)
*/
function borrowBalanceStoredInternal(address account) internal view returns (MathError, uint) {
/* Note: we do not assert that the market is up to date */
MathError mathErr;
uint principalTimesIndex;
uint result;
/* Get borrowBalance and borrowIndex */
BorrowSnapshot storage borrowSnapshot = accountBorrows[account];
/* If borrowBalance = 0 then borrowIndex is likely also 0.
* Rather than failing the calculation with a division by 0, we immediately return 0 in this case.
*/
if (borrowSnapshot.principal == 0) {
return (MathError.NO_ERROR, 0);
}
/* Calculate new borrow balance using the interest index:
* recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex
*/
(mathErr, principalTimesIndex) = mulUInt(borrowSnapshot.principal, borrowIndex);
if (mathErr != MathError.NO_ERROR) {
return (mathErr, 0);
}
(mathErr, result) = divUInt(principalTimesIndex, borrowSnapshot.interestIndex);
if (mathErr != MathError.NO_ERROR) {
return (mathErr, 0);
}
return (MathError.NO_ERROR, result);
}
현재 대출에 대한 총량의 경우 => 현재 총 대출량과 발생한 이자에 대한 양이 한꺼번에 리턴 된다.
이자가 더 이상 발생하지 않는 조건 하에 accountBorrows[account]를 조회해야 한다.
그 후 새로운 borrow의 잔고는 borrow Snap Shot에 borrow Index를 곱한 값으로 나오고 여기에 IntersestIndex를 나눈 값이 현재 잔고 이다.
함수 실행 전 새로 계산한 대출 금액에 대한 잔고 조회 (작동 후의 잔고 조회 함수): borrowbalanceStoredInternal
5. 이자 발생 후 교환 비율 생성
exchange rate current() => 이자 발생 시킨후 exchange rate Stored() 실행
/**
* @notice Accrue interest then return the up-to-date exchange rate
* @return Calculated exchange rate scaled by 1e18
*/
function exchangeRateCurrent() public nonReentrant returns (uint) {
require(accrueInterest() == uint(Error.NO_ERROR), "accrue interest failed");
return exchangeRateStored();
}
/**
* @notice Calculates the exchange rate from the underlying to the CToken
* @dev This function does not accrue interest before calculating the exchange rate
* @return Calculated exchange rate scaled by 1e18
*/
function exchangeRateStored() public view returns (uint) {
(MathError err, uint result) = exchangeRateStoredInternal();
require(err == MathError.NO_ERROR, "exchangeRateStored: exchangeRateStoredInternal failed");
return result;
}
실제 내부로직
만약 총 공급량이 0일 경우에는 exchangeRate = 최초 교환 비율로 산정
아니라면
교환 비율 = (totalCash + totalBorrows - totalReserves) / total Supply 을 통해
나오게 된다. 이는 화이트페이퍼에서 확인이 가능한 공식이다.
이때 Exp를 사용하는데 이 경우 두개를 나누면서 동시에 10 ** 18 을 곱한 값이 나오게 된다.
/**
* @notice Calculates the exchange rate from the underlying to the CToken
* @dev This function does not accrue interest before calculating the exchange rate
* @return (error code, calculated exchange rate scaled by 1e18)
*/
function exchangeRateStoredInternal() internal view returns (MathError, uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
/*
* If there are no tokens minted:
* exchangeRate = initialExchangeRate
*/
return (MathError.NO_ERROR, initialExchangeRateMantissa);
} else {
/*
* Otherwise:
* exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
*/
uint totalCash = getCashPrior();
uint cashPlusBorrowsMinusReserves;
Exp memory exchangeRate;
MathError mathErr;
(mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves);
if (mathErr != MathError.NO_ERROR) {
return (mathErr, 0);
}
(mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply);
if (mathErr != MathError.NO_ERROR) {
return (mathErr, 0);
}
return (MathError.NO_ERROR, exchangeRate.mantissa);
}
}
6. 이자 발생 시키기 accure Interest
상세 내용은 컴파운드 화이트페이퍼에서 확인 가능
/*
* 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
*/
이 부분은 주석과 화이트페이퍼를 보고 파악이 가능
- 일단 이자율을 계산하기 위한 기본 수식을 구한다.
기본적인 이자율 = 이자율 * 지난 이자 계산으로부터 지나간 블록의 개수
- 모인 총 이자율에 대한 계산을 진행한다.
모인 총 이자율 계산 = 기본적인 이자율 * 총 대출양
- 새로운 대출 이자 계산 = 발생한 이자 * 총 대출량
- 새로운 수수료 계산 = (발생한 이자 * 수수료 비율) + 총 수수료
- 새로운 대출 인덱스 = 기본적인 이자율 * 대출 인덱스 + 대출 인덱스
*화이트 페이퍼의 3.2.1 Market Dynamics를 보면 index 부분의 이자율을 의미하며 해당 인덱스를 기반으로 하여 이자율을 곱하여서 새로운 인덱스를 구하는 부분이 나온다.
마켓의 totalBorrowBalance가 여기서 업데이트 되게 되면 이전 인덱스에서 업데이트 된다고 볼 수 있다.
이를 통해서 새로 도출된 변수들을 스토리지 영역에 저장하면서 이벤트를 남긴다.
/**
* @notice Applies accrued interest to total borrows and reserves
* @dev This calculates interest accrued from the last checkpointed block
* up to the current block and writes new checkpoint to storage.
*/
function accrueInterest() public returns (uint) {
/* Remember the initial block number */
//최초 블록 번호 기억
uint currentBlockNumber = getBlockNumber();
uint accrualBlockNumberPrior = accrualBlockNumber;
/* Short-circuit accumulating 0 interest */
if (accrualBlockNumberPrior == currentBlockNumber) {
return uint(Error.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 */
(MathError mathErr, uint blockDelta) = subUInt(currentBlockNumber, accrualBlockNumberPrior);
require(mathErr == MathError.NO_ERROR, "could not calculate block delta");
//여태까지 지나간 블록에 개수 산정 (현재 블록 숫자 - 이전 블록 숫자)
/*
* 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;
uint interestAccumulated;
uint totalBorrowsNew;
uint totalReservesNew;
uint borrowIndexNew;
(mathErr, simpleInterestFactor) = mulScalar(Exp({mantissa: borrowRateMantissa}), blockDelta);
if (mathErr != MathError.NO_ERROR) {
return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_SIMPLE_INTEREST_FACTOR_CALCULATION_FAILED, uint(mathErr));
}
(mathErr, interestAccumulated) = mulScalarTruncate(simpleInterestFactor, borrowsPrior);
if (mathErr != MathError.NO_ERROR) {
return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_ACCUMULATED_INTEREST_CALCULATION_FAILED, uint(mathErr));
}
(mathErr, totalBorrowsNew) = addUInt(interestAccumulated, borrowsPrior);
if (mathErr != MathError.NO_ERROR) {
return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_TOTAL_BORROWS_CALCULATION_FAILED, uint(mathErr));
}
(mathErr, totalReservesNew) = mulScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);
if (mathErr != MathError.NO_ERROR) {
return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_TOTAL_RESERVES_CALCULATION_FAILED, uint(mathErr));
}
(mathErr, borrowIndexNew) = mulScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);
if (mathErr != MathError.NO_ERROR) {
return failOpaque(Error.MATH_ERROR, FailureInfo.ACCRUE_INTEREST_NEW_BORROW_INDEX_CALCULATION_FAILED, uint(mathErr));
}
/////////////////////////
// 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 uint(Error.NO_ERROR);
}
7. Mint의 내부 함수
/**
* @notice Sender supplies assets into the market and receives cTokens in exchange
* @dev Accrues interest whether or not the operation succeeds, unless reverted
* @param mintAmount The amount of the underlying asset to supply
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount.
*/
function mintInternal(uint mintAmount) internal nonReentrant returns (uint, uint) {
uint error = accrueInterest();
if (error != uint(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed
return (fail(Error(error), FailureInfo.MINT_ACCRUE_INTEREST_FAILED), 0);
}
// mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to
return mintFresh(msg.sender, mintAmount);
}
struct MintLocalVars {
Error err;
MathError mathErr;
uint exchangeRateMantissa;
uint mintTokens;
uint totalSupplyNew;
uint accountTokensNew;
uint actualMintAmount;
}
먼저 mint Internal 함수를 실행하게 되면 accureInterest를 해서 이자를 업데이트 시킨다. ( 이자가 업데이트 되는 경우 중 하나가 여기에 해당 됨)
MintLocalVars 의 경우 mint에서 지역적으로 사용되는 변수를 나타낸다.
Mint를 하게 될 경우의
mintFresh의 경우
mint Allowed => 민팅이 가능한지 체크한다.
getBlockNumber => 블록의 숫자를 체크한다.
exchangeRateStoredInternal => 내부적으로 저장되어 있는 교환 비율을 가져온다.
교환 비율을 가져와서 사용하는 용도는 바로 해당 비율을 가져와서 실제로 발행하는 양에다가 나누어서 계산하기 때문이다.
그리고 do transfer In 은 실제로 들어오는 양을 계산해서 리턴해준다.
해당 do transferIn의 경우 실제로는 상위 경로에 존재하게 된다. CERC20의 경우 토큰 잔고 조정을 CEther의 경우 이더리움을 받아서 처리해주는 부분이다.
/*
* We get the current exchange rate and calculate the number of cTokens to be minted:
* mintTokens = actualMintAmount / exchangeRate
*/
/**
* @notice User supplies assets into the market and receives cTokens in exchange
* @dev Assumes interest has already been accrued up to the current block
* @param minter The address of the account which is supplying the assets
* @param mintAmount The amount of the underlying asset to supply
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount.
*/
function mintFresh(address minter, uint mintAmount) internal returns (uint, uint) {
/* Fail if mint not allowed */
uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);
if (allowed != 0) {
return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.MINT_COMPTROLLER_REJECTION, allowed), 0);
}
/* Verify market's block number equals current block number */
if (accrualBlockNumber != getBlockNumber()) {
return (fail(Error.MARKET_NOT_FRESH, FailureInfo.MINT_FRESHNESS_CHECK), 0);
}
MintLocalVars memory vars;
(vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal();
if (vars.mathErr != MathError.NO_ERROR) {
return (failOpaque(Error.MATH_ERROR, FailureInfo.MINT_EXCHANGE_RATE_READ_FAILED, uint(vars.mathErr)), 0);
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/*
* We call `doTransferIn` for the minter and the mintAmount.
* Note: The cToken must handle variations between ERC-20 and ETH underlying.
* `doTransferIn` reverts if anything goes wrong, since we can't be sure if
* side-effects occurred. The function returns the amount actually transferred,
* in case of a fee. On success, the cToken holds an additional `actualMintAmount`
* of cash.
*/
vars.actualMintAmount = doTransferIn(minter, mintAmount);
/*
* We get the current exchange rate and calculate the number of cTokens to be minted:
* mintTokens = actualMintAmount / exchangeRate
*/
(vars.mathErr, vars.mintTokens) = divScalarByExpTruncate(vars.actualMintAmount, Exp({mantissa: vars.exchangeRateMantissa}));
require(vars.mathErr == MathError.NO_ERROR, "MINT_EXCHANGE_CALCULATION_FAILED");
/*
* We calculate the new total supply of cTokens and minter token balance, checking for overflow:
* totalSupplyNew = totalSupply + mintTokens
* accountTokensNew = accountTokens[minter] + mintTokens
*/
(vars.mathErr, vars.totalSupplyNew) = addUInt(totalSupply, vars.mintTokens);
require(vars.mathErr == MathError.NO_ERROR, "MINT_NEW_TOTAL_SUPPLY_CALCULATION_FAILED");
(vars.mathErr, vars.accountTokensNew) = addUInt(accountTokens[minter], vars.mintTokens);
require(vars.mathErr == MathError.NO_ERROR, "MINT_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED");
/* We write previously calculated values into storage */
totalSupply = vars.totalSupplyNew;
accountTokens[minter] = vars.accountTokensNew;
/* We emit a Mint event, and a Transfer event */
emit Mint(minter, vars.actualMintAmount, vars.mintTokens);
emit Transfer(address(this), minter, vars.mintTokens);
/* We call the defense hook */
comptroller.mintVerify(address(this), minter, vars.actualMintAmount, vars.mintTokens);
return (uint(Error.NO_ERROR), vars.actualMintAmount);
}
8. RepayBorrow (상환)
먼저 Ctoken에서 RepayBorrowInternal 호출함
/**
* @notice Sender repays their own borrow
* @param repayAmount The amount to repay
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount.
*/
function repayBorrowInternal(uint repayAmount) internal nonReentrant returns (uint, uint) {
uint error = accrueInterest();
if (error != uint(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed
return (fail(Error(error), FailureInfo.REPAY_BORROW_ACCRUE_INTEREST_FAILED), 0);
}
// repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to
return repayBorrowFresh(msg.sender, msg.sender, repayAmount);
}
그리고 다른 사람이 대신 상환을 해주는 repayBorrowBehalfInternal이 있다.
/**
* @notice Sender repays a borrow belonging to borrower
* @param borrower the account with the debt being payed off
* @param repayAmount The amount to repay
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount.
*/
function repayBorrowBehalfInternal(address borrower, uint repayAmount) internal nonReentrant returns (uint, uint) {
uint error = accrueInterest();
if (error != uint(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed
return (fail(Error(error), FailureInfo.REPAY_BEHALF_ACCRUE_INTEREST_FAILED), 0);
}
// repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to
return repayBorrowFresh(msg.sender, borrower, repayAmount);
}
둘다 공통점은 이자를 먼저 업데이트 시킨다는 점이다.
결국 컨트렉트의 이자 업데이트 시점은 유저들이 빌리거나 상환할 때 지난 블록의 시간에 비례햐여 업데이트 되게 된다.
차이점은 msg.sender가 자신의 빚을 갚느냐 borrower의 빚을 갚느냐 이다.
아무튼 내부 함수 호출을 보면 일단 RepayBorrowLocalVars를 먼저 정의한다.
struct RepayBorrowLocalVars {
Error err;
MathError mathErr;
uint repayAmount;
uint borrowerIndex;
uint accountBorrows;
uint accountBorrowsNew;
uint totalBorrowsNew;
uint actualRepayAmount;
}
RepayBorrowLocalVars 구조체에서 변수들을 이미 담아 둔 후에 이에 접근하여 업데이트 하는 식으로 진행한다.
/**
* @notice Borrows are repaid by another user (possibly the borrower).
* @param payer the account paying off the borrow
* @param borrower the account with the debt being payed off
* @param repayAmount the amount of undelrying tokens being returned
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount.
*/
function repayBorrowFresh(address payer, address borrower, uint repayAmount) internal returns (uint, uint) {
/* Fail if repayBorrow not allowed */
uint allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount);
if (allowed != 0) {
return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REPAY_BORROW_COMPTROLLER_REJECTION, allowed), 0);
}
/* Verify market's block number equals current block number */
if (accrualBlockNumber != getBlockNumber()) {
return (fail(Error.MARKET_NOT_FRESH, FailureInfo.REPAY_BORROW_FRESHNESS_CHECK), 0);
}
RepayBorrowLocalVars memory vars;
/* We remember the original borrowerIndex for verification purposes */
vars.borrowerIndex = accountBorrows[borrower].interestIndex;
/* We fetch the amount the borrower owes, with accumulated interest */
(vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(borrower);
if (vars.mathErr != MathError.NO_ERROR) {
return (failOpaque(Error.MATH_ERROR, FailureInfo.REPAY_BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)), 0);
}
/* If repayAmount == -1, repayAmount = accountBorrows */
if (repayAmount == uint(-1)) {
vars.repayAmount = vars.accountBorrows;
} else {
vars.repayAmount = repayAmount;
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/*
* We call doTransferIn for the payer and the repayAmount
* Note: The cToken must handle variations between ERC-20 and ETH underlying.
* On success, the cToken holds an additional repayAmount of cash.
* doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred.
* it returns the amount actually transferred, in case of a fee.
*/
vars.actualRepayAmount = doTransferIn(payer, vars.repayAmount);
/*
* We calculate the new borrower and total borrow balances, failing on underflow:
* accountBorrowsNew = accountBorrows - actualRepayAmount
* totalBorrowsNew = totalBorrows - actualRepayAmount
*/
(vars.mathErr, vars.accountBorrowsNew) = subUInt(vars.accountBorrows, vars.actualRepayAmount);
require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED");
(vars.mathErr, vars.totalBorrowsNew) = subUInt(totalBorrows, vars.actualRepayAmount);
require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED");
/* We write the previously calculated values into storage */
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
/* We emit a RepayBorrow event */
emit RepayBorrow(payer, borrower, vars.actualRepayAmount, vars.accountBorrowsNew, vars.totalBorrowsNew);
/* We call the defense hook */
comptroller.repayBorrowVerify(address(this), payer, borrower, vars.actualRepayAmount, vars.borrowerIndex);
return (uint(Error.NO_ERROR), vars.actualRepayAmount);
}
상환 로직의 경우 실제로 dotransferIn을 하여 상환한 양만큼을 actualPayAmount에 담아 둔 후에
현재 총 대출량에서 차감을 하고, 계정의 대출량에서 차감을 하는 식으로 계산한다.
그리고 이벤트를 발생시키며 마지막에 검증을 한다.
9. 청산 로직
청산시 내부적으로 호출되는 로직이다.
일단 이자부터 발생시키는데 전체 컨트렉트에서 이자를 발생시키며,
cToken의 Collateral에도 이자를 발생시킨다.(접근하는 김에 이자율 업데이트 하는 느낌같음)
어차피 담보물과 빌린 자산은 다른 자산이니 접근 시에 이자를 발생시키는 느낌
/**
* @notice The sender liquidates the borrowers collateral.
* The collateral seized is transferred to the liquidator.
* @param borrower The borrower of this cToken to be liquidated
* @param cTokenCollateral The market in which to seize collateral from the borrower
* @param repayAmount The amount of the underlying borrowed asset to repay
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount.
*/
function liquidateBorrowInternal(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal nonReentrant returns (uint, uint) {
uint error = accrueInterest();
if (error != uint(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed
return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_BORROW_INTEREST_FAILED), 0);
}
error = cTokenCollateral.accrueInterest();
if (error != uint(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed
return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_COLLATERAL_INTEREST_FAILED), 0);
}
// liquidateBorrowFresh emits borrow-specific logs on errors, so we don't need to
return liquidateBorrowFresh(msg.sender, borrower, repayAmount, cTokenCollateral);
}
실제 실행이 되게 되면 liquidateBorrowFresh가 실행되게 된다.
/**
* @notice The liquidator liquidates the borrowers collateral.
* The collateral seized is transferred to the liquidator.
* @param borrower The borrower of this cToken to be liquidated
* @param liquidator The address repaying the borrow and seizing collateral
* @param cTokenCollateral The market in which to seize collateral from the borrower
* @param repayAmount The amount of the underlying borrowed asset to repay
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount.
*/
function liquidateBorrowFresh(address liquidator, address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal returns (uint, uint) {
/* Fail if liquidate not allowed */
uint allowed = comptroller.liquidateBorrowAllowed(address(this), address(cTokenCollateral), liquidator, borrower, repayAmount);
if (allowed != 0) {
return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.LIQUIDATE_COMPTROLLER_REJECTION, allowed), 0);
}
/* Verify market's block number equals current block number */
if (accrualBlockNumber != getBlockNumber()) {
return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_FRESHNESS_CHECK), 0);
}
/* Verify cTokenCollateral market's block number equals current block number */
if (cTokenCollateral.accrualBlockNumber() != getBlockNumber()) {
return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_COLLATERAL_FRESHNESS_CHECK), 0);
}
/* Fail if borrower = liquidator */
if (borrower == liquidator) {
return (fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_LIQUIDATOR_IS_BORROWER), 0);
}
/* Fail if repayAmount = 0 */
if (repayAmount == 0) {
return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_ZERO), 0);
}
/* Fail if repayAmount = -1 */
if (repayAmount == uint(-1)) {
return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_UINT_MAX), 0);
}
/* Fail if repayBorrow fails */
(uint repayBorrowError, uint actualRepayAmount) = repayBorrowFresh(liquidator, borrower, repayAmount);
if (repayBorrowError != uint(Error.NO_ERROR)) {
return (fail(Error(repayBorrowError), FailureInfo.LIQUIDATE_REPAY_BORROW_FRESH_FAILED), 0);
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/* We calculate the number of collateral tokens that will be seized */
(uint amountSeizeError, uint seizeTokens) = comptroller.liquidateCalculateSeizeTokens(address(this), address(cTokenCollateral), actualRepayAmount);
require(amountSeizeError == uint(Error.NO_ERROR), "LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED");
/* Revert if borrower collateral token balance < seizeTokens */
require(cTokenCollateral.balanceOf(borrower) >= seizeTokens, "LIQUIDATE_SEIZE_TOO_MUCH");
// If this is also the collateral, run seizeInternal to avoid re-entrancy, otherwise make an external call
uint seizeError;
if (address(cTokenCollateral) == address(this)) {
seizeError = seizeInternal(address(this), liquidator, borrower, seizeTokens);
} else {
seizeError = cTokenCollateral.seize(liquidator, borrower, seizeTokens);
}
/* Revert if seize tokens fails (since we cannot be sure of side effects) */
require(seizeError == uint(Error.NO_ERROR), "token seizure failed");
/* We emit a LiquidateBorrow event */
emit LiquidateBorrow(liquidator, borrower, actualRepayAmount, address(cTokenCollateral), seizeTokens);
/* We call the defense hook */
comptroller.liquidateBorrowVerify(address(this), address(cTokenCollateral), liquidator, borrower, actualRepayAmount, seizeTokens);
return (uint(Error.NO_ERROR), actualRepayAmount);
}
청산시 comptroller로 내부 호출되는 함수는 다음과 같다.
1. seize 할 담보 자산을 계산한다.
function liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint actualRepayAmount) external view returns (uint, uint) {
/* Read oracle prices for borrowed and collateral markets */
uint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed));
uint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral));
if (priceBorrowedMantissa == 0 || priceCollateralMantissa == 0) {
return (uint(Error.PRICE_ERROR), 0);
}
/*
* Get the exchange rate and calculate the number of collateral tokens to seize:
* seizeAmount = actualRepayAmount * liquidationIncentive * priceBorrowed / priceCollateral
* seizeTokens = seizeAmount / exchangeRate
* = actualRepayAmount * (liquidationIncentive * priceBorrowed) / (priceCollateral * exchangeRate)
*/
uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error
uint seizeTokens;
Exp memory numerator;
Exp memory denominator;
Exp memory ratio;
MathError mathErr;
(mathErr, numerator) = mulExp(liquidationIncentiveMantissa, priceBorrowedMantissa);
if (mathErr != MathError.NO_ERROR) {
return (uint(Error.MATH_ERROR), 0);
}
(mathErr, denominator) = mulExp(priceCollateralMantissa, exchangeRateMantissa);
if (mathErr != MathError.NO_ERROR) {
return (uint(Error.MATH_ERROR), 0);
}
(mathErr, ratio) = divExp(numerator, denominator);
if (mathErr != MathError.NO_ERROR) {
return (uint(Error.MATH_ERROR), 0);
}
(mathErr, seizeTokens) = mulScalarTruncate(ratio, actualRepayAmount);
if (mathErr != MathError.NO_ERROR) {
return (uint(Error.MATH_ERROR), 0);
}
return (uint(Error.NO_ERROR), seizeTokens);
}
seize 할 양 = 실제로 다시 제불하는 금액 * 청산 인센티브 * (대출한 자산의 가격 / 담보의 가격)
청산 인센티브가 여기서 곱해져서 청산자는 돈을 더 많이 가져오게 되고 대출한 자산의 가격에 비해 담보의 가격이 높을 수록 비율이 더 줄어들게 된다.
코인의 가격을 나누게 되면 결국에는 교환 비율이 나오게 되기 때문에 교환비를 곱해준다고 이해하였다.
교환비에 청산 인센티브를 곱하고 내가 지불하는 금액 만큼을 계산하면 실제 청산 양이 나오게 되는 것이다.
그러므로 다음과 같은 결과가 나오게 된다.
청산양에 대한 계산 = liquidateCalculateSeizeTokens
seize 할 토큰들 = sezie 할 양
교환비 = 실제 갚을 양 * (청산 인센티브 * 대출받은 가격) / (담보물 가격 * 교환비율)
이때 가격은 oracle에서 가져온다.
빌린 자산에 대한 가격과 청산물에 대한 가격을 각각 가져오는데 담보와 빌린것에 대한 자산이 각각 다르기 때문이다.
2. 청산 진행
if (address(cTokenCollateral) == address(this)) {
seizeError = seizeInternal(address(this), liquidator, borrower, seizeTokens);
} else {
seizeError = cTokenCollateral.seize(liquidator, borrower, seizeTokens);
}
seize를 외부 호출로 할지 내부 호출로 할지만 뻬고는 같은 로직
담보물과 빌린 토큰이 같은지, 다른지 여부에 따라
re entaracney 공격을 고려하여 호출 방법을 다르게 진행 시킨다.
/**
* @notice Transfers collateral tokens (this market) to the liquidator.
* @dev Will fail unless called by another cToken during the process of liquidation.
* Its absolutely critical to use msg.sender as the borrowed cToken and not a parameter.
* @param liquidator The account receiving seized collateral
* @param borrower The account having collateral seized
* @param seizeTokens The number of cTokens to seize
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
*/
function seize(address liquidator, address borrower, uint seizeTokens) external nonReentrant returns (uint) {
return seizeInternal(msg.sender, liquidator, borrower, seizeTokens);
}
seizeInternal
담보 토큰을 청산자에게 보내주는 함수
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/* We write the previously calculated values into storage */
accountTokens[borrower] = borrowerTokensNew;
accountTokens[liquidator] = liquidatorTokensNew;
/* Emit a Transfer event */
emit Transfer(borrower, liquidator, seizeTokens);
/* We call the defense hook */
comptroller.seizeVerify(address(this), seizerToken, liquidator, borrower, seizeTokens);
체크를 한번 해준 후에 검증을 진행한다.
9. 나머지 관리자 set 함수들
관리자가 초기에 세팅을 해주고 dao 컨트렉트에 따라서 투표 결과에 따라 함수 값들이 업데이트 된다.
'블록체인 > 디파이' 카테고리의 다른 글
Uniswap V2 컨셉 & Uniswap V3 컨셉 (0) | 2023.07.04 |
---|---|
compound 청산로직 추가 자료 (0) | 2022.12.27 |
컴파운드 분석 2 편) 컴파운드에서의 이자율 계산 (0) | 2022.12.12 |
컴파운드 분석 1 편) 전체 구조와 C Token (0) | 2022.12.07 |
ZapperIn 분석 (2) | 2022.10.27 |