美文网首页
Defi部署教程 Compound部署 搭建去中心化借贷银行

Defi部署教程 Compound部署 搭建去中心化借贷银行

作者: walker_1992 | 来源:发表于2022-04-08 00:05 被阅读0次

    本文环境
    操作系统:maxOS 10.15.6
    科学上网
    公链网络:BSC Testnet
    测试工具:Remix IDE、MetaMask
    合约源码:https://github.com/compound-finance/compound-protocol.git

    在 remix 进行编译部署时,勾选启用优化。

    Compound中含有的合约代码量很大,文件数量30+,一开始笔者也忍不住打了退堂鼓。然而学习就是一个从难到易的过程,只有花时间去努力学习,才能慢慢理解它的结构和细节,最终会赞美Compound团队提供的优秀代码,优秀方案!
    下图是网上找的Compound合约结构图,以飨读者。在部署前,先梳理清楚各个合约之间的关系,并将其分组,梳理出各个模块,及初始化参数。

    Compound 合约结构

    一、COMP 模块

    激励资产合约,可以使用标准 ERC20 合约。
    因为 Comptroller 中会使用到 COMP 的地址,因此我们最先部署 COMP 合约,得到合约地址: 0x1fe7FF222D59B6648D640090046661313A1CF0a2
    部署完成后,到合约 ComptrollerG7.sol (或者计划使用的 Comptroller 合约)进行配置,更改为自己的 COMP 合约地址。
    也可以直接使用Comp.sol部署 compound自带的COMP token合约,修改一下name,symbol,totalSupply等。

         /**
         * @notice Return the address of the COMP token
         * @return The address of COMP
         */
        function getCompAddress() public view returns (address) {
            return 0x1fe7FF222D59B6648D640090046661313A1CF0a2;
        }
    

    二、comptroller 模块

    在compound设计中,unitroller 是代理合约,comptroller 是逻辑实现合约,通过 delegatecall 来实现远程合约调用。

    2.1 部署 Unitroller.sol

    使用 account1 账号进行部署,成功:

    contract address: 0x268e3eF4380DA46e55B77a6263810910722a875E
    

    2.2 部署 ComptrollerG7.sol

    使用 account1 账号进行部署;成功:

    contract address: 0x67006E2110119Abfd40b2A743A85b4d3bF8967b9
    

    三、priceOracle 模块

    3.1 部署 SimplePriceOracle.sol

    使用 account1 账号进行部署

    contract address: 0x5991199a9aB1801A229a2E49a042471eDE997a21
    

    四、绑定与设置

    4.1 代理绑定

    • 第一步: 在 Unitroller.sol 合约调用 _setPendingImplementation;
      参数 address newPendingImplementation,这里设置为 ComptrollerG7.sol 地址

    • 第二步: 在 ComptrollerG7.sol 合约调用 _become,
      参数 Unitroller unitroller,这里设置为 Unitroller.sol 地址

    代理绑定,第一步转移所有权,第二步新的 Comptroller 接受所有权,这样就可以防止意外地升级到无效的合约;
    备注:设置完成后对外提供 Comptroller 合约地址时, 提供的是 Unitroller 合约地址。

    以下步骤,请 unitrollerProxy = ComptrollerG7(address(unitroller));
    at Address unitrollerAddr得到unitrollerProxy合约,名字还是ComptrollerG7

    4.2 设置 closeFactor

    在 ComptrollerG7.sol 合约调用 _setCloseFactor,
    参数 uint newCloseFactorMantissa,这里设置为 50%,即:0.5 * 1 ^18 = 500000000000000000

    4.3 设置 liquidationIncentiveMantissa

    在 ComptrollerG7.sol 合约调用 _setLiquidationIncentive,
    参数 uint newLiquidationIncentiveMantissa,设置流动性激励为 8%,参数值就是1.08 * 1 ^ 18 = 1080000000000000000

    4.4 设置 oracle

    在 ComptrollerG7.sol 合约调用 _setPriceOracle,
    参数 PriceOracle newOracle,这里设置为 SimplePriceOracle.sol 地址:0x5991199a9aB1801A229a2E49a042471eDE997a21

    五、interestRate 模块

    拐点利率模型

    5.1 部署 JumpRateModelV2.sol

    部署时参数:

    • uint baseRatePerYear, 实际设置为 0
    • uint multiplierPerYear, 实际设置 7%, 即 0.07 * 10 ^ 18 = 70000000000000000
    • uint jumpMultiplierPerYear, 实际设置 3, 即 3 * 10 ^ 18 = 3000000000000000000
    • uint kink_, 实际设置 75%, 即 0.75 * 10 ^ 18 = 750000000000000000
    • address owner_, 实际设置 msg.sender

    使用 account1 账号进行部署,成功:

    contract address: 0x8A517DA790929D2aC3527210f9472E2822424180
    

    备注: 部署后, 参数都可以用 updateJumpRateModel 方法进行修改;

    5.2 部署另一个 JumpRateModelV2.sol

    如果是测试,只需要部署一个就可以了,使用erc20的,
    因为 cToken 跟 JumpRateModelV2 需要一一对应的关系,因此再次部署该合约,用于后面分别与 CErc20Delegator.sol 和 CEther.sol 对应.
    部署时参数跟5.1节相同;
    使用 account1 账号进行部署,成功:

    contract address: 0x0cca4ccD1ED542B5D7F3Ebbcf49D92DCB0a8D04e
    

    六、CToken 模块(ERC20)

    6.1 部署 ERC20Token.sol

    部署一个标准 ERC20 代币,作为基础资产用于测试,
    例子:使用 account1 账号进行部署usdt合约,成功:

    contract address:  0xBEA207ec294BCe7a866C3a598195A61Bb7E8D599
    

    6.2 部署 CErc20Delegate.sol

    此合约给支持代理的 cToken 合约使用,不支持代理的 cToken 不需要使用这个合约;
    所有 ERC20 基础资产的 CToken 采用委托代理模式,所以我们先部署一个实现合约:
    使用 account1 账号进行部署,成功:

    contract address: 0xc176eD65274b2a2d422126d597Be715fc97d2e98
    

    6.3 部署 CErc20Delegator.sol

    此合约即为与代币类型(ERC20)的标的资产对应的 cToken 合约;
    部署时参数:

    • address underlying_, erc20标的资产地址,见6.1节
    • ComptrollerInterface comptroller_, ComptrollerG7.sol 合约地址,见2.2节
    • InterestRateModel interestRateModel_, JumpRateModelV2合约地址,见5.1节
    • uint initialExchangeRateMantissa_, 初始汇率,按 1:1 设置,比列见备注说明,本文 1 * 10 ^ 18 = 100000000000000000
    • string memory name_, cToken 的 name COMPOUND USD
    • string memory symbol_, cToken 的 symbol cUSD
    • uint8 decimals_, cToken 的 decimals, 设为 18
    • address payable admin_, 应该是时间锁定合约地址,此处设为 msg.sender
    • address implementation_, CErc20Delegate 合约地址,见6.2节
    • bytes memory becomeImplementationData, 额外初始数据,此处填入0x;即无数据

    备注:initialExchangeRateMantissa_ = 1 * 10 ^ (18 + underlyingDecimals - cTokenDecimals)

    使用 account1 账号进行部署,成功:

    contract address: 0x209C9b6a0Ec37b91d0758514070A8439B14B9B3c
    

    七、CToken 模块(ETH)

    7.1 部署 CEther.sol

    此合约即为与主币类型(ETH)对应的 cToken 合约,
    部署时参数:

    • ComptrollerInterface comptroller_, unitroller合约地址,见2.1节
    • InterestRateModel interestRateModel_, JumpRateModelV2合约地址,见5.2节
    • uint initialExchangeRateMantissa_, 初始汇率,按 1:1 设置,本文 1 * 10 ^ 18 = 100000000000000000
    • string memory name_, cToken 的 name COMPOUND ETHER
    • string memory symbol_, cToken 的 symbol cETH
    • uint8 decimals_, cToken 的 decimals,设为 18
    • address payable admin_, 设为 msg.sender

    使用 account1 账号进行部署,成功:

    contract address: 0xf3feeab27E8B8b71ED92040be19E5aA80baf9B01
    

    八、设置市场价格

    在SimplePriceOracle.sol合约里调用setUnderlyingPrice:

    8.1设置cUSD的价格:

    CToken cToken, CErc20Delegator.sol 地址
    uint underlyingPriceMantissa, 1 * 10 ^ 18 = 1000000000000000000
    使用 account1 账号进行cUSD价格设置操作,成功

    8.2设置cETH的价格:

    CToken cToken, CEther.sol 地址
    uint underlyingPriceMantissa, 2000 * 10 ^ 18 = 2000000000000000000000
    使用 account1 账号进行cETH价格设置操作,成功

    九、CToken 配置

    9.1 设置 ReserveFactor

    设置保证金系数

    9.1.1 在 CErc20Delegator.sol 调用合约方法 _setReserveFactor:

    设置时参数:

    • uint newReserveFactorMantissa , 新的保证金系数, 本文 0.1 * 10 ^ 18 = 100000000000000000

    9.1.2 在 CEther.sol 调用合约方法 _setReserveFactor:

    设置时参数:

    • uint newReserveFactorMantissa , 新的保证金系数, 本文 0.2 * 10 ^ 18 = 200000000000000000

    9.2 CToken 加入市场

    在 ComptrollerG7.sol 调用合约方法 _supportMarket:
    设置时参数:

    • CToken cToken, CErc20Delegator.sol 或 CEther.sol 地址

    本文操作两次,将前面部署的 CErc20Delegator.sol 和 CEther.sol 均加入;
    使用 account1 账号进行操作

    9.3 设置 CollateralFactor

    设置抵押率;
    在 ComptrollerG7.sol 调用合约方法 _setCollateralFactor:
    设置时参数:

    • CToken cToken, CErc20Delegator.sol 地址
    • uint newCollateralFactorMantissa, 抵押率,本文使用 0.6 * 10 ^ 18 = 600000000000000000

    使用 account1 账号进行操作,成功

    Remix部署完合约以后,如下图:

    部署的合约

    十、COMP奖励

    用户存和借cToken都会有奖励,如果cToken市场设置了compSpeed。
    compSpeed: 整数,表示协议将COMP分配给市场供应商或借款人的速率。价值是分配给市场的每个区块的COMP(单位:wei)。 请注意,并非每个市场都向其参与者分发了COMP。可以设置成0。速度表明市场供应商或借款人获得了多少红利,因此将这个数字翻一番,可以显示市场供应商和借款人获得的红利之和。
    //代码示例实现了读取每个以太坊区块分配到单个市场的COMP量。

        /**
         * @notice Set COMP speed for a single market
         * @param cToken The market whose COMP speed to update
         * @param compSpeed New COMP speed for market
         */
        function _setCompSpeed(CToken cToken, uint compSpeed) public {
            require(adminOrInitializing(), "only admin can set comp speed");
            setCompSpeedInternal(cToken, compSpeed);
        }
    

    owner才可以设置
    参数

    • cToken 相应的市场cToken地址
    • compSpeed 价值是分配给市场的每个区块的COMP(单位:wei)
      这个方法不执行,默认为0,不分发comp

    计算compspeed:需要翻倍计算

    const cTokenAddress = '0xabc...';
    const comptroller = new web3.eth.Contract(comptrollerAbi, comptrollerAddress);
    let compSpeed = await comptroller.methods.compSpeeds(cTokenAddress).call();
    compSpeed = compSpeed / 1e18;
    // COMP issued to suppliers OR borrowers
    const compSpeedPerDay = compSpeed * 4 * 60 * 24;
    // COMP issued to suppliers AND borrowers
    const compSpeedPerDayTotal = compSpeedPerDay * 2;
    

    十一 提取(Claim COMP)

    每个 Compound 用户都会为他们提供给协议或从协议中借用的每个区块累积COMP。用户可以随时调用Comptroller的 claimComp 方法,将累积的COMP转移到他们的地址。
    合约方法:

    // Claim all the COMP accrued by holder in all markets
    function claimComp(address holder) public
    // Claim all the COMP accrued by holder in specific markets
    function claimComp(address holder, CToken[] memory cTokens) public
    // Claim all the COMP accrued by specific holders in specific markets for their supplies and/or borrows
    function claimComp(address[] memory holders, CToken[] memory cTokens, bool borrowers, bool suppliers) public
    

    可以使用claimComp()方法提取个人的comp奖励

    const comptroller = new web3.eth.Contract(comptrollerAbi, comptrollerAddress);
    await comptroller.methods.claimComp("0x1234...").send({ from: sender });
    

    测试模块

    前面我们部署了comptroller合约,现在我们需要写一部分测试,看具体的合约逻辑执行。在最小可运行的compound合约中,我们部署了抵押usd,以及compound铸造出来的token:cUSD. 并部署了cUSD实际调用的逻辑cErc20Delegate, 然后cUSD的借贷模型中采用的是JumpRateModelV2,对应的审计合约是comptrollerG7.

    下面我们分别就compound中,最核心的用户交互逻辑来编写5个测试,简单验证逻辑可行性。

    1、存 mint

    用户向compound中存款的逻辑是:用户向compound中存入USD代币, compound根据当前的汇率算出铸造的cUSD代币数量,将对应的cUSD代币转账给用户。

    用户函数:enterMarkets

    用户的地址中对应用户的所有资产列表,当计算一个用户的所有流动性时。在借贷一种资产前,一个或者多种资产必须被提供给compound以用作抵押。在借贷发生前,任何借贷出的资产必须通过这种方式添加进入compound中。该函数的返回值是一个列表,即该用户的所有资产列表。
    在 ComptrollerG7.sol 调用合约方法 enterMarkets:
    参数:cTokens: [
    "0x209C9b6a0Ec37b91d0758514070A8439B14B9B3c", // cUSD 地址
    "0xf3feeab27E8B8b71ED92040be19E5aA80baf9B01" // cETH 地址
    ]

    ComptrollerG7(address(unitroller)).enterMarkets(addrs);
    //此时alice调用enterMarkets后,全局变量accountAssets[alice] = cToken[cUni], markets[cWALKER]={true, 60%,{alice:true},false}
    //alice 调用cWALKER的mint方法
    WALKER.approve(address(cWALKER),uint(-1));
    cWALKER.mint(200000000000000000000); //200
    // 200000000000000000000/1000000000000000000  //1:1
    cWALKER.balanceOf(alice) =  200000000000000000000; //200
    cWALKER.totalSupply() = 200000000000000000000
    cWALKER.getCash() = 200000000000000000000
    cWALKER.supplyRatePerBlock() = 0 //此时没有借款,利用率为0
    ComptrollerG7(address(unitroller)).getAccountLiquidity(alice) = 120000000000000000000 = 200000000000000000000 * 0.6 // 120 用户流动性:为UnderlyingToken * 0.6 * price 
    
    mint

    2、借 borrow

    借币逻辑是:用户在compound中有多种cToken资产,记录在accountAssets中。然后用户向compound借出一定量的usd资产,同时增加用户的负债额度。compound在接受用户的借款请求时,首先会检查cToken有没有上市,再检查用户是否enterMarket,然后根据现在的预言机报价检查用户的账户流动性。

    //alice 在compound中存入了200000000000000000000的WALKER代币,获得了200000000000000000000的cWALKER代币
    //alice 向compound提出借款50000000000000000000的WALKER代币
    cWALKER.borrow(50000000000000000000);//50
    cWALKER.totalBorrows() = 50000000000000000000;//50
    cWALKER.getCash() == 150000000000000000000 = 200000000000000000000 - 50000000000000000000//150
    cWALKER.supplyRatePerBlock = 2219685438
    cWALKER.exchangeRateStored() = 1000000000000000000
    cWALKER.borrowRatePerBlock() = 11098427194
    利用率:utilization = cWALKER.supplyRatePerBlock / cWALKER.borrowRatePerBlock * (1- 0.25) = 
    cWALKER.borrowIndex() = 1000000000000000000
    cWALKER.accrualBlockNumber() =  18301817
    
    borrow

    3、还 repay

    repay操作是borrow的逆操作,可以通过repayBorrow偿还自己的贷款,repayBorrowBehalf代为偿还他人贷款,其具体逻辑是用户批准cToken合约使用其underlying token,先调用accuralInterest计算目前利率指数和对全部借贷额计息,然后调用comptroller.repayBorrowAllowed函数检查是否可以偿还,最后调用repayBorrowFresh偿还。

    WALKER.approve(cWALKER, 50000000000000000000);
    cWALKER.repayBorrow(50000000000000000000);
    cWALKER.totalBorrows() = 129296676810100;
    cWALKER.getCash() = 200000000000000000000;//200
    cWALKER.supplyRatePerBlock = 0
    cWALKER.exchangeRateStored() = 1000000517186707240
    cWALKER.borrowRatePerBlock() = 28699
    利用率:utilization = cWALKER.supplyRatePerBlock / cWALKER.borrowRatePerBlock * (1- 0.25) = 
    cWALKER.borrowIndex() = 1000002585933536202
    cWALKER.accrualBlockNumber() =  18302050
    
    repay

    4、取 redeem

    redeem是mint的逆运算,但在实际逻辑中,增加了一个检查账户虚拟流动性的一项。用户可以调用redeem来偿还给定数量的cToken,或者调用redeemUnderlying来偿还某数量的cToken得到给定数量的underlying Token. redeem操作的步骤是用户批准cUSD合约使用用户的cUSD代币,然后调用accuralInterest函数,来计算最新的利率指数Index,并对totalBorrows计息。再然后是调用comptroller.redeemAllowed函数,计算用户的虚拟流动性,看是否用户有足够的流动性来取走token。最后是redeemFresh函数根据要取走的数值,更新accountBorrow中的数值和totalBorrows。

    5、清算 liquidity

    发生清算的一种典型情况是,用户enterMarkets了两个market,分别是cUni和cUSDT资金池。然后用户在cUni池中,存入Uni获得一定的cUni。用户凭借cUni在cUSDT资金池中借贷出USDT。然而,由于Uni的价格波动,导致Uni/USDT的价格突然下跌,此时用户放置在cUni池中的cUni的总价值小于了借出的USDT的价值,从而触发外部清算者进行清算。

    清算过程整体分为两部分:第一部分是repayBorrower部分,代为偿还underlying token,另一部分是seize部分,即将被清算者的cToken及奖励金一起奖励给清算者。由于清算涉及到两种cToken,故在清算的第一步是分别调用两种cToken的accural Interest函数,计算各自最新的利率指数Index,并计算含息债务总额。然后调用comptroller.liquidateBorrowAllowed函数,计算被清算账户的流动性,如果被清算账户的流动性为正,则不允许清算,如果被清算账户的流动性为负,并验算单笔交易的清算量不能超过被清算账户的最大可清算量,则允许清算。具体清算时,要求清算者不能是被清算者自己,然后计算转给被清算者的cToken数量。

    在执行转账cToken到清算者之前,需调用comptroller.seizeAllowed函数,作用是验证调用seize函数的msg.sender和address(this)的comptroller保持一致。然后将清算者的账户余额加上seizeTokens,被清算者的余额减去seizeTokens。在完成seize部分后,函数跳转到repayBorrow部分,代为偿还underlying token。
    具体清算的概念,可以看清算概述,比较详细简单。

    前端调用的方法

    CErc20Delegator.sol是给普通erc20用的,CEther.sol是给链的主币用的,基础的都是cToken。
    里面的mint,redeem,redeemUnderlying,borrow,repayBorrow,repayBorrowBehalf都类似的。

        /**
         * @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 0=success, otherwise a failure (see ErrorReporter.sol for details)
         */
        function mint(uint mintAmount) external returns (uint) {
            bytes memory data = delegateToImplementation(abi.encodeWithSignature("mint(uint256)", mintAmount));
            return abi.decode(data, (uint));
        }
        /**
         * @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 将被赎回的cToken的数量
         * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
         * redeem 方法将指定数量的 cToken 转换为标的资产,并将其返还给用户。收到的标的数量等于赎回的 cToken 数量乘以当前汇率。
         * 赎回额必须小于用户的账户流动性和市场可用的流动性。
         */
        function redeem(uint redeemTokens) external returns (uint) {
            return redeemInternal(redeemTokens);
        }
    
        /**
         * @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)
         * redeem underlying 方法将 cToken兑换成指定数量的标的资产,并返回给用户。赎回的 cToken的数量等于收到的标的数量除以当前汇率。
         * 赎回额必须小于用户的账户流动性和市场可用的流动性。
         */
        function redeemUnderlying(uint redeemAmount) external returns (uint) {
            return redeemUnderlyingInternal(redeemAmount);
        }
    
        /**
          * @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)
          * borrow 方法将协议中的标的资产转移给用户,并创建一个借款余额,根据该资产的借款利率开始累积利息。
          * 借款额必须小于用户的账户流动性和市场可用的流动性。
          */
        function borrow(uint borrowAmount) external returns (uint) {
            return borrowInternal(borrowAmount);
        }
    
        /**
         * @notice Sender repays their own borrow
         * @dev Reverts upon any failure
         * repay 方法将标的资产转移到协议中,并减少用户的借款余额。
         */
        function repayBorrow() external payable {
            (uint err,) = repayBorrowInternal(msg.value);
            requireNoError(err, "repayBorrow failed");
        }
    
        /**
         * @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 {
            (uint err,) = repayBorrowBehalfInternal(borrower, msg.value);
            requireNoError(err, "repayBorrowBehalf failed");
        }
    
    

    在 ComptrollerG7.sol 调用合约方法 enterMarkets:

    参数:cTokens: [
    "0x209C9b6a0Ec37b91d0758514070A8439B14B9B3c", // cUSD 地址
    "0xf3feeab27E8B8b71ED92040be19E5aA80baf9B01" // cETH 地址
    ]

    ComptrollerG7(address(unitroller)).enterMarkets(addrs);
    

    参考:

    1. 第108篇 Compound 简单部署
    2. Compound 合约部署
    3. 清算概述
    4. Compound学习

    相关文章

      网友评论

          本文标题:Defi部署教程 Compound部署 搭建去中心化借贷银行

          本文链接:https://www.haomeiwen.com/subject/wxrssrtx.html