← Blog

The smart contract behind a 48-hour settlement


The marketing line is "verified work settles in 48 hours, not 12 weeks." This post is about what's actually running on-chain to make that true. Two Stellar Soroban contracts, six core mutating functions, a five-stage lifecycle, and a 48-hour OEM objection window encoded directly in the state machine. Source: github.com/fairb-dev/Fairfoundry.

The shape

Two contracts, three roles, five lifecycle stages.

The settlement contract holds OEM escrow, tracks each manufacturing service through the lifecycle, takes a split platform fee (0.375% from each side, 0.75% total Cloud), and releases payment when the verification oracle attests completion. The SVT contract issues soulbound (non-transferable) Service Validation Tokens — portable on-chain quality credentials.

Three roles are set once at init: OEM (buyer), Factory (manufacturing partner), QA (quality assurance staking collateral). A fourth role, the FairBuild oracle, is registered separately via register_fairbuild_oracle.

Five lifecycle stages are first-class citizens in the type system:

pub enum ServiceStage {
    EVT,         // Engineering Validation Test
    DVT,         // Design Validation Test
    PVT,         // Production Validation Test
    MP,          // Mass Production
    Sustaining,  // Ongoing production + maintenance
}

Each stage carries the same on-chain mechanics — escrow, attestation, settlement — but the SVTs minted at each stage compose a permanent quality record specific to that stage of the program.

The state machine

A service order moves through five states. Each transition is one contract call:

Requested ──accept_service_order(factory)───→ Accepted Accepted ──submit_artifacts(factory, root)──→ Delivered Delivered ──attest_completion(oracle)─────────→ Validated ┌─ mints SVT Validated ──settle_service(any caller)────────→ Settled └─ releases escrow │ └─ as_credit=true requires factory auth

The OrderStatus enum encodes the same machine. Cancelled is the only off-path destination, reachable only from Requested (pre-accept). Once the factory has accepted, the order can only proceed through validation or land in dispute.

Why 48 hours, not "instant"

When the oracle attests at attest_completion, the order moves to Validated. Settlement does not auto-fire. There's a 48-hour OEM objection window. If the OEM raises a dispute via request_reinspect within that window, escrow freezes and the dispute path takes over.

If 48 hours pass with no objection, anyone can call settle_service and the contract releases funds to the factory. The "any caller" property is intentional — it means a factory's automation script, an OEM's relayer, or a third-party operator can all trigger settlement once the SVT exists. The factory always receives the funds at their own address; the caller is just the gas-payer.

One caveat shipped recently: settling as credits instead of direct payment now requires factory authorization. Credit settlement routes funds to an on-ledger ledger entry the factory must later redeem. That's the factory's economic choice; pre-fix, any third party could have forced it.

pub fn settle_service(env: Env, order_id: String, as_credit: bool) -> i128 {
    // ...status checks...

    // Credit-mode settlement requires factory auth.
    // Direct payment is callable by anyone (relayer-friendly).
    if as_credit {
        order.factory.require_auth();
    }

    // ...fee deduction + transfer...
}

Disputes, and the 96-hour bound

If the OEM objects within the 48-hour window, escrow freezes. The challenge mechanism is bounded by design — both sides have skin in the game and the resolution time is encoded in the contract. The QA stakes collateral via stake_qa; their stake locks during a challenge and slashes on a missed-deadline default.

Two recent guard fixes shipped here too: late QA responses now revert (now > due_byTooEarlyOrLate) so missed-deadline penalties can't be silently bypassed by a late response that flips the challenge to Responded. And challenge_default_slash now rejects slash_bps == 0 and refuses to accept the QA itself as the caller — closing a path where the very party being slashed could clear their own default.

What's tested

61 tests pass in 0.15 seconds. They cover happy paths (direct payment, credit settlement, multi-stage progression EVT → MP), negatives (auth failures, state violations, escrow underflows, fee-rate validation, role mismatches), and properties (escrow conservation, SVT monotonicity, accumulation/redemption invariants).

What they don't yet cover, and we know it: try-call negative tests for the new auth guards, late post-deadline reinspection responses, artifact-root mismatches between factory submission and oracle attestation, and re-init attempts. Test-coverage hardening is its own backlog item.

What's not on-chain

The marketing copy says "verified by an automated layer." On-chain, that means the oracle attests that it verified. The actual ML/metrology pipeline that turns 10,000 MTF measurements into a pass/fail call runs off-chain — at FairBuild, today. The oracle's attestation is a signed claim, not a proof.

This is the trust assumption the system depends on. For a centralized pilot where FairBuild operates the oracle itself, it's acceptable and arguably preferable (lower latency, fewer infra dependencies, clear accountability). For decentralized claims later, it becomes a multi-oracle quorum or a cryptographic-proof problem. We've documented this as a roadmap item rather than a "done" checkbox.

Why Soroban

EVM was the obvious choice; we didn't pick it. The reasons fit in a separate post — short version: built-in soulbound primitives, first-class fiat-asset rails, deterministic replay, and a smaller blast radius for the kind of contract that's gated by an off-chain oracle anyway. Coming next on this blog.


Jisoo Lee is CEO of Fairbuild. Source: github.com/fairb-dev/Fairfoundry.

Want the architecture diagrams?

FairFoundry Fabric — state machine, payment pipeline, governance timelock, data model. All on the platform page.