广义的Oracle包括链上链下各种繁杂信息的转译和传递,要能实现无障碍的信息交互还是道阻且长,但在细分场景下,对Price Oracle的落地应用已经有较为成熟的解决方案。
Chainlink是去中心化网络,其中的节点将链下数据和信息通过预言机发送给智能合约。Link token用于支付节点提供的数据服务。
1. oracle作用:作为链上链下数据的“翻译机”
2. oracle提供数据服务process:
(1)Requesting Contract发出数据请求
(2)Chainlink协议将请求注册为event,监听event创建SLA Contract(Chainlink Service Level Agreement Contract)
(3)SLA合约生成三个sub-contracts:
Chainlink Reputation Contract:验证oracle provider的历史记录,拒绝历史记录不良Node的服务;
Chainlink Order-Matching Contract:转发Requesting Contract的请求到Chainlink Node,接收Node对请求的报价后选择满足需求的Node响应请求;
Chainlink Aggregating Contract:接收预言机提供的数据,并验证整合后得到一个准确值。
(4)Chainlink Node数据验证的可靠性:
Chainlink Node将请求数据通过“Chainlink Core”软件将链上程序语言翻译为链下服务可理解的编程语言。
通过外部API获取数据,再将数据翻译后发送给Aggregating Contract
Aggregating通过比对Node的数据,验证单个API数据源。重复该过程即可验证多个数据源。
3. Link Token用途
(1)Requesting Contract所有者使用Link支付Chainlink Node运营商提供的服务。
(2)Chainlink Node运营商将Link质押,作为数据真实性的信用凭证。质押量作为Reputation Contract评价的参考指标。Node运营商的不良行为会被惩罚,即被Chainlink协议征从staked Link中收惩罚税。
由于直接获取Dex的价格数据容易遭受三明治攻击等闪电贷攻击,因此,TWAP(Time Weight Average Price)被用于创建有效防止价格操作的链上价格预言机。
1. UniswapV2Pair实现过程:
计算当前区块时间与上一次更新的时间差timeElapsed
更新累加价格和记录本次更新的区块时间
2. 如何防止价格操纵:
每次计算是在每个区块的第一笔交易对价格影响生效前进行
Chainlink Price Oracle实现
从API获取数据,提交上链
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; contract Link is ChainlinkClient { using Chainlink for Chainlink.Request; // 构造函数参数:_links是link token address, _oracle和_specId是Node地址、该预言机下的任务Id constructor(address _link, address _oracle, bytes32 _specId) { setChainlinkToken(_link); setChainlinkOracle(_oracle); specId = _specId; } bytes32 internal specId; bytes32 public currentPrice; event RequestFulfilled( bytes32 indexed requestId, // User-defined ID bytes32 indexed price ); function requestEthereumPrice(string memory _currency, uint256 _payment) public { requestEthereumPriceByCallback(_currency, _payment, address(this)); } function requestEthereumPriceByCallback( string memory _currency, uint256 _payment, address _callback ) public { Chainlink.Request memory req = buildChainlinkRequest(specId, _callback, this.fulfill.selector); req.add("get", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,JPY"); string[] memory path = new stringUnsupported embed; path[0] = _currency; req.addStringArray("path", path); sendChainlinkRequest(req, _payment); } function fulfill(bytes32 _requestId, bytes32 _price) public recordChainlinkFulfillment(_requestId) { emit RequestFulfilled(_requestId, _price); currentPrice = _price; } }从Aggregator合约获取报价
// 设置合约地址 AggregatorInterface internal ref; constructor(address _aggregator) public { ref = AggregatorInterface(_aggregator); } // 调用接口获取报价信息 interface AggregatorInterface { // 最新聚合价格 function latestAnswer() external view returns (int256); // 最新聚合时间戳 function latestTimestamp() external view returns (uint256); // 最新聚合轮次号 function latestRound() external view returns (uint256); // 最新聚合结果 function getAnswer(uint256 roundId) external view returns (int256); // 轮次号对应的时间戳 function getTimestamp(uint256 roundId) external view returns (uint256); event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); event NewRound(uint256 indexed roundId, address indexed startedBy, uint256 startedAt); }
以Compound为例:使用 Chainlink 进行喂价并加入 Uniswap TWAP 进行边界校验,防止价格波动太大时,交易受异常极值影响
询价接口
// 询价接口
function priceInternal(TokenConfig memory config) internal view returns (uint) {
if (config.priceSource == PriceSource.REPORTER) return prices[config.symbolHash];
if (config.priceSource == PriceSource.FIXED_USD) return config.fixedPrice;
if (config.priceSource == PriceSource.FIXED_ETH) {
uint usdPerEth = prices[ethHash];
require(usdPerEth > 0, "ETH price not set, cannot convert to dollars");
return mul(usdPerEth, config.fixedPrice) / ethBaseUnit;
}
}
报价接口
自建报价服务
function putInternal(address source, uint64 timestamp, string memory key, uint64 value) internal returns (string memory) { // Only update if newer than stored, according to source Datum storage prior = data[source][key]; if (timestamp > prior.timestamp && timestamp < block.timestamp + 60 minutes && source != address(0)) { data[source][key] = Datum(timestamp, value); emit Write(source, key, timestamp, value); } else { emit NotWritten(prior.timestamp, timestamp, block.timestamp); } return key; }报价数据源1 - 获取锚点价格(从Dex询价)
// Fetches the current token/usd price from uniswap, with 6 decimals of precision. function fetchAnchorPrice(string memory symbol, 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; anchorPrice = mul(unscaledPriceMantissa, config.baseUnit) / ethBaseUnit / expScale; emit AnchorPriceUpdated(symbol, anchorPrice, oldTimestamp, block.timestamp); return anchorPrice; }报价数据源2-Chainlink喂价
// Each ValidatorProxy is the only valid reporter for the underlying asset price function putInternal(address source, uint64 timestamp, string memory key, uint64 value) internal returns (string memory) { // Only update if newer than stored, according to source Datum storage prior = data[source][key]; if (timestamp > prior.timestamp && timestamp < block.timestamp + 60 minutes && source != address(0)) { data[source][key] = Datum(timestamp, value); emit Write(source, key, timestamp, value); } else { emit NotWritten(prior.timestamp, timestamp, block.timestamp); } return key; }整合报价信息
function postPriceInternal(string memory symbol, uint ethPrice) internal { TokenConfig memory config = getTokenConfigBySymbol(symbol); require(config.priceSource == PriceSource.REPORTER, "only reporter prices get posted"); bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); uint reporterPrice = priceData.getPrice(reporter, symbol); uint anchorPrice; if (symbolHash == ethHash) { anchorPrice = ethPrice; } else { anchorPrice = fetchAnchorPrice(symbol, config, ethPrice); } if (reporterInvalidated) { prices[symbolHash] = anchorPrice; emit PriceUpdated(symbol, anchorPrice); } else if (isWithinAnchor(reporterPrice, anchorPrice)) { prices[symbolHash] = reporterPrice; emit PriceUpdated(symbol, reporterPrice); } else { emit PriceGuarded(symbol, reporterPrice, anchorPrice); } }
