Skip to main content

Troubleshooting

Errors you'll hit during integration, what they mean, and how to fix them. Sorted by frequency of "we've seen this".

My CreateAssertion was accepted but no AssertionCreated arrived

Symptom: You sent CreateAssertion, factory tx is confirmed, factory's count getter incremented — but your consumer never received the AssertionCreated callback.

Cause: The factory uses SEND_MODE_IGNORE_ERRORS on the TEP-89 round-trip. If the jetton master is unreachable, malformed, or the bind step bounces silently, the spawn completes but the callback never fires.

Fix: Send ReprovideWallet to the factory with the original queryId. It re-asks the master and re-fires the bind. If you don't know the queryId, call factory.pendingCount() and try the most recent ones.

import {Address, beginCell, toNano} from '@ton/core';

const factory = Address.parse('kQC3VuD4cMSbUXmZE1REqO2Khc2B7oxNd2PYHaWsww_ylXVX');

// Send to factory:
const body = beginCell()
.storeUint(0x0a08, 32) // OP_REPROVIDE_WALLET
.storeUint(0n, 64) // queryId (try sequential from pendingCount())
.storeUint(YOUR_QUERY_ID, 64)
.endCell();
// value: toNano('0.2')

Propose silently refunded — status still Open

Symptom: You sent a jetton transfer with OP_PROPOSE_TRUE, the notification arrived at the assertion (you can see it in the trace), but the assertion's status getter still returns STATUS_OPEN.

Causes (most common first):

  1. Underfunded — your transfer amount was less than bondAmount + protocolFee. The assertion refunds and stays Open.
  2. Wrong status — the assertion is already Proposed or beyond. The contract refunds and ignores.
  3. Wrong jetton wallet — your wallet sent from a non-bound wallet. The notification gets rejected before any business logic.

Fix: Check the assertion's monetization ref-cell for the exact protocolFee (you don't need to assume it's 2 USDT). The required amount is bondAmount + protocolFee. Re-send.

// Get the factory's current monetization (it's what spawned assertions inherit)
const fee = await factory.protocolFee(); // bigint, in nano-units
const required = bondAmount + fee;

OracleResult was delivered twice — double payout

Symptom: Your consumer credited a winner twice. Looking at traces, you see two OracleResult messages, both with the same questionId.

Cause: You forgot to dedupe by questionId. OracleResult is at-least- once. ReemitResult is permissionless — anyone can pay 0.02 TON to retrigger the callback.

Fix: Add the dedupe pattern. See Patterns → Dedupe.

:::danger This is a vulnerability, not a UX bug A naive consumer can be drained by an attacker who repeatedly calls ReemitResult between your transfers. Always dedupe. Always. :::

Market stuck in M_AWAITING forever

Symptom: Your Market consumer called RequestResolution, status is now M_AWAITING, but no AssertionCreated ever came back. The market is locked.

Cause: The factory's SendIgnoreErrors dropped the inner CreateAssertion hop. Rare, but possible (e.g., malformed factory config, network glitch).

Fix: The reference Market contract has CancelAwaiting (opcode 0x0d09). After AWAITING_TIMEOUT (1 day), the creator can send CancelAwaiting to revert to M_OPEN and try again.

// Wait until awaitingDeadline passes, then:
const body = beginCell().storeUint(0x0d09, 32).storeUint(0n, 64).endCell();
// Send to your Market address with value toNano('0.05')

The contract checks state.oracleAssertion == null — if AssertionCreated did land between your cancel attempt and the contract receiving it, the cancel gets OracleAlreadyBound and the assertion takes priority.

My assertion went Disputed and the resolver never settled

Symptom: Status flipped to STATUS_DISPUTED, the resolver received the OpenVote, but no Resolve ever fired.

Cause: The resolver (committee or DVM) failed to reach quorum, or the committee is offline. Real failure mode for early launch.

Fix: After ARBITRATION_WINDOW (7 days), anyone can call TimeoutRefund (opcode 0x0a0b) on the assertion. Both bonds are refunded; the assertion moves to Cancelled. Your consumer should treat Cancelled as "go back to square one".

const body = beginCell().storeUint(0x0a0b, 32).endCell();
// Send to assertion address with value toNano('0.05')

My consumer was sent OracleResult from a sender I don't recognize

Symptom: Your consumer received an OracleResult message, but in.senderAddress doesn't match any assertion you've bound. You might be under attack.

Cause: Someone is spoofing the opcode. They've deployed a contract with assertionResolvedCallback-shaped output and pointed it at your address.

Fix: Reject every OracleResult whose sender isn't an assertion you explicitly bound. Don't trust the opcode alone:

OracleResult => {
// YES — verify against your bound assertion
assert (in.senderAddress == state.oracleAssertion!) throw Err.NotOurOracle;
// (continue handling)
}

The reference Market contract enforces this. If you copy-pasted the handler, make sure you carried the sender check with it.

SetJettonWallet returns WalletLocked

Symptom: You're trying to call SetJettonWallet on an assertion to fix a bind, but you get exit code 113 (WalletLocked).

Cause: The assertion's status is past STATUS_OPEN. Once anyone has proposed, the wallet is locked.

Fix: If the first bind succeeded, you're done. If it failed (e.g., the TEP-89 hop dropped) and the assertion is still Open, your bind should be accepted by the creator. If it's past Open and the wallet is wrong, the assertion is unrecoverable — let it timeout via TimeoutRefund and spawn a new one.

Why am I seeing exit code 0xFFFF?

Symptom: exitCode: 65535 in the trace, no clear reason.

Cause: This is InvalidMessage — the contract received a message it doesn't know how to parse. Either the opcode was unrecognized, or the body didn't match the expected TL-B layout for any known message.

Fix: Check the body you're sending. Common causes:

  • Sending to the wrong contract (e.g., factory body to an assertion)
  • Stale opcode constant from an old SDK version (use Reference)
  • Forgetting beginCell() and serializing raw bytes

Bridge / wrapped USDT — bond accepted but contract acts weird

Symptom: You used a wrapped-USDT jetton (not the canonical USDT master). The bond intake succeeded but post-resolution refunds don't match expected amounts.

Cause: The jetton's forwardTon semantics might differ. Some wrappers don't honor the forwardTon field, breaking the assertion's gas accounting on the return path.

Fix: Use canonical USDT. If you need to support a wrapper, test it end-to-end first with a small bond. The SDK doesn't validate jetton compatibility.

I'm seeing "cell overflow" when sending messages

Symptom: Your local test framework (or wallet) throws "Cannot send message: cell overflow" when you try to send to a OMY contract.

Cause: Some Acton / @ton/ton wrapper versions struggle to encode coin values above ~10 TON cleanly into the message cell. Known edge.

Fix: Use raw nano values (bigint) instead of the toNano('100') macro:

// Instead of toNano('100'):
const amount = 100_000_000_000n; // 100 TON in nano

Where to ask

If your issue isn't here:

  • Open a GitHub issue on Omy-network/omy
  • Include: opcode you sent, contract address, tx hash, and any error code from the trace
  • For security issues: do not include exploit details publicly — open an issue and we'll move to a private channel.