
Date: June 23, 2026
Chain: Polygon
Loss: 261,162.93 USDC net to the attacker, about $261K
Class: transfer hook that counted zero-amount transfers as real ownership
On June 23, 2026, at 16:27:52 UTC, a single Polygon transaction sent a batch of 100 transfers of the same music royalty token, and every one of them was for zero units. Nothing actually changed hands. Royal's legacy Royalties contract still recorded the receiver as owning 100 of those tokens, and a few calls later it paid out 263,808.95 USDC to match.
Royal let fans buy shares of songs' streaming royalties as on-chain tokens, and an old royalties contract tied to that system still held about $261,000 in USDC. The attacker needed only 2,638 USDC of borrowed money to empty it, because the code that tracked who owned what kept its own count of holdings and that count went up on transfers of nothing.
Royal is a music platform co-founded by DJ and producer Justin Blau, known as 3LAU, that sold fractional streaming royalty rights as tokens it called Limited Digital Assets, or LDAs. Each LDA belonged to a tier, and a tier's share of a song's royalties was split among the holders of that tier. Artists including Nas and Diplo released drops on the platform. The LDAs lived on Polygon in a contract called Royal1155LDA, built on ERC1155, a token standard that lets one contract manage many different token types, each with its own balance per holder.
Royalty payouts ran through a separate Royalties contract. When USDC royalties were deposited for a tier, holders could call claim() to collect their pro-rata share, and the Royalties contract worked out that share by asking Royal1155LDA how many tokens of that tier the claimer held, through a function called tierBalanceOf(). Both contracts sit behind proxies, and their implementation code is verified on Polygonscan.
ERC1155 already keeps a balance for every holder and every token ID. Royal1155LDA kept a second set of books on top of it, a custom tier balance, a list of owned tokens and an owner record, all updated inside a transfer hook, the function that runs automatically before every transfer. The Royalties contract paid out from that second set of books.
According to DarkNavy's analysis, the hook looped over the token IDs in a transfer but never looked at the amounts. A transfer of zero units still ran the full custom accounting: it called the Royalties transfer hook, added the ID to the receiver's owned-token list, incremented the receiver's custom balance and overwrote the owner record. CertiK's follow-up pinned the same flaw to Royal1155LDA.beforeLdaTransfer(), and the DeFiHackLabs reproduction describes the condition that made it reachable: with the owned-token backfill still incomplete, a sender holding zero tokens could batch-transfer zero-amount tier-42 LDAs to a fresh receiver and inflate that receiver's tier balance.
That left two ledgers that were supposed to describe the same ownership and could be pushed apart at will. It works like a club that checks guests against its guest list instead of their wristbands, while the door staff add a name to the list every time someone walks up, even if they never come in. Whenever a contract keeps its own copy of a balance that a standard already tracks, every path that updates the copy has to move in lockstep with the real thing, including transfers of zero and the same ID repeated in one batch.
DarkNavy reconstructs the whole attack inside transaction 0x7a92...b42e in block 89,018,051. The attacker's EOA called an attack contract, which routed execution through a helper contract. The helper flash-swapped 2,638.09 USDC from a QuickSwap WMATIC/USDC pair, a Uniswap V2-style pool, borrowing it for the length of the transaction.
Inside the swap callback, the helper called safeBatchTransferFrom() on Royal1155LDA with 100 copies of the same tier-42 LDA ID and 100 amounts of zero. No standard ERC1155 balance moved, but the receiver contract's custom tier-42 balance went from 0 to 100. The helper then approved the Royalties proxy and deposited the borrowed 2,638.09 USDC as a royalty for tier 42.
Tier 42 had a supply of one token, so a genuine holder would have been entitled to the whole deposit. The receiver contract called claim() for tier 42 with a recorded balance of 100, and the Royalties contract paid out 263,808.95 USDC, a hundred times the deposit, drawn from the USDC the contract already held. Its balance fell from 261,170.86 USDC to 0.000037. The helper repaid 2,646.03 USDC to the pair and kept about 261,162.93 USDC.
TenArmor's alert flagged the transaction on X with a loss of about $261,200, and CertiK followed with a $263K figure, the gross payout before the flash swap was repaid, citing the 100 zero-value transfers. Defimon's alert named Royal as the affected project. Cryip reported that the attack stayed isolated to this one contract and that Royal hadn't issued an official statement at the time. As of October 19, 2026, Royal hadn't published a statement on the exploit, recovery or affected holders, and its X account, @join_royal, showed no posts and a bio saying the team is now building Bond.
DarkNavy's recommendations follow directly from the flaw: skip all custom accounting when an amount is zero, require an amount of exactly one for non-fungible LDA transfers, reject or aggregate duplicate IDs in a batch, and have Royalties read canonical ERC1155 balances instead of a mutable mirror. It also recommends repairing the corrupted tier balances and owner records before claims and deposits reopen.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and takes in the surrounding context, here the fact that Royalties pays out on whatever tierBalanceOf() reports, to infer the rules a system relies on but never writes down. The invariant in this case fits in one sentence: a holder's custom tier balance can never exceed the ERC1155 tokens of that tier they actually hold.
The paths through Royal1155LDA's transfer hook look endless once batch sizes, duplicate IDs and amounts vary, but the engine maps them, keeps the feasible ones and narrows to the few that can move a tier balance. It picks those out by signals, patterns in the code that mark a path as dangerous, and the one here is a hook that iterates over token IDs and writes ownership state without ever reading the matching amount. From there the engine attacks the path two ways. Counterexample-Guided Invariant Refutation starts from the tier-balance invariant and executes paths until one breaks it, and Bounded Adversarial Verification starts from the zero-amount signal and throws known attack strategies at it, with symbolic execution underneath both.
Either route ends in a counterexample: a batch transfer of 100 zero-amount entries for one tier-42 ID into a fresh address leaves balanceOf at 0 and tierBalanceOf at 100, and a deposit and claim turn that gap into a payout a hundred times the deposit. Olympix returns that sequence of state changes, the minimal conditions that trigger it, and a generated Forge test that replays it, which is the June 23 drain produced in development instead of on Polygon. Because it runs automatically in development, at every merge and before release, a change to a transfer hook gets checked against the contracts that consume its numbers, which is where two separately reviewed pieces like Royal1155LDA and Royalties meet.
Royal's Royalties contract did exactly what it was told, paying each holder by the balance Royal1155LDA reported, and that balance could be inflated with transfers of nothing. Any system that keeps its own record of ownership beside a token standard needs that record tested against the canonical balance on every path, zero amounts and duplicate IDs included. To see how Olympix tests the accounting your contracts rely on and hands back a runnable counterexample when two ledgers drift apart, book a technical walkthrough.
Protocol: Royal (legacy Royalties system)
Chain: Polygon
Date: June 23, 2026, 16:27:52 UTC
Loss: 261,162.93 USDC net (263,808.95 USDC claimed before flash-swap repayment)
Attacker EOA: 0xbd82...bd56
Attack contract: 0x7fd7...ca52
Helper contract (depositor): 0x11ca...6adb
Receiver contract (claimer): 0xbab4...f684
Royalties proxy (victim): 0xfE16...7EC7
Royalties implementation: 0x1e05...9074
Royal1155LDA proxy: 0x7c88...8075
Royal1155LDA implementation (vulnerable): 0xd5b2...5c78
Flash-swap source (QuickSwap WMATIC/USDC pair): 0x6e7a...4827
Exploit tx: 0x7a92...b42e
Block: 89018051
DarkNavy: Royal Royalties Zero-Amount ERC1155 Accounting Inflation Drain
DeFiHackLabs: RoyalRoyalties exploit proof of concept
TenArmor Alert: Royalties contract exploit alert
CertiK Alert: beforeLdaTransfer() root cause follow-up
Cryip: Old Royalties Contract on Polygon Attacked, $261,200 Lost
CoinDesk: Diplo Joins Nas With NFT Drop on Tokenized Royalties Platform Royal