价格预言机的使用总结(一):Chainlink篇

文章首发于公众号: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 链,目前支持了 SolanaTerra 。不过,我对非 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 ,像下面这样:

1.png

可以看到,每个 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)
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );

  function latestRoundData()
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );
}

就 5 个查询方法而已,简单介绍下这几个方法:

  • decimals() :返回的价格数据的精度位数,一般为 8 或 18
  • description() :一般为交易对名称,比如 ETH / USD
  • version() :主要用来标识 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 读取价格数据,如下图:

2.png

官方给的使用示例代码则如下:

// 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_4217
  address public constant USD = address(840);
  address public constant GBP = address(826);
  address public constant EUR = address(978);
  
  // ... other fiat currencies
}

FeedRegistryInterface 则定义了不少函数,包括和 AggregatorV3Interface 一样的几个函数,只是每个函数相比 AggregatorV3Interface 多了两个参数:basequote如下:

interface FeedRegistryInterface {
 // V3 AggregatorV3Interface
 function 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)
   external
   view
   returns (
     uint80 roundId,
     int256 answer,
     uint256 startedAt,
     uint256 updatedAt,
     uint80 answeredInRound
   );
 
 function getRoundData(address base, address quote, uint80 _roundId)
   external
   view
   returns (
     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.ETH,quote 为 Denominations.BTC。

另外,也可以通过 getFeed(address base, address quote) 直接读取到 priceFeed。

可以发现,使用 Feed Registry 的方式,主要都是用 base/quote 的方式进行查询。

FeedRegistry 里的每个 priceFeed 则是通过先后调用 proposeFeed()confirmFeed() 两个函数设置的,不过这两个函数只有 FeedRegistry 的 owner 才可以调用。

喂价机制

至此,我们已经知道如何接入 Chainlink Data Feeds 来获取价格信息了,但还不够,我们还要了解背后的喂价机制,也要了解价格数据多久更新一次的,如此才能更好地判定 Chainlink 的价格预言机是否能满足具体的场景需求。

首先,Price Feed 的价格是通过多个层级的数据聚合得到的。实际上有三个数据聚合层:数据源聚合、节点运营商聚合、预言机网络聚合

3.png

最原始的价格数据主要来源于币安、火币、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 的这两个参数的值可能会不一样。

4.png

比如,ETH/USD 的偏差阈值为 0.5%,即表示新一轮的价格点跟上一次更新的价格偏差超过 0.5% 的时候才会更新链上价格;而心跳阈值为 3600 秒,即表示上一次价格更新后过了 1 小时后才会更新链上价格。另外,因为每一轮的数据聚合都不是实时的,也需要时间,再加上偏差阈值的限制,所以,有时候,要隔几十分钟才会有价格更新,这点比较关键,需要清楚。有些 Price Feed 的偏差阈值比较大,会高达十几个小时才会有价格更新,比如下面这个:

5.png

可看到,其偏差阈值高达 5%,且已经长达 11 个小时没有价格更新了,而它的心跳阈值其实也比较高,长达 24 小时。

高达 5% 的偏差阈值,且这么长时间都没有更新价格,这如果是应用到一些高杠杆的交易产品,可能就不太合适了。

总结

总而言之,Chainlink 价格预言机接入方便,且安全性还是比较高的,但因为其价格更新机制存在偏差阈值,导致价格更新比较慢,短则几分钟或几十分钟更新一次,长则可能达 24 小时才更新一次,因此,一般只适用于对价格更新不太敏感的应用。这也是 Chainlink 价格预言机的局限性,并无法适用所有场景的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/778547.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vb.netcad二开自学笔记1:万里长征第一步Hello CAD!

已入门的朋友请绕行! 今天开启自学vb.net 开发autocad,网上相关资料太少了、太老了。花钱买课吧,穷!又舍不得,咬牙从小白开始摸索自学吧,虽然注定是踏上了一条艰苦之路,顺便作个自学笔记备忘!积…

网络安全领域国标分类汇总大全V1.0版:共计425份标准文档,全部可免费下载

《网络安全法》、《数据安全法》、《个人信息保护法》落地实施需要大量国家标准的支撑,博主耗时三周时间,吐血整理了从1999年至今相关的所有涉及安全的国家标准,梳理出《网络安全领域国标分类汇总大全V1.0版》,共计 425 项现行标准…

深度解析 Raft 分布式一致性协议

本文参考转载至:浅谈 Raft 分布式一致性协议|图解 Raft - 白泽来了 - 博客园 (cnblogs.com) 深度解析 Raft 分布式一致性协议 - 掘金 (juejin.cn) raft-zh_cn/raft-zh_cn.md at master maemual/raft-zh_cn (github.com) 本篇文章将模拟一个KV数据读写服…

ShardingSphere实战

ShardingSphere实战 文章目录 ShardingSphere实战分库分表实战建表建表sql利用存储过程建表Sharding-jdbc分库分表配置 基于业务的Sharding-key考虑订单id用户id分片策略订单id的设计与实现**设计思想**:设计思路: 具体分片策略实现测试数据插入商户商品…

【pyqt-实训训练】串口助手

串口助手 前言一、ui设计二、ui的控件命名三、ui转py使用类的方法【扩展】使用ui文件导入!P7的小错误解决办法 总结 前言 我的惯例就是万物之始,拜见吾师🥰⇨pyqt串口合集 最开始的时候我想的是,学了那么久的pyqt,我…

进程的控制-孤儿进程和僵尸进程

孤儿进程 : 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程( 进程号为 1) 所收养,并由 init 进程对它们完成状态收集工作 为了释放子进程的占用的系统资源: …

VS code修改底部的行号的状态栏颜色

VSCode截图 相信很多小伙伴被底部的蓝色状态栏困扰很久了 处理的方式有两种: 1、隐藏状态栏 2、修改其背景颜色 第一种方法大伙都会,今天就使用第二种方法。 1、点击齿轮进入setting 2、我现在用的新版本,设置不是以前那种json格式展示&…

ubuntu系统盘扩容

目录 1 介绍 2 步骤 2.1 关闭虚拟机 2.2 编辑虚拟机设置 2.3 设置扩展大小 2.4 打开虚拟机 2.5 找到磁盘管理 2.6 扩展 1 介绍 本部分主要记述怎么给ubuntu系统盘扩展存储容量,整个过程相对简单,扩容方式轻松、容易。 2 步骤 2.1 关闭虚拟机 2…

尚庭公寓——数据库设计

1. 数据的关系 一对一,一对多(多对一),多对多 2. 实体关系模型 实体关系模型常用ER图(enity relationship graph)表示; 矩形表示实体(类似Java中的对象,如学生就是一…

C++基础(八):类和对象 (下)

经过前面的学习,我们已经翻过了两座大山,类和对象入门知识就剩下这一讲了,加油吧,少年! 目录 一、再谈构造函数 1.1 构造函数体赋值 1.2 初始化列表(理解) 1.3 explicit关键字(C…

51单片机STC89C52RC——15.1 AD/DA(模数数模)

目的/效果 1 LCD1602 显示 可调电阻、光敏电阻、热敏电阻值(AD) 2 模拟信号控制LED明暗(DA) 一,STC单片机模块 二,AD/DA 2.1 AD/DA 介绍 AD(Analog to Digital):模拟…

anaconda中下载压缩包并用conda安装包

有时直接conda安装包时会出错;报错PackagesNotFoundError: The following packages are not available from current channels 比如 conda install -y bioconda::ucsc-gtftogenepred #直接安装报错 #直接下载压缩包安装https://blog.csdn.net/weixin_45552562/ar…

Apache Seata 高可用部署实践

本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。 本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。 Apache Seata 高可用部署实践 Seata 高可用部署实践 使用配置中心和数据库来实现 Seata 的高…

Linux|信号

Linux|信号 信号的概念信号处理的三种方式捕捉信号的System Call -- signal 1.产生信号的5种方式2.信号的保存2.1 core 标志位 2.信号的保存2.1 对pending 表 和 block 表操作2.2 阻塞SIGINT信号 并打印pending表例子 捕捉信号sigaction 函数验证当前正在处理某信号&#xff0c…

nginx配置尝试

from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi.responses import JSONResponse, FileResponse, HTMLResponse import logging import os from datetime import datetime import uvicorn# 初始化日志 logging.basicConfig(filenamefile_server.lo…

详解AT_dp_l Deque(区间动态规划)

题目 思路 考虑模拟博弈过程。 题目可以看成:先手希望X - Y最大,后手希望X - Y最小。 显然游戏过程中剩下的数必然是连续的一段。设 dp[i,j]​ 表示剩下下标为 [i,j] 的数时,先手(并非当前的先手而是开始时的先手,下同&#xf…

[数据结构] 基于交换的排序 冒泡排序快速排序

标题:[数据结构] 基于交换的排序 冒泡排序&&快速排序 水墨不写bug (图片来源于网络) 目录 (一)冒泡排序 优化后实现: (二)快速排序 I、实现方法: &#…

中英双语介绍百老汇著名歌剧:《猫》(Cats)和《剧院魅影》(The Phantom of the Opera)

中文版 百老汇著名歌剧 百老汇(Broadway)是世界著名的剧院区,位于美国纽约市曼哈顿。这里汇集了许多著名的音乐剧和歌剧,吸引了全球各地的观众。以下是两部百老汇的经典音乐剧:《猫》和《剧院魅影》的详细介绍。 1.…

C++友元函数和友元类的使用

1.友元介绍 在C++中,友元(friend)是一种机制,允许某个类或函数访问其他类的私有成员。通过友元,可以授予其他类或函数对该类的私有成员的访问权限。友元关系在一些特定的情况下很有用,例如在类之间共享数据或实现特定的功能。 友元可以分为两种类型:类友元和函数友元。…

推荐好玩的工具之OhMyPosh使用

解除禁止脚本 Set-ExecutionPolicy RemoteSigned 下载Oh My Posh winget install oh-my-posh 或者 Install-Module oh-my-posh -Scope AllUsers 下载Git提示 Install-Module posh-git -Scope CurrentUser 或者 Install-Module posh-git -Scope AllUser 下载命令提示 Install-Mo…