Vehicle & The STEAM Standard
Vehicles are smart contracts that implement the STEAM (State Transition Engine for Asset Management) interface. Each Vehicle wraps a specific DeFi protocol (Aave V3, Morpho Blue, any ERC-4626 or ERC-7540 vault, etc.) and exposes a unified interface for deposits, redeems, and state management.
By standardizing how assets move in and out of protocols, STEAM allows for a modular and composable architecture where different strategies can be swapped or combined without changing the core integration logic.
Why STEAM? (Beyond ERC-4626)
While ERC-4626 provides a standard for tokenized vaults, it has several limitations that STEAM addresses:
- Synchronous vs. Asynchronous: Traditional ERC-4626 vaults assume operations complete instantly. Many DeFi operations are inherently async, involving withdrawal queues (ERC-7540), epoch-based settlement (Lagoon), or multi-step strategies.
- Error Recovery: ERC-4626 has no built-in error recovery. If an operation fails, it simply reverts, often losing the context of why it failed or making it difficult to recover user assets.
- Structured Requests: STEAM introduces Queries—structured requests with persistent identity and a clear state machine.
- State Management: Every operation moves through defined states: Creation → Execution → Settlement.
- Explicit Error Paths: Instead of silent reverts, STEAM provides a
RECOVERINGstate, allowing for graceful failure handling and asset return.
The Query Model
The core of the STEAM standard is the Query struct, which defines the parameters of an operation:
struct Query {
address owner; // Who controls the query
address receiver; // Who receives the output
Asset input; // What goes in
Asset output; // What comes out
Mode mode; // DEPOSIT or REDEEM
bytes32 salt; // Unique identifier component
bytes data; // Protocol-specific parameters
}
struct Asset {
address asset; // Token address
uint256 value; // Amount
} input and output are a single Asset each, not arrays — this scalarisation changed the selector of every function that takes a query (create, unlock, convert, demand, recover, resume).
output is not merely informational: BaseVehicle._validateOutput requires the output asset to be the vehicle itself for a deposit and the underlying asset for a redeem, and output.value acts as a floor on the result — the query reverts rather than settling below it.
A Query ID is computed as keccak256(abi.encode(block.chainid, vehicle, query)) — note the chain id and the vehicle address are part of the preimage, so the same query struct yields different ids on different chains and different vehicles. The salt field then ensures repeated operations with otherwise identical parameters still get unique IDs.
Field order matters. Because the ID is abi.encode of the whole struct, computing it by hand requires exactly the order above.
STEAM State Machine
Every query in a Vehicle follows a strict state machine. There are 7 possible states:
| State | Description | Terminal? |
|---|---|---|
| EMPTY | No query exists with this ID | No |
| PROCESSING | Vehicle received assets, performing protocol operations | No |
| PAUSED | Paused for external condition (cooldown, oracle, queue) | No |
| UNLOCKING | Processing complete, output ready for claim | No |
| SETTLED | Query complete, assets distributed to receiver | Yes |
| RECOVERING | Error occurred, assets being recovered | No |
| REJECTED | Query failed, assets returned to owner | Yes |
Transition Flows
Synchronous Flow (Aave V3, Morpho Blue, ERC4626):
EMPTY → UNLOCKING → SETTLED The Vehicle completes the entire operation during the create() call, immediately reaching the UNLOCKING state.
Asynchronous Flow (ERC7540, Lagoon):
EMPTY → PROCESSING → (PAUSED →) UNLOCKING → SETTLED The Vehicle enters PROCESSING, waits for external conditions (like a cooldown or queue), and then advances to UNLOCKING.
Error Recovery:
PROCESSING → RECOVERING → REJECTED Lifecycle Methods
The following methods drive the state transitions of a query:
| Method | From States | To States | Description |
|---|---|---|---|
create(query) | EMPTY | PROCESSING or UNLOCKING | Initiates a new query. Pulls input assets from the caller. |
resume(query) | PAUSED | PROCESSING | Resumes a paused query after an external condition is met. |
unlock(query) | UNLOCKING | SETTLED | Finalizes the query and distributes the output assets to the receiver. |
recover(query) | RECOVERING | REJECTED | Recovers from an error state and returns the input assets to the owner. |
Key Constraints
- Path Isolation: A query in the
UNLOCKINGstate cannot reachRECOVERING. The success path is strictly separated from the error path once processing is complete. - Terminality:
SETTLEDandREJECTEDare terminal states. No further transitions are possible once a query reaches these states. - Atomic Creation: There is no direct transition to
REJECTEDfromcreate(). Ifcreate()fails, the transaction reverts and no query is ever recorded on-chain.
View Methods
Vehicles provide several view methods to inspect state and simulate operations:
| Method | Returns | Description |
|---|---|---|
state(query) | State | Returns the current state of a specific query. |
estimate(asset, mode, estimationType) | Asset | Estimated output including fees. What the user will actually receive. |
convert(asset, sharesToAssets) | Asset | Pure mathematical conversion excluding fees. Useful for share price calculation. |
asset() | address | The underlying base asset of the vehicle (e.g., USDC). |
routes() | (Route[], Route[]) | Supported deposit routes and redeem routes, as two separate arrays. |
totalAssets() | uint256 | Total value of all assets managed by the vehicle. |
maxDeposit(account) | Asset | Maximum that can currently be deposited, for that account. |
maxRedeem(account) | Asset | Maximum that can currently be redeemed, for that account. |
demand(query) | uint256 | The input the query still needs before it can proceed. |
ready() | bool | Whether the vehicle is ready to take queries. Takes no arguments. |
error(query) | bytes | Returns the error data if a query has entered the RECOVERING state. |
The lifecycle calls return the resulting state, which is worth capturing rather than
re-reading: create(query) -> uint8, resume(query) -> uint8, and both unlock(query) and recover(query) return (uint8 newState, Asset paidAsset).
Fee Semantics: It is important to distinguish between estimate() and convert(). estimate() provides the “real world” expected output after protocol and vehicle fees are applied. convert() is used for internal accounting and calculating the value of shares.
Vehicle Types
The indexer’s VehicleType enum is the authoritative list: AAVE_V3_VEHICLE, MORPHO_BLUE_VEHICLE, ERC4626_VEHICLE, ERC7540_VEHICLE, LAGOON_VEHICLE, WRAPPER_VEHICLE, MULTI_VEHICLE, LIGHT_STEAM_MOCK_VEHICLE and UNKNOWN.
Synchronous Vehicles
These vehicles complete their operations in a single transaction:
- Aave V3 Vehicle (
AAVE_V3_VEHICLE): Supplies and withdraws assets from Aave V3 lending pools. - Morpho Blue Vehicle (
MORPHO_BLUE_VEHICLE): Supplies to and withdraws from Morpho Blue markets. - ERC4626 Vehicle (
ERC4626_VEHICLE): A wrapper that makes any standard ERC-4626 vault STEAM-compliant. - Wrapper Vehicle (
WRAPPER_VEHICLE): Wraps and unwraps an asset one-for-one.
Asynchronous Vehicles
These vehicles require multiple transactions or waiting periods:
- ERC7540 Vehicle (
ERC7540_VEHICLE): Integrates ERC-7540 asynchronous vaults, whose deposits and redeems settle through request queues. - Lagoon Vehicle (
LAGOON_VEHICLE): Integrates Lagoon vaults, which settle in epochs.
Meta-Strategy
- MultiVehicle: A complex vehicle that orchestrates multiple sub-vehicles. It uses a queue-based allocation system to distribute assets across different protocols.
Utility
- Wrapper Vehicle: Used to wrap non-STEAM contracts into a STEAM-compliant interface for use within the broader ecosystem.
Sync vs Async Deep Dive
Why Async?
Many modern DeFi protocols have inherent delays that make synchronous operations impossible or dangerous:
- Cooldown Periods: Some protocols require users to wait after requesting an unstake before they can claim their assets.
- Withdrawal Queues: ERC-7540 vaults process withdrawals through request queues to manage liquidity.
- Per-Address Limits: Some protocols only allow one active request per wallet address at a time.
The Account Clone Pattern
To handle protocols with per-address limitations, the system uses the Account Clone Pattern. Lightweight Account contract clones are deployed on-the-fly. Each clone acts as a temporary wallet for a single query, enabling hundreds of concurrent operations without address conflicts.
Impact on Callers
When interacting with asynchronous vehicles, callers must follow a multi-step process:
- Call
create()— The query enters thePROCESSINGstate. - Monitor
ready(query)— Wait until it returnstrue. - Call the appropriate transition method (usually
unlock()) to finalize the operation.
In many cases, a Keeper network can automate steps 2 and 3 for the user.
Vehicle Inheritance
The Vehicle architecture uses a hierarchical inheritance pattern to maximize code reuse while allowing for protocol-specific logic:
IVehicle (Interface)
└── BaseVehicle (Abstract)
├── SingleAssetBaseVehicle (Abstract)
│ ├── AaveV3Vehicle
│ ├── MorphoBlueVehicle
│ ├── ERC4626Vehicle
│ ├── ERC7540Vehicle
│ └── LagoonVehicle
└── MultiVehicle - IVehicle: The core STEAM interface defining all lifecycle methods, views, and events.
- BaseVehicle: Implements common functionality like ERC20 shares, query management, and reentrancy protection. It uses a facet pattern to stay within the 24KB Ethereum bytecode limit.
- SingleAssetBaseVehicle: Provides default routing, input/output validation, and standard share pricing math for vehicles dealing with a single underlying asset.
- Concrete Vehicles: The final implementation layer containing the specific logic for interacting with external protocols (e.g., calling Aave’s
supplyorwithdraw).