This page covers the smart contract implementation details. See Glossary.
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
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.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
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 monitoringUpdatedevents.
possibleNext must itself be a legal transition out of the state being reported, so the hint can never point more than one step ahead.
The Query Registry
Vehicles hold the economic logic; a single shared Query Registry owns query state. Every Vehicle callsregister() 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) — validatestoredState -> newState, emit oneUpdated. - Two-leg (
state(query) != storedState) — validatestoredState -> state(query), thenstate(query) -> newState, emitting an extraUpdatedfor the virtual catch-up leg with an emptypossibleNext.
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 withtokenId = 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.
Transition flows
Synchronous flow (Aave V3, Morpho Blue, ERC-4626)
create(), immediately reaching UNLOCKING. Call unlock() to finalize.
Asynchronous flow (Ethena, ERC-7540)
Error recovery
recover() to reclaim assets.
Partial settlement
Whenunlock() or recover() cannot fully distribute assets in a single call, the Query returns to PROCESSING instead of reaching a terminal state.
Asset distributed in that step. The cycle repeats until all assets are fully claimed or recovered.
Critical constraints
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
Theestimate() and convert() methods serve different purposes:
estimate()includes all applicable entry/exit fees, slippage, and operational costsconvert()excludes all fees, providing a pure share-to-asset conversion
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