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