广义的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:
(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实现过程:
2. 如何防止价格操纵:
从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 string[](1);
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;
}
// 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;
}
// 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);
}
}