做网站推广的工作好吗,扬州、常州、扬州、泰州,免费网站站长查询,长沙科技网站设计哪家专业文章首发于公众号#xff1a;Keegan小钢 前言
价格预言机已经成为了 DeFi 中不可获取的基础设施#xff0c;很多 DeFi 应用都需要从价格预言机来获取稳定可信的价格数据#xff0c;包括借贷协议 Compound、AAVE、Liquity #xff0c;也包括衍生品交易所 dYdX、PERP 等等。…文章首发于公众号Keegan小钢 前言
价格预言机已经成为了 DeFi 中不可获取的基础设施很多 DeFi 应用都需要从价格预言机来获取稳定可信的价格数据包括借贷协议 Compound、AAVE、Liquity 也包括衍生品交易所 dYdX、PERP 等等。
目前最主流的价格预言机主要有 Chainlink、UniswapV2、UniswapV3 这几种价格预言机的接入方式和适用场景都不太一样可以单独使用也可以结合使用。鉴于不少同学还不知道这些预言机具体有哪些接入方式也不了解背后的机制更不清楚如何才能做到保证安全性的同时又能以最小的成本接入。下面我将分享下我的经验总结以供参考。
Chainlink
先从 Chainlink 的价格预言机开始聊起这应该是使用最广泛的价格预言机了。
其实Chainlink 提供的产品不只是价格预言机还有其他产品包括 Verifiable Random Numbers (VRF)、Call External APIs、Chainlink Keepers 。当然使用最广泛的还是价格预言机叫 Data Feeds 。
Chainlink Data Feeds 目前已经支持了多条链主要还是 EVM 链包括 Ethereum、BSC、Heco、Avalanche 等也包括 Arbitrum、Optimism、Polygon 等 L2 的链。另外也支持了非 EVM 链目前支持了 Solana 和 Terra 。不过我对非 EVM 链并不熟悉所以只讲 EVM 链的使用。
DeFi 应用接入使用 Chainlink Data Feeds 其实很简单而且还有不同的使用方式下面就来看看最常用的使用方式。
Price Feed
第一种使用方式官方给的示例代码是这样的
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;import chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol;contract PriceConsumerV3 {AggregatorV3Interface internal priceFeed;/*** Network: Kovan* Aggregator: ETH/USD* Address: 0x9326BFA02ADD2366b30bacB125260Af641031331*/constructor() {priceFeed AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);}/*** Returns the latest price*/function getLatestPrice() public view returns (int) {(uint80 roundID, int price,uint startedAt,uint timeStamp,uint80 answeredInRound) priceFeed.latestRoundData();return price;}
}首先每个交易对都有一个单独的 Price Feed 也叫 Aggregator 其实就是一个个 AggregatorProxy 像下面这样 可以看到每个 Pair 都有一个对应的 Proxy读取价格其实就是从 Proxy 提供的方法读取的。Proxy 的具体实现稍微有点复杂但 DeFi 应用要接入的话只要知道 Interface 就够了这个 Interface 则很简单就是示例代码中所引入的 AggregatorV3Interface其代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;interface AggregatorV3Interface {function decimals() external view returns (uint8);function description() external view returns (string memory);function version() external view returns (uint256);// getRoundData and latestRoundData should both raise No data present// if they do not have data to report, instead of returning unset values// which could be misinterpreted as actual reported values.function getRoundData(uint80 _roundId)externalviewreturns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound);function latestRoundData()externalviewreturns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound);
}就 5 个查询方法而已简单介绍下这几个方法
decimals() 返回的价格数据的精度位数一般为 8 或 18description() 一般为交易对名称比如 ETH / USDversion() 主要用来标识 Proxy 所指向的 Aggregator 类型getRoundData(_roundId) 根据 round ID 获取当时的价格数据latestRoundData() 获取最新的价格数据
大部分应用场景可能只需要读取最新价格即调用最后一个方法其返回参数中answer 就是最新价格。
另外大部分应用读取 token 的价格都是统一以 USD 为计价单位的若如此你会发现以 USD 为计价单位的 Pair精度位数都是统一为 8 位的所以一般情况下也无需根据不同 token 处理不同精度的问题。
当然在实际应用中肯定不会只读取一个固定 Token 的价格更多场景是根据 Token 读取该 Token 的 USD 价格因此可以将前面的示例合约升级为如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;import chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol;
import openzeppelin/contracts/access/Ownable.sol;contract PriceConsumerV3 is Ownable {mapping(address AggregatorV3Interface) internal priceFeedMap;function setPriceFeed(address token, address priceFeed) external onlyOwner {priceFeedMap[token] AggregatorV3Interface(priceFeed);}/*** Returns the latest price*/function getLatestPrice(address token) public view returns (int) {(uint80 roundID, int price,uint startedAt,uint timeStamp,uint80 answeredInRound) priceFeedMap[token].latestRoundData();return price;}
}原先的示例只能读取 ETH/USD 一个 Pair 的价格而现在则可以设置和读取多个不同 token 的价格。比如现在想要读取 UNI 的 USD 价格就可以先查出 UNI/USD 的 priceFeed查出其 Proxy 为 0x553303d460EE0afB37EdFf9bE42922D8FF63220e 而 UNI token 地址为 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984那就可以调用
setPriceFeed(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984, 0x553303d460EE0afB37EdFf9bE42922D8FF63220e);如此UNI 所使用的 priceFeed 就设置好了想读取 UNI 的最新价格时调用 getLatestPrice() 就可读取到结果了如下
int price getLatestPrice(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984);另外mapping 所使用的 key 也可以不用 token address改用 token symbol 或其它具有唯一标识性的属性也是可以的。
虽然该示例比较简单很多实际应用中可能比这复杂但基本核心功能是差不多的了。
Feed Registry
第一种接入方式虽然已经很简单但每个 token 都需要 owner 执行 setPriceFeed 治理成本其实有点高对某些场景来说就不太灵活。这时候就可以考虑使用第二种方式来接入 Chainlink Data Feeds 了通过使用 Feed Registry 的方式来接入。
Feed Registry 可以简单理解为 PriceFeeds 的聚合器已经聚合了多个 priceFeed有了它使用者就无需自己去设置 priceFeed 了可直接通过 Feed Registry 读取价格数据如下图 官方给的使用示例代码则如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;import chainlink/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol;
import chainlink/contracts/src/v0.8/Denominations.sol;contract PriceConsumer {FeedRegistryInterface internal registry;/*** Network: Ethereum Kovan* Feed Registry: 0xAa7F6f7f507457a1EE157fE97F6c7DB2BEec5cD0*/constructor(address _registry) {registry FeedRegistryInterface(_registry);}/*** Returns the ETH / USD price*/function getEthUsdPrice() public view returns (int) {(uint80 roundID,int price,uint startedAt,uint timeStamp,uint80 answeredInRound) registry.latestRoundData(Denominations.ETH, Denominations.USD);return price;}/*** Returns the latest price*/function getPrice(address base, address quote) public view returns (int) {(uint80 roundID, int price,uint startedAt,uint timeStamp,uint80 answeredInRound) registry.latestRoundData(base, quote);return price;}
}可看到开头引入了两个 sol 文件FeedRegistryInterface 和 Denominations。Denominations 是一个很简单的 library主要定义了各种货币的地址如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;library Denominations {address public constant ETH 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;address public constant BTC 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB;// Fiat currencies follow https://en.wikipedia.org/wiki/ISO_4217address public constant USD address(840);address public constant GBP address(826);address public constant EUR address(978);// ... other fiat currencies
}FeedRegistryInterface 则定义了不少函数包括和 AggregatorV3Interface 一样的几个函数只是每个函数相比 AggregatorV3Interface 多了两个参数base 和 quote 如下
interface FeedRegistryInterface {// V3 AggregatorV3Interfacefunction decimals(address base, address quote) external view returns (uint8);function description(address base, address quote) external view returns (string memory);function version(address base, address quote) external view returns (uint256);function latestRoundData(address base, address quote)externalviewreturns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound);function getRoundData(address base, address quote, uint80 _roundId)externalviewreturns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound);// ... other functions
}假设交易对为 UNI/USD那 base 为 UNI 的 token 地址quote 则为 USD 的地址即为 Denominations.USD假设交易对为 ETH/BTC那 base 则为 Denominations.ETHquote 为 Denominations.BTC。
另外也可以通过 getFeed(address base, address quote) 直接读取到 priceFeed。
可以发现使用 Feed Registry 的方式主要都是用 base/quote 的方式进行查询。
FeedRegistry 里的每个 priceFeed 则是通过先后调用 proposeFeed() 和 confirmFeed() 两个函数设置的不过这两个函数只有 FeedRegistry 的 owner 才可以调用。
喂价机制
至此我们已经知道如何接入 Chainlink Data Feeds 来获取价格信息了但还不够我们还要了解背后的喂价机制也要了解价格数据多久更新一次的如此才能更好地判定 Chainlink 的价格预言机是否能满足具体的场景需求。
首先Price Feed 的价格是通过多个层级的数据聚合得到的。实际上有三个数据聚合层数据源聚合、节点运营商聚合、预言机网络聚合 。 最原始的价格数据主要来源于币安、火币、Coinbase 等中心化交易平台以及 Uniswap、Sushi 等去中心化交易平台。存在一些专门做数据聚合的服务商比如 amberdata、CoinGecko 会从这些交易平台收集原始的价格数据并对这些数据源进行加工整合比如根据交易量、流动性和时差等进行加权计算。
这就是第一个层面的聚合对数据源的聚合 。拥有可靠的价格数据源的关键是要有全面的市场覆盖才能保证一个价格点能代表所有交易环境的精确聚合而不是单个交易所或少数交易所的价格以防止数据被人为操纵和出现价格偏差。也因此为了确保数据具有高度的防篡改和可靠性Chainlink Data Feeds 只会从优质的数据聚合服务商获取数据这意味着每个数据源都代表一个从所有中心化和去中心化交易所聚合的经过交易量调整的精细价格点也因此可以有效抵抗闪电贷或价格异常偏差等攻击。
第二层则是 Chainlink Node Operators 所做的聚合。每个 Chainlink Node Operator 主要负责运行用于在区块链上获取和广播外部市场数据的 Chainlink 核心软件。Node Operators 会从多个独立的数据聚合服务商获取价格数据并获取它们之间的中值剔除掉异常值和 API 停机时间。比如从 A 数据聚合服务商获取到价格点为 7.0从 B 服务商获取到价格点为 7.2那取中值后的价格点为 7.1。这意味着不仅每个单独的数据源反映了来自所有交易环境的聚合价格点而且每个单独的节点的响应代表了来自多个数据源的聚合进一步防止任何单一来源成为故障点即避免了单点故障。
最后一层则是整个预言机网络的聚合其聚合的方式有多种但最常见的聚合方式是当响应节点数量达到预设值时对数据取中值。比如总共有 31 个节点预设值为 21即收到了 21 个节点的响应后就取这些节点的价格数据的中值作为最终的价格。不过并非每一轮的价格结果都会更新到链上只有满足两个触发参数之一的时候才会更新偏差阈值Deviation Threshold和心跳阈值Heartbeat Threshold 。而且不同 PriceFeed 的这两个参数的值可能会不一样。 比如ETH/USD 的偏差阈值为 0.5%即表示新一轮的价格点跟上一次更新的价格偏差超过 0.5% 的时候才会更新链上价格而心跳阈值为 3600 秒即表示上一次价格更新后过了 1 小时后才会更新链上价格。另外因为每一轮的数据聚合都不是实时的也需要时间再加上偏差阈值的限制所以有时候要隔几十分钟才会有价格更新这点比较关键需要清楚。有些 Price Feed 的偏差阈值比较大会高达十几个小时才会有价格更新比如下面这个 可看到其偏差阈值高达 5%且已经长达 11 个小时没有价格更新了而它的心跳阈值其实也比较高长达 24 小时。
高达 5% 的偏差阈值且这么长时间都没有更新价格这如果是应用到一些高杠杆的交易产品可能就不太合适了。
总结
总而言之Chainlink 价格预言机接入方便且安全性还是比较高的但因为其价格更新机制存在偏差阈值导致价格更新比较慢短则几分钟或几十分钟更新一次长则可能达 24 小时才更新一次因此一般只适用于对价格更新不太敏感的应用。这也是 Chainlink 价格预言机的局限性并无法适用所有场景的应用。