本文详细探讨了在集成Chainlink价格预言机时需注意的安全问题,列举了多种潜在漏洞及应对策略,包括过期价格检查、L2序列器状态检查、价格精度处理等。
Chainlink 允许智能合约开发者接收多种链外数据,其中最常用的功能是接收链外随机性和链外定价数据。将你的智能合约与 Chainlink 集成,会带来一系列潜在的安全漏洞,攻击者可以对此加以利用;以下是智能合约开发者和审计人员需要注意的常见漏洞。
· 未检查过时价格
· 重新请求随机性
许多智能合约使用 Chainlink 请求链外定价数据,但一个常见错误是智能合约未检查该数据是否过时。考虑这个来自 Sherlock USSD 审计的 过时定价数据发现:
// @audit 无检查过时价格数据
(, int256 price, , , ) = priceFeedDAIETH.latestRoundData();
return
(wethPriceUSD * 1e18) /
((DAIWethPrice + uint256(price) * 1e10) / 2);
预言机数据馈送出于各种 原因 可能返回过时的定价数据。如果返回的定价数据过时,这段代码将以不反映当前定价的价格执行,从而导致用户和/或协议可能损失资金。智能合约应始终检查 latestRoundData()
返回的 updatedAt
参数,并将其与过时阈值进行比较:
// @audit 已修正以检查过时价格数据
(, int256 price, , uint256 updatedAt, ) = priceFeedDAIETH.latestRoundData();
if (updatedAt < block.timestamp - 60 * 60 /* 1 小时 */) {
revert("过时价格馈送");
}
return
(wethPriceUSD * 1e18) /
((DAIWethPrice + uint256(price) * 1e10) / 2);
过时阈值应对应于预言机价格馈送的心跳。这可以在查阅 Chainlink 的以太坊主网价格馈送列表 时通过勾选“显示更多详情”框找到,该框会显示每个馈送的“心跳”列。对于以太坊主网以外的网络,请确保在该页面上选择所需的 L1/L2,然后再读取数据列。
更多示例:[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
在使用 Chainlink 与 Arbitrum等 L2 链时,智能合约必须 检查 L2 排序器是否停机,以避免新鲜的陈旧定价数据 - Chainlink 的官方文档提供了一个 示例 实现。当审计人员看到需调用 latestRoundData()
的价格代码时,应注意缺少 L2 排序器活动检查,尤其是在要部署在 L2 的项目中。
智能合约通常使用多个预言机价格馈送来追踪多个资产的价格。假设相同的时间间隔心跳可以用作每个馈送的过时检查是一个错误,因为 不同的馈送可能有不同的心跳。考虑这个来自 JOJO 的 Sherlock 审计 的 代码:
function getMarkPrice() external view returns (uint256 price) {
int256 rawPrice;
uint256 updatedAt;
// @audit 第一个馈送
(, rawPrice, , updatedAt, ) = IChainlink(chainlink).latestRoundData();
// @audit 第二个馈送
(, int256 USDCPrice,, uint256 USDCUpdatedAt,) = IChainlink(USDCSource).latestRoundData();
require( // @audit 馈送 #1 使用相同的心跳间隔进行过时检查
block.timestamp - updatedAt <= heartbeatInterval,
"ORACLE_HEARTBEAT_FAILED"
);
// @audit 馈送 #2 使用相同的心跳间隔进行过时检查
require(block.timestamp - USDCUpdatedAt <= heartbeatInterval, "USDC_ORACLE_HEARTBEAT_FAILED");
uint256 tokenPrice = (SafeCast.toUint256(rawPrice) * 1e8) / SafeCast.toUint256(USDCPrice);
return tokenPrice * 1e18 / decimalsCorrection;
}
在此示例中,第一个价格馈送的心跳为 1 小时,而第二个的心跳为 24 小时,因此它们的过时检查需要使用不同的心跳。可以在查阅 Chainlink 的以太坊主网价格馈送列表 时,通过勾选“显示更多详情”框找到适当的心跳,这将显示每个馈送的“心跳”列。对于以太坊主网以外的网络,请确保在该页面上选择所需的 L1/L2,然后再读取数据列。
在选择使用哪些价格预言机时必须小心;使用 不常更新的预言机价格馈送 会导致以不准确的价格进行计算,无法反映资产的真实价值。
Chainlink 预言机目前是最安全的选择,但即便如此,在选择哪个价格馈送时也必须小心; 类似价格馈送可能具有不同的心跳和偏差阈值;心跳越长,偏差阈值越高,预言机价格与真实当前价格的差异就越大。
智能合约开发者应使用并且审计人员应检查使用最低心跳和偏差阈值的价格馈送,以确保预言机报告的价格尽可能接近真实当前价格。
在请求随机性时, 请求确认参数必须大于目标链上的链重组的常见深度,因为链重组会重排区块和交易,这可能会影响返回的随机性。
这可能导致一个赢家变成输家或反之,因为链重组重新排序了随机性请求,导致不同的随机性结果。该参数在继承自 VRFConsumerBaseV2
的合约中找到:
contract VRFv2Consumer is VRFConsumerBaseV2 {
// @audit 请求确认 = 在收到随机性之前确认了多少个块。
// 必须大于目标链上常见的链重组深度。
//
// 例如 Polygon 每天有 5 个以上区块重组,深度 > 3 个区块
// 并且频繁的重组深度 < 30 个区块
//
// 当请求随机性的交易被移动到不同区块时
// 返回的随机性可以发生变化
// 这意味着通过返回的随机性确定的赢家也可以变化!
uint16 internal constant REQUEST_CONFIRMATIONS = 3;
这个参数通常值为 3
,因为这是官方 Chainlink 教程 中的默认值,因此开发者往往没有考虑就直接复制了。智能合约开发者和审计人员应确认 REQUEST_CONFIRMATIONS 的值是否适合将在其上部署的目标链。如果智能合约将在多个链上部署,则每个部署可能需要不同的 REQUEST_CONFIRMATIONS 值。
更多示例:[ 1]
在处理预言机价格馈送时,开发者必须考虑 不同价格馈送具有不同的小数精度;假设每个价格馈送都以相同的精度报告价格是一个错误。一般来说, 非 ETH 货对以 8 位小数报价,而 ETH 货对以 18 位小数报价。
如果假设精度,就很容易在开发者中犯错误,因为例如, ETH/USD 使用 8 位小数报告,因为它被视为非 ETH 货对,因而其报价用 USD 报告。还有一些报价例如 AMPL/USD 使用 18 位小数进行报价,这违背了 USD 报价以 8 位小数报告的常规规则。
智能合约可以调用 AggregatorV3Interface.decimals() 来获取被调用价格馈送的确切小数位数。
更多示例:[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
一些项目将预言机价格馈送地址硬编码。其他的则会在部署脚本中设置在合约部署时。无论地址位于何处,审计人员应检查它们是否指向正确的预言机价格馈送。请检查这段来自 Sherlock USSD 竞赛的 代码:
// @audit 此处地址正确,但构造函数中的地址错误
// chainlink btc/usd priceFeed 0xf4030086522a5beea4988f8ca5b36dbc97bee88c;
contract StableOracleWBTC is IStableOracle {
AggregatorV3Interface priceFeed;
constructor() {
priceFeed = AggregatorV3Interface(
// @audit 错误的地址;这是 ETH/USD 而不是 BTC/USD!
0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
);
}
在这里,BTC/USD 价格馈送的正确地址出现在注释中,但在构造函数中,错误地显示了 ETH/USD 价格馈送的地址。审计人员应通过查阅 Chainlink 的以太坊主网价格馈送列表 验证价格馈送地址是否正确。对于部署在 L2 或替代 L1 的项目,审计人员应验证这些网络上使用的价格馈送地址是否正确。
更多示例:[ 1]
一些允许用户存入抵押品并根据其价格来铸造/销毁稳定币的协议可能会面临被对协议的价值提取的风险,因为它们的 预言机更新被三明治攻击。
预言机价格更新可能因仅在价格变化设定的偏差%后进行更新而滞后于真实价格,加上攻击者可能会在内存池(mempool)中看到预言机更新并对其进行前置。这是一个复杂问题;可能的解决方案包括:
有关更多信息,请查看 Angle的研究系列 和 Synthetix 与前置攻击的历史。
对 预言机的调用可能会回退,这可能导致完全的拒绝服务,影响依赖于它们的智能合约。Chainlink 多签名可以随时阻止对价格馈送的访问,因此仅仅因为某个价格馈送今天有效,并不意味着它会无限期继续有效。智能合约应通过以下方式处理这一问题:
如果已配置的预言机馈送发生故障或停止工作,但智能合约没有任何替代数据源,且合约不允许更新数据源,则该合约将永久被冻结。
这一点对存款/借贷平台和稳定币协议来说尤其糟糕,因为大量用户的价值以抵押品的形式存储,如果由于对价格预言机的调用出现回退,用户将无法提取这些价值。
考虑一个借贷协议,在其中:
如果 WBTC 跨链桥被攻破,导致 WBTC 从 BTC 失去挂钩,则该协议将继续使用 BTC/USD 的汇率为 WBTC 定价,即使 WBTC 的价值因桥的破坏而瞬间变得远低于原始 BTC。
用户则可以以远低于原始 BTC 的价格购买 WBTC,将其存入协议,并使用原始 BTC 的价值进行借贷。这将允许攻击者在跨链桥破裂导致失去挂钩事件时抽走协议中的资产。
为此,协议可以使用 Chainlink 的 WBTC/BTC 价格馈送 来监控失去挂钩事件。
更多示例:[ 1]
Chainlink 价格馈送有内置的最低和最高价格;如果在闪崩、桥的破裂或失去挂钩事件期间,资产的价值跌破价格馈送的最低价格,预言机价格馈送将继续报告 (现在不正确的) 最低价格。
攻击者可以:
这种攻击将让攻击者从借贷平台中抽取价值。为了帮助减轻这种攻击,智能合约可以检查 minAnswer < receivedAnswer < maxAnswer。
这种攻击也可以通过链下监控来潜在减轻,链下监控可以将 Chainlink 最新报告的价格与其他链外源(如中心化交易所和/或汇总多个链外价格源以产生一个指数价格的流动指标)进行比较;如果外部源报告的价格低于 Chainlink 的 minAnswer,链下监控可以禁用智能合约对该资产的价格馈送,迫使所有交易回退。
开发者和审计人员可以通过以下方式找到 Chainlink 的预言机馈送 [minAnswer, maxAnswer] 的值:
市场参与者不应能在 随机性请求 之后下注或输入其他数据,因为攻击者能够通过检查中奖结果,然后使用中奖输入购买门票来前置随机性响应。
更多示例:[ 1]
重新请求随机性 允许 VRF 服务提供者在结果不利于他们时拒绝履行,请求后的重发,然后仅在对他们有利的情况下返回随机性。
- 原文链接: [medium.com/cyfrin/chainl...]()
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!