Bug
When a single Polygon transaction emits multiple MessageSent events from the OracleChildTunnel (e.g. two OO disputes batched in one transaction), only the first message is ever relayed to the OracleRootTunnel on mainnet. The remaining messages are never received, so those price requests never reach the DVM and the corresponding OO requests stay stuck in Disputed with bonds locked.
Root cause
Relayer._relayMessage builds the exit proof with:
proof = await this.maticPosClient.exitUtil.buildPayloadForExit(
transactionHash,
POLYGON_MESSAGE_SENT_EVENT_SIG,
false
);
matic.js buildPayloadForExit(burnTxHash, logEventSig, isFast, index = 0) proves the index-th log matching the event signature in the transaction, defaulting to the first. The relayer loops over all fetched MessageSent events but never passes an index, so every event in the same transaction produces an identical proof for message #1. The first submission succeeds; subsequent ones revert with FxRootTunnel: EXIT_ALREADY_PROCESSED, which the catch block swallows silently — so the failure is invisible in logs.
Real-world impact (2026-07)
Two Polymarket dispute transactions on Polygon each bridged two disputes atomically; in both cases the second dispute never reached the DVM:
- 0x0874d7b0… — relayed: Will "Blue" be said…; stuck: Will "Honestly" be said…
- 0xc6d74727… — relayed: Map Handicap: paiN (-1.5)…; stuck: Map 3 Rounds Handicap: paiN (-3.5)…
Both stuck messages have since been relayed manually with proofs built at tokenIndex=1 (0x37ba42b6…, 0xeaf7b45c…), confirming both the diagnosis and that recovery is permissionless.
Fix
For each MessageSent event, compute its position among all logs matching the MessageSent signature in its transaction receipt and pass that index to buildPayloadForExit. Also log (debug) instead of silently returning when a relay reverts with EXIT_ALREADY_PROCESSED.
Bug
When a single Polygon transaction emits multiple
MessageSentevents from theOracleChildTunnel(e.g. two OO disputes batched in one transaction), only the first message is ever relayed to theOracleRootTunnelon mainnet. The remaining messages are never received, so those price requests never reach the DVM and the corresponding OO requests stay stuck inDisputedwith bonds locked.Root cause
Relayer._relayMessagebuilds the exit proof with:matic.js
buildPayloadForExit(burnTxHash, logEventSig, isFast, index = 0)proves the index-th log matching the event signature in the transaction, defaulting to the first. The relayer loops over all fetchedMessageSentevents but never passes an index, so every event in the same transaction produces an identical proof for message #1. The first submission succeeds; subsequent ones revert withFxRootTunnel: EXIT_ALREADY_PROCESSED, which the catch block swallows silently — so the failure is invisible in logs.Real-world impact (2026-07)
Two Polymarket dispute transactions on Polygon each bridged two disputes atomically; in both cases the second dispute never reached the DVM:
Both stuck messages have since been relayed manually with proofs built at
tokenIndex=1(0x37ba42b6…, 0xeaf7b45c…), confirming both the diagnosis and that recovery is permissionless.Fix
For each
MessageSentevent, compute its position among all logs matching theMessageSentsignature in its transaction receipt and pass that index tobuildPayloadForExit. Also log (debug) instead of silently returning when a relay reverts withEXIT_ALREADY_PROCESSED.