关于Price Oracle的原理及实现
January 25th, 2022

广义的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.
More from Mobius

Skeleton

Skeleton

Skeleton