
Date: May 28, 2026 on-chain, reported by SlowMist on May 29, 2026
Chain: BNB Chain
Loss: 85,519.47 USDT, about $85.5K
Class: signature check that accepts a blank signature when the admin slot is empty
MoneyMon's LegendaryMoneyMonNft contract on BNB Chain held a balance of MON, the platform's token, and released it through a reward function whose name was misspelled in the source as cliamRewred(). Anyone could call it and name the amount, but the transfer only went through if the call carried a signature from the contract's admin.
In block 100937039, an attacker called cliamRewred() with a signature made of zeroes, took the contract's entire MON balance, and swapped it on PancakeSwap V3 for 85,519.47 USDT. The admin's key was never involved. The contract's admin had been set to the zero address, and the signature check compared that empty address against what a blank signature recovers to, which is also the zero address.
LegendaryMoneyMonNft is MoneyMon's NFT contract, and it also held the MON that players could claim as rewards. The design kept the reward decision off-chain: MoneyMon's backend would sign a claim, and the contract would check that signature before paying. According to SlowMist's incident record, the check only compared the recovered signer against the stored admin address.
That pattern is common and fine when the pieces hold. Every bit of authority in cliamRewred() lived in a single comparison, so the safety of the whole MON balance came down to two values being what the code assumed they were: the recovered signer and the admin.
Contracts check signatures with ecrecover, a built-in function of the EVM that takes a message and a signature and returns the address that signed it. It doesn't revert when the signature is invalid. For malformed input, such as a signature with r=0, s=0 and v=27, it returns the zero address, address(0), which is the EVM's way of saying no one. Libraries like OpenZeppelin's ECDSA wrap ecrecover and reject that zero result, and the DeFiHackLabs proof of concept notes that this contract never did.
On its own, that gap only matters if address(0) can ever win the comparison. The second piece was changeadmin(), which accepted the zero address as a new admin. Once the admin slot held address(0), the check became "does no one equal no one", and a signature of zeroes passed every time.
The sources differ on how the admin got there. DeFiHackLabs says changeadmin() had already been used to set it before the attack, and the exploit transaction fits that account, since its logged events show only the MON leaving the contract and the swap, with nothing touching the admin. SlowMist's record says the attacker set it, and neither source names the transaction that did.
The lesson for builders is that a signature check is only as strong as the empty case. ecrecover answers "no one" for garbage input, so any stored signer that can also be "no one" turns garbage into a valid signature. It's like a door scanner that compares your badge to the name on the authorized list: if the list is blank and a missing badge reads as blank, everyone walks in.
DeFiHackLabs reproduces the attack with no flash loan and no starting capital. The attacker's account at 0xE158...E787 used EIP-7702 to delegate to an attack contract at 0x157b...c1d9, which is a way for an ordinary wallet to run contract code. The proof of concept confirms that the admin reads as address(0), builds the signature with r=0, s=0 and v=27, and calls cliamRewred() for the contract's full MON balance.
The contract recovered address(0) from that signature, found it matched the admin, and transferred 24,306.53 MON to the attacker's account. The attacker then sold it through the PancakeSwap V3 router for 85,519.47 USDT, all in transaction 0x15c8...f4a2 at 14:16:32 UTC on May 28. SlowMist's record describes the result as the contract's tokens drained and swapped for USDT.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it uses the context around the code to infer the rules the system depends on but never writes down. In LegendaryMoneyMonNft, only two pieces of state decide who gets paid, the admin slot and the MON balance, and only two entry points touch them, changeadmin() and cliamRewred(). The state space of even a small contract looks endless, but it isn't, and the engine maps the paths through it and uses signals to choose which ones to attack. Here the signal sits in recoverSigner(): a raw ecrecover result is compared straight against a stored address with no rejection of address(0), on a path that ends in a token transfer, while another function in the same contract can write address(0) into that very slot. That narrows the search to the handful of feasible paths that run through changeadmin() and then into cliamRewred().
The rule MoneyMon's backend relied on fits in one sentence: MON should leave cliamRewred() only when a real, nonzero admin signed for it. The engine attacks a flagged path from two directions. Counterexample-Guided Invariant Refutation takes that rule as its target and executes the paths through the two functions until a sequence breaks it, working outward from what the system is supposed to guarantee, while Bounded Adversarial Verification starts at the unguarded ecrecover comparison and runs known signature-forgery strategies against it. Symbolic execution is the shared machinery underneath both.
Olympix would catch this one by counterexample. The path is short: admin set to address(0) through changeadmin(), then cliamRewred() called with r=0, s=0 and v=27, then the full MON balance sent to the caller. The engine returns that sequence, the minimal conditions that trigger it, and a generated Forge test that replays it, which is the May 28 drain produced in development instead of on BNB Chain.
Because it runs automatically in development, at every merge and before release, the finding shows up whether the zero-address gap arrives with the verifier or with a later change to changeadmin().
ecrecover's habit of answering zero instead of failing is documented and old, and it only becomes dangerous where it meets a stored address that can also be zero. MoneyMon's contract had both halves, written in two separate functions, and an engine that reads them together sees the path between them. If you want to see the paths through your own signature checks mapped and replayed as Forge tests, book a technical walkthrough.
Protocol: MoneyMon (LegendaryMoneyMonNft)
Chain: BNB Chain
Date: May 28, 2026, 14:16:32 UTC
Loss: 24,306.53 MON drained and swapped for 85,519.47 USDT
Attacker EOA: 0xE158...E787
Attack contract (EIP-7702 delegate): 0x157b...c1d9
Vulnerable contract (LegendaryMoneyMonNft): 0x92D6...e5B6
MON token: 0xA1C1...4100
Exploit tx: 0x15c8...f4a2
Block: 100937039
SlowMist Hacked: MoneyMon incident record