# The STEAM standard \[A universal interface for DeFi yield operations]

STEAM (State Transition Engine for Asset Management) defines how every deposit and redemption works across Railnet. A contract implementing it is a **Vehicle**. ERC-4626 assumes assets in, shares out, same transaction, and has nowhere to record an operation still running. STEAM tracks state transitions instead, so a cooldown, a settlement batch or a multi-step allocation stays addressable for its whole life.

## The Query

A **Query** is one deposit or redemption.

```solidity [Solidity]
struct Query {
    address owner;      // Who controls the query
    address receiver;   // Who receives the output
    Asset input;        // The asset supplied
    Asset output;       // Expected output; value is a slippage floor (0 disables it)
    Mode mode;          // DEPOSIT or REDEEM
    bytes32 salt;       // Unique identifier component
    bytes data;         // Protocol-specific parameters
}

struct Asset {
    address asset;
    uint256 value;
}
```

The query ID is `keccak256(abi.encode(chainId, vehicleAddress, query))`, wrapped in the `Id` user-defined type. The whole struct is hashed, `salt` and `data` included, so repeated operations still get unique IDs, and the Vehicle address keeps identical queries distinct across Vehicles. The ID never changes.

## States

| State | Meaning | Terminal |
|-------|---------|----------|
| `EMPTY` | No query exists yet | No |
| `PROCESSING` | The Vehicle holds the input and is working | No |
| `PAUSED` | Waiting on an external condition (cooldown, oracle, KYC) | No |
| `UNLOCKING` | Succeeded, output ready to claim | No |
| `RECOVERING` | Failed, input being recovered | No |
| `REJECTED` | Input returned to the owner | Yes |
| `SETTLED` | Output distributed to the receiver | Yes |

Transitions are either method-driven (someone calls the Vehicle) or protocol-driven (an external event or an elapsed delay changes the answer `state()` gives).

## Lifecycle methods

| Method | Valid from | Transitions to | Effect |
|--------|-----------|----------------|--------|
| `create(query)` | `EMPTY` | `PROCESSING` or `UNLOCKING` | Pulls the input, starts the operation. Returns `State`. |
| `resume(query)` | `PAUSED` | `PROCESSING` | Continues once the condition is met. Returns `State`. |
| `unlock(query)` | `UNLOCKING` | `SETTLED`, or `PROCESSING` on a partial claim | Pays the output. Returns `(State, Asset)`. |
| `recover(query)` | `RECOVERING` | `REJECTED`, or `PROCESSING` on a partial recovery | Returns the input. Returns `(State, Asset)`. |

All four require `msg.sender` to be the effective owner: `query.owner`, or the ERC-721 holder when wrapped.

## One query, end to end

```solidity [Solidity]
// Sync Vehicle (Aave V3, Morpho Blue, ERC-4626)
create()  -> UNLOCKING          // same transaction
unlock()  -> SETTLED

// Async Vehicle (Ethena cooldown, ERC-7540 queue)
create()  -> PROCESSING
          -> PAUSED             // protocol-driven, optional
resume()  -> PROCESSING
          -> UNLOCKING          // protocol-driven, once the protocol is ready
unlock()  -> SETTLED

// Failure path, async only
          -> RECOVERING
recover() -> REJECTED
```

When `unlock()` or `recover()` cannot distribute everything at once, the query returns to `PROCESSING` and you call again as each tranche is released, receiving the single `Asset` moved in that step.

## The Query Registry

Vehicles hold the economic logic; a shared **Query Registry** owns query state. A Vehicle calls `register()` inside `create()` and routes every later change through `transition()`. The Registry validates, commits, and is the sole emitter of `Created` (once, carrying the full `Query`), `Updated` (every committed transition) and `Wrapped`. Its record per `Id` holds the registering `vehicle`, `storedState`, `outcome`, `transitionCount` and an optional `transferGate`; the `Query` itself is never stored, so reconstruct it from `Created`. Both events carry `possibleNext`, the states the query may reach without anyone calling the Vehicle: empty means stable until the owner acts, non-empty means keep polling `state(query)`.

`storedState` is only what has been committed, and it lags: a cooldown can expire on-chain so `state(query)` already reports `UNLOCKING` while `storedState` says `PROCESSING`. `state(query)` is authoritative, so never read `storedState` as the current state of an async query. `transition()` validates `storedState -> state(query) -> newState` and emits an extra `Updated` for the catch-up leg.

`outcome` stays `PENDING` until the query first reaches `UNLOCKING` (locking `SUCCESS`) or `RECOVERING` (locking `FAILURE`); a later transition into the other branch reverts `OutcomeLocked`. Same-branch round-trips through `PROCESSING` stay legal, which is what makes partial settlement work.

`transitionCount` increments on every committed transition and never resets, so `(Id, transitionCount)` proves a query has not moved since you observed it.

`query.receiver` may `wrap()` a registered, non-terminal query into an ERC-721 with `tokenId = uint256(Id)`. The holder becomes the effective owner and receiver, and the token burns on termination. The `transferGate` is immutable: `address(0)` on a role-gated Vehicle lets anyone bypass that gate by receiving the token.

## Constraints

* `SETTLED` and `REJECTED` are final, and only ever reached synchronously through `unlock()` or `recover()`.
* `create()` can never produce `REJECTED` or `RECOVERING`: any failure reverts the transaction.
* Self-loops on `UNLOCKING` and `RECOVERING` are forbidden; acting in those states must change the state.

## View methods

| Method | Returns | Description |
|--------|---------|-------------|
| `state(query)` | `State` | Live state. `EMPTY` for unknown queries, never reverts. |
| `estimate(asset_, mode, estimationType)` | `Asset` | Expected output including fees, non-binding. |
| `convert(asset_, sharesToAssets)` | `Asset` | Pure conversion, excluding fees. |
| `error(query)` | `bytes` | Error data for a `REJECTED` query, empty otherwise. |
| `asset()` | `address` | The base asset. |
| `routes()` | `(Route[], Route[])` | Deposit and redeem route combinations. |
| `totalAssets()` | `uint256` | Total value managed. |
| `maxDeposit(account)` / `maxRedeem(account)` | `Asset` | Per-account limits. |
| `ready()` | `bool` | Whether the Vehicle accepts new queries. |

Transactional fees come out of the payout in `unlock()`, not when `create()` pulls the input, so the `output.value` floor is checked against a fee-inclusive estimate. `recover()` charges no transactional fee.

## Next steps

[Sync vs async operations](/developers/contracts/sync-vs-async) covers the protocol-driven legs and keeper settlement. [Accounting and flow of funds](/developers/contracts/accounting) covers how a MultiVehicle books in-flight queries.
