{"content":{"title":"如何安全的使用 Chainlink 预言机价格","body":"> 价格数据的可靠性是很多 DeFi 协议可靠运行的基石，Chainlink 作为预言机头部平台，一般来说它提供的价格数据是非常可靠的。但是在我们这个行业从来都不缺二般情况...\r\n\r\n## 0x01 直接从 Chainlink 读取价格数据\r\n官方给的[示例](https://docs.chain.link/data-feeds/using-data-feeds)是非常简单直接的。下面的代码就是原封不动的从官方文档拷贝过来的。\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.7;\r\n\r\nimport \"@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol\";\r\n\r\ncontract PriceConsumerV3 {\r\n    AggregatorV3Interface internal priceFeed;\r\n\r\n    /**\r\n     * Network: Sepolia\r\n     * Aggregator: BTC/USD\r\n     * Address: 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43\r\n     */\r\n    constructor() {\r\n        priceFeed = AggregatorV3Interface(\r\n            0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43\r\n        );\r\n    }\r\n\r\n    /**\r\n     * Returns the latest price.\r\n     */\r\n    function getLatestPrice() public view returns (int) {\r\n        // prettier-ignore\r\n        (\r\n            /* uint80 roundID */,\r\n            int price,\r\n            /*uint startedAt*/,\r\n            /*uint timeStamp*/,\r\n            /*uint80 answeredInRound*/\r\n        ) = priceFeed.latestRoundData();\r\n        return price;\r\n    }\r\n}\r\n```\r\n\r\n这个代码直接通过调用 AggregatorV3Interface 的 latestRoundData 方法来获取价格数据，并且完全忽略掉了 latestRoundData 方法返回的其它数据。\r\n\r\n估计很多在正式环境部署的 DeFi 协议也是这样使用 Chainlink 价格数据的，这样会有什么问题呢？最大的问题就是过于依赖 Chainlink 单一平台，并假定 Chainlink 永远不会有问题。当初币安链上最大的借贷应用 Venus 估计也是这样假设的，于是在上次 Luna 事件中出现了安全事故，我在[该文](https://learnblockchain.cn/article/4061)也做过相应分析。Luna 事件暴露出来的预言机问题 Chainlink 很快就修复掉了，但谁又能保证没有其它 Chainlink 没有完全考虑到的情况呢？Chainlink 那边价格出现的一个小问题，对于依赖的 DeFi 应用可能都会带来灭顶之灾。\r\n\r\n## 0x02 为 Chainlink 预言机加上容错机制\r\n1. 对 Chainlink 返回的数据进行完整性检查\r\nChainlink 返回的数据可不是只有价格，还有与价格相关的上下文信息：\r\n```\r\nfunction latestRoundData() external view\r\n    returns (\r\n        uint80 roundId,\r\n        int256 answer,\r\n        uint256 startedAt,\r\n        uint256 updatedAt,\r\n        uint80 answeredInRound\r\n    )\r\n```\r\n**roundId:** Chainlink 每次更新价格都是一个 round（轮次），round id 会加 1，正常情况下 roundId 是个大于 0 的数字。\r\n**answer:** 价格数据，对于币种价格来说，正常情况下是个大于 0 的数字\r\n**startedAt:** 该轮报价开始时间\r\n**updatedAt:** 该轮报价信息更新时间\r\n**answeredInRound:** 现在已经不推荐使用\r\n我们应该对这些返回值做基本的完整性检查：\r\n```\r\nif (\r\n    roundId != 0 &&\r\n    answer > 0 &&\r\n    updatedAt != 0 && \r\n    updatedAt <= block.timestamp\r\n) {\r\n    // 通过检查\r\n} else {\r\n   // 未通过检查\r\n}\r\n```\r\n\r\n2. 对数据的实效性检查\r\n```\r\nif (block.timestamp - updatedAt <= TIMEOUT) {\r\n    // 通过检查\r\n} else {\r\n   // 未通过检查\r\n}\r\n```\r\nTIMEOUT 是我们根据应用对数据的实时性要求自定义的一个时间，比如 30 分钟\r\n\r\n3. 记下来上次通过检查的报价并计算价格偏差\r\nlastGoodPrice = answer;\r\nif (abs(answer - lastGoodPrice) / lastGoodPrice <= ACCETABLE_DEVIATON) {\r\n   // 通过检查\r\n} else {\r\n   // 未通过检查\r\n}\r\n\r\n4. 如果价格未通过检查，启动容错机制，这里可能需要准备一个备用预言机，当 Chainlink 预言机不可用时，可以切换到备用预言机来读取价格。\r\n对于备用预言机，可以用 Uniswap TWAP，可以用另一个可靠性还可以的第三方预言机，也可以用自建预言机。如果备用预言机也不好使了，可以返回上次报价数据并通知应用预言机处于不可用状态，这样对应用来说，相当于价格停止更新了，可以根据场景做针对性的处理。比如对于借贷应用来说，价格停更一段时间后再重新更新时，对清算操作做一定的缓冲处理，避免预言机价格剧烈波动带来的不合理清算。\r\n\r\n## 0x03 在 L2 上使用 Chainlink 价格\r\n今年开始基于 rollup 的 L2 开始变得非常火爆，L2 极大的提高了以太坊的交易处理能力。但有一点儿我们要注意，就是 L2 排序器当机的概率要远远高于以太坊网络。\r\n\r\n假设我们在 L2 上部署了一个借贷协议，有一天该 L2 排序器出现故障，交易无法处理，预言机价格无法更新。若干小时之后，当排序器重新上线并且预言机更新它们的价格时，停机期间发生的所有价格变动都会立即起作用。如果这些价格变动幅度很大，就可能会造成非常大的混乱。借款人会急于保住头寸，而清算人会急于清算借款人。由于清算主要由机器人处理，借款人可能会面临被大规模清算的风险。\r\n\r\n这对借款人是不公平的，因为如果不是因为 L2 故障，借款人可以有足够的时间来还款或补充抵押物。这时候如果我们的借贷协议能够检测到 L2 故障，并在 L2 恢复后给借款人一个处理借款头寸的缓冲期，就可以避免此类问题了。\r\n\r\nChainlink 专门提供了[相关服务](https://docs.chain.link/data-feeds/l2-sequencer-feeds) 来检测 L2 的状态。\r\n\r\nAAVE V3 也做了[专门处理](https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/configuration/PriceOracleSentinel.sol) 可以作为实现参考。"},"author":{"user":"https://learnblockchain.cn/people/29","address":null},"history":"QmUCbn5pG727VvRq2aYPbi4qk8PXdLnqPnvwUzLjmeZGXA","timestamp":1681819187,"version":1}