# Compound学习——借贷和清算

By [rbtree](https://paragraph.com/@rbtree) · 2022-09-27

---

1 CToken、CErc20、CEther
----------------------

这三个合约对应compound中的cToken，当我们给compound lend资产时，就会收到相应的cToken。例如lend ETH会得到cETH，lend DAI会得到cDAI。

关于某个erc20代币/eth的lend和borrow的信息，都会存储在相应的cToken合约之中。例如lend和borrow DAI的一切信息，都会记录在cDAI合约之中。我们可以简单看一下cToken的接口方法。

其中一部分是ERC20标准的函数，例如transfer、approve，可见我们lend之后得到的cToken本身具备ERC20代币的性质。

有一些是因为借贷而特有的，例如balanceOfUnderlying获取某个用户的cToken对应的底层资产数量，supplyRatePerBlock/borrowRatePerBlock获得当前利率。

还有一些是当前资金池的状态，getCash获得当前资金池的底层资产数量。

另外还有一些管理员函数，用于修改资金池核心控制参数，这些是通过DAO治理投票才可以修改的。

    /*** User Interface ***/
    function transfer(address dst, uint amount) virtual external returns (bool);
    function transferFrom(address src, address dst, uint amount) virtual external returns (bool);
    function approve(address spender, uint amount) virtual external returns (bool);
    function allowance(address owner, address spender) virtual external view returns (uint);
    function balanceOf(address owner) virtual external view returns (uint);
    
    function balanceOfUnderlying(address owner) virtual external returns (uint);
    function getAccountSnapshot(address account) virtual external view returns (uint, uint, uint, uint);
    function borrowRatePerBlock() virtual external view returns (uint);
    function supplyRatePerBlock() virtual external view returns (uint);
    function totalBorrowsCurrent() virtual external returns (uint);
    function borrowBalanceCurrent(address account) virtual external returns (uint);
    function borrowBalanceStored(address account) virtual external view returns (uint);
    function exchangeRateCurrent() virtual external returns (uint);
    function exchangeRateStored() virtual external view returns (uint);
    function getCash() virtual external view returns (uint);
    function accrueInterest() virtual external returns (uint);
    function seize(address liquidator, address borrower, uint seizeTokens) virtual external returns (uint);
    
    /*** Admin Functions ***/
    function _setPendingAdmin(address payable newPendingAdmin) virtual external returns (uint);
    function _acceptAdmin() virtual external returns (uint);
    function _setComptroller(ComptrollerInterface newComptroller) virtual external returns (uint);
    function _setReserveFactor(uint newReserveFactorMantissa) virtual external returns (uint);
    function _reduceReserves(uint reduceAmount) virtual external returns (uint);
    function _setInterestRateModel(InterestRateModel newInterestRateModel) virtual external returns (uint);
    

由于ETH并不是ERC20代币，所以接口的封装和Erc20有所不同（对于erc20代币，amount通过函数参数带进去；而对于eth，amount需要通过msg.value带进去）。因此在CToken合约的基础上，出现了CErc20和CEther两个上层封装合约，这两个合约都继承了CToken。

下图是CErc20合约接口：

mint：用户lend给借贷池，为用户mint相应的cToken代币。

redeem/redeemUnderlying：用户取出自己借给借贷池的资产，销毁对应的cToken。

borrow：用户从借贷池borrow资产。

repayBorrow/repayBorrowBehalf：用户偿还borrow的资产。

liquidateBorrow：清算。

sweepToken：管理员函数，如果有错误被转入该合约的ERC20代币，可用此方法把代币转给管理员。

\_addReserves：增加借贷池的reserve资产。

    abstract contract CErc20Interface is CErc20Storage {
        /*** User Interface ***/
        function mint(uint mintAmount) virtual external returns (uint);
        function redeem(uint redeemTokens) virtual external returns (uint);
        function redeemUnderlying(uint redeemAmount) virtual external returns (uint);
        function borrow(uint borrowAmount) virtual external returns (uint);
        function repayBorrow(uint repayAmount) virtual external returns (uint);
        function repayBorrowBehalf(address borrower, uint repayAmount) virtual external returns (uint);
        function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) virtual external returns (uint);
        function sweepToken(EIP20NonStandardInterface token) virtual external;
    
        /*** Admin Functions ***/
        function _addReserves(uint addAmount) virtual external returns (uint);
    }
    

对于CEth，接口功能基本一致，只是把eth的amount参数移除（需要通过msg.value）带进去：

    contract CEther is CToken {
        function mint() external payable;
        function redeem(uint redeemTokens) external returns (uint);
        function redeemUnderlying(uint redeemAmount) external returns (uint);
        function borrow(uint borrowAmount) external returns (uint);
        function repayBorrow() external payable;
        function repayBorrowBehalf(address borrower) external payable;
        function liquidateBorrow(address borrower, CToken cTokenCollateral) external payable;
        function _addReserves() external payable returns (uint);
    }
    

还有一点需要指出，ERC20的CToken实际上是通过代理合约CErc20Delegator调用的。

可能因为是早期实现的，它并不是我们现在流行的uups或者transparent可升级合约，不过基本原理还是一样，主要是通过delegatecall调用CErc20Delegate的代码。不过对于view函数，并没有这样做，而是通过staticcall调用了自身的代码。为什么要这样做呢？或许是觉得这样做更加安全，因为delegatecall不能保证不修改数据。

    contract CErc20Delegate is CErc20, CDelegateInterface {
        ......
    }
    
    contract CErc20Delegator is CTokenInterface, CErc20Interface, CDelegatorInterface {
        address public implementation;
        ......
    
        function _setImplementation(address implementation_, bool allowResign, bytes memory becomeImplementationData)override public {
            require(msg.sender == admin, "CErc20Delegator::_setImplementation: Caller must be admin");
    
            if (allowResign) {
                delegateToImplementation(abi.encodeWithSignature("_resignImplementation()"));
            }
    
            address oldImplementation = implementation;
            implementation = implementation_;
    
            delegateToImplementation(abi.encodeWithSignature("_becomeImplementation(bytes)", becomeImplementationData));
    
            emit NewImplementation(oldImplementation, implementation);
        }
    
        function mint(uint mintAmount) override external returns (uint) {
            bytes memory data = delegateToImplementation(abi.encodeWithSignature("mint(uint256)", mintAmount));
            return abi.decode(data, (uint));
        }
        ......
    
        function getAccountSnapshot(address account) override external view returns (uint, uint, uint, uint) {
            bytes memory data = delegateToViewImplementation(abi.encodeWithSignature("getAccountSnapshot(address)", account));
            return abi.decode(data, (uint, uint, uint, uint));
        }
        ......
    
        function delegateTo(address callee, bytes memory data) internal returns (bytes memory) {
            (bool success, bytes memory returnData) = callee.delegatecall(data);
            assembly {
                if eq(success, 0) {
                    revert(add(returnData, 0x20), returndatasize())
                }
            }
            return returnData;
        }
    
        function delegateToImplementation(bytes memory data) public returns (bytes memory) {
            return delegateTo(implementation, data);
        }
    
        function delegateToViewImplementation(bytes memory data) public view returns (bytes memory) {
            (bool success, bytes memory returnData) = address(this).staticcall(abi.encodeWithSignature("delegateToImplementation(bytes)", data));
            assembly {
                if eq(success, 0) {
                    revert(add(returnData, 0x20), returndatasize())
                }
            }
            return abi.decode(returnData, (bytes));
        }
    
        fallback() external payable {
            require(msg.value == 0,"CErc20Delegator:fallback: cannot send value to fallback");
    
            // delegate all other functions to current implementation
            (bool success, ) = implementation.delegatecall(msg.data);
    
            assembly {
                let free_mem_ptr := mload(0x40)
                returndatacopy(free_mem_ptr, 0, returndatasize())
    
                switch success
                case 0 { revert(free_mem_ptr, returndatasize()) }
                default { return(free_mem_ptr, returndatasize()) }
            }
        }
    }
    

2 Comptroller
-------------

上一节说的cToken合约，每一种不同资产是相互独立的，例如dai和usdc对应的是不同的ctoken合约。但对于某个具体用户来说，如果要统计ta的资产状况、计算ta还剩多少借款额度等，则需要把compound支持的所有资产都考虑进来。此时，cToken合约就不能胜任这个任务了，需要一个总的控制合约来，这就是Comptroller。

下面截取了一些Comptroller但重要接口函数。

enterMarkets：选取某些代币资产作为抵押物。

exitMarket：让某种代币资产不再作为抵押物。

borrowAllowed：计算借款额度是否足够。

repayBorrowAllowed：计算还款数额是否合法。

liquidateBorrowAllowed：计算清算数额是否合法。

seizeAllowed：清算时清算人需要指定获取的抵押物，计算是否合法。

liquidateCalculateSeizeTokens：计算清算后可以获得的token数目。

    abstract contract ComptrollerInterface {
        /*** Assets You Are In ***/
        function enterMarkets(address[] calldata cTokens) virtual external returns (uint[] memory);
        function exitMarket(address cToken) virtual external returns (uint);
    
        /*** Policy Hooks ***/
        function borrowAllowed(address cToken, address borrower, uint borrowAmount) virtual external returns (uint);
        function repayBorrowAllowed(
            address cToken,
            address payer,
            address borrower,
            uint repayAmount) virtual external returns (uint);
        function liquidateBorrowAllowed(
            address cTokenBorrowed,
            address cTokenCollateral,
            address liquidator,
            address borrower,
            uint repayAmount) virtual external returns (uint);
        function seizeAllowed(
            address cTokenCollateral,
            address cTokenBorrowed,
            address liquidator,
            address borrower,
            uint seizeTokens) virtual external returns (uint);
    
    
        /*** Liquidity/Liquidation Calculations ***/
        function liquidateCalculateSeizeTokens(
            address cTokenBorrowed,
            address cTokenCollateral,
            uint repayAmount) virtual external view returns (uint, uint);
        ......
    }
    

存贷款的逻辑无需解释，下面看一下清算模型。

用户可以选择将自己存入compound的某种资产作为抵押物（enterMarkets函数）。为了控制风险，抵押都是超额抵押，例如存入价值10000USD的ETH，能借出来的资产价值一定是小于10000USD的。控制该额度的参数叫collateralfactor，每种资产的factor都不一样，并且是可以经过DAO治理修改的。目前ETH的factor是82.5%，这意味着存入10000USD价值的ETH，最多只能借出8250USD的资产。

![](https://storage.googleapis.com/papyrus_images/1a78e0bcd76091619a8ea09cebb6a682ec5b6fb3686039943e1a23e2459292de.png)

我们在compound官网主页上可以看到borrow limit，这就是我们所有的存入资产乘以相应的factor之后相加的总量。这也是我们所能够借出来的资产价值上限。

![](https://storage.googleapis.com/papyrus_images/1fa1c9f9f856d9b7813c519656a5e18f556cf865afc1348e472b0592e9b6066f.png)

我们不可能通过超额借款让自己被清算，因为此时智能合约会执行失败。不过，如果市场波动币价下跌，是有可能让我们的实际借款总额超过可借款总额的，作为用户应该尽量避免这种情况发生。

一旦发生这种情况，我们的账户就进入可清算状态。此时任何人都可以替我们偿还一部分借款，同时拿走相应价值的抵押物。为了鼓励清算者及时清算维持协议的健康运行，清算者将会获得奖励。被清算人会承担一定比例的损失（目前是8%），损失的资产大部分会奖励给清算者，小部分(2.5%，如果抵押物是eth则没有这部分)会被协议没收。清算奖励参数可以通过DAO治理投票修改。

清算的时候会有一个最大比例closeFactor，例如被清算人借了10000DAI，如果closeFactor为50%，那么在一次交易中最多只能清算5000DAI。不过这样的清算可以一直继续下去，直到被清算人不再是超额借款为止。

和cToken类似，Comptroller也是一个代理合约，unitroller

有点特别的是，合约升级被分成了2步，先setPending，然后再由新的Implement发起accept。

    contract Unitroller is UnitrollerAdminStorage, ComptrollerErrorReporter {
        ......
        fallback() payable external {
            // delegate all other functions to current implementation
            (bool success, ) = comptrollerImplementation.delegatecall(msg.data);
    
            assembly {
                  let free_mem_ptr := mload(0x40)
                  returndatacopy(free_mem_ptr, 0, returndatasize())
    
                  switch success
                  case 0 { revert(free_mem_ptr, returndatasize()) }
                  default { return(free_mem_ptr, returndatasize()) }
            }
        }
    
        /*** Admin Functions ***/
        function _setPendingImplementation(address newPendingImplementation) public returns (uint) {
            if (msg.sender != admin) {
                return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_IMPLEMENTATION_OWNER_CHECK);
            }
            ......
        }
    
        function _acceptImplementation() public returns (uint) {
                    // Check caller is pendingImplementation and pendingImplementation ≠ address(0)
                    if (msg.sender != pendingComptrollerImplementation || pendingComptrollerImplementation == address(0)) {
                        return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK);
                    }
            ......
        }
    }
    

那么implement是怎样的呢？因为代码很多，这里只看storage部分，我们可以看到迄今为止，comtroller至今已到到了第7个版本，每次更新都会在之前的基础上增加一些内容。

    contract ComptrollerV1Storage is UnitrollerAdminStorage {
        ......
    }
    
    contract ComptrollerV2Storage is ComptrollerV1Storage {
        ......
    }
    
    contract ComptrollerV3Storage is ComptrollerV2Storage {
        ...... 
    }
    
    contract ComptrollerV4Storage is ComptrollerV3Storage {
        ......
    }
    
    contract ComptrollerV5Storage is ComptrollerV4Storage {
        ......
    }
    
    contract ComptrollerV6Storage is ComptrollerV5Storage {
        ......
    }
    
    contract ComptrollerV7Storage is ComptrollerV6Storage {
        ......
    }
    
    contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerErrorReporter, ExponentialNoError {
        ......
    }
    

刚才提到的implement发起\_acceptImplementation完成升级的函数如下：

    function _become(Unitroller unitroller) public {
        require(msg.sender == unitroller.admin(), "only unitroller admin can change brains");
        require(unitroller._acceptImplementation() == 0, "change not authorized");
    }
    

3 InterestRateModel
-------------------

对于借贷协议，利率模型固然是非常重要的。

很显然，当资金池的资金利用率低的时候（借钱的人少），应该鼓励借钱、不鼓励存钱，因此存贷利率都应该更低；反之存贷利率都应该更高。按照白皮书的描述，compound的利率是随资金利用率线性递增的。

借款利率 = 2.5% + 资金利用率 \* 20%

存款利率 = 借款利率 \* 资金利用率

![](https://storage.googleapis.com/papyrus_images/f2644ce5114e835333686975a32d1ffc2fd4a167a566c042e42be5624a24b130.png)

    function getBorrowRate(uint cash, uint borrows, uint reserves) override public view returns (uint) {
            uint ur = utilizationRate(cash, borrows, reserves);
            return (ur * multiplierPerBlock / BASE) + baseRatePerBlock;
    }
    

但实际上，compound目前的支持的资产貌似都没有使用白皮书中的利率模型，而是使用了JumpRateModel，在资金利用率超过某个阈值（往往是80%）时，利率增长会变得非常陡峭，这样做是为了最大程度地避免资金池流动性枯竭。

利率模型是可以经由DAO治理投票通过\_setInterestRateModel进行修改的。

    function getBorrowRateInternal(uint cash, uint borrows, uint reserves) internal view returns (uint) {
        uint util = utilizationRate(cash, borrows, reserves);
    
        if (util <= kink) {
            return ((util * multiplierPerBlock) / BASE) + baseRatePerBlock;
        } else {
            uint normalRate = ((kink * multiplierPerBlock) / BASE) + baseRatePerBlock;
            uint excessUtil = util - kink;
            return ((excessUtil * jumpMultiplierPerBlock) / BASE) + normalRate;
        }
    }
    

不同的资产，使用的具体参数不尽相同，可以在这里查看。

[https://observablehq.com/@jflatow/compound-interest-rates](https://observablehq.com/@jflatow/compound-interest-rates)

目前DAI的利率和资金利用率的关系图如下：

![](https://storage.googleapis.com/papyrus_images/d647a69ec3fabcbb08291ef92f46d6924d6c558c97a9ef15183cc925269f70cf.png)

在确定利率模型之后，还有一点重要的事情是，利率的计算，这里核心的问题是计算出累积利率。

从理论上来说，AccrueInterest(t1) = AccrueInterest(t0) \* (1 + Interest)^(t1 - t0)

如果t1-t0足够小，那么可以把乘方简化为乘法：

AccrueInterest(t1) = AccrueInterest(t0) \* (1 + Interest) \* (t1 - t0)

在compound中，任意一次借贷操作都会触发一次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;
        }
    
        ......
    
        /* 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;
    
        Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta);
    
        ......
        return NO_ERROR;
    }
    

4 一个实例
------

上面简单介绍了compound协议中最核心的几个内容，下面我们将从最重要的几个用户接口切入，具体看协议中各个功能是如何实现的。

例子如下：

    contract TestScript is Script, Test {
        address payable user1 = payable(0x755557E102286F31F83BdE39c007cEE46D12D321);
        address payable user2 = payable(0xAdfaD0B8ccbAD46a009fAa4480E7986378a679bb);
    
        CEther cETH = CEther(payable(0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5));
        CErc20 cDAI = CErc20(0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643);
    
        IERC20 dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
        PriceOracle oracle = PriceOracle(0x65c816077C29b557BEE980ae3cC2dCE80204A0C5);
    
        Comptroller comptroller = Comptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B);
    
        function setUp() public view {}
    
        function run() public {
            vm.startPrank(user1);
            vm.deal(user1, 20 ether);
            deal(address(dai), user2, 10000 ether);
    
            console.log("ETH price: ", oracle.getUnderlyingPrice(cETH));
            console.log("DAI price: ", oracle.getUnderlyingPrice(cDAI));
    
            cETH.mint{value: 15 ether}();
            cETH.redeemUnderlying(5 ether);
    
            (, uint collateralFactorMantissa, ) = comptroller.markets(address(cETH));
            console2.log("eth collateral factor: ", collateralFactorMantissa);
    
            address[] memory collateralToken = new addressUnsupported embed;
            collateralToken[0] = address(cETH);
            comptroller.enterMarkets(collateralToken);
            cDAI.borrow(10500 ether);
            dai.approve(address(cDAI), 500 ether);
            cDAI.repayBorrow(500 ether);
    
            (, uint liquidity, uint shortfall) = comptroller.getAccountLiquidity(user1);
            console2.log("before adjust: liquidity: ",liquidity, ";shortfall: ", shortfall);
    
            vm.stopPrank();
            vm.prank(0x6d903f6003cca6255D85CcA4D3B5E5146dC33925);
            comptroller._setCollateralFactor(cETH, 100000000 gwei);
            vm.startPrank(user2);
    
            (, liquidity, shortfall) = comptroller.getAccountLiquidity(user1);
            console2.log("after adjust: liquidity: ",liquidity, ";shortfall: ", shortfall);
    
            dai.approve(address(cDAI), 5000 ether);
            cDAI.liquidateBorrow(user1, 5000 ether, cETH);
    
            (, liquidity, shortfall) = comptroller.getAccountLiquidity(user1);
            console2.log("after liquidation: liquidity: ",liquidity, ";shortfall: ", shortfall);
    
            console2.log("user1 eth balance: ",cETH.balanceOfUnderlying(user1));
            console2.log("user2 eth balance: ",cETH.balanceOfUnderlying(user2));
    
            vm.stopPrank();
    }
    

本次试验中，ETH价格1386.916674USD，DAI价格1.000114USD，ETH的collateral factor为82.5%.

1.  user1存入15ETH，再取出5ETH，还剩下10ETH。
    
2.  user1把eth设定为抵押物。
    
3.  user1借出10500DAI，在偿还500DAI，还剩下10000DAI的债务。
    
4.  此时查看getAccountLiquidity的借款额度：13869.16674\*0.825-10001.14=1440.92256
    
5.  利用foundry的prank功能模拟管理员账户，修改ETH的collateral factor为10%. 此时再查看借款额度：13869.16674\*0.1-10001.14=-8614.22333。可见该账户进入可清算状态。
    
6.  user2账户对user1发起清算，偿还5000DAI。偿还之后，user2获得user1的ETH抵押物:(5000\*1.000114/1386.916674)\*1.08 = 3.89397265261
    

![](https://storage.googleapis.com/papyrus_images/1d1c15354b336f9ab6569f1d00d34cf5236ca4dc0de616e1b25f6a28cbb4fb11.png)

### 4.1 存款

![](https://storage.googleapis.com/papyrus_images/22b7a90b1ed6445ca7b4aae077bdd08ff143b0a47c8199e5d7489b4bf23e6caf.png)

存款的逻辑比较简单，直接调用相应cToken的mint函数。

首先需要调用accrueInterest()计算累积利率，这一点上面已经提到过，任何借贷行为都会引发累积利率的更新，本节后面的几个操作也都会触发这一函数。

    function mintInternal(uint mintAmount) internal nonReentrant {
        accrueInterest();
        // mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to
        mintFresh(msg.sender, mintAmount);
    }
    

接下来就是mint。

首先需要调用comptroller的mintAllowed函数判断，当前compound是否支持该资产。

接下来要根据存入的数目计算应该给用户发放多少cToken，这个比例显然和累积利率相关。cToken price/token price的值是随时间增长而不断变大的，所以用户持有的cToken值一直不变，但是因为比例不断变大，所以对应的token会逐渐变多。

然后， uint actualMintAmount = doTransferIn(minter, mintAmount);把用户存入的token归入资金池。这里还要计算一个actualMintAmount，是考虑到某些token的转账要收手续费，所以最终入账的amount不一定和参数中的amount一样。

最后accountTokens\[minter\] = accountTokens\[minter\] + mintTokens;给用户mint相应的cToken。

    function mintFresh(address minter, uint mintAmount) internal {
        uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);
        if (allowed != 0) {
            revert MintComptrollerRejection(allowed);
        }
    
        if (accrualBlockNumber != getBlockNumber()) {
            revert MintFreshnessCheck();
        }
    
        Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});
    
        uint actualMintAmount = doTransferIn(minter, mintAmount);
    
        uint mintTokens = div_(actualMintAmount, exchangeRate);
    
        totalSupply = totalSupply + mintTokens;
        accountTokens[minter] = accountTokens[minter] + mintTokens;
    
        emit Mint(minter, actualMintAmount, mintTokens);
        emit Transfer(address(this), minter, mintTokens);
    }
    

### 4.2 取款

![](https://storage.googleapis.com/papyrus_images/26ee99ecb28b46c9be42949eb113577f7e5e334c052184fd8efca78c11446ea5.png)

总体上是存款的逆过程。值得注意的是，compound支持2个redeem函数，一个是指定cToken的amount，另一个是指定底层资产的amount，分别对应redeemFresh中第一个if判断的2个分支。

    function redeemUnderlyingInternal(uint redeemAmount) internal nonReentrant {
        accrueInterest();
        redeemFresh(payable(msg.sender), 0, redeemAmount);
    }
    
    function redeemFresh(address payable redeemer, uint redeemTokensIn, uint redeemAmountIn) internal {
        require(redeemTokensIn == 0 || redeemAmountIn == 0, "one of redeemTokensIn or redeemAmountIn must be zero");
    
        Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal() });
    
        uint redeemTokens;
        uint redeemAmount;
        if (redeemTokensIn > 0) {
            redeemTokens = redeemTokensIn;
            redeemAmount = mul_ScalarTruncate(exchangeRate, redeemTokensIn);
        } else {
            redeemTokens = div_(redeemAmountIn, exchangeRate);
            redeemAmount = redeemAmountIn;
        }
    
        uint allowed = comptroller.redeemAllowed(address(this), redeemer, redeemTokens);
        if (allowed != 0) {
            revert RedeemComptrollerRejection(allowed);
        }
    
        if (accrualBlockNumber != getBlockNumber()) {
            revert RedeemFreshnessCheck();
        }
    
        if (getCashPrior() < redeemAmount) {
            revert RedeemTransferOutNotPossible();
        }
        totalSupply = totalSupply - redeemTokens;
        accountTokens[redeemer] = accountTokens[redeemer] - redeemTokens;
    
        doTransferOut(redeemer, redeemAmount);
    
        emit Transfer(redeemer, address(this), redeemTokens);
        emit Redeem(redeemer, redeemAmount, redeemTokens);
    
        comptroller.redeemVerify(address(this), redeemer, redeemAmount, redeemTokens);
    }
    

不过取款可能导致抵押物不够，所以需要判断一下，这就是comptroller.redeemAllowed所做的事情。

当前的例子中，因为还没有把ETH作为抵押物，所以在redeemAllowedInternal的if (!markets\[cToken\].accountMembership\[redeemer\])就是直接返回。

如果用户已经设置了抵押物，那么会继续往下走。核心逻辑在getHypotheticalAccountLiquidityInternal，函数较长，只展示关键代码，它会遍历用户名下涉及的所有资产，以比较用户的总借款额和最大允许借款额。

    function redeemAllowedInternal(address cToken, address redeemer, uint redeemTokens) internal view returns (uint) {
        if (!markets[cToken].isListed) {
            return uint(Error.MARKET_NOT_LISTED);
        }
    
        if (!markets[cToken].accountMembership[redeemer]) {
            return uint(Error.NO_ERROR);
        }
    
        (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);
    }
    
    function getHypotheticalAccountLiquidityInternal(
        ......
        // For each asset the account is in
        CToken[] memory assets = accountAssets[account];
        for (uint i = 0; i < assets.length; i++) {
            ...
            vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);
            vars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral);
            vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);
            // Calculate effects of interacting with cTokenModify
            if (asset == cTokenModify) {
                vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);
                vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.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.3 设置抵押物

![](https://storage.googleapis.com/papyrus_images/628e73259675fd6b38356ee2a11db79ecf5d540d9c9e8f661629eb3416b3855c.png)

逻辑非常简单，只要在合约里做个记录就好了。无论当前用户是否出于可清算状态，增加抵押物都是被允许的，所以不需要做相关的判断。

    function addToMarketInternal(CToken cToken, address borrower) internal returns (Error) {
        Market storage marketToJoin = markets[address(cToken)];
    
        if (!marketToJoin.isListed) {
            return Error.MARKET_NOT_LISTED;
        }
    
        if (marketToJoin.accountMembership[borrower] == true) {
            return Error.NO_ERROR;
        }
    
        marketToJoin.accountMembership[borrower] = true;
        accountAssets[borrower].push(cToken);
    
        emit MarketEntered(cToken, borrower);
    
        return Error.NO_ERROR;
    }
    

不过，如果是想取消某个抵押物，则需要判断是否会导致用户进入可清算状态。

我们可以看到下面这行代码：

uint allowed = redeemAllowedInternal(cTokenAddress, msg.sender, tokensHeld);

假设当前用户把某个资产全部取出，不会导致清算，此时，才允许把这项资产取消抵押。

    function exitMarket(address cTokenAddress) override external returns (uint) {
        ......
    
        /* Fail if the sender is not permitted to redeem all of their tokens */
        uint allowed = redeemAllowedInternal(cTokenAddress, msg.sender, tokensHeld);
        if (allowed != 0) {
            return failOpaque(Error.REJECTION, FailureInfo.EXIT_MARKET_REJECTION, allowed);
        }
    
        ......
        delete marketToExit.accountMembership[msg.sender];
        CToken[] memory userAssetList = accountAssets[msg.sender];
        uint len = userAssetList.length;
        uint assetIndex = len;
        for (uint i = 0; i < len; i++) {
            if (userAssetList[i] == cToken) {
                assetIndex = i;
                break;
            }
        }
        assert(assetIndex < len);
    
        CToken[] storage storedList = accountAssets[msg.sender];
        storedList[assetIndex] = storedList[storedList.length - 1];
        storedList.pop();
    
        emit MarketExited(cToken, msg.sender);
    
        return uint(Error.NO_ERROR);
    }
    

### 4.4 借款

![](https://storage.googleapis.com/papyrus_images/f198ae1c528a2235c855efc405cd1d78d0b66f21d44f19f62dd1d225b0bca412.png)

看总体流程和mint很像，不同之处是需要判断是否允许借款。

comptroller.borrowAllowed判断抵押物是否足够。

if (getCashPrior() < borrowAmount)判断资金池是否有足够的token。

    function borrowFresh(address payable borrower, uint borrowAmount) internal {
        /* Fail if borrow not allowed */
        uint allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount);
        if (allowed != 0) {
            revert BorrowComptrollerRejection(allowed);
        }
        ......
        /* Fail gracefully if protocol has insufficient underlying cash */
        if (getCashPrior() < borrowAmount) {
            revert BorrowCashNotAvailable();
        }
    
        uint accountBorrowsPrev = borrowBalanceStoredInternal(borrower);
        uint accountBorrowsNew = accountBorrowsPrev + borrowAmount;
        uint totalBorrowsNew = totalBorrows + borrowAmount;
    
        accountBorrows[borrower].principal = accountBorrowsNew;
        accountBorrows[borrower].interestIndex = borrowIndex;
        totalBorrows = totalBorrowsNew;
    
        doTransferOut(borrower, borrowAmount);
    
        emit Borrow(borrower, borrowAmount, accountBorrowsNew, totalBorrowsNew);
    }
    

我们主要来看comptroller.borrowAllowed。

首先判断该cToken的借款是否被暂停以及是否被支持。

然后判断是否要borrow的amount是否超过了borrowCap，这是一个可以通过DAO治理修改的数字。目前大部分资产应该没有设置此限制，即borrowCap=0.

接下来通过getHypotheticalAccountLiquidityInternal判断借款后是否会导致用户超额借款，这个函数已经在取款那一小节里看过。

    function borrowAllowed(address cToken, address borrower, uint borrowAmount) override external returns (uint) {
        require(!borrowGuardianPaused[cToken], "borrow is paused");
    
        if (!markets[cToken].isListed) {
            return uint(Error.MARKET_NOT_LISTED);
        }
    
        ......
    
        uint borrowCap = borrowCaps[cToken];
        // Borrow cap of 0 corresponds to unlimited borrowing
        if (borrowCap != 0) {
            uint totalBorrows = CToken(cToken).totalBorrows();
            uint nextTotalBorrows = add_(totalBorrows, borrowAmount);
            require(nextTotalBorrows < borrowCap, "market borrow cap reached");
        }
    
        (Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(borrower, CToken(cToken), 0, borrowAmount);
        if (err != Error.NO_ERROR) {
            return uint(err);
        }
        if (shortfall > 0) {
            return uint(Error.INSUFFICIENT_LIQUIDITY);
        }
        ......
    
        return uint(Error.NO_ERROR);
    }
    

### 4.5 还款

![](https://storage.googleapis.com/papyrus_images/fae728885f3fa135304946a3bba08baf7eec02202164476cb9eb2bec12f2852a.png)

这个函数的流程比较简单。

一开始是判断repay是否合法：comptroller.repayBorrowAllowed。显然repay是不会导致超额借款的，所以这里实际上仅仅是判断了当前compound协议是否list了该cToken。

可以注意到一点，如果repayAmount写-1(最大uint数字)，那么会被认为是偿还所有该token的债务。

接下来还有一个actualRepayAmount的问题，因为某些代币转账会收取手续费导致actualRepayAmount小于repayAmount。

    function repayBorrowFresh(address payer, address borrower, uint repayAmount) internal returns (uint) {
        uint allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount);
        if (allowed != 0) {
            revert RepayBorrowComptrollerRejection(allowed);
        }
    
        if (accrualBlockNumber != getBlockNumber()) {
            revert RepayBorrowFreshnessCheck();
        }
    
        uint accountBorrowsPrev = borrowBalanceStoredInternal(borrower);
    
        /* If repayAmount == -1, repayAmount = accountBorrows */
        uint repayAmountFinal = repayAmount == type(uint).max ? accountBorrowsPrev : repayAmount;
    
        uint actualRepayAmount = doTransferIn(payer, repayAmountFinal);
    
        uint accountBorrowsNew = accountBorrowsPrev - actualRepayAmount;
        uint totalBorrowsNew = totalBorrows - actualRepayAmount;
    
        accountBorrows[borrower].principal = accountBorrowsNew;
        accountBorrows[borrower].interestIndex = borrowIndex;
        totalBorrows = totalBorrowsNew;
    
        emit RepayBorrow(payer, borrower, actualRepayAmount, accountBorrowsNew, totalBorrowsNew);
        return actualRepayAmount;
    }
    

值得注意的是，这里并没有判断repayAmount小于borrowAmount，如果repay超额，会在下面这句overflow，导致交易失败：

uint accountBorrowsNew = accountBorrowsPrev - actualRepayAmount;

### 4.6 清算

![](https://storage.googleapis.com/papyrus_images/517bdcc36d2e7882e3b8bc5cf4d3f4147ba01003a9e6e9d30b5695cd59f4ea0b.png)

![](https://storage.googleapis.com/papyrus_images/b8285ad59b5b9597ca76a7db16822f679c427210913cc023e2c0a7122f9c8df0.png)

这是最复杂的一个流程。我们来看一下liquidate的接口函数。

这是cToken合约的函数，cToken合约本身指定了我们想要清算（为清算人偿还）何种资产，borrower是被清算人，repayAmount是清算amount。上面有说到，单次交易最大只能清算50%（撰写本文时Comptroller合约的设定）的借款额。

此外还有一个cTokenCollateral函数，用户有可能有多种资产作为抵押物，因此我们需要指定repay之后，我们可以得到哪种抵押物。我们得到抵押物的价值应该是我们repay的价值的1.08倍，不过有一种情况是用户根本没有那么多的cTokenCollateral抵押物，此时liqudate会执行失败。所以在设置repayAmount和cTokenCollateral的时候需要自己预先计算一下。

    function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral) override external returns (uint)
    

清算函数的主体如下，大致可以划分为4个部分。

    function liquidateBorrowFresh(address liquidator, address borrower, uint repayAmount, CTokenInterface cTokenCollateral) internal {
        // 1.判断是否可清算
        /* Fail if liquidate not allowed */
        uint allowed = comptroller.liquidateBorrowAllowed(address(this), address(cTokenCollateral), liquidator, borrower, repayAmount);
        if (allowed != 0) {
            revert LiquidateComptrollerRejection(allowed);
        }
    
    
        ......
    
        // 2.清算（偿还借款）,返回实际清算额
        uint actualRepayAmount = repayBorrowFresh(liquidator, borrower, repayAmount);
    
    
        // 3.计算可获取的抵押物数量
        (uint amountSeizeError, uint seizeTokens) = comptroller.liquidateCalculateSeizeTokens(address(this), address(cTokenCollateral), actualRepayAmount);
        require(amountSeizeError == NO_ERROR, "LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED");
        require(cTokenCollateral.balanceOf(borrower) >= seizeTokens, "LIQUIDATE_SEIZE_TOO_MUCH");
    
        // 4.分配抵押物
        // If this is also the collateral, run seizeInternal to avoid re-entrancy, otherwise make an external call
        if (address(cTokenCollateral) == address(this)) {
            seizeInternal(address(this), liquidator, borrower, seizeTokens);
        } else {
            require(cTokenCollateral.seize(liquidator, borrower, seizeTokens) == NO_ERROR, "token seizure failed");
        }
    
        /* We emit a LiquidateBorrow event */
        emit LiquidateBorrow(liquidator, borrower, actualRepayAmount, address(cTokenCollateral), seizeTokens);
    }
    

*   判断是否可清算
    

我们首先关注到的是isDeprecated(CToken(cTokenBorrowed)), 这个函数用于判断cToken池子是否废弃。如果已经被废弃，那么只要repayAmount不超过borrowBalance，就直接判定可以清算并返回。此时，无需判断用户抵押资产是否足够。

如果cToken池没有废弃，则需要使用getAccountLiquidityInternal函数计算，用户是否有超额借款，只有超额借款的用户才可以被清算。

接下来，还需要判断是否超过closeFactor，这一点刚才已经解释过。如果这项判断也通过，则可返回成功。

我们注意到，在这里并没有判断borrowBalance >= repayAmount。如果borrowBalance < repayAmount，那么后面更新债务数额的时候会overflow导致交易失败，所以repayAmount时不可能超过borrowBalance的。不过，repayAmout是有可能超过用户的超额借款量的，这意味着用户又可能被多清算一些资产。这里可以看出closeFactor对用户具有一定的保护作用。

    function liquidateBorrowAllowed(
        address cTokenBorrowed,
        address cTokenCollateral,
        address liquidator,
        address borrower,
        uint repayAmount) override external returns (uint) {
        ......
        uint borrowBalance = CToken(cTokenBorrowed).borrowBalanceStored(borrower);
    
        /* allow accounts to be liquidated if the market is deprecated */
        if (isDeprecated(CToken(cTokenBorrowed))) {
            require(borrowBalance >= repayAmount, "Can not repay more than the total borrow");
        } else {
            /* The borrower must have shortfall in order to be liquidatable */
            (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower);
            if (err != Error.NO_ERROR) {
                return uint(err);
            }
    
            if (shortfall == 0) {
                return uint(Error.INSUFFICIENT_SHORTFALL);
            }
    
            /* The liquidator may not repay more than what is allowed by the closeFactor */
            uint maxClose = mul_ScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance);
            if (repayAmount > maxClose) {
                return uint(Error.TOO_MUCH_REPAY);
            }
        }
        return uint(Error.NO_ERROR);
    }
    

*   清算（偿还借款）,返回实际清算额
    

其实，这就是我们上面已经讲过的还款流程，不再重复叙述。因为可能的transfer手续费，实际清算额可能不等于之前传入的清算额。

*   计算可获取的抵押物数量
    

从预言机中获取清算token和抵押物token的价格，然后计算可以获取多少抵押物。重点关注liquidationIncentiveMantissa，这是清算人的清算奖励，也是对被清算人的惩罚。

    function liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint actualRepayAmount) override 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);
        }
    
        uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error
        uint seizeTokens;
        Exp memory numerator;
        Exp memory denominator;
        Exp memory ratio;
    
        numerator = mul_(Exp({mantissa: liquidationIncentiveMantissa}), Exp({mantissa: priceBorrowedMantissa}));
        denominator = mul_(Exp({mantissa: priceCollateralMantissa}), Exp({mantissa: exchangeRateMantissa}));
        ratio = div_(numerator, denominator);
    
        seizeTokens = mul_ScalarTruncate(ratio, actualRepayAmount);
    
        return (uint(Error.NO_ERROR), seizeTokens);
    }
    

*   分配抵押物
    

这一步，抵押物是ETH和是ERC20代币的情况略有不同，我们先看ERC20代币。

重点关注protocolSeizeShareMantissa和protocolSeizeAmount，我们可以看到抵押物并没有全部分配给清算者，而是有一部分被资金池没收了。这个比例目前是写死在代码里的，2.8%。

    function seizeInternal(address seizerToken, address liquidator, address borrower, uint seizeTokens) internal {
        ......
        uint protocolSeizeTokens = mul_(seizeTokens, Exp({mantissa: protocolSeizeShareMantissa}));
        uint liquidatorSeizeTokens = seizeTokens - protocolSeizeTokens;
        Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});
        uint protocolSeizeAmount = mul_ScalarTruncate(exchangeRate, protocolSeizeTokens);
        uint totalReservesNew = totalReserves + protocolSeizeAmount;
    
        totalReserves = totalReservesNew;
        totalSupply = totalSupply - protocolSeizeTokens;
        accountTokens[borrower] = accountTokens[borrower] - seizeTokens;
        accountTokens[liquidator] = accountTokens[liquidator] + liquidatorSeizeTokens;
    
        emit Transfer(borrower, liquidator, liquidatorSeizeTokens);
        emit Transfer(borrower, address(this), protocolSeizeTokens);
        emit ReservesAdded(address(this), protocolSeizeAmount, totalReservesNew);
    }
    

而cETH的seize函数并没有收取protocol费用，本文的例子中抵押物正是ETH，大家可以从合约执行过程图中看出来。下面的链接可以看到cETH合约代码：

[https://etherscan.io/address/0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5#code](https://etherscan.io/address/0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5#code)

但是目前compound的github库中已经没有对cETH做这样的处理了。不过链上的cETH合约并非可升级代理合约（其他ERC20代币cToken都是可升级代理），因此并不容易修改，如果想修改只能将原本的cETH池子废弃重新部署一个新的。不知道是因为早期并没有考虑到升级因素还是项目方有意这般设计。

    function seize(address liquidator, address borrower, uint seizeTokens) external nonReentrant returns (uint) {
        ......
    
        (mathErr, borrowerTokensNew) = subUInt(accountTokens[borrower], seizeTokens);
        if (mathErr != MathError.NO_ERROR) {
            return failOpaque(Error.MATH_ERROR, FailureInfo.LIQUIDATE_SEIZE_BALANCE_DECREMENT_FAILED, uint(mathErr));
        }
    
        (mathErr, liquidatorTokensNew) = addUInt(accountTokens[liquidator], seizeTokens);
        if (mathErr != MathError.NO_ERROR) {
            return failOpaque(Error.MATH_ERROR, FailureInfo.LIQUIDATE_SEIZE_BALANCE_INCREMENT_FAILED, uint(mathErr));
        }
    
        /* We write the previously calculated values into storage */
        accountTokens[borrower] = borrowerTokensNew;
        accountTokens[liquidator] = liquidatorTokensNew;
    
        emit Transfer(borrower, liquidator, seizeTokens);
        comptroller.seizeVerify(address(this), msg.sender, liquidator, borrower, seizeTokens);
    
        return uint(Error.NO_ERROR);
    }

---

*Originally published on [rbtree](https://paragraph.com/@rbtree/compound)*
