分布式价格预言机
当消费者请求预言机服务时,预言机可能会因为各种各样的原因无法及时响应,从而造成单点故障。因此,ChainLink 采用了分布式价格预言机的设计来为用户提供服务。例如,一个提供 BTC 美元价格的服务,聚合了 31 个价格预言机为用户提供服务。
该聚合器的合约源码可以在 Etherscan 上查看:https://etherscan.io/address/0xae74faa92cb67a95ebcab07358bc222e33a34da7#readContract
其中,通过调用合约中的 transmitters 方法即可查看该聚合器包含的所有链下预言机。
每一个链下预言机可以通过调用 transmit 方法来提供价格数据,以响应聚合器中用户的请求。这些链下预言机是一些 EOA 账户,他们不仅为 BTC/USD 聚合器提供价格数据,还可能为其他聚合器提供价格数据,例如 ETH/USD。
链上合约:
1. 首先,读取当前合约状态,并进行一系列的检查:
2. 这些都通过后,可以进行一些准备工作了:
3. 接下来是使用 ecrecover() 对对每一个签名数据进行验签,校验 hash 值是对 _report 做的 hash。同时还要检查签名者的角色是否是 Signer,且要检查签名的重复性。
4. 最后,检查观察值是否按照顺序排列好。再从排好顺序的观察值中选取中位数 median,并确保 median 不超过上下两个阈值。一切都没问题后,在 s_transmissions 中记录下本次预言机的 answer。此外,还要对 answer 进行校验:
这里经过一系列(中间有 Proxy 合约)的 call 最终调用了 UniswapAnchoredView 合约 (Compound 使用的价格预言机 ) 的 validate 方法:
关键是比较了两边预言机给的价格的偏差是否在一个范围内:
Feed Registry
前面的使用方式虽然已经很简单,但如果需要不同 token 的价格就得对每个 token 执行 setPriceFeed,治理成本其实有点高,对某些场景来说就不太灵活。这时候,就可以考虑使用 Feed Registry 的方式来接入。
Feed Registry 可以简单理解为 PriceFeeds 的聚合器,已经聚合了多个 priceFeed,有了它,使用者就无需自己去设置 priceFeed 了,可直接通过 Feed Registry 读取价格数据,如下图:
喂价机制
首先,Price Feed 的价格是通过多个层级的数据聚合得到的。实际上有三个数据聚合层:数据源聚合、节点运营商聚合、预言机网络聚合。
最原始的价格数据主要来源于币安、火币、Coinbase 等中心化交易平台,以及 Uniswap、Sushi 等去中心化交易平台。存在一些专门做数据聚合的服务商(比如 amberdata、CoinGecko),会从这些交易平台收集原始的价格数据,并对这些数据源进行加工整合,比如根据交易量、流动性和时差等进行加权计算。
这就是第一个层面的聚合,对数据源的聚合。拥有可靠的价格数据源的关键是要有全面的市场覆盖,才能保证一个价格点能代表所有交易环境的精确聚合,而不是单个交易所或少数交易所的价格,以防止数据被人为操纵和出现价格偏差。
第二层则是 Chainlink Node Operators 所做的聚合。每个 Chainlink Node Operator 主要负责运行用于在区块链上获取和广播外部市场数据的 Chainlink 核心软件。Node Operators 会从多个独立的数据聚合服务商获取价格数据,并获取它们之间的中值,剔除掉异常值和 API 停机时间。
最后一层则是整个预言机网络的聚合,其聚合的方式有多种,但最常见的聚合方式是当响应节点数量达到预设值时对数据取中值。比如总共有 31 个节点,预设值为 21 ,即收到了 21 个节点的响应后,就取这些节点的价格数据的中值作为最终的价格。不过,并非每一轮的价格结果都会更新到链上,只有满足两个触发参数之一的时候才会更新:偏差阈值(Deviation Threshold)和心跳阈值(Heartbeat Threshold)。而且,不同 PriceFeed 的这两个参数的值可能会不一样。
总而言之,Chainlink 价格预言机接入方便,且安全性还是比较高的,但因为其价格更新机制存在偏差阈值,导致价格更新比较慢,短则几分钟或几十分钟更新一次,长则可能达 24 小时才更新一次,因此,一般只适用于对价格更新不太敏感的应用。这也是 Chainlink 价格预言机的局限性,并无法适用所有场景的应用。