本文環(huán)境
操作系統(tǒng):maxOS 10.15.6
科學上網(wǎng)
公鏈網(wǎng)絡:BSC Testnet
測試工具:Remix IDE笑旺、MetaMask
合約源碼:https://github.com/compound-finance/compound-protocol.git
在 remix 進行編譯部署時,勾選啟用優(yōu)化鸟蟹。
Compound中含有的合約代碼量很大建钥,文件數(shù)量30+熊经,一開始筆者也忍不住打了退堂鼓镐依。然而學習就是一個從難到易的過程槐壳,只有花時間去努力學習务唐,才能慢慢理解它的結構和細節(jié),最終會贊美Compound團隊提供的優(yōu)秀代碼枫笛,優(yōu)秀方案来农!
下圖是網(wǎng)上找的Compound合約結構圖,以饗讀者崇堰。在部署前沃于,先梳理清楚各個合約之間的關系涩咖,并將其分組,梳理出各個模塊檩互,及初始化參數(shù)。
一咨演、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 是邏輯實現(xiàn)合約,通過 delegatecall 來實現(xiàn)遠程合約調用锌畸。
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;
參數(shù) address newPendingImplementation比默,這里設置為 ComptrollerG7.sol 地址第二步: 在 ComptrollerG7.sol 合約調用 _become,
參數(shù) Unitroller unitroller,這里設置為 Unitroller.sol 地址
代理綁定卸耘,第一步轉移所有權退敦,第二步新的 Comptroller 接受所有權,這樣就可以防止意外地升級到無效的合約;
備注:設置完成后對外提供 Comptroller 合約地址時蚣抗, 提供的是 Unitroller 合約地址侈百。
以下步驟,請 unitrollerProxy = ComptrollerG7(address(unitroller));
at Address unitrollerAddr得到unitrollerProxy合約翰铡,名字還是ComptrollerG7
4.2 設置 closeFactor
在 ComptrollerG7.sol 合約調用 _setCloseFactor钝域,
參數(shù) uint newCloseFactorMantissa,這里設置為 50%锭魔,即:0.5 * 1 ^18 = 500000000000000000
4.3 設置 liquidationIncentiveMantissa
在 ComptrollerG7.sol 合約調用 _setLiquidationIncentive例证,
參數(shù) uint newLiquidationIncentiveMantissa,設置流動性激勵為 8%迷捧,參數(shù)值就是1.08 * 1 ^ 18 = 1080000000000000000
4.4 設置 oracle
在 ComptrollerG7.sol 合約調用 _setPriceOracle织咧,
參數(shù) PriceOracle newOracle胀葱,這里設置為 SimplePriceOracle.sol 地址:0x5991199a9aB1801A229a2E49a042471eDE997a21
五、interestRate 模塊
5.1 部署 JumpRateModelV2.sol
部署時參數(shù):
- 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
備注: 部署后, 參數(shù)都可以用 updateJumpRateModel 方法進行修改;
5.2 部署另一個 JumpRateModelV2.sol
如果是測試抵屿,只需要部署一個就可以了,使用erc20的捅位,
因為 cToken 跟 JumpRateModelV2 需要一一對應的關系轧葛,因此再次部署該合約,用于后面分別與 CErc20Delegator.sol 和 CEther.sol 對應.
部署時參數(shù)跟5.1節(jié)相同;
使用 account1 賬號進行部署艇搀,成功:
contract address: 0x0cca4ccD1ED542B5D7F3Ebbcf49D92DCB0a8D04e
六尿扯、CToken 模塊(ERC20)
6.1 部署 ERC20Token.sol
部署一個標準 ERC20 代幣,作為基礎資產用于測試焰雕,
例子:使用 account1 賬號進行部署usdt合約衷笋,成功:
contract address: 0xBEA207ec294BCe7a866C3a598195A61Bb7E8D599
6.2 部署 CErc20Delegate.sol
此合約給支持代理的 cToken 合約使用,不支持代理的 cToken 不需要使用這個合約;
所有 ERC20 基礎資產的 CToken 采用委托代理模式淀散,所以我們先部署一個實現(xiàn)合約:
使用 account1 賬號進行部署右莱,成功:
contract address: 0xc176eD65274b2a2d422126d597Be715fc97d2e98
6.3 部署 CErc20Delegator.sol
此合約即為與代幣類型(ERC20)的標的資產對應的 cToken 合約;
部署時參數(shù):
- address underlying_, erc20標的資產地址蚜锨,見6.1節(jié)
- ComptrollerInterface comptroller_, ComptrollerG7.sol 合約地址档插,見2.2節(jié)
- InterestRateModel interestRateModel_, JumpRateModelV2合約地址,見5.1節(jié)
- 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節(jié)
- bytes memory becomeImplementationData, 額外初始數(shù)據(jù),此處填入0x;即無數(shù)據(jù)
備注:initialExchangeRateMantissa_ = 1 * 10 ^ (18 + underlyingDecimals - cTokenDecimals)
使用 account1 賬號進行部署如捅,成功:
contract address: 0x209C9b6a0Ec37b91d0758514070A8439B14B9B3c
七棍现、CToken 模塊(ETH)
7.1 部署 CEther.sol
此合約即為與主幣類型(ETH)對應的 cToken 合約,
部署時參數(shù):
- ComptrollerInterface comptroller_, unitroller合約地址镜遣,見2.1節(jié)
- InterestRateModel interestRateModel_, JumpRateModelV2合約地址己肮,見5.2節(jié)
- 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
設置保證金系數(shù)
9.1.1 在 CErc20Delegator.sol 調用合約方法 _setReserveFactor:
設置時參數(shù):
- uint newReserveFactorMantissa , 新的保證金系數(shù), 本文 0.1 * 10 ^ 18 = 100000000000000000
9.1.2 在 CEther.sol 調用合約方法 _setReserveFactor:
設置時參數(shù):
- uint newReserveFactorMantissa , 新的保證金系數(shù), 本文 0.2 * 10 ^ 18 = 200000000000000000
9.2 CToken 加入市場
在 ComptrollerG7.sol 調用合約方法 _supportMarket:
設置時參數(shù):
- CToken cToken, CErc20Delegator.sol 或 CEther.sol 地址
本文操作兩次肋乍,將前面部署的 CErc20Delegator.sol 和 CEther.sol 均加入;
使用 account1 賬號進行操作
9.3 設置 CollateralFactor
設置抵押率;
在 ComptrollerG7.sol 調用合約方法 _setCollateralFactor:
設置時參數(shù):
- CToken cToken, CErc20Delegator.sol 地址
- uint newCollateralFactorMantissa, 抵押率,本文使用 0.6 * 10 ^ 18 = 600000000000000000
使用 account1 賬號進行操作,成功
Remix部署完合約以后敷存,如下圖:
十墓造、COMP獎勵
用戶存和借cToken都會有獎勵,如果cToken市場設置了compSpeed锚烦。
compSpeed: 整數(shù)觅闽,表示協(xié)議將COMP分配給市場供應商或借款人的速率。價值是分配給市場的每個區(qū)塊的COMP(單位:wei)涮俄。 請注意蛉拙,并非每個市場都向其參與者分發(fā)了COMP〕骨祝可以設置成0孕锄。速度表明市場供應商或借款人獲得了多少紅利,因此將這個數(shù)字翻一番苞尝,可以顯示市場供應商和借款人獲得的紅利之和畸肆。
//代碼示例實現(xiàn)了讀取每個以太坊區(qū)塊分配到單個市場的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才可以設置
參數(shù)
- cToken 相應的市場cToken地址
- compSpeed 價值是分配給市場的每個區(qū)塊的COMP(單位:wei)
這個方法不執(zhí)行宙址,默認為0轴脐,不分發(fā)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 用戶都會為他們提供給協(xié)議或從協(xié)議中借用的每個區(qū)塊累積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合約注益,現(xiàn)在我們需要寫一部分測試碴巾,看具體的合約邏輯執(zhí)行。在最小可運行的compound合約中丑搔,我們部署了抵押usd厦瓢,以及compound鑄造出來的token:cUSD. 并部署了cUSD實際調用的邏輯cErc20Delegate, 然后cUSD的借貸模型中采用的是JumpRateModelV2,對應的審計合約是comptrollerG7.
下面我們分別就compound中低匙,最核心的用戶交互邏輯來編寫5個測試旷痕,簡單驗證邏輯可行性。
1顽冶、存 mint
用戶向compound中存款的邏輯是:用戶向compound中存入USD代幣, compound根據(jù)當前的匯率算出鑄造的cUSD代幣數(shù)量欺抗,將對應的cUSD代幣轉賬給用戶。
用戶函數(shù):enterMarkets
用戶的地址中對應用戶的所有資產列表强重,當計算一個用戶的所有流動性時绞呈。在借貸一種資產前贸人,一個或者多種資產必須被提供給compound以用作抵押。在借貸發(fā)生前佃声,任何借貸出的資產必須通過這種方式添加進入compound中艺智。該函數(shù)的返回值是一個列表,即該用戶的所有資產列表圾亏。
在 ComptrollerG7.sol 調用合約方法 enterMarkets:
參數(shù):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
2夭问、借 borrow
借幣邏輯是:用戶在compound中有多種cToken資產,記錄在accountAssets中曹铃。然后用戶向compound借出一定量的usd資產缰趋,同時增加用戶的負債額度。compound在接受用戶的借款請求時陕见,首先會檢查cToken有沒有上市秘血,再檢查用戶是否enterMarket,然后根據(jù)現(xiàn)在的預言機報價檢查用戶的賬戶流動性评甜。
//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
3、還 repay
repay操作是borrow的逆操作蜕着,可以通過repayBorrow償還自己的貸款谋竖,repayBorrowBehalf代為償還他人貸款红柱,其具體邏輯是用戶批準cToken合約使用其underlying token承匣,先調用accuralInterest計算目前利率指數(shù)和對全部借貸額計息,然后調用comptroller.repayBorrowAllowed函數(shù)檢查是否可以償還锤悄,最后調用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
4、取 redeem
redeem是mint的逆運算零聚,但在實際邏輯中袍暴,增加了一個檢查賬戶虛擬流動性的一項。用戶可以調用redeem來償還給定數(shù)量的cToken隶症,或者調用redeemUnderlying來償還某數(shù)量的cToken得到給定數(shù)量的underlying Token. redeem操作的步驟是用戶批準cUSD合約使用用戶的cUSD代幣政模,然后調用accuralInterest函數(shù),來計算最新的利率指數(shù)Index蚂会,并對totalBorrows計息淋样。再然后是調用comptroller.redeemAllowed函數(shù),計算用戶的虛擬流動性胁住,看是否用戶有足夠的流動性來取走token趁猴。最后是redeemFresh函數(shù)根據(jù)要取走的數(shù)值刊咳,更新accountBorrow中的數(shù)值和totalBorrows。
5儡司、清算 liquidity
發(fā)生清算的一種典型情況是娱挨,用戶enterMarkets了兩個market,分別是cUni和cUSDT資金池捕犬。然后用戶在cUni池中跷坝,存入Uni獲得一定的cUni。用戶憑借cUni在cUSDT資金池中借貸出USDT碉碉。然而探孝,由于Uni的價格波動,導致Uni/USDT的價格突然下跌誉裆,此時用戶放置在cUni池中的cUni的總價值小于了借出的USDT的價值顿颅,從而觸發(fā)外部清算者進行清算。
清算過程整體分為兩部分:第一部分是repayBorrower部分足丢,代為償還underlying token粱腻,另一部分是seize部分,即將被清算者的cToken及獎勵金一起獎勵給清算者斩跌。由于清算涉及到兩種cToken,故在清算的第一步是分別調用兩種cToken的accural Interest函數(shù)绍些,計算各自最新的利率指數(shù)Index,并計算含息債務總額耀鸦。然后調用comptroller.liquidateBorrowAllowed函數(shù)柬批,計算被清算賬戶的流動性,如果被清算賬戶的流動性為正袖订,則不允許清算氮帐,如果被清算賬戶的流動性為負,并驗算單筆交易的清算量不能超過被清算賬戶的最大可清算量洛姑,則允許清算上沐。具體清算時,要求清算者不能是被清算者自己楞艾,然后計算轉給被清算者的cToken數(shù)量参咙。
在執(zhí)行轉賬cToken到清算者之前,需調用comptroller.seizeAllowed函數(shù)硫眯,作用是驗證調用seize函數(shù)的msg.sender和address(this)的comptroller保持一致蕴侧。然后將清算者的賬戶余額加上seizeTokens,被清算者的余額減去seizeTokens两入。在完成seize部分后净宵,函數(shù)跳轉到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的數(shù)量
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
* redeem 方法將指定數(shù)量的 cToken 轉換為標的資產疏哗,并將其返還給用戶呛讲。收到的標的數(shù)量等于贖回的 cToken 數(shù)量乘以當前匯率。
* 贖回額必須小于用戶的賬戶流動性和市場可用的流動性返奉。
*/
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 將被贖回的標的的資產數(shù)量
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
* redeem underlying 方法將 cToken兌換成指定數(shù)量的標的資產贝搁,并返回給用戶。贖回的 cToken的數(shù)量等于收到的標的數(shù)量除以當前匯率芽偏。
* 贖回額必須小于用戶的賬戶流動性和市場可用的流動性雷逆。
*/
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 方法將協(xié)議中的標的資產轉移給用戶,并創(chuàng)建一個借款余額污尉,根據(jù)該資產的借款利率開始累積利息膀哲。
* 借款額必須小于用戶的賬戶流動性和市場可用的流動性。
*/
function borrow(uint borrowAmount) external returns (uint) {
return borrowInternal(borrowAmount);
}
/**
* @notice Sender repays their own borrow
* @dev Reverts upon any failure
* repay 方法將標的資產轉移到協(xié)議中被碗,并減少用戶的借款余額某宪。
*/
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:
參數(shù):cTokens: [
"0x209C9b6a0Ec37b91d0758514070A8439B14B9B3c", // cUSD 地址
"0xf3feeab27E8B8b71ED92040be19E5aA80baf9B01" // cETH 地址
]
ComptrollerG7(address(unitroller)).enterMarkets(addrs);
參考: