체인의정석

컴파운드 분석 1 편) 전체 구조와 C Token 본문

블록체인/디파이

컴파운드 분석 1 편) 전체 구조와 C Token

체인의정석 2022. 12. 7. 16:43
728x90
반응형

*외부 공개용/강의용이 아닌 개인 공부용입니다. 틀린 내용이 있을 수 있으며 계속해서 수정할 예정입니다.

 

1. 전체 구조 파악 

유저가 직접 상호작용하는 컨트렉트로 컴파운드에서 대출한 증표로 주는 토큰

Cether-> CToken

Cerc20 -> CToken

 

Cether => native coin을 의미

Cerc20 => erc20 token을 의미

 

 

Cether, Ctoken 모두 내부적으로는 CToken 호출

일단 대부분의 행위에서 이자를 주는 부분이 존재

accrueInterest()

 

CToken은 내부적으로 다시 Compotroller 호출

comptroller에서는 검증 로직이 있어서 호출 시에 검증 진행

 

Comptroller에서는 Governance나 PriceOracle을 호출하여서 검증 시 활용

 

 

2-1.  Cether 

일단 각각 internal로 보내는 모습

mint는 mint internal로 부르고 이때 msg.value의 값을 받는다.

 

reddem은 전체 인출이다. redeemTOKEN 주소를 주면 전체를 redeem하는 거 같고 redeem underlying의 경우에는 redeemAmount를 입력하면 마찬가지로 internal함수로 또 호출을 하는 부분이 존재한다.

 

리턴 값의 경우 NO_ERROR를 리턴하는데 ErrorReporter.sol에서 오류 처리를 해준다.

내가 사용을 해보지 않은 패턴이므로 한번 살펴봐야겠다. (3번에 작성)

    /**
     * @notice Sender supplies assets into the market and receives cTokens in exchange
     * @dev Reverts upon any failure
     */
    function mint() external payable {
        mintInternal(msg.value);
    }

    /**
     * @notice Sender redeems cTokens in exchange for the underlying asset
     * @dev Accrues interest whether or not the operation succeeds, unless reverted
     * @param redeemTokens The number of cTokens to redeem into underlying
     * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
     */
    function redeem(uint redeemTokens) external returns (uint) {
        redeemInternal(redeemTokens);
        return NO_ERROR;
    }

    /**
     * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset
     * @dev Accrues interest whether or not the operation succeeds, unless reverted
     * @param redeemAmount The amount of underlying to redeem
     * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
     */
    function redeemUnderlying(uint redeemAmount) external returns (uint) {
        redeemUnderlyingInternal(redeemAmount);
        return NO_ERROR;
    }

일단 mint의 경우 호출이 되는 부분을 하나 더 발견했는데 그것은 바로 msg.value를 바로 쏴주는 케이스 이다.

msg.value를 바로 쏴주게 되면 mintInternal이 자동적으로 발생한다.

 

* receive에 대해서 찾아보니 fallback function이 fallback 부분과 receive로 바뀌었다고 한다.

한마디로 이더리움을 받는 부분과 컨트렉트 단에서 처리하는 부분이 분리되는 것이다.

 

일단 CEthertoken은 fallback은 없었다.

 

https://ethereum-blockchain-developer.com/028-fallback-view-constructor/02-receive-fallback-function/

 

Receive Fallback Function - Become Ethereum Blockchain Developer

The Receive Fallback Function Solidity 0.6/0.8 Update Prior Solidity 0.6 the fallback function was simply an anonymous function that looked like this: function () external { } It's now two different functions. receive() to receive money and fallback() to j

ethereum-blockchain-developer.com

    /**
     * @notice Send Ether to CEther to mint
     */
    receive() external payable {
        mintInternal(msg.value);
    }

다음으로 넘어가서 봐보니 borrow, repayBorrow, repayBorrowBehalf 3개의 함수가 보인다.

borrow의 경우에는 일단 대출에 대한 코드이다. 

repay borrow는 상환에 대한 코드이고 

repay borrowBehalf는 대신 대출을 갚아주는 부분이다.

요걸 상환을 못하게 되면 아래의 liquidate Borrow가 되며 시장가 보다 청산 프리미엄을 더한 만큼 싸게 청산 될 수 있다.

    /**
      * @notice Sender borrows assets from the protocol to their own address
      * @param borrowAmount The amount of the underlying asset to borrow
      * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
      */
    function borrow(uint borrowAmount) external returns (uint) {
        borrowInternal(borrowAmount);
        return NO_ERROR;
    }

    /**
     * @notice Sender repays their own borrow
     * @dev Reverts upon any failure
     */
    function repayBorrow() external payable {
        repayBorrowInternal(msg.value);
    }

    /**
     * @notice Sender repays a borrow belonging to borrower
     * @dev Reverts upon any failure
     * @param borrower the account with the debt being payed off
     */
    function repayBorrowBehalf(address borrower) external payable {
        repayBorrowBehalfInternal(borrower, msg.value);
    }

다음 부분을 한번 보면

liquidate Borrow가 있다. 빌리는 사람의 주소가 있고 , CToken이 있고, cTokenCollateral 부분이 있다.

이걸 실행하게 되면 담보물이 청산자에게 가게 된다. 이렇게 될 때 시강가보다 더 저렴하게 청산이 된다.

 

addreserves는 그냥 예치를 해서 담보물을 추가하는것 같은데.. 이건 설명도 딱히 안보인다.

Ctoken 컨트렉트에서 내용을 확인해보니 정말 그냥 더해주는 역할인거 같다.

Delegateor 컨트렉트 중에 아래 문구가 있는 것을 보니 이건 그냥 관리자가 모종의 이유로 예치하고 이자를 발생시키는 함수 같다.

근데 관리자가 아니라 일반 유저가 실행시키면 그냥 기부금이 되는 구조 인거 같다.

    /**
     * @notice Accrues interest and adds reserves by transferring from admin
     * @param addAmount Amount of reserves to add
     * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
     */

https://docs.compound.finance/v2/ctokens/#liquidate-borrow

    /**
     * @notice The sender liquidates the borrowers collateral.
     *  The collateral seized is transferred to the liquidator.
     * @dev Reverts upon any failure
     * @param borrower The borrower of this cToken to be liquidated
     * @param cTokenCollateral The market in which to seize collateral from the borrower
     */
    function liquidateBorrow(address borrower, CToken cTokenCollateral) external payable {
        liquidateBorrowInternal(borrower, msg.value, cTokenCollateral);
    }

    /**
     * @notice The sender adds to reserves.
     * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
     */
    function _addReserves() external payable returns (uint) {
        return _addReservesInternal(msg.value);
    }

자 그럼 다음코드로 넘어가 보자

 

getCashPrior()의 경우

현재 컨트렉트의 주소의 잔고에서 msg.value를 뺀 값인데 이더리움을 넣기 전에 이전 값을 불러오는 함수이다. 여기저기서 많이 쓰일거 같이 생겼다.

 

doTransferIn의 경우

from의 주소를 체크하고 msg.value를 체크하여서 이더리움의 전송이 제대로 되었는지를 체크한다.

 

doTransferOut의 경우

내부적으로 to.transfer를 실행한다.

 

이더리움을 전송하고 전송받는 코드를 넣어야 하기 때문에 추가된 것 같다.

    /*** Safe Token ***/

    /**
     * @notice Gets balance of this contract in terms of Ether, before this message
     * @dev This excludes the value of the current message, if any
     * @return The quantity of Ether owned by this contract
     */
    function getCashPrior() override internal view returns (uint) {
        return address(this).balance - msg.value;
    }

    /**
     * @notice Perform the actual transfer in, which is a no-op
     * @param from Address sending the Ether
     * @param amount Amount of Ether being sent
     * @return The actual amount of Ether transferred
     */
    function doTransferIn(address from, uint amount) override internal returns (uint) {
        // Sanity checks
        require(msg.sender == from, "sender mismatch");
        require(msg.value == amount, "value mismatch");
        return amount;
    }

    function doTransferOut(address payable to, uint amount) virtual override internal {
        /* Send the Ether, with minimal gas and revert on failure */
        to.transfer(amount);
    }

 

2-2.  Cerc20

다른 부분만 살펴보도록 하겠다.

 

sweepToken 남은 잔고를 관리자에게 보내주는 역할을 한다.

    /**
     * @notice A public function to sweep accidental ERC-20 transfers to this contract. Tokens are sent to admin (timelock)
     * @param token The address of the ERC-20 token to sweep
     */
    function sweepToken(EIP20NonStandardInterface token) override external {
        require(msg.sender == admin, "CErc20::sweepToken: only admin can sweep tokens");
        require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token");
        uint256 balance = token.balanceOf(address(this));
        token.transfer(admin, balance);
    }

다음은 doTransferIn/out 인데 

transferFrom을 실행하는 역할을 한다. 다만 transferFrom는 원래 return true를 보내지만 erc20 표준대로 리턴을 안하는 경우를 위해 어셈블리어로 추가적인 처리를 해준다.

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }
    /**
     * @dev Similar to EIP20 transfer, except it handles a False result from `transferFrom` and reverts in that case.
     *      This will revert due to insufficient balance or insufficient allowance.
     *      This function returns the actual amount received,
     *      which may be less than `amount` if there is a fee attached to the transfer.
     *
     *      Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value.
     *            See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
     */
    function doTransferIn(address from, uint amount) virtual override internal returns (uint) {
        // Read from storage once
        address underlying_ = underlying;
        EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying_);
        uint balanceBefore = EIP20Interface(underlying_).balanceOf(address(this));
        token.transferFrom(from, address(this), amount);

        bool success;
        assembly {
            switch returndatasize()
                case 0 {                       // This is a non-standard ERC-20
                    success := not(0)          // set success to true
                }
                case 32 {                      // This is a compliant ERC-20
                    returndatacopy(0, 0, 32)
                    success := mload(0)        // Set `success = returndata` of override external call
                }
                default {                      // This is an excessively non-compliant ERC-20, revert.
                    revert(0, 0)
                }
        }
        require(success, "TOKEN_TRANSFER_IN_FAILED");

        // Calculate the amount that was *actually* transferred
        uint balanceAfter = EIP20Interface(underlying_).balanceOf(address(this));
        return balanceAfter - balanceBefore;   // underflow already checked above, just subtract
    }

    /**
     * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory
     *      error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to
     *      insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified
     *      it is >= amount, this should not revert in normal conditions.
     *
     *      Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value.
     *            See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
     */
    function doTransferOut(address payable to, uint amount) virtual override internal {
        EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying);
        token.transfer(to, amount);

        bool success;
        assembly {
            switch returndatasize()
                case 0 {                      // This is a non-standard ERC-20
                    success := not(0)          // set success to true
                }
                case 32 {                     // This is a compliant ERC-20
                    returndatacopy(0, 0, 32)
                    success := mload(0)        // Set `success = returndata` of override external call
                }
                default {                     // This is an excessively non-compliant ERC-20, revert.
                    revert(0, 0)
                }
        }
        require(success, "TOKEN_TRANSFER_OUT_FAILED");
    }

 

3. ErrorReporter.sol

예전부터 보였던 패턴이긴 한데 에러를 직접적으로 명시해서 관리해 준다. 보니까 기존 revert와 마찬가지로 에러처리를 해주지만 error를 사용하면 기존의 revert가 다이나믹 배열 형태로 들어가기 때문에 배포시에 가스비가 더 든다고 한다.

 

이에 따라 배포시 가스비가 많이 들때는 에러를 일부러 정의해준다고도 한다.

사실 revert를 사용해도 되겠지만 점점 컨트렉트가 복잡해지는 상황이니 에러 리포터를 사용하는 것이 더 맞을것 같다.

https://medium.com/coinmonks/solidity-revert-with-custom-error-explained-with-example-d9dff8937ef4

 

Solidity revert with custom error explained with example !!

Since v0.8.4, Solidity has introduced an additional way to revert a transaction. The new method allows users to define a custom error and…

medium.com

// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.10;

contract ComptrollerErrorReporter {
    enum Error {
        NO_ERROR,
        UNAUTHORIZED,
        COMPTROLLER_MISMATCH,
        INSUFFICIENT_SHORTFALL,
        INSUFFICIENT_LIQUIDITY,
        INVALID_CLOSE_FACTOR,
        INVALID_COLLATERAL_FACTOR,
        INVALID_LIQUIDATION_INCENTIVE,
        MARKET_NOT_ENTERED, // no longer possible
        MARKET_NOT_LISTED,
        MARKET_ALREADY_LISTED,
        MATH_ERROR,
        NONZERO_BORROW_BALANCE,
        PRICE_ERROR,
        REJECTION,
        SNAPSHOT_ERROR,
        TOO_MANY_ASSETS,
        TOO_MUCH_REPAY
    }

    enum FailureInfo {
        ACCEPT_ADMIN_PENDING_ADMIN_CHECK,
        ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK,
        EXIT_MARKET_BALANCE_OWED,
        EXIT_MARKET_REJECTION,
        SET_CLOSE_FACTOR_OWNER_CHECK,
        SET_CLOSE_FACTOR_VALIDATION,
        SET_COLLATERAL_FACTOR_OWNER_CHECK,
        SET_COLLATERAL_FACTOR_NO_EXISTS,
        SET_COLLATERAL_FACTOR_VALIDATION,
        SET_COLLATERAL_FACTOR_WITHOUT_PRICE,
        SET_IMPLEMENTATION_OWNER_CHECK,
        SET_LIQUIDATION_INCENTIVE_OWNER_CHECK,
        SET_LIQUIDATION_INCENTIVE_VALIDATION,
        SET_MAX_ASSETS_OWNER_CHECK,
        SET_PENDING_ADMIN_OWNER_CHECK,
        SET_PENDING_IMPLEMENTATION_OWNER_CHECK,
        SET_PRICE_ORACLE_OWNER_CHECK,
        SUPPORT_MARKET_EXISTS,
        SUPPORT_MARKET_OWNER_CHECK,
        SET_PAUSE_GUARDIAN_OWNER_CHECK
    }

    /**
      * @dev `error` corresponds to enum Error; `info` corresponds to enum FailureInfo, and `detail` is an arbitrary
      * contract-specific code that enables us to report opaque error codes from upgradeable contracts.
      **/
    event Failure(uint error, uint info, uint detail);

    /**
      * @dev use this when reporting a known error from the money market or a non-upgradeable collaborator
      */
    function fail(Error err, FailureInfo info) internal returns (uint) {
        emit Failure(uint(err), uint(info), 0);

        return uint(err);
    }

    /**
      * @dev use this when reporting an opaque error from an upgradeable collaborator contract
      */
    function failOpaque(Error err, FailureInfo info, uint opaqueError) internal returns (uint) {
        emit Failure(uint(err), uint(info), opaqueError);

        return uint(err);
    }
}

contract TokenErrorReporter {
    uint public constant NO_ERROR = 0; // support legacy return codes

    error TransferComptrollerRejection(uint256 errorCode);
    error TransferNotAllowed();
    error TransferNotEnough();
    error TransferTooMuch();

    error MintComptrollerRejection(uint256 errorCode);
    error MintFreshnessCheck();

    error RedeemComptrollerRejection(uint256 errorCode);
    error RedeemFreshnessCheck();
    error RedeemTransferOutNotPossible();

    error BorrowComptrollerRejection(uint256 errorCode);
    error BorrowFreshnessCheck();
    error BorrowCashNotAvailable();

    error RepayBorrowComptrollerRejection(uint256 errorCode);
    error RepayBorrowFreshnessCheck();

    error LiquidateComptrollerRejection(uint256 errorCode);
    error LiquidateFreshnessCheck();
    error LiquidateCollateralFreshnessCheck();
    error LiquidateAccrueBorrowInterestFailed(uint256 errorCode);
    error LiquidateAccrueCollateralInterestFailed(uint256 errorCode);
    error LiquidateLiquidatorIsBorrower();
    error LiquidateCloseAmountIsZero();
    error LiquidateCloseAmountIsUintMax();
    error LiquidateRepayBorrowFreshFailed(uint256 errorCode);

    error LiquidateSeizeComptrollerRejection(uint256 errorCode);
    error LiquidateSeizeLiquidatorIsBorrower();

    error AcceptAdminPendingAdminCheck();

    error SetComptrollerOwnerCheck();
    error SetPendingAdminOwnerCheck();

    error SetReserveFactorAdminCheck();
    error SetReserveFactorFreshCheck();
    error SetReserveFactorBoundsCheck();

    error AddReservesFactorFreshCheck(uint256 actualAddAmount);

    error ReduceReservesAdminCheck();
    error ReduceReservesFreshCheck();
    error ReduceReservesCashNotAvailable();
    error ReduceReservesCashValidation();

    error SetInterestRateModelOwnerCheck();
    error SetInterestRateModelFreshCheck();
}
728x90
반응형
Comments