Finite State Machines for Trustless Escrow: How We Built a Decentralized Marketplace on TON

Published: (February 27, 2026 at 11:21 PM EST)
7 min read
Source: Dev.to

Source: Dev.to

Buying a Telegram channel is sketchy. Buying any social‑media asset is sketchy. The seller could take your money and vanish. The buyer could receive the asset and dispute the payment. Every centralized escrow service is a single point of failure — and a single point of trust.

We built ITOhub to solve this with on‑chain escrow, oracle‑confirmed transfers, and a deal lifecycle managed entirely by a Finite State Machine (FSM) in a FunC smart contract.

Below is an overview of why FSMs are the perfect pattern for on‑chain business logic and how we implemented them.

Why Finite State Machines?

Every marketplace deal follows a predictable flow:

created → funded → asset transferred → confirmed → completed (or disputed)

That is textbook FSM territory.

Benefits of encoding the flow in a smart contract

  • Every state transition is on‑chain – fully auditable.
  • Invalid transitions are impossible – the contract rejects them.
  • No human arbitrator needed for the happy path.
  • Dispute resolution has clear rules – not “vibes”.

FSM Diagram

┌─────────┐    fund()    ┌──────────┐   confirm_transfer()   ┌────────────────┐
│ CREATED │────────────▶│  FUNDED  │──────────────────────▶│ TRANSFER_SENT │
└─────────┘              └──────────┘                        └───────┬───────┘
     │                        │                                      │
     │ cancel()               │ cancel()              oracle_confirm()
     │                        │                                      │
     ▼                        ▼                                      ▼
┌─────────┐              ┌──────────┐                       ┌───────────────┐
│CANCELLED│              │ REFUNDED │                       │   COMPLETED   │
└─────────┘              └──────────┘                       └───────────────┘

                                                              auto‑release
                                                              vault → seller

The Smart Contract (FunC)

TON’s smart contracts run on the TVM (TON Virtual Machine). We chose FunC – lower‑level than Tact, but it gives us precise control over gas and message handling.

State Encoding

;; Deal states
const int STATE_CREATED       = 0;
const int STATE_FUNDED        = 1;
const int STATE_TRANSFER_SENT = 2;
const int STATE_COMPLETED     = 3;
const int STATE_CANCELLED     = 4;
const int STATE_REFUNDED      = 5;
const int STATE_DISPUTED      = 6;

;; Deal storage layout
;; [state:3][buyer:267][seller:267][amount:120][asset_id:256][oracle:267][created_at:32][timeout:32]

State‑Transition Guard

Every operation checks the current state before executing. This is the core FSM enforcement:

() assert_state(int current, int expected) impure inline {
  throw_unless(400, current == expected);  ;; 400 = invalid state transition
}

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
  int op = in_msg_body~load_uint(32);

  ;; Load deal state
  var ds = get_data().begin_parse();
  int state = ds~load_uint(3);
  slice buyer  = ds~load_msg_addr();
  slice seller = ds~load_msg_addr();
  int amount   = ds~load_coins();
  ;; ... rest of fields

  if (op == op::fund) {
    assert_state(state, STATE_CREATED);
    ;; Verify sender is buyer
    throw_unless(401, equal_slices(sender, buyer));
    ;; Verify sent amount matches deal
    throw_unless(402, msg_value >= amount);

    ;; Transition: CREATED → FUNDED
    save_state(STATE_FUNDED, buyer, seller, amount, ...);
    return ();
  }

  if (op == op::confirm_transfer) {
    assert_state(state, STATE_FUNDED);
    ;; Only seller can confirm they've initiated the transfer
    throw_unless(401, equal_slices(sender, seller));

    ;; Transition: FUNDED → TRANSFER_SENT
    save_state(STATE_TRANSFER_SENT, buyer, seller, amount, ...);
    return ();
  }

  if (op == op::oracle_confirm) {
    assert_state(state, STATE_TRANSFER_SENT);
    ;; Only designated oracle can confirm
    throw_unless(401, equal_slices(sender, oracle));

    ;; Transition: TRANSFER_SENT → COMPLETED
    ;; Release vault to seller
    send_raw_message(build_transfer_msg(seller, amount), 64);
    save_state(STATE_COMPLETED, buyer, seller, amount, ...);
    return ();
  }

  ;; ... cancel, dispute, timeout handlers
}

The Vault Pattern

Funds don’t sit in a shared pool — each deal gets its own contract instance (a child contract deployed from a factory). This means:

;; Factory contract deploys a new deal contract per transaction
() create_deal(slice buyer, slice seller, int amount, int asset_id, slice oracle) impure {
  cell state_init = build_deal_state_init(buyer, seller, amount, asset_id, oracle);
  slice deal_address = calculate_address(state_init);

  ;; Deploy deal contract with initial state
  send_raw_message(
    begin_cell()
      .store_uint(0x18, 6)          ;; external inbound message header
      .store_slice(deal_address)   ;; destination address
      .store_coins(50000000)        ;; 0.05 TON for gas
      .store_uint(6, 107)          ;; state_init flag
      .store_ref(state_init)        ;; contract code+data
      .store_uint(op::init, 32)    ;; init op
    .end_cell(),
    1
  );
}

Why per‑deal contracts?

  • Isolation – a bug in one deal can’t drain the funds of another.
  • Deterministic vault – each contract is single‑purpose and predictable.

The Oracle Layer

The trickiest part: how do you verify that a Telegram channel was actually transferred?

We built a confirmation oracle that:

  • Monitors the asset (e.g., checks Telegram channel admin list via Bot API)
  • Sends on‑chain confirmation when the transfer is verified
  • Has a timeout – if the oracle doesn’t confirm within N hours, the buyer can dispute
// Oracle service (NestJS)
@Injectable()
export class TelegramTransferOracle {
  constructor(
    private readonly tonClient: TonClient,
    private readonly httpService: HttpService,
  ) {}

  // Called by off‑chain worker when transfer is detected
  async confirmTransfer(dealAddress: string, proof: TransferProof): Promise<void> {
    // Build on‑chain message
    const msg = beginCell()
      .storeUint(op::oracle_confirm, 32)
      .storeUint(proof.timestamp, 32)
      .storeSlice(proof.signature)
      .endCell();

    await this.tonClient.sendExternalMessage({
      to: dealAddress,
      body: msg,
      value: 0, // no TON needed, just a signal
    });
  }

  // Periodic health‑check – if timeout passes, expose dispute endpoint
}

TL;DR

  • FSMs give us a mathematically provable flow – every state change is enforced on‑chain.
  • FunC lets us implement that flow with fine‑grained gas control.
  • Per‑deal vault contracts isolate funds, eliminating a single point of failure.
  • An off‑chain oracle bridges the real‑world asset transfer back to the blockchain, completing the trust‑less escrow cycle.

That’s how ITOhub removes the trust problem from digital‑asset marketplaces.

TransferOracle

class TransferOracle {
  async verifyChannelTransfer(deal: Deal): Promise<boolean> {
    const channel = await this.telegramBot.getChat(deal.assetId);
    const admins = await this.telegramBot.getChatAdministrators(deal.assetId);

    // Check if buyer is now an admin with full rights
    const buyerAdmin = admins.find(
      a => a.user.id === deal.buyerTelegramId && a.status === 'creator',
    );

    if (buyerAdmin) {
      // Send on‑chain confirmation
      await this.tonService.sendOracleConfirmation(deal.contractAddress);
      return true;
    }

    return false;
  }

  @Cron('*/60 * * * * *') // Check every minute
  async pollPendingTransfers() {
    const pending = await this.dealRepo.findByState('TRANSFER_SENT');
    for (const deal of pending) {
      await this.verifyChannelTransfer(deal);
    }
  }
}

Oracle Trust Minimization

The oracle can only do two things: confirm or not confirm. It cannot:

  • Redirect funds
  • Change deal parameters
  • Cancel a funded deal unilaterally

If the oracle is compromised, the worst case is a stalled deal — which the timeout mechanism resolves automatically.

The Telegram Mini App

The frontend runs as a Telegram Mini App (TMA), giving users a native feel:

// Deal creation flow
async function createDeal(params: DealParams) {
  // 1. Create deal record in backend
  const deal = await api.post('/deals', {
    assetType: 'telegram-channel',
    assetId: params.channelId,
    price: params.price,
    sellerTelegramId: params.sellerId,
  });

  // 2. Deploy on‑chain contract via TonConnect
  const tx = {
    validUntil: Math.floor(Date.now() / 1000) + 600,
    messages: [
      {
        address: FACTORY_CONTRACT,
        amount: toNano('0.1').toString(), // Deploy gas
        payload: buildCreateDealPayload(deal),
      },
    ],
  };

  await tonConnect.sendTransaction(tx);

  // 3. Redirect to deal tracking page
  navigate(`/deals/${deal.id}`);
}

Lessons from Production

  1. FSMs make auditing trivial – every deal has a complete state history on‑chain.
  2. Per‑deal contracts are worth the deployment cost – on TON, deploying a contract costs 0.05 TON ($0.10).
  3. Oracle design is the real security challenge – we’re moving toward a multi‑oracle model (2‑of‑3 confirmations).
  4. Timeouts prevent deadlocks – each state has a timeout that allows the counter‑party to cancel or dispute.

The Numbers

Since launch on ITOhub:

  • Deal lifecycle: CREATED → COMPLETED averages 2.3 hours
  • Zero fund losses from contract bugs
  • Dispute rate: < 5 % (most resolved by oracle timeout)

Full deal audit available via TON Explorer.

When to Use This Pattern

The FSM + on‑chain vault pattern works for any marketplace where:

  • Assets can be verified programmatically (channel ownership, domain transfer, NFT holding)
  • Both parties need protection (not just the buyer)
  • The happy path is common (disputes are the exception)

It doesn’t work well when:

  • Asset transfer verification requires human judgment
  • The asset is physical (requires a shipping oracle)
  • Transaction volume demands sub‑second finality (contract deployment adds latency)

Open‑Source Plans

We plan to open‑source the core FSM escrow contract as a reusable template. If you’re building a marketplace on TON and want early access, check out the documentation at docs.itohub.org or ping us in the Telegram bot.

Gerus Lab builds decentralized protocols, AI‑powered products, and Telegram‑native applications. From NFT staking to DCA bots to escrow systems — we turn complex architectures into production software.

0 views
Back to Blog

Related posts

Read more »

Google Gemini Writing Challenge

What I Built - Where Gemini fit in - Used Gemini’s multimodal capabilities to let users upload screenshots of notes, diagrams, or code snippets. - Gemini gener...