Documentation

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 RECOVERING state, 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:

StateDescriptionTerminal?
EMPTYNo query exists with this IDNo
PROCESSINGVehicle received assets, performing protocol operationsNo
PAUSEDPaused for external condition (cooldown, oracle, queue)No
UNLOCKINGProcessing complete, output ready for claimNo
SETTLEDQuery complete, assets distributed to receiverYes
RECOVERINGError occurred, assets being recoveredNo
REJECTEDQuery failed, assets returned to ownerYes

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:

MethodFrom StatesTo StatesDescription
create(query)EMPTYPROCESSING or UNLOCKINGInitiates a new query. Pulls input assets from the caller.
resume(query)PAUSEDPROCESSINGResumes a paused query after an external condition is met.
unlock(query)UNLOCKINGSETTLEDFinalizes the query and distributes the output assets to the receiver.
recover(query)RECOVERINGREJECTEDRecovers from an error state and returns the input assets to the owner.

Key Constraints

  • Path Isolation: A query in the UNLOCKING state cannot reach RECOVERING. The success path is strictly separated from the error path once processing is complete.
  • Terminality: SETTLED and REJECTED are terminal states. No further transitions are possible once a query reaches these states.
  • Atomic Creation: There is no direct transition to REJECTED from create(). If create() 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:

MethodReturnsDescription
state(query)StateReturns the current state of a specific query.
estimate(asset, mode, estimationType)AssetEstimated output including fees. What the user will actually receive.
convert(asset, sharesToAssets)AssetPure mathematical conversion excluding fees. Useful for share price calculation.
asset()addressThe underlying base asset of the vehicle (e.g., USDC).
routes()(Route[], Route[])Supported deposit routes and redeem routes, as two separate arrays.
totalAssets()uint256Total value of all assets managed by the vehicle.
maxDeposit(account)AssetMaximum that can currently be deposited, for that account.
maxRedeem(account)AssetMaximum that can currently be redeemed, for that account.
demand(query)uint256The input the query still needs before it can proceed.
ready()boolWhether the vehicle is ready to take queries. Takes no arguments.
error(query)bytesReturns 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:

  1. Call create() — The query enters the PROCESSING state.
  2. Monitor ready(query) — Wait until it returns true.
  3. 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 supply or withdraw).