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):
- Underfunded — your transfer amount was less than
bondAmount + protocolFee. The assertion refunds and stays Open. - Wrong status — the assertion is already
Proposedor beyond. The contract refunds and ignores. - 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.