关于Price Oracle的原理及实现

广义的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中收惩罚税。

链上预言机(eg. Dex)

由于直接获取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 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;
        }
    
    • 报价数据源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);
            }
        }
    

Reference:

  1. https://www.gemini.com/cryptopedia/what-is-chainlink-and-how-does-it-work
  2. https://soliditydeveloper.com/uniswap-oracle
  3. https://compound.finance/docs/prices
  4. https://learnblockchain.cn/article/1056
Subscribe to Mobius
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.