October 20, 2026
|
Exploit Postmortem
Cover image for "MOKE Token's $907K Loss and How Olympix Would Have Prevented It"

MOKE Token's $907K Loss and How Olympix Would Have Prevented It

Date: August 2, 2026

Chain: BNB Chain

Loss: about $907K, roughly 1,546 BNB extracted according to BlockSec

Class: price-setting function guarded only by a tx.origin check

MOKE's release contract had a rule for who could set the price that decided how many tokens every vesting user received: the owner, an approved settler, or any caller where msg.sender equals tx.origin. That last clause was meant to let ordinary wallets refresh the price while keeping contracts out. At 20:50:11 UTC on August 2, 2026, a wallet carrying its own contract code passed that test, latched a price it had just pushed down with borrowed BNB, and claimed about 41.7 million MOKE for a release quota worth 24,766 USDT.

Within the same transaction, that MOKE was sold through MOKE's own dividend contract and paid back out as 1,664.6 BNB to 65 addresses lined up for the attack. TenArmor put the loss at about $907.7K. The attack worked because the one function that wrote the settlement price trusted a check that no longer proves what it was written to prove.

Vesting Quotas, a Settled Price and a BNB Dividend

MOKE ran three contracts that matter here. MokeVault took USDT deposits through participate() and turned them into release quota, an entitlement denominated in USDT that unlocks over time. MokeRelease, at 0x684d...02a7, tracked each user's quota and paid it out in MOKE through claim(). MokeLPDividend, at 0x5ae5...a377, collected MOKE and BNB and shared BNB with holders of the MOKE/WBNB liquidity token.

The key number in MokeRelease is settledMokePrice. When a user claims, the contract divides the USDT value they're owed by that price to decide how much MOKE to release from a reserve pool. A lower settled price means more MOKE per dollar of quota. The price itself comes from getMokeUsdtPrice(), which reads the live reserves of two PancakeSwap pools, MOKE/WBNB and WBNB/USDT, and multiplies the two spot prices together.

Spot reserves can be moved by anyone with enough capital for the length of one transaction, so the settled price was the value to protect. MokeRelease protected it with settle(). From the verified MokeRelease source on Sourcify:

    function settle() external override {
        require(
            isSettler[msg.sender] || msg.sender == owner() || msg.sender == tx.origin,
            "Only authorized or EOA"
        );
        require(
            block.timestamp >= lastSettleTime + settleMinInterval,
            "Settle cooldown"
        );
        _trySettle();
        uint256 price = getMokeUsdtPrice();
        if (price > 0) {
            settledMokePrice = price;
            lastSettleTime = block.timestamp;
            emit PriceSettled(price);
        }
    }

The first require admits three kinds of caller. The first two are real permissions, an address on the settler list or the owner. The third, msg.sender == tx.origin, admits anyone whose call wasn't relayed through a contract, the classic way of saying "only a human wallet". The second require is a 10-minute cooldown, since settleMinInterval defaults to 600 seconds. After that, the function reads the live spot price and stores it as the price every claim will use, with no check on how far it moved.

Why a tx.origin Check Stopped Meaning What It Said

The tx.origin test rests on an assumption: if the caller is the account that signed the transaction, there's no contract code standing between the signer and this function, so nobody can bundle a price manipulation into the same call. EIP-7702 removed that assumption. It lets an ordinary account delegate to contract code, and when that account sends a transaction to itself, its delegated code runs with msg.sender and tx.origin both equal to the account. A wallet with code passes the "only a human" test as easily as a person does.

On-chain, that's what happened. At 20:48:56 UTC, the attacker's address 0xe454...cf8a sent a type-4 transaction carrying an EIP-7702 authorization that pointed its account at code deployed at 0xc7fd...6ff7. The exploit transaction 75 seconds later was sent from 0xe454...cf8a to itself, so every call its code made into MokeRelease came from an address that was also the transaction's origin.

The claim() function had one more guard. It compares the live price with settledMokePrice and reverts if they differ by more than maxPriceDeviation, 20% by default. That check assumes the settled price is honest and the live price might not be. When the attacker sets both in the same transaction from the same manipulated reserves, they agree perfectly.

The lesson for builders is narrow and worth keeping: msg.sender == tx.origin no longer tells you the caller has no code. Any function that uses it to keep contracts away from a sensitive write is effectively open to everyone.

Sixty-Six Records and One Settled Price

According to BlockSec's weekly review, the preparation started about ten days earlier, when the attacker deposited USDT through MokeVault's participate() and received about $45,000 of release quota. BlockSec also describes the second weakness the attacker lined up: MokeLPDividend's syncUserLP() records an address's LP balance from whatever it holds at that moment, so the same LP tokens can be recorded at one address, moved, and recorded again at the next.

The delegation transaction at 20:48:56 UTC did exactly that. Its logs show 66 UserLPSynced events on MokeLPDividend, starting with the attacker's own address being recorded at 1,334.18 LP, as the same tokens moved from address to address and were synced at each stop.

The exploit transaction followed in block 113,652,609. BlockSec reports that the attacker swapped flash-loaned BNB into the WBNB/USDT pool, which made BNB look cheaper in USDT and dragged the computed MOKE/USDT price down. MokeRelease's logs then show a PriceSettled event at 0.000594 USDT per MOKE, followed by a Claimed event releasing 41,684,057 MOKE to the attacker's address for 24,766.2 USDT of quota. Four Claimed events in the transaction, for four different addresses, released about 43.86 million MOKE in total.

The MOKE then went to MokeLPDividend, and its distributeDividend() swapped it to BNB. The DividendDistributed event records 1,664.62 BNB spread over a total LP supply of 86,373.55 tokens. Then 65 synced addresses claimed, and the LP balances recorded for them, as logged in their claim-time syncs, add up to 86,373.55, the entire supply. Their 65 DividendClaimed events total 1,664.62 BNB, the whole distribution.

BlockSec puts the attacker's take at 1,546 BNB after the round trip, and TenArmor's alert valued the loss at about $907.7K. In the verified MokeLPDividend source, distributeDividend() swaps whatever MOKE the contract holds into BNB, so the duplicated records had a large pile to collect only because the release contract had just handed out MOKE at a price the attacker set.

After the Drain

TenArmor raised the alert a few hours after the exploit, and BlockSec included the incident in its weekly review with a full breakdown. Crypto Times reported on August 3 that the MOKE team hadn't published a post-mortem and that there was no update on recovering funds. Neither the alerts nor the coverage named the people behind the attacking wallets.

How Olympix Finds a Price Anyone Can Write

Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it takes in the surrounding design, here the vesting model and the role of the settled price, to infer the invariants the system relies on. In MokeRelease, the state that decides who gets paid what is settledMokePrice, and every entry point that writes it is a path to map. There are only a few: setSettledMokePrice() behind onlyOwner, and settle().

The signal Olympix's static analysis surfaces is the access check on settle(): a privileged state write whose only open branch is msg.sender == tx.origin, sitting in a contract where every other price and quota setter is restricted to the owner or authorized settlers. Olympix has flagged this class before in other codebases. That signal narrows the search from every path through the vault, release and dividend contracts to the feasible ones that reach settle(), and then to those that change the price claim() will use in the same transaction.

The invariant in this case is plain: the MOKE a user receives from claim() should match the USDT value of their quota at a price no single caller can set inside one transaction. BugPoCer infers that rule from how claim() converts quota into MOKE and how the price is supposed to be guarded, then runs Counterexample-Guided Invariant Refutation, working outward from the release system and executing paths until one breaks it.

The counterexample it returns is August 2 in its smallest form: an externally owned account with delegated code moves the WBNB/USDT reserves, calls settle() from its own address so the tx.origin check passes, calls claim() while live and settled prices agree, and walks away with far more MOKE than its quota is worth. The minimal conditions are an account with pending quota, enough capital to move one pool's reserves, and an expired cooldown. BugPoCer returns that sequence with a generated Forge test that replays it, which is the exploit produced in development instead of on BNB Chain. Because Olympix runs at every merge and before release, the finding would have surfaced at the merge that added the tx.origin branch to settle(), and the same invariant would be retested each time the price logic or the release contract changed.

A Check That Aged Out

MOKE's settle() was written as if msg.sender == tx.origin still separated people from programs, and EIP-7702 had already made that untrue. For builders, any guard that leans on tx.origin deserves a second look, and any function that writes a price other functions trust should be restricted to roles you control or fed by a source that a single transaction can't move. To see how Olympix traces your contracts' price writes back to every caller that can reach them, book a technical walkthrough.

On-Chain References

Protocol: MOKE (MokeRelease, MokeLPDividend)

Chain: BNB Chain

Date: August 2, 2026, 20:50:11 UTC

Loss: about $907K (TenArmor), 1,546 BNB extracted (BlockSec)

Attacker EOA (EIP-7702 delegated): 0xe454...cf8a

Delegated code: 0xc7fd...6ff7

MokeRelease (vulnerable settle()): 0x684d...02a7

MokeLPDividend: 0x5ae5...a377

Preparation tx (EIP-7702 authorization, LP syncs): 0xc0f1...6154

Exploit tx: 0x0776...6a8f

Block: 113652609

Sources

BlockSec: Weekly Web3 Security Incident Roundup covering MOKE Token

TenArmor: Security alert on the MOKE attack

Sourcify: Verified MokeRelease source

Crypto Times: MOKE Suffers $907K Exploit in Suspected BNB Chain Attack

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.