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
  • Withdrawal queues — Syrup processes withdrawals in a FIFO queue
  • 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.
struct Query {
    address owner;      // Who controls the query
    address receiver;   // Who receives the output
    Asset[] input;      // What goes in
    Asset[] output;     // What comes out (filled on settlement)
    Mode mode;          // DEPOSIT or REDEEM
    bytes32 salt;       // Unique identifier component
    bytes data;         // Protocol-specific parameters
}

struct Asset {
    address asset;      // Token address
    uint256 value;      // Amount
}
The Query ID is computed as keccak256(abi.encode(chainId, vehicleAddress, query)). The salt field ensures that repeated operations with identical parameters produce unique IDs.

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

StateDescriptionTerminal
EMPTYNo query exists yet. Default state before creation.No
PROCESSINGVehicle received assets, performing protocol operations.No
WAITINGPaused for an external condition (cooldown, oracle, KYC).No
UNLOCKINGOperation succeeded, output assets ready for claim.No
RECOVERINGError occurred, assets being recovered.No
REJECTEDQuery failed, assets returned to owner.Yes
SETTLEDQuery complete, assets distributed to receiver.Yes

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 all input assets from the owner and starts the operation.
  • For sync protocols (Aave, Compound): the Query transitions directly to UNLOCKING
  • For async protocols (Ethena, Syrup): 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 WAITING 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 assets. The Query transitions to SETTLED and assets or shares are distributed to the receiver.If the operation failed, you call recover() instead. The Query transitions to REJECTED and input assets are returned.

Lifecycle methods

MethodValid fromTransitions toDescription
create(query)EMPTYPROCESSING or UNLOCKINGInitiates a new query. Pulls input assets.
resume(query)WAITINGPROCESSINGResumes after an external condition is met.
unlock(query)UNLOCKINGSETTLED or PROCESSING (partial)Distributes output assets to receiver.
recover(query)RECOVERINGREJECTED or PROCESSING (partial)Returns input assets after failure.

Events

Vehicles emit two events to enable off-chain tracking and automation:
EventParametersEmitted when
Created(id, query)Indexed query ID + full Query structA new query is created via create()
Updated(id, state, possibleNext)Indexed query ID + new state + array of possible next statesAny state transition occurs
The possibleNext array in the Updated event signals whether the state may change asynchronously:
  • 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 monitor for further Updated events.
Use Updated events with non-empty possibleNext arrays to drive automation. See Keeper setup for patterns.

Transition flows

Synchronous flow (Aave, Compound, ERC-4626)

// Complete in a single transaction
// EMPTY -> UNLOCKING -> SETTLED
The Vehicle completes the entire operation during create(), immediately reaching UNLOCKING. Call unlock() to finalize.

Asynchronous flow (Ethena, Syrup)

// Requires multiple transactions
// EMPTY -> PROCESSING -> (WAITING ->) UNLOCKING -> SETTLED
The Vehicle enters PROCESSING, waits for external conditions, and advances to UNLOCKING when ready. A Keeper typically automates this.

Error recovery

// Error path
// PROCESSING -> RECOVERING -> REJECTED
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.
// Partial claim cycle
// UNLOCKING -> PROCESSING -> ... -> UNLOCKING -> SETTLED

// Partial recovery cycle
// RECOVERING -> PROCESSING -> ... -> RECOVERING -> REJECTED
This happens with protocols that release assets in batches. Each partial call returns Asset[] showing what was 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 — Transitions between UNLOCKING (success path) and RECOVERING (error path) are strictly forbidden, even when going through PROCESSING.
  • Atomic creationcreate() can never produce REJECTED or RECOVERING. Any failure during creation reverts the entire transaction.
  • Owner enforcement — All method-driven transitions must verify that msg.sender is the query.owner.
  • 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:
MethodReturnsDescription
state(query)StateCurrent state of a specific query
estimate(assets, mode, type)Asset[]Expected output including fees (non-binding)
convert(assets, sharesToAssets)Asset[]Pure conversion excluding fees
error(query)bytesError data for queries in RECOVERING or REJECTED
asset()addressThe base asset of the vehicle (e.g., USDC)
routes()(Route[], Route[])Deposit and redeem route combinations
totalAssets()uint256Total value of all managed assets
maxDeposit(account)Asset[]Maximum deposit amounts per asset for an account
maxRedeem(account)Asset[]Maximum redeemable shares per asset for an account
ready()boolWhether the vehicle accepts new queries
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
Fees are applied during create() when assets are pulled. They are not applied during recovery operations, allowing users to reclaim assets without additional charges.

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

Create a sync Vehicle

Build a Vehicle for protocols with instant settlement

Create an async Vehicle

Build a Vehicle for protocols with delayed settlement