Hacxyk团队在Aave V3上线三周后发现价格预言机存在漏洞,未经授权的fallback oracle合约允许任何人设置任何资产的价格,可能导致恶意行为者操纵预言机,从而耗尽Aave V3上的资金。该团队联系了Aave团队并及时修复了该问题,并提出了安全审计以及合约配置审核的建议。

4月7日,在 Aave V3 推出3周后,我们发现了 Aave V3 价格预言机上的一个问题。 更具体地说,是回退预言机。 在最初未能联系到 Aave 之后,我们联系了 @samczsun,他帮助通知了 Aave 团队,问题得到了及时解决。 尽管不易被利用,但如果被利用,任何人都可以耗尽 Aave V3 在所有 L2 部署上的资金。 (在撰写本文时价值超过 29 亿美元)。
五家不同的安全公司 审计了 Aave V3,其中包括 OpenZeppelin 和 Trail of Bits 等世界知名公司。 然而,由于 Aave 部署了未经审查的、专为测试设计的合约。 我们将描述该问题,解释为什么它没有在安全审计中被发现,以及如何避免将来出现类似的错误。
为了让借贷协议确定资产的价格,会使用价格预言机来获取链上或链下的价格。 链上预言机遇到了很多问题,这些问题允许价格操纵。 因此,Aave 依赖于链下预言机 Chainlink 来进行价格报告。 这样做更安全,因为价格是从受信任方的各种来源(例如交易所)获取的。
虽然 Chainlink 预言机是安全且经过实战检验的,但协议仍然需要将其正确集成到他们的合约中。
以下是 AaveOracle.sol 中感兴趣的代码。
function getAssetPrice(address asset) public view override returns (uint256) {
AggregatorInterface source = assetsSources[asset]; if (asset == BASE_CURRENCY) {
return BASE_CURRENCY_UNIT;
} else if (address(source) == address(0)) {
return _fallbackOracle.getAssetPrice(asset);
} else {
int256 price = source.latestAnswer();
if (price > 0) {
return uint256(price);
} else {
return _fallbackOracle.getAssetPrice(asset);
}
}
}
在正常情况下,价格是通过 AggregatorInterface.latestAnswer 从 Chainlink 获取的。 在以下两种情况下会使用回退预言机:
if (address(source) == address(0))if (price > 0) 的反面
现在,让我们看看回退预言机是什么样的,以 Polygon 上的 Aave V3 为例。
https://polygonscan.com/tx/0xecff0cf908300c0b205b426a2d0d858e0769f6f70db312698365f2569f97e86e

从调用 setFallbackoracle 的交易中,我们知道回退预言机合约设置为 https://polygonscan.com/address/0xaA5890362f36FeaAe91aF248e84e287cE6eCD1A9#code。 源代码经过验证,我们可以了解 PriceOracle 是如何编写的。
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.10;import {IPriceOracle} from '../../interfaces/IPriceOracle.sol';contract PriceOracle is IPriceOracle {
// Map of asset prices (asset => price)
mapping(address => uint256) internal prices; uint256 internal ethPriceUsd; event AssetPriceUpdated(address asset, uint256 price, uint256 timestamp);
event EthPriceUpdated(uint256 price, uint256 timestamp); function getAssetPrice(address asset) external view override returns (uint256) {
return prices[asset];
} function setAssetPrice(address asset, uint256 price) external override {
prices[asset] = price;
emit AssetPriceUpdated(asset, price, block.timestamp);
} function getEthUsdPrice() external view returns (uint256) {
return ethPriceUsd;
} function setEthUsdPrice(uint256 price) external {
ethPriceUsd = price;
emit EthPriceUpdated(price, block.timestamp);
}
}
你应该不用花1分钟就能意识到函数 setAssetPrice 没有任何访问控制。 换句话说,任何人都可以为任何资产设置任何价格。
恶意行为者可以操纵预言机,使其报告某个资产的天文数字价格,存入极少量的该资产,然后继续借用 Aave 上的所有可用资金。
我们提出了两种可能被利用的情况。
从理论上讲,资产的价值几乎不可能为 0 美元。 但是,由于 Aave 使用的是已弃用的函数 latestAnswer 而不是 Trail of Bits 指出的 latestRoundData,如果未达成任何答案,则该函数可能会返回 0。

如果在配置资产的价格信息 之前 将新资产添加为抵押品,则会有一小段时间使用回退预言机,因此会被操纵。
不正确的顺序如下所示。
启用 aToken
设置 aToken 基础资产的价格信息
如果没有使用错误的回退预言机,这不会有问题,因为价格将仅为 0,因此借贷能力也将为 0。我们查看了 Aave 的文档,但找不到添加新资产的标准程序。
Aave V3 的代码库与 Aave V2 非常相似。 事实上,它们的预言机合约几乎相同。

但是,Aave v2 使用的回退预言机具有适当的访问控制。
https://etherscan.io/address/0x5b09e578cfeaa23f1b11127a658855434e4f3e09#code
contract AaveFallbackOracle is Ownable, IPriceOracleGetter {
using SafeMath for uint256; struct Price {
uint64 blockNumber;
uint64 blockTimestamp;
uint128 price;
} event PricesSubmitted(address sybil, address[] assets, uint128[] prices);
event SybilAuthorized(address indexed sybil);
event SybilUnauthorized(address indexed sybil); uint256 public constant PERCENTAGE_BASE = 1e4; mapping(address => Price) private _prices; mapping(address => bool) private _sybils; modifier onlySybil {
_requireWhitelistedSybil(msg.sender);
_;
} function authorizeSybil(address sybil) external onlyOwner {
_sybils[sybil] = true; emit SybilAuthorized(sybil);
} function unauthorizeSybil(address sybil) external onlyOwner {
_sybils[sybil] = false; emit SybilUnauthorized(address sybil);
} function submitPrices(address[] calldata assets, uint128[] calldata prices) external onlySybil {
require(assets.length == prices.length, 'INCONSISTENT_PARAMS_LENGTH');
for (uint256 i = 0; i < assets.length; i++) {
_prices[assets[i]] = Price(uint64(block.number), uint64(block.timestamp), prices[i]);
} emit PricesSubmitted(msg.sender, assets, prices);
} function getAssetPrice(address asset) external view override returns (uint256) {
return uint256(_prices[asset].price);
} function isSybilWhitelisted(address sybil) public view returns (bool) {
return _sybils[sybil];
} function getPricesData(address[] calldata assets) external view returns (Price[] memory) {
Price[] memory result = new Price[](assets.length);
for (uint256 i = 0; i < assets.length; i++) {
result[i] = _prices[assets[i]];
}
return result;
} function filterCandidatePricesByDeviation(
uint256 deviation,
address[] calldata assets,
uint256[] calldata candidatePrices
) external view returns (address[] memory, uint256[] memory) {
require(assets.length == candidatePrices.length, 'INCONSISTENT_PARAMS_LENGTH');
address[] memory filteredAssetsWith0s = new address[](assets.length);
uint256[] memory filteredCandidatesWith0s = new uint256[](assets.length);
uint256 end0sInLists;
for (uint256 i = 0; i < assets.length; i++) {
uint128 currentOraclePrice = _prices[assets[i]].price;
if (
uint256(currentOraclePrice) >
candidatePrices[i].mul(PERCENTAGE_BASE.add(deviation)).div(PERCENTAGE_BASE) ||
uint256(currentOraclePrice) <
candidatePrices[i].mul(PERCENTAGE_BASE.sub(deviation)).div(PERCENTAGE_BASE)
) {
filteredAssetsWith0s[end0sInLists] = assets[i];
filteredCandidatesWith0s[end0sInLists] = candidatePrices[i];
end0sInLists++;
}
}
address[] memory resultAssets = new address[](end0sInLists);
uint256[] memory resultPrices = new uint256[](end0sInLists);
for (uint256 i = 0; i < end0sInLists; i++) {
resultAssets[i] = filteredAssetsWith0s[i];
resultPrices[i] = filteredCandidatesWith0s[i];
} return (resultAssets, resultPrices);
} function _requireWhitelistedSybil(address sybil) internal view {
require(isSybilWhitelisted(sybil), 'INVALID_SYBIL');
}
}
函数 submitPrices 由 onlySybil 修饰符保护。
为了给审计公司辩护,PriceOracle.sol 位于 mocks/oracle/PriceOracle.sol。 这意味着该文件不应部署在生产环境中,而应部署在本地 forks 中以进行测试。 我们可以通过查看测试套件来验证这一点。
it('Get price of asset with no asset source', async () => {
const { aaveOracle, oracle } = testEnv;
const fallbackPrice = oneEther; // Register price on FallbackOracle
expect(await oracle.setAssetPrice(mockToken.address, fallbackPrice)); // Asset has no source
expect(await aaveOracle.getSourceOfAsset(mockToken.address)).to.be.eq(ZERO_ADDRESS); // Returns 0 price
expect(await aaveOracle.getAssetPrice(mockToken.address)).to.be.eq(fallbackPrice);
});
我们怀疑 Aave 通过复制测试套件在生产环境中部署了合约。 我们无法证明这一说法,因为部署脚本不是公开的。
话虽如此,这个问题暴露了安全审计的局限性。 审计通常在合约部署到主网之前完成。 如果问题出在部署脚本中,除非在范围内,否则它们不会被发现。 为了实现最大安全性,还应审计部署过程,最好是在部署之前和之后。
Aave 在收到问题通知后,迅速修复了所有支持链上的问题。 修复涉及禁用回退预言机,方法是将 _fallbackOracle 设置为地址 0x0,如 https://polygonscan.com/tx/0x194abde14ec6baaabbdff3ef7973a70c70e109021859091bc90abe0970fc34d7 所示。
协议应在合约部署 后 审计其合约配置。 此外,任何新合约在上线之前也应经过审计。 这个问题被忽视了一个月的事实表明,一旦执行审计,就没人关心了,正如最近导致 1.82 亿美元损失的 Beanstalk 黑客事件 中也看到的那样。
4月7日 - 我们通过 security@aave.com 联系了 Aave
4月19日 - 在没有收到 Aave 回复后,我们在 Telegram 上联系了 @samczsun
4月19日 - Aave 收到了问题通知
4月20日 - 问题已修复; 赏金待定
7月12日 - 奖励我们 50,000 美元赏金的治理提案获得通过
我们是 Hacxyk,我们对区块链应用程序的智能合约进行非常详尽的分析,以纠正设计问题、代码错误或识别安全漏洞。 我们执行手动分析和自动化测试,以确保你的智能合约应用程序或 DeFi 平台已准备好用于主网。
如果你有兴趣为你的项目获得审计,请告诉我们。
在 audit[at] hacxyk.com 或 Telegram 上的 @hacxyk 找到我们
- 原文链接: medium.com/@hacxyk/aave-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!