随着DeFi应用创新在以太坊网络上的加速迭代,各类DeFi协议的自由组合,以及预言机桥接外部数据的加持,逻辑复杂度更高的DeFi应用协议在不断涌现。一方面,对开发者来说,部署一个具有一定复杂度的智能合约项目所要付出的成本开始逐渐攀升,动辄十几个到几十个以太坊的费用,价格不菲。另一方面,对于用户而言,使用这些DeFi协议所要付出的gas费用也在逐渐增加。由于以太坊网络本身受限于PoW共识算法(现阶段主流,非以太坊2.0)带来的低TPS,使得交易的手续费变高,但变高是用的人多所导致的市场博弈的结果。

而从另一个积极的角度来看,用的人多正反映着生态的繁荣,以及市场的关注度高,创新在以太坊生态中层出不穷,所以如何保持智能合约应用仍然留存在以太坊生态的同时还能降低手续费、提升交易速度等体验,正是Layer 2网络想要探讨的课题。

在本文中我们将通过实例来演示如何在Layer 2网络xDAIChain上部署智能合约,并通过Chainlink预言机为部署在xDAIChain上的智能合约提供高质量的、防篡改的外部数据,在案例部分我们会展示Chainlink的喂价机制如何在xDAIChain上使用。

xDAIChain简介

xDAIChain是一种以太坊网络的Layer 2实现方案,所采用的实现方式是基于侧链机制,它采用了新的共识算法POSDAO,一种基于dPoS的共识算法模型。xDAIChain上的交易确认时间更快,约为5秒,同时在xDAIChain上的交易手续费更低,500笔交易所要付出的费用约为0.01刀。另外正如它的名称所表示的,在xDAIChain链上的原生代币是XDAI,和主网上的DAI1:1兑换,支付交易费用为稳定币,能够给予用户更好的交易成本预期。

合约调用喂价和链下调用喂价的比较

Matic也是一种的侧链实现,在Chainlink社区的之前文章中有专门讲解如何在Matic网络上使用Chainlink预言机。Matic网络上已经完成了Chainlink的整合,因此可以在Matic网络上链上调用Chainlink合约,包含喂价,随机数生成等功能。当前在xDAI上,Chainlink的整合还未完全公开,因此本文中的示例中,适用于已经部署在主网或者Kovan等测试网上的Chainlink智能合约,并以Kovan测试网作为示例,主网或者其他测试网可以类比迁移。架构如下:

以Chainlink喂价为例,现在Chainlink官方文档上提供了Matic的喂价合约地址,但是xDAI没有,所以能够在xDAIChain上获取链下价格数据所要采用的方式略有不同。我们将通过Node.js脚本取以太坊网络上喂价合约提供的数据,并将其主动更新到xDAIChain上的合约中。

下面我们将进入到教程的实践环节。

xDAI智能合约的部署准备工作

为了后续步骤的推进,我们需要先准备好部署合约的账户,本教程采用的钱包为MetaMask,关于钱包的网络设定,以及代币的获取会依次展开。

但在此之前,需要说明的一个点是,xDAI网络没有测试网络可以使用,所以部署合约也是直接部署到xDAI侧链的主网,所消耗的是具有真实价值的资产。虽然费用不会很高,但基于测试目的,我们可以选择在另一个测试网络POA Sokol网络上执行部署、交互测试。等确认无误后,只需要修改一点配置文件,即可无缝迁移到xDAI测试网络。

另外,xDAIChain上的原生代币为XDAI,POA Sokol上的原生代币为POA。xDAIChain提供的获取原生代币的水龙头地址为:https://xdai-faucet.top/,可以获取0.01XDAI。

但是在行文时,访问此水龙头发现并不能通过这个网站完成XDAI的获取,通过debug发现,访问人数过多导致无法请求,在官方社群中有开发者给出答案说是暂时不可用,因为这是有真实价值的代币。在社群中可以请求管理员给0.01XDAI用于开发测试,告知你的钱包地址即可,或者可以通过购买DAI,并通过资产桥:https://docs.tokenbridge.net/xdai-bridge/about 转换为在xDAIChain上的XDAI。

另注:在社群中询问此问题时,会有一些直接私信你的其他用户,常常顶着社群管理员的头像和名字(社群允许名称和头像与真实管理员相同,且私聊不会显示社群中的tag)会给出一些所谓的通过walletconnect这样的网站,一般会说因为数据库更新导致无法连接,然后给出一个钓鱼网站,需要你输入自己的私钥或者助记词来重新连接,这都是属于骗取私有信息,注意防范。

POA Sokol测试网络信息添加

POA Sokol网络是另一种侧链实现,基于POA共识算法,其中POA共识算法可以看作是对dPoS算法的一种改进,它要求参与竞选的节点提供真实世界的信用信息。

在POA Sokol网络上获取测试代币POA则简单许多。步骤展开如下。

步骤一:设置钱包网络信息

点击MetaMask菜单栏,选择网络,下拉到Custom RPC,点击进入,配置网络信息如下:

关键信息字段如下:

配置完成后,点击保存,此时查看钱包余额:

下面进入测试代币获取步骤。

步骤二:获取测试代币

访问测试代币水龙头地址https://faucet-sokol.herokuapp.com/并输入自己的钱包地址:

查看钱包地址余额,有两种方式:

显示余额为:100 POA

总的来说这个步骤和在Kovan测试网上获取代币流程几乎一致,需要稍加注意的是设定钱包的RPC信息有所不同。

xDAIChain网络信息补充

xDAIChain的钱包中配置网络RPC信息如下:

钱包准备就绪后,开始执行项目创建和部署阶段,我们将基于truffle框架来演示此过程,此框架对于工程合约项目开发来说必不可少。

在xDAI智能合约中使用PriceFeed

注:本部分的项目工程文件托管在Github仓库,地址为:https://github.com/Bingyy/Use-Chainlink-On-xDAIChain

下面演示的是如何从头开始创建项目并部署。

1. 项目创建

如果机器上没有安装truffle,可以通过命令行执行:

npm install -gtruffle

新建一个项目名:Use-Chainlink-On-xDAIChain

mkdir Use-Chainlink-On-xDAIChain
cd Use-Chainlink-On-xDAIChain

truffle项目初始化:

truffle init

truffle项目结构如下:

这里的client目录非truffle项目所包含,build目录则是编译合约后存储ABI数据的位置。我们主要编写合约,合约部署脚本以及项目配置信息。

2. 编写合约

在contracts文件夹中,新建一个合约:GetETHUSDPrice.sol,内容如下:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract GetETHUSDPrice {
    uint256 eth_usd_price; // 安全起见建议用SafeMath
    function setETHUSDPrice(uint256 _price) external {
    eth_usd_price = _price;
    }
    function getETHUSDPrice() public view returns(uint256) {
    return eth_usd_price;
    }
}

此合约有一个状态变量,eth_usd_price用于保存从Chainlink喂价获取的真实的价格数据。

3. 合约部署

3.1 配置文件编写

编辑truffle-config.js:

const HDWalletProvider = require('@truffle/hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
const privateKey = fs.readFileSync(".secret").toString().trim();

这里项目脚本依赖@truffle/hdwallet-provider,安装:

npm install @truffle/hdwallet-provider

部署时需要助记词或者私钥签名,在项目顶层目录中建立一个文件,.secret,如果是要使用Github仓库保存项目,注意将此文件名添加到.gitignore文件中,否则会泄露敏感信息,造成损失。

配置网络信息,在networks字段中添加:

sokol: {
    provider: () => new HDWalletProvider(privateKey, `https://sokol.poa.network`),
    network_id: 77,
    gas: 500000,
    gasPrice: 1000000000
},
xdai: {
    provider: () => new HDWalletProvider(privateKey, "https://dai.poa.network"),
    network_id: 100,
    gas: 500000,
    gasPrice: 1000000000
}

为了方便,这里直接将两个网络信息都配置进来,执行部署的时候指定网络名即可。

3.2 合约部署脚本

在migrations目录下,新建一个脚本,3_get_ethusd_price.js,内容如下:

const GetETHUSDPrice = artifacts.require("GetETHUSDPrice");
    module.exports = function (deployer) {
    deployer.deploy(GetETHUSDPrice);
};

truffle中部署脚本的编号是有意义的,会按照顺序执行,我们也可以执行单个部署脚本,这里的数字编号需要根据当前项目中的合约来安排。

3.3 执行部署命令

truffle migrate -f 3 --to 3 --network xdai --skip-dry-run

参数解释:只执行第三号脚本,同时注意添加--skip-dry-run,如果不添加这个指令,则会部署到xdai-fork网络上,这是一个模拟网络,非真实网络,且在区块浏览器中不可查。也可以在truffle-config中配置字段skipDryRun,这里特别放到命令行是为了强调一下。

部署过程如下:

3_get_ethusd_price.js
=====================
Deploying 'GetETHUSDPrice'
--------------------------
> transaction hash:    0x1ee31f52fcccef0899e31d040233222f51949998046ac9762528089bff2d4980
> Blocks: 2            Seconds: 9
> contract address:    0xd3a86c2b36fD3DA6bbd64423d1a494eed47a2052
> block number:        14137854
> block timestamp:     1611206580
> account:             0x28383b9717ca0468C580dF7b970A4897a0f11202
> balance:             0.009903595
> gas used:            96405(0x17895)
> gas price:           1gwei
> value sent:          0 ETH
> total cost:          0.000096405 ETH
> Saving artifacts
-------------------------------------
> Total cost:         0.000096405 ETH
Summary
=======
> Total deployments:   1
> Final cost:          0.000096405 ETH

这里显示的是ETH为单位,实际上是消耗的POA代币,在POA Sokol上的原生代币是POA,地位相当于ETH在Ethereum网络上。

现在合约部署完成,可以看出部署合约的费用消耗相比于以太坊来说极低。合约部署完成后,我们进入到与合约交互的步骤。

4. 与合约交互

在项目文件夹中建立一个client文件夹,用于存放交互脚本。我们将通过Node.js来编写脚本,并使用web3.js库,安装:

npm install web3

为了与合约交互,我们需要知道合约的ABI和部署的合约地址。

const Web3 = require('web3')
const fs = require('fs');
const HDWalletProvider = require('@truffle/hdwallet-provider');
// 账户相关敏感信息,私钥签名,无0x前缀
const privateKey = fs.readFileSync("../.secret").toString().trim();
// 账户地址:自己的账户地址
const account1 = '0x28383b9717ca0468C580dF7b970A4897a0f11202'

这里需要交互的账户地址,需要有一定的测试代币,本文以部署合约账户为例。

设置RPC:

// 设置sokol网络的RPC
const provider = new HDWalletProvider(privateKey, 'https://sokol.poa.network')
const web3 = new Web3(provider)
// Kovan上的RPC需要从Infura中获取
const provider_kovan = new HDWalletProvider(privateKey, 'https://kovan.infura.io/v3/afc48dd54b2b408aa43e79ce09c5d1f5')
const web3_kovan = new Web3(provider_kovan)

这里我们需要两个web3实例,一个用于POA Sokol网络,另一个应用于Kovan网络。

已部署的合约信息:

// 侧链合约信息,用于存取喂价数据
const GetETHUSDPriceABI = [{ "inputs": [{ "internalType": "uint256", "name": "_price", "type": "uint256" }], "name": "setETHUSDPrice", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "getETHUSDPrice", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }]
const GETETHUSDPriceAddress = "0xd3a86c2b36fD3DA6bbd64423d1a494eed47a2052"

其中ABI数据可以在build/contracts目录下找到对应的合约的ABI数据,另外推荐代码中将其浓缩为一行,可以使用在线转换工具:https://tools.knowledgewalls.com/online-multiline-to-single-line-converter

构建合约实例:

const getETHUSDPriceInstance = new web3.eth.Contract(GetETHUSDPriceABI, GETETHUSDPriceAddress)

现在我们就有了可以交互的合约实例,但在此之前我们需要先了解如何使用在Kovan上的Chainlink的喂价合约,我们无需部署,只需要知道它的ABI和合约地址即可,其中合约地址可以访问:https://docs.chain.link/docs/reference-contracts,在Kovan部分找到合约地址。地址为:0x9326BFA02ADD2366b30bacB125260Af641031331

合约的ABI可以在Kovan区块浏览器获取:

https://kovan.etherscan.io/address/0x9326BFA02ADD2366b30bacB125260Af641031331

为了与之交互,我们也需要得到它的实例,代码如下:

// KOVAN ETHUSD PriceFeed ABI
const ETHUSDPriceFeedABI = [{ "inputs": [{ "internalType": "address", "name": "_aggregator", "type": "address" }, { "internalType": "address", "name": "_accessController", "type": "address" }], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "int256", "name": "current", "type": "int256" }, { "indexed": true, "internalType": "uint256", "name": "roundId", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "updatedAt", "type": "uint256" }], "name": "AnswerUpdated", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "uint256", "name": "roundId", "type": "uint256" }, { "indexed": true, "internalType": "address", "name": "startedBy", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "startedAt", "type": "uint256" }], "name": "NewRound", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }], "name": "OwnershipTransferRequested", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }], "name": "OwnershipTransferred", "type": "event" }, { "inputs": [], "name": "acceptOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "accessController", "outputs": [{ "internalType": "contract AccessControllerInterface", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "aggregator", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "_aggregator", "type": "address" }], "name": "confirmAggregator", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "description", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "_roundId", "type": "uint256" }], "name": "getAnswer", "outputs": [{ "internalType": "int256", "name": "", "type": "int256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint80", "name": "_roundId", "type": "uint80" }], "name": "getRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "_roundId", "type": "uint256" }], "name": "getTimestamp", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "latestAnswer", "outputs": [{ "internalType": "int256", "name": "", "type": "int256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "latestRound", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "latestRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "latestTimestamp", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [{ "internalType": "address payable", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint16", "name": "", "type": "uint16" }], "name": "phaseAggregators", "outputs": [{ "internalType": "contract AggregatorV2V3Interface", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "phaseId", "outputs": [{ "internalType": "uint16", "name": "", "type": "uint16" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "_aggregator", "type": "address" }], "name": "proposeAggregator", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "proposedAggregator", "outputs": [{ "internalType": "contract AggregatorV2V3Interface", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint80", "name": "_roundId", "type": "uint80" }], "name": "proposedGetRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "proposedLatestRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "_accessController", "type": "address" }], "name": "setController", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "_to", "type": "address" }], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "version", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }]
const ETHUSDPriceFeedAddress = "0x9326BFA02ADD2366b30bacB125260Af641031331"

构建喂价合约实例:

const ETHUSDPriceFeedInstance = new web3_kovan.eth.Contract(ETHUSDPriceFeedABI, ETHUSDPriceFeedAddress)

写两个函数,一个用于从我们自己部署的合约获取数据,一个通过从Kovan上读取Chainlink的喂价数据并将其更新到POA Sokol链上。

// 将Kovan上的数据更新到侧链上
async function setETHUSDPriceOnSideChain() {
// step 1: get data
let res = await ETHUSDPriceFeedInstance.methods.latestRoundData().call({
from: account1
})
const ETHUSDPrice = res.answer;
console.log("ETHUSD Price is: ", ETHUSDPrice) // BigNumber
// step 2: set data to sidechain
getETHUSDPriceInstance.methods.setETHUSDPrice(ETHUSDPrice).send({
from: account1
}).on('receipt', receipt => {
console.log('receipt: ', receipt)
})
}
// 从侧链上获取ETHUSD价格信息
async function getETHUSDPriceFromSideChain() {
// 读取此喂价合约
let price = await getETHUSDPriceInstance.methods.getETHUSDPrice().call({
from: account1
})
console.log("latest price: ", price)
}

4. 脚本获取Chainlink喂价数据

调用setETHUSDPriceOnSideChain函数,即可将ETHUSD价格信息数据更新到侧链,通过调用getETHUSDPriceFromSideChain函数即可从侧链上获取价格信息。

latest price:  132972000000

5. xDAIChain智能合约部署和交互

部署只需要执行:

truffle migrate -f 3 --to 3 --network xdai --skip-dry-run

xDAIChain上部署的合约地址为:0xe8713F35044eFf25C6340b6837C777F0d04a8461

与xDAIChain上合约交互需要修改几个信息:

const provider = new HDWalletProvider(privateKey, 'https://dai.poa.network')
const web3 = new Web3(provider)
const GETETHUSDPriceAddress = "0xe8713F35044eFf25C6340b6837C777F0d04a8461"

其他信息不用修改,使用流程相同。

总结

以上就是如何在侧链上使用Chainlink喂价的一种方法,对于已经完成集成Chainlink预言机的Layer 2网络,如Matic,可以直接在合约中调用Chainlink预言机,本文采用的方法是一种链下脚本桥接侧链和主链的一种方式,具体执行时需要注意脚本执行的定时刷新,以及监控脚本的运行状态,如果不更新,会出现价格偏离,从而被其他的套利机器人攻击。

参考工具链接:

JSON转换地址:https://tools.knowledgewalls.com/online-multiline-to-single-line-converter

Github地址:https://github.com/Bingyy/Use-Chainlink-On-xDAIChain

xDAI文档:https://www.xdaichain.com/

xDAI水龙头地址:https://xdai-faucet.top/

POA水龙头地址https://faucet-sokol.herokuapp.com/

如果你是开发者,想要快速为你的应用连接到Chainlink的价格参考数据,访问开发者文档,并接入Discord技术讨论。如果你想要安排一个电话来更深入讨论Matic/Chainlink的集成,请在此处联系。

English channels

Website |  Twitter | Reddit | YouTube| Telegram | Events | GitHub| Price Feeds | DeFi

中文渠道

中文官网 | 知乎 | SegmentFault| CSDN |