Compound 代币和价格预言

本文作者:ripwu[1]

Compound 白皮书和核心代码,大佬已经写了很详细的文档,见

Compound 从白皮书看业务逻辑 [2] Compound 合约部署 [3] 合约升级模式-以 compound 为例 [4]

这里补充下周边:COMP 代币 和 价格预言

COMP

投放计划

为了激励用户,用户每次存款或者借款,Compound 都会奖励 COMP 代币,可以用于治理投票

COMP 每日总产出约为 2312 枚,各市场的分布见 文档 [5],部分市场如下

MarketPer Day

DAI 880.38
Ether 141.25
USDC 880.38
USDT 126.80

每个市场,借款和存款产出的 COMP,分别占 50%

以 USDC 市场为例,每日共产出 880.38 枚 COMP,其中通过借款的方式投放 440.19 枚 COMP,借款用户按其借款额度占总借款额度的比例分配;存款同理

配置

如上所述,根据各市场每日产出的 COMP 数量,按每 15 秒一个区块的假设,可以得到每个区块产出的 COMP 数量,记录在 ComptrollerV6Storage 中

contract ComptrollerV6Storage is ComptrollerV5Storage { // https://compound.finance/governance/comp /// @notice The rate at which comp is distributed to the corresponding borrow market (per block) mapping(address => uint) public compBorrowSpeeds; /// @notice The rate at which comp is distributed to the corresponding supply market (per block) mapping(address => uint) public compSupplySpeeds; }

compBorrowSpeeds 和 comSupplySpeeds 为 cToken 到每区块产出 COMP 数量的映射

比如对 cUSDC 来说,它在两个映射表中的值都为 67000000000000000 (COMP 的精度为 )

存款挖矿

用户每次操作,只要可能更新存款,如存款操作,会触发 mintAllowed(),它进一步

  • 调用 updateCompSupplyIndex() 更新当前市场的 COMP 存款指数

  • 调用 distributeSupplierComp() 分发当前用户此前未结算的存款产出的 COMP

function mintAllowed(address cToken, address minter, uint mintAmount) external returns (uint) { // Keep the flywheel moving updateCompSupplyIndex(cToken); distributeSupplierComp(cToken, minter); return uint(Error.NO_ERROR); }

-- 当前市场的 COMP 存款指数更新逻辑如下

/** * @notice Accrue COMP to the market by updating the supply index * @param cToken The market whose supply index to update * @dev Index is a cumulative sum of the COMP per cToken accrued. */ function updateCompSupplyIndex(address cToken) internal { CompMarketState storage supplyState = compSupplyState[cToken]; uint supplySpeed = compSupplySpeeds[cToken]; uint32 blockNumber = safe32(getBlockNumber(), "block number exceeds 32 bits"); uint deltaBlocks = sub_(uint(blockNumber), uint(supplyState.block)); if (deltaBlocks > 0 && supplySpeed > 0) { uint supplyTokens = CToken(cToken).totalSupply(); uint compAccrued = mul_(deltaBlocks, supplySpeed); Double memory ratio = supplyTokens > 0 ? fraction(compAccrued, supplyTokens) : Double({mantissa: 0}); supplyState.index = safe224(add_(Double({mantissa: supplyState.index}), ratio).mantissa, "new index exceeds 224 bits"); supplyState.block = blockNumber; } else if (deltaBlocks > 0) { supplyState.block = blockNumber; } }

首先判断距离上次更新指数,经过了几个区块 deltaBlocks,另外根据 supplySpeed 判断当前市场是否产出 COMP (0x, Aave 等配置为 0,表示不产出)

条件都满足后,计算 COMP 产出数量,除以 cToken 总供给,得到这几个区块间,平均每个 cToken 对应的 COMP 产出,即代码中的 ratio

也就是说,ratio 可以理解为每持有一个 cToken ,可以得到多少 COMP

最后将 ratio 累加进 COMP 存款指数

-- 当前用户此前未结算的 COMP 分发逻辑如下

/** * @notice Calculate COMP accrued by a supplier and possibly transfer it to them * @param cToken The market in which the supplier is interacting * @param supplier The address of the supplier to distribute COMP to */ function distributeSupplierComp(address cToken, address supplier) internal { // TODO: Don't distribute supplier COMP if the user is not in the supplier market. // This check should be as gas efficient as possible as distributeSupplierComp is called in many places. // - We really don't want to call an external contract as that's quite expensive. CompMarketState storage supplyState = compSupplyState[cToken]; uint supplyIndex = supplyState.index; uint supplierIndex = compSupplierIndex[cToken][supplier]; // Update supplier's index to the current index since we are distributing accrued COMP compSupplierIndex[cToken][supplier] = supplyIndex; if (supplierIndex == 0 && supplyIndex >= compInitialIndex) { // Covers the case where users supplied tokens before the market's supply state index was set. // Rewards the user with COMP accrued from the start of when supplier rewards were first // set for the market. supplierIndex = compInitialIndex; } // Calculate change in the cumulative sum of the COMP per cToken accrued Double memory deltaIndex = Double({mantissa: sub_(supplyIndex, supplierIndex)}); uint supplierTokens = CToken(cToken).balanceOf(supplier); // Calculate COMP accrued: cTokenAmount * accruedPerCToken uint supplierDelta = mul_(supplierTokens, deltaIndex); uint supplierAccrued = add_(compAccrued[supplier], supplierDelta); compAccrued[supplier] = supplierAccrued; emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex); }

首先获取市场最新的 COMP 存款指数,以及用户此前结算时的指数,相减得到 deltaIndex

然后乘以用户持有的 cToken 数量,得到用户这段时间应该获得的 COMP

-- 需要说明的是,这里结算的是用户之前的存款,占当前总供给的百分比,不会算入用户接下来马上将改变的存款

换句话说,存款余额的修改,要在至少一个区块之后才会被用于结算 COMP,即用户操作与 COMP 结算是跨区块的

算是降低了被闪电贷攻击的风险

借款挖矿

与存款挖矿大同小异,稍微复杂一些,这里不再赘述

通胀

根据 messari,COMP 的 Inflation Rate 为 27.50%[6]

我没找到其确切公式,不过我们可以自行计算,根据 2021-11-05 和 2022-11-04 的 流动性投放计划 [7] ,简单相除得到通胀系数为 27.34%;和 messari 数据相比,算是大差不差了

Compound 代币和价格预言

Compound 代币和价格预言

但是,这里有个统计陷阱:Founders & team 分批 vest 且 Future team members 也未兑现,部分流动性没有进入市场,因此分母偏大了

也就是说,实际通胀率还要高出不少

不管怎样,通胀率接近甚至超过 30% 的资产,价格稳定在 300;我看不懂,但我大受震撼~

安全

9 月 29 日 Compound 发生一起安全事件,详见 [事件分析] 9 月 29 日 Compound 62 号提案 所引发的可怕 Bug[8]

其中,Robert Leshner 提到的 Reservori 合约 (地址 [9]),就是上面投放计划中 User (借贷挖矿) 的 COMP 来源

价格预言机

Compound 同时使用 Uniswap v2 和 Chainlink v2 作为价格预言机

Chainlink 价格以 Uniswap 价格为锚,前者作为实际价格,后者作为基准价格

Chainlink 价格需要在 Uniswap 价格的某段浮动范围内,才能作为有效价格被更新到预言机

代码

compound-finance/open-oracle[10] 中只有 Uniswap 相关代码,我找遍 branches 和 tags 都没找到 Chainlink 部分

最后在 Compound 社区找到这个关于添加 Chainlink 预言机提案的精彩讨论 Oracle Infrastructure: Chainlink Proposal[11]

成果是 Chainlink 团队在 Compound 原有 Open Price Feed 的代码基础上,集成了 Chainlink 聚合器的报价,并进一步做了部署和测试;Compound 社区通过治理,应用了新的预言机

然而,Chainlink 提交的 PR:Oracle Improvement (Chainlink Price Feeds) #150[12],改动较多,还卡在审核阶段,未被合并 ..

因此,最新代码不在官方仓库中

审计报告见 Trail of Bits: Chainlink Open-Oracle Summary Report[13]

以下分析基于 Chainklink fork 的仓库 smartcontractkit/open-source[14]

实现

/** * @notice This is called by the reporter whenever a new price is posted on-chain * @dev called by AccessControlledOffchainAggregator * @param currentAnswer the price * @return valid bool */ function validate(uint256/* previousRoundId */, int256 /* previousAnswer */, uint256 /* currentRoundId */, int256 currentAnswer) external override returns (bool valid) { // NOTE: We don't do any access control on msg.sender here. The access control is done in getTokenConfigByReporter, // which will REVERT if an unauthorized address is passed. TokenConfig memory config = getTokenConfigByReporter(msg.sender); uint256 reportedPrice = convertReportedPrice(config, currentAnswer); uint256 anchorPrice = calculateAnchorPriceFromEthPrice(config); PriceData memory priceData = prices[config.symbolHash]; if (priceData.failoverActive) { require(anchorPrice < 2**248, "Anchor price too large"); prices[config.symbolHash].price = uint248(anchorPrice); emit PriceUpdated(config.symbolHash, anchorPrice); } else if (isWithinAnchor(reportedPrice, anchorPrice)) { require(reportedPrice < 2**248, "Reported price too large"); prices[config.symbolHash].price = uint248(reportedPrice); emit PriceUpdated(config.symbolHash, reportedPrice); valid = true; } else { emit PriceGuarded(config.symbolHash, reportedPrice, anchorPrice); } }

核心代码如上所示

validate() 由 Chainlink 调用,参数 currentAnswer 表示 Chainlink 链下统计的价格,单位由 Chainlink 控制

以 DAI 为例,假设 currentAnswer 为 100055330

为了方便处理,convertReportedPrice() 将其转为内部单位,得到 1000553

calculateAnchorPriceFromEthPrice() 通过向交易对询价得到链上 Uniswap 交易所的价格,比如为 1001190

接下来判断 failoverActive,这是由社区投票决定的一项配置,表示当前市场 (DAI) 是否忽略 Chainlink 价格,以 Uniswap 价格为准

否则,通过 isWithAnchor() 确认 Chainlink 价格在 Uniswap 价格浮动范围内 ([85%, 115%])

--``` /** * @notice Calculate the anchor price by fetching price data from the TWAP * @param config TokenConfig * @return anchorPrice uint */ function calculateAnchorPriceFromEthPrice(TokenConfig memory config) internal returns (uint anchorPrice) { uint ethPrice = fetchEthAnchorPrice(); require(config.priceSource == PriceSource.REPORTER, "only reporter prices get posted"); if (config.symbolHash == ethHash) { anchorPrice = ethPrice; } else { anchorPrice = fetchAnchorPrice(config.symbolHash, config, ethPrice); } }

/** * @dev Fetches the current eth/usd price from uniswap, with 6 decimals of precision. * Conversion factor is 1e18 for eth/usdc market, since we decode uniswap price statically with 18 decimals. */ function fetchEthAnchorPrice() internal returns (uint) { return fetchAnchorPrice(ethHash, getTokenConfigBySymbolHash(ethHash), ethBaseUnit); }/** * @dev Fetches the current token/usd price from uniswap, with 6 decimals of precision. * @param conversionFactor 1e18 if seeking the ETH price, and a 6 decimal ETH-USDC price in the case of other assets */ function fetchAnchorPrice(bytes32 symbolHash, TokenConfig memory config, uint conversionFactor) internal virtual returns (uint) { (uint nowCumulativePrice, uint oldCumulativePrice, uint oldTimestamp) = pokeWindowValues(config); // This should be impossible, but better safe than sorry require(block.timestamp > oldTimestamp, "now must come after before"); uint timeElapsed = block.timestamp - oldTimestamp; // Calculate uniswap time-weighted average price // Underflow is a property of the accumulators: https://uniswap.org/audit.html#orgc9b3190 FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(uint224((nowCumulativePrice - oldCumulativePrice) / timeElapsed)); uint rawUniswapPriceMantissa = priceAverage.decode112with18(); uint unscaledPriceMantissa = mul(rawUniswapPriceMantissa, conversionFactor); uint anchorPrice; // Adjust rawUniswapPrice according to the units of the non-ETH asset // In the case of ETH, we would have to scale by 1e6 / USDC_UNITS, but since baseUnit2 is 1e6 (USDC), it cancels // In the case of non-ETH tokens // a. pokeWindowValues already handled uniswap reversed cases, so priceAverage will always be Token/ETH TWAP price. // b. conversionFactor = ETH price * 1e6 // unscaledPriceMantissa = priceAverage(token/ETH TWAP price) * expScale * conversionFactor // so -> // anchorPrice = priceAverage * tokenBaseUnit / ethBaseUnit * ETH_price * 1e6 // = priceAverage * conversionFactor * tokenBaseUnit / ethBaseUnit // = unscaledPriceMantissa / expScale * tokenBaseUnit / ethBaseUnit anchorPrice = mul(unscaledPriceMantissa, config.baseUnit) / ethBaseUnit / expScale; emit AnchorPriceUpdated(symbolHash, anchorPrice, oldTimestamp, block.timestamp); return anchorPrice; }接下来,简单看下 Uniswap 询价逻辑首先通过 `fetchEthAnchorPrice()` 从交易对 USDC-WETH[15] 获得按 USDC 计价 (单位 ) 的 WETH 的价格,比如为 4351156768然后通过 `fetchAnchorPrice()` 从交易对 DAI-WETH[16] 获得按 WETH 计价 (单位 ) 的 DAI 的价格,比如为 230097482692738上面两个价格相乘,得到 1001190219118269813150784最后,转换单位,得到按 USDC 计价的 DAI 价格,即上面的 1001190### 代理`UniswapAnchoredView` 自身可能升级,因此会存在新旧合约实例;升级过程中,我们必须保证两个合约的价格预言同步,且经过一段时间验证后,经由社区投票,用新合约代替旧合约,以此完成升级然而,依据 Chainlink 的设计,聚合器只能向一个合约地址发送喂价为了解决这个问题,在 Chainlink 聚合器与 Compound 之间,引入了一层代理合约 `ValidatorProxy`,它将聚合器的报价同时转发给新旧 `UniswapAnchoredView` 合约由于采用的是 报价 (push) 而非 询价 (pull) 的方式,更新价格的成本由 Chainlink 承担,因此 Compound 用户无须额外支付代理层带来的 gas审计报告见 Sigma Prime: Chainlink ValidatorProxy Security Assessment Report[17]代码在另一个仓库中 : smartcontractkit/chainlink[18]function validate( uint256 previousRoundId, int256 previousAnswer, uint256 currentRoundId, int256 currentAnswer ) external override returns (bool) { // Send the validate call to the current validator ValidatorConfiguration memory currentValidator = s_currentValidator; address currentValidatorAddress = address(currentValidator.target); require(currentValidatorAddress != address(0), "No validator set"); currentValidatorAddress.call( abi.encodeWithSelector( AggregatorValidatorInterface.validate.selector, previousRoundId, previousAnswer, currentRoundId, currentAnswer ) ); // If there is a new proposed validator, send the validate call to that validator also if (currentValidator.hasNewProposal) { address(s_proposedValidator).call( abi.encodeWithSelector( AggregatorValidatorInterface.validate.selector, previousRoundId, previousAnswer, currentRoundId, currentAnswer ) ); } return true; }

```

逻辑非常直白了 ..

参考资料

[1]

ripwu:https://learnblockchain.cn/people/3911

[2]

Compound 从白皮书看业务逻辑 :https://learnblockchain.cn/article/2781

[3]

Compound 合约部署 :https://learnblockchain.cn/article/2915

[4]

合约升级模式-以 compound 为例 :https://learnblockchain.cn/article/2802

[5]

文档 :https://compound.finance/governance/com

[6]

27.50%:https://messari.io/asset/compound/metrics/supply

[7]

流动性投放计划 :https://messari.io/asset/compound/profile/supply-schedule

[8]

[事件分析] 9 月 29 日 Compound 62 号提案 所引发的可怕 Bug:https://github.com/rebase-network/Dapp-Learning/blob/main/defi/Compound/contract/[事件分析] 9月29日 Compound 62号提案 所引发的可怕Bug.md

[9]

地址 :https://etherscan.io/address/0x2775b1c75658Be0F640272CCb8c72ac986009e38

[10]

compound-finance/open-oracle:https://github.com/compound-finance/open-oracle

[11]

Oracle Infrastructure: Chainlink Proposal:https://www.comp.xyz/t/oracle-infrastructure-chainlink-proposal/1272

[12]

Oracle Improvement (Chainlink Price Feeds) #150:https://github.com/compound-finance/open-oracle/pull/150

[13]

Trail of Bits: Chainlink Open-Oracle Summary Report:https://drive.google.com/file/d/1TsOXhBLenStjdd2mxF1Sfmmh_Na9X527/view

[14]

smartcontractkit/open-source:https://github.com/smartcontractkit/open-oracle/blob/master/contracts/Uniswap/UniswapAnchoredView.sol

[15]

USDC-WETH:https://etherscan.io/address/0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc

[16]

DAI-WETH:https://etherscan.io/address/0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11

[17]

Sigma Prime: Chainlink ValidatorProxy Security Assessment Report:https://drive.google.com/file/d/1u12kitAyQKwe3mJVFh5ePzabTmwhjA2Y/view

[18]

smartcontractkit/chainlink:https://github.com/smartcontractkit/chainlink/blob/develop/contracts/hide/v0.8/ValidatorProxy.sol

Compound 代币和价格预言

免责声明:作为区块链信息平台,本站所发布文章仅代表作者个人观点,与链闻 ChainNews 立场无关。文章内的信息、意见等均仅供参考,并非作为或被视为实际投资建议。

原创文章,作者:币圈吴彦祖,如若转载,请注明出处:https://www.kaixuan.pro/news/6268/