DEV Community

Cover image for HTLC Timelocks in Cross-Chain Swap Design: A STONfi/Omniston Case Study
Web3KD
Web3KD

Posted on

HTLC Timelocks in Cross-Chain Swap Design: A STONfi/Omniston Case Study

⏳ HTLC Timelocks in Cross-Chain Swap Design: A STON.fi/Omniston Case Study

Most explanations of atomic swaps stop at "funds are locked behind a hash and a timer, so either both sides complete or both refund." That sentence is true and almost entirely useless for understanding why the design works, what it costs, or how a production system builds a real product on top of it.

The interesting part isn't the hashlock. Hashlocks are trivial — a one-way function, a preimage, a comparison. The interesting part is the timelock, and specifically the relationship between two timelocks on two chains that don't know each other exists. Get that relationship backwards and the atomicity guarantee inverts into an exploitable free option. Get it right and you have settlement that needs no bridge, no custodian, and no governance intervention when things go wrong.

"The hashlock is the part everyone explains. The timelock ordering is the part that actually keeps you from losing money."


🔐 Section 1: The Primitive — Two Doors, Never Both Open

An HTLC holds funds behind exactly two release conditions, and the entire discipline of the design is that these two paths can never be open simultaneously.

A minimal shape, expressed as the state a contract actually stores:

interface HtlcState {
  hashlock: Uint8Array;   // sha256(secret) — public from the start
  deadline: number;       // unix timestamp, after which refund opens
  sender: Address;        // gets funds back via refund()
  receiver: Address;      // gets funds via claim(secret)
  amount: bigint;
}
Enter fullscreen mode Exit fullscreen mode

Two methods, and the guard on each is what matters:

function claim(secret: Uint8Array) {
  require(now() < state.deadline,              "claim window closed");
  require(sha256(secret) === state.hashlock,   "wrong preimage");
  transfer(state.receiver, state.amount);
}

function refund() {
  require(now() >= state.deadline,             "too early to refund");
  require(caller() === state.sender,           "not the sender");
  transfer(state.sender, state.amount);
}
Enter fullscreen mode Exit fullscreen mode

Look at the two now() checks. claim requires before the deadline; refund requires at or after it. They are exact complements — there is no timestamp at which both succeed, and no timestamp at which both fail. Before the deadline, exactly one party can act (the receiver, if they have the preimage). After it, exactly one party can act (the sender). There is no third state and no gap.

The academic framing describes this as a tuple of three algorithms — Lock, Unlock, Refund — and that framing is worth internalizing, because it makes explicit that "failure" here is a first-class designed outcome, not an exception path bolted on afterward. A refund isn't the protocol breaking. It's the protocol working.

Why the hash function choice actually matters. The preimage has to be unguessable and the hash has to be identically computable on both chains. That second requirement is more constraining than it sounds — it rules out any pairing of chains that don't share a hash primitive, and it's why SHA-256 dominates in practice rather than anything more exotic:

// The secret must be generated with real entropy, not derived
// from anything predictable — a timestamp, a nonce, a counter.
const secret = crypto.randomBytes(32);
const hashlock = sha256(secret);

// hashlock is published immediately and publicly.
// secret stays private until the moment of claim.
Enter fullscreen mode Exit fullscreen mode

A secret derived from anything guessable — a block hash, a sequence number, a timestamp — hands the counterparty the ability to claim without waiting. The randomness isn't a detail; it's load-bearing.

There's also a subtlety in where the secret becomes public. It isn't revealed by any out-of-band message or any trusted relay. It becomes public as a side effect of being used — the claim transaction carries it, and once that transaction is in a block, the preimage is simply readable chain state that anyone can observe. That's what makes the second leg trustless: nobody has to send the counterparty anything.

Here's where one HTLC stops being enough: this construction gives you a conditional payment, not a swap. Alice can pay Bob conditionally. Nothing above causes Bob to pay Alice. A cross-chain swap needs two of these, one per chain, and coordinating their deadlines is the genuinely hard problem.


⚖️ Section 2: The Asymmetry That Makes It Atomic

Two HTLCs, two chains, same hashlock. Alice locks on the source chain, Bob locks on the destination chain. Alice generated the secret, so initially only Alice can unlock anything.

The question that decides whether this is safe or catastrophically broken:

             ┌─────────────────────────────────────────┐
  SOURCE     │  Alice's lock          deadline: ???     │
             └─────────────────────────────────────────┘
             ┌─────────────────────────────────────────┐
  DEST       │  Bob's lock            deadline: ???     │
             └─────────────────────────────────────────┘

             Which deadline comes first?
Enter fullscreen mode Exit fullscreen mode

The answer: they must be staggered, not synchronized, and the direction is not arbitrary. The party holding the secret must have the later deadline.

// The one invariant the entire protocol rests on:
assert(takerDeadline < originatorDeadline);
Enter fullscreen mode Exit fullscreen mode

Why this direction, specifically? Run the broken ordering and watch it fail.

Suppose Bob's deadline came after Alice's. Alice, holding the secret, simply does nothing. Her own lock expires first and she refunds — she has her original funds back, free and clear. But Bob's lock is still live, still behind the same hash, and Alice still knows the preimage. She reveals it, claims Bob's asset, and leaves holding both sides. The hashlock performed flawlessly. The deadline ordering destroyed the trade.

Now the correct ordering. Alice wants the destination asset, so she must reveal the secret on the destination chain — and she must do it before the earlier deadline. The moment she does, the preimage is public chain state. Bob reads it and claims on the source chain, and he is guaranteed a window to do so, because the source deadline is strictly later:

  t₀ ─────────────────────────────────────────────────────▶ time

  DEST    [ Bob's lock ────────────────── ✕ takerDeadline ]
                          ▲
                          │ Alice reveals secret here
                          │ (must be before ✕)
                          ▼
  SOURCE  [ Alice's lock ─────────────────────── ✕ originatorDeadline ]
                          └──── Bob's safety window ────┘
Enter fullscreen mode Exit fullscreen mode

That gap — the span between the two deadlines — is Bob's protection. It isn't slack or padding. It's the entire reason he can safely go second.

The formal literature states this tightly: HTLCs realize cross-chain atomic swaps by coordinating two contracts on different chains with synchronized hashlocks and staggered timelocks, and atomicity holds in the sense that either both assets transfer or both parties refund. Synchronized hash, staggered time. Those four words are the protocol.

Enumerate every branch and it holds:

What happens Source leg Destination leg
Alice reveals and claims Bob claims with revealed secret Alice claims
Alice never reveals Refunds at later deadline Refunds at earlier deadline
Alice reveals at the last instant Bob still has the full gap to act Alice claims
Bob never locks at all Alice's lock expires, refunds Never existed

There is no ordering of events in which one side claims while the other side fails to either claim or refund. And note what's absent from that table: any row requiring a human to intervene. No multisig committee, no admin key, no bridge operator adjudicating whose claim is legitimate. Recovery is encoded in the contract and fires on a timer whether or not anyone is watching.

Sizing that gap is its own engineering decision. It can't be arbitrarily tight, because the second party needs real wall-clock time to observe the revealed secret, build a transaction, broadcast it, and get it confirmed — under whatever congestion exists at that moment:

const takerDeadline      = now + destinationChainFinality * SAFETY_FACTOR;
const originatorDeadline = takerDeadline + reactionWindow;

// reactionWindow must exceed:
//   worst-case source-chain confirmation time
// + realistic reorg depth
// + the counterparty's own detection latency
Enter fullscreen mode Exit fullscreen mode

Too tight, and a congestion spike or reorg genuinely costs someone the trade. Too wide, and capital sits locked far longer than necessary in the failure case. Every production HTLC system is making a claim about the worst-case latency of the slower chain and encoding that claim as a number.


🔄 Section 3: What Omniston Adds — Discovery Before Settlement

The textbook atomic swap has a problem that makes it useless as a consumer product: it assumes Alice and Bob have already found each other and already agreed a price. In reality, finding a counterparty willing to take the other side of a cross-chain trade — competitively, right now, for your size — is the hard part. HTLC solves settlement. It does nothing about discovery.

Omniston's architecture puts a competitive quoting layer in front of settlement, and the ordering is the whole point:

  PHASE 1 — off-chain, free, reversible
  ────────────────────────────────────────────────────────
  user intent
      └─▶ RFQ broadcast ──▶ resolver A  ─┐
                        ──▶ resolver B  ─┼─▶ best quote wins
                        ──▶ resolver C  ─┘
                        ──▶ AMM pool reads

      ⚠️  NO HTLC EXISTS YET. Nothing has touched any chain.

  PHASE 2 — on-chain, committed, timelocked
  ────────────────────────────────────────────────────────
  winning quote
      └─▶ user locks source side    (later deadline)
      └─▶ resolver locks dest side  (earlier deadline)
      └─▶ reveal ──▶ claim ──▶ claim
Enter fullscreen mode Exit fullscreen mode

Phase one costs nothing. Resolvers — independent market makers running their own pricing services over persistent gRPC streams — each decide independently what to offer for that size and pair. Quotes come back, get compared against AMM pool liquidity, best terms win.

If no acceptable quote arrives, the RFQ simply expires. No HTLC was created. There is nothing to refund and nothing to unwind. This is exactly the right place to put the "no counterparty found" failure: free, off-chain, and instant.

Phase two instantiates the two-contract construction for real — but only after a specific resolver has won at a specific price.

The boundary between those two phases deserves attention, because it's where a surprising amount of practical safety lives. A quote isn't a standing offer — it carries its own validity deadline, entirely separate from and much shorter than the HTLC timelocks that follow:

// Three distinct clocks, easy to conflate, doing different jobs:
quoteValidUntil       // seconds — how long this price is honored
takerDeadline         // minutes — destination-side HTLC expiry
originatorDeadline    // minutes — source-side HTLC expiry, strictly later
Enter fullscreen mode Exit fullscreen mode

Conflating those is a genuine source of confusion. A user who hesitates past quoteValidUntil hasn't lost anything — no HTLC existed yet, and re-requesting simply produces a fresh price. A user who hesitates after locking is in a different regime entirely, where the relevant clocks are the timelocks and the outcome is a refund rather than a re-quote.

The structural detail worth highlighting: who funds the destination side. Not a bridge contract holding pooled user deposits. Not the protocol treasury. The resolver — a market maker who just won a competitive auction and is now putting its own capital behind the quote it gave.

// Bridge model — concentrated, permanent, growing target
bridgeContract.lockedValue += everyUserDeposit;

// Resolver model — per-trade, per-counterparty, time-bounded
resolverCapital.commit(thisTradeOnly, releasesAt: takerDeadline);
Enter fullscreen mode Exit fullscreen mode

That's a materially different risk shape. Exposure is bounded per trade and expires on a timer, instead of accumulating in one contract that becomes a larger target every day.

This also explains something users routinely misread: why cross-chain swaps take noticeably longer than same-chain ones on STON.fi. A same-chain TON swap inherits atomicity from TON's own transaction model — complete or revert, settled as fast as the chain includes it. A cross-chain swap can inherit nothing, because two independent blockchains offer each other no guarantees whatsoever. The extra time isn't latency awaiting better engineering. It is the timelock window — a correctness guarantee, purchased in seconds.

TON's async execution adds a wrinkle the classic papers don't contemplate. The canonical atomic swap literature assumes synchronous execution: call a contract, it succeeds or reverts, you know immediately. TON passes messages instead — a message sent in one block resolves in a later one. A STON.fi swap already traverses a real message chain:

user's jetton wallet
  └─▶ Router's jetton wallet   (transfer_notification)
      └─▶ Router               (decode payload, dispatch)
          └─▶ Pool             (execute AMM math)
              └─▶ settlement
Enter fullscreen mode Exit fullscreen mode

Layering HTLC settlement on top of that means "did the claim succeed?" is not answerable synchronously — which is precisely why trade tracking is a streaming subscription rather than a return value:

omniston.trackTrade({ rfqId }).subscribe(({ state }) => {
  // 'filled' | 'partiallyFilled' | 'aborted'
  // async settlement means you observe, you don't await
});
Enter fullscreen mode Exit fullscreen mode

💸 Section 4: What Atomicity Actually Costs

An engineering article that only lists advantages isn't an engineering article. This guarantee is paid for in four distinct currencies.

  • ⏱️ Latency — paid by the user. The timelock window can't compress below the slower chain's worst-case confirmation time plus margin. The trade-off is explicit in the literature: HTLCs trade a little latency — you pay for the timelock window — for the guarantee that at no point does any party hold the other's asset without an enforceable reason to release it. Seconds for certainty is usually a good trade. It's still a trade.

  • 🔒 Capital lockup — paid by the resolver. From funding its leg until settlement, that capital is committed and unusable. If the swap refunds, the resolver recovers principal but earned nothing for the time it was immobilized. Pure opportunity cost — and a real component of why cross-chain quotes are structurally wider than same-chain AMM pricing. You are partly paying for the capital your trade froze.

  • Gas on the failure path — paid by whoever refunds. A refund is a transaction and costs gas. A swap that correctly returns everyone's funds is a successful outcome by design, but both parties are still slightly out of pocket on a trade that produced nothing. Small, but worth naming because it's the cost users least expect.

  • 🎣 Griefing surface — paid in expected value. A counterparty who locks and then never reveals costs the other side nothing in principal — the refund handles it — but does cost them the window during which capital was frozen. Repeatedly initiating and abandoning swaps is a denial-of-service pattern against a resolver's working capital. The literature is candid that basic HTLCs aren't expressive enough for every multi-party scenario, and that advanced models exist specifically to improve incentive compatibility and resist bribery and collusion. Production systems generally mitigate this at the reputation layer — a resolver seeing repeated abandonment simply stops quoting that source — rather than cryptographically.

None of these invalidate the design. They define its domain of applicability. For a high-frequency, low-value, same-chain swap this would be absurd overhead. For moving meaningful size across a chain boundary, where the alternative is trusting a custodian or a bridge holding concentrated value, it's a sound trade.


🧭 Section 5: Walking Every Failure Mode

Understanding a settlement design means answering "what happens if" for every branch. Here is each realistic failure and exactly what the construction does.

  • 📭 No resolver quotes the trade. RFQ expires. Nothing touched any chain, no HTLC existed, nothing to refund. Adjust size or slippage and re-submit. The cheapest possible failure, correctly placed first.

  • 🚪 User gets a quote but never confirms. Same outcome — quotes carry their own validity deadline. Someone who walks away mid-flow loses nothing but time.

  • 🔇 Resolver locks, user abandons before revealing. Source leg refunds at the later deadline, destination leg at the earlier one. Both recover principal; both are out refund gas; the resolver ate the opportunity cost of frozen capital. The trade simply didn't happen.

  • 🐌 User reveals, destination chain congests severely. This is the exact scenario the gap exists for. The secret is now public. The resolver has the entire window between deadlines to use it on the source side. Sized with a realistic worst case, the claim succeeds despite congestion. Sized optimistically, this is where the design fails — which is why gap sizing is a serious parameter, not a config afterthought.

  • 🔀 Chain reorganization reverses a claim. Functionally similar: re-broadcast, re-confirm, and survivability depends on whether the gap exceeds realistic reorg depth. Every cross-chain design makes a finality assumption here; an HTLC system just makes it legible as a number instead of burying it in a validator set.

  • Everything works. User reveals on the destination chain and receives their asset. Resolver reads the secret from public chain state and claims on the source. Both legs settle. Elapsed time is roughly two chains' confirmation latency plus reaction time — measurably slower than same-chain, and correct by construction rather than by trust.

The property worth restating: across every branch above, no outcome requires manual intervention. No support ticket resolves a stuck HTLC, because there is no stuck state. The refund isn't a customer service process — it's a timer that fires whether or not anyone is paying attention.


🏁 Bottom Line

HTLC-based cross-chain settlement is one of the rare designs where the security argument is genuinely complete — you can enumerate every branch and verify each terminates fairly, without appealing to anyone's honesty or any committee's diligence. The hashlock provides conditional release. The staggered timelocks, with the secret-holder's leg expiring strictly later, provide atomicity. Reverse that stagger and you've converted a safe swap into a free option for whoever holds the preimage.

What Omniston contributes on top is the part HTLC alone can't: discovery. Running a competitive RFQ auction among independent resolvers before any funds touch any chain separates "can I get a good price" — which should be free and off-chain — from "will this settle safely," which is where the cryptography belongs. Resolvers funding the destination side with their own capital keeps failure exposure per-trade and time-bounded rather than concentrated and permanent.

The costs are real: users pay in latency, resolvers in immobilized capital, both in gas on the failure path. In exchange there's no custodian, no bridge validator set, and no state where funds sit stranded awaiting a human decision. For anyone evaluating cross-chain designs seriously, that last property — a failure mode that resolves on a timer rather than through governance — is the one worth weighing most heavily.


🔗 Sources & Further Reading


This article reflects public cryptographic literature and STON.fi's developer documentation as of mid-2026. Code samples are illustrative pseudocode written to clarify the mechanism — they are not copied from any production contract. Protocol mechanics, supported chains, and timelock parameters evolve as the system ships updates; verify against docs.ston.fi before building or trading against any of it.

Top comments (0)