Skip to main content
This page covers the smart contract implementation details. See Glossary.
STEAM (State Transition Engine for Asset Management) is a standardized interface that defines how deposit and redemption operations work across all of Railnet. Smart contracts implementing STEAM are called Vehicles.

Why STEAM exists

Traditional vault standards like ERC-4626 assume synchronous operations: deposit assets, receive shares instantly. But many DeFi operations are inherently asynchronous:
  • Cooldown periods — Ethena requires waiting before unstaking sUSDe
  • Request queues — ERC-7540 vaults settle deposits and redemptions in curator-driven batches
  • Multi-step strategies — Multi-Vehicles coordinate across multiple protocols
ERC-4626 has no mechanism for tracking in-flight operations. Failed transactions simply revert, losing all context. STEAM solves this by shifting from balance tracking to state transitions.
STEAM is more than an interface — it is a framework for building reliable DeFi infrastructure. By standardizing how time, failure, and complex state are handled, STEAM enables Railnet to orchestrate assets across diverse yield sources under a single operational model.

The Query model

The core of STEAM is the Query — a structured request representing a deposit or redemption operation. Each Query carries its own identity, ownership, and state.
The Query ID is computed as keccak256(abi.encode(chainId, vehicleAddress, query)) and wrapped in a bytes32 user-defined type, Id. Including the Vehicle address gives otherwise-identical queries distinct IDs across Vehicles. The whole struct is hashed — salt and data included — so repeated operations with the same parameters still produce unique IDs, and the ID stays fixed for the query’s entire lifecycle.

The state machine

Every Query moves through a defined set of states. This makes the status of any operation transparent and auditable at all times.

States

State diagram

Transition types

State transitions are categorized by their trigger mechanism:
  • Method-driven (synchronous) — Occur within the same transaction as a direct call to a Vehicle method (create(), resume(), unlock(), recover())
  • Protocol-driven (asynchronous) — Triggered by external protocol events, off-chain verification, or settlement delays

The Query lifecycle

Every operation follows three phases:
1

Creation

You call create() with a Query struct. The Vehicle pulls the input asset from the owner and starts the operation.
  • For sync protocols (Aave V3, Morpho Blue, ERC-4626): the Query transitions directly to UNLOCKING
  • For async protocols (Ethena with cooldown active, ERC-7540 vaults): the Query enters PROCESSING — see Sync vs async operations
If create() fails for any reason, the entire transaction reverts. No assets are transferred, no query is created.
2

Execution

The Vehicle interacts with the underlying protocol. For async operations, the Query may pass through PAUSED if an external condition must be met (cooldown period, oracle update).Once the condition is satisfied, resume() moves the Query back to PROCESSING, and eventually the protocol signals success (UNLOCKING) or failure (RECOVERING).
3

Settlement

You call unlock() to claim the output asset. The Query transitions to SETTLED and the asset or shares are distributed to the receiver.If the operation failed, you call recover() instead. The Query transitions to REJECTED and the input asset is returned.

Lifecycle methods

Events

The Query Registry is the sole emitter of query lifecycle events. Vehicles never declare or emit them — a Vehicle calls the Registry, and the Registry writes the state and emits. The vehicle field namespaces the qid, and receiver is indexed so integrators can filter by beneficiary. The full Query struct appears only in Created — the Registry never stores it on-chain, so reconstruct it off-chain from that event. The possibleNext array signals whether the state may change without anyone calling the Vehicle:
  • Empty array — The state is stable. No further transitions will occur until the owner calls a method (unlock(), recover(), resume()).
  • Non-empty array — The state may change asynchronously via protocol-driven transitions. Off-chain systems should keep polling state(query) or monitoring Updated events.
Every entry in possibleNext must itself be a legal transition out of the state being reported, so the hint can never point more than one step ahead.
Use Updated events with non-empty possibleNext arrays to drive automation: an off-chain runner watches for them and calls the matching method once the protocol is ready.

The Query Registry

Vehicles hold the economic logic; a single shared Query Registry owns query state. Every Vehicle calls register() once inside create() and routes every later state change through transition(). The Registry validates the transition, commits it, and emits the events above. The Registry keeps one slim record per query, keyed by its Id: The Query itself is not stored on-chain. A query exists if and only if its record’s vehicle is non-zero.

State resolution and the storedState lag

storedState is only what has been committed. The Vehicle’s state(query) is the live — possibly virtual — state, and that is the authoritative one. For an async Vehicle the live state may lead storedState by one leg: a cooldown has expired on-chain, so state() already reports UNLOCKING while storedState still says PROCESSING. transition() therefore validates one of two paths:
  • Single-leg (state(query) == storedState) — validate storedState -> newState, emit one Updated.
  • Two-leg (state(query) != storedState) — validate storedState -> state(query), then state(query) -> newState, emitting an extra Updated for the virtual catch-up leg with an empty possibleNext.
Never treat storedState as the current state of an async query. Read state(query) on the Vehicle instead — storedState is stale for the whole virtual-state window.

The outcome lock

outcome is what actually enforces path isolation. It stays PENDING until the query first enters UNLOCKING (locking it to SUCCESS) or RECOVERING (locking it to FAILURE). Any later transition into the opposite branch reverts OutcomeLocked. Same-branch round-trips through PROCESSING stay legal, which is what makes partial settlement work.

The transition witness

transitionCount increments on every committed transition — by one on the single-leg path, by two on the two-leg path — and never resets. (Id, transitionCount) is therefore a compact witness that a query has not moved since you last observed it: snapshot it, then require it unchanged at execution time so the owner cannot front-run you by unlocking in between.

Transferable ownership

A query may optionally be wrapped into an ERC-721 token with tokenId = uint256(Id). wrap() must be called by query.receiver on a registered, non-terminal query. While wrapped, the token holder is the bearer owner: effectiveOwner and effectiveReceiver return the holder instead of query.owner and query.receiver, and the Vehicle routes the query accordingly. The token burns automatically when the query reaches a terminal state.
The transferGate passed to wrap() is immutable. Passing address(0) against a Vehicle that gates access by role lets anyone bypass that gate by receiving the token, so install a gate matching the Vehicle’s access policy.

Transition flows

Synchronous flow (Aave V3, Morpho Blue, ERC-4626)

The Vehicle completes the entire operation during create(), immediately reaching UNLOCKING. Call unlock() to finalize.

Asynchronous flow (Ethena, ERC-7540)

The Vehicle enters PROCESSING, waits for external conditions, and advances to UNLOCKING when ready. An off-chain automation runner typically drives this.

Error recovery

If an operation fails, the Query enters RECOVERING. Call recover() to reclaim assets.

Partial settlement

When unlock() or recover() cannot fully distribute assets in a single call, the Query returns to PROCESSING instead of reaching a terminal state.
This happens with protocols that release assets in batches. Each partial call returns the single Asset distributed in that step. The cycle repeats until all assets are fully claimed or recovered.

Critical constraints

These constraints are enforced by the STEAM standard and must never be violated:
  • Terminality — SETTLED and REJECTED are final states. No further transitions are possible.
  • Path isolation — Once a query enters UNLOCKING it can never reach RECOVERING, and vice versa, even by routing through PROCESSING. The Registry’s outcome lock enforces this and reverts OutcomeLocked. Same-branch round-trips through PROCESSING remain legal — that is exactly what partial settlement relies on.
  • Atomic creationcreate() can never produce REJECTED or RECOVERING. Any failure during creation reverts the entire transaction.
  • No idle loops — Self-loops on UNLOCKING and RECOVERING are forbidden. Any action taken in those states must change the state.
  • No virtual termination — SETTLED and REJECTED are only ever reached synchronously, through unlock() or recover(). A protocol-driven transition can never terminate a query.
  • Owner enforcement — All method-driven transitions must verify that msg.sender is the query’s effective owner: query.owner, or the ERC-721 holder when the query is wrapped.
  • State stability — If the possibleNext array in the Updated event is empty, the state is stable until the next user-initiated method call.

View methods

Vehicles provide methods to inspect state and simulate operations:
estimate() provides a best-effort preview, not a commitment. Always implement slippage protection when using estimates for transaction previews.

Fee semantics

The estimate() and convert() methods serve different purposes:
  • estimate() includes all applicable entry/exit fees, slippage, and operational costs
  • convert() excludes all fees, providing a pure share-to-asset conversion
Transactional fees are taken out of the payout in unlock(), not when create() pulls the input. create() only previews them, so the output.value floor is checked against a fee-inclusive estimate. recover() charges no transactional fee, letting users reclaim assets after a failure without additional cost.

Next steps

Sync vs async operations

How synchronous and asynchronous Vehicles differ in practice

Accounting and flow of funds

How assets move through Multi-Vehicles and Sectors