Skip to main content
In Railnet smart contracts, a Yield Source is connected via a Vehicle adapter. See Glossary for all terminology.
This tutorial walks you through wrapping an existing ERC-4626 vault as a Railnet yield source using the built-in ERC4626Vehicle adapter. By the end, you will have a deployed adapter that accepts deposits, issues shares, and processes redemptions — all through the STEAM state machine. A synchronous adapter completes its operations within a single transaction. When create() is called, the query transitions directly from EMPTY to UNLOCKING, meaning assets are immediately deposited and ready for the user to claim.

Prerequisites

  • A deployed ERC4626VehicleFactory (or the factory for your adapter type) — see Supported protocols for factory addresses per chain
  • An underlying yield source (e.g., an ERC-4626 vault address)
  • Underlying assets (e.g., USDC) for the initial deposit
  • Foundry installed and configured

Tutorial

1

Understand the deployment model

Adapters are deployed through factory contracts. The factory handles deterministic CREATE2 deployment, performs an initial deposit to protect against inflation attacks, burns the initial shares, and enables the adapter for public use.You call the factory’s spawn function with a SpawnParams struct that configures your adapter.
2

Prepare spawn parameters

Define the configuration for your adapter deployment. This includes references to your yield source, access control, fee management, and initial deposit sizing.
ERC4626VehicleFactory.SpawnParams memory params = ERC4626VehicleFactory.SpawnParams({
    vault: address(myVault),
    accessControl: myAccessControl,
    feeManager: myFeeManager,
    modulesManager: myModulesManager,
    querySalt: bytes32(0),
    deploymentSalt: bytes32(uint256(1)),
    initialDepositSize: 1e18, // 1 unit of underlying asset
    initialExpectedSupply: 1e18
});
The initialDepositSize bootstraps the adapter with real liquidity. The factory burns the shares minted from this deposit to prevent inflation attacks on the share price.
3

Deploy the adapter

Call the factory’s spawn function. This deploys the Vehicle proxy, initializes it, performs the initial deposit, burns shares, validates supply, and enables the adapter.
ERC4626Vehicle vehicle = factory.spawn(params);
The factory emits a SpawnedERC4626Vehicle event with the new adapter address.
4

Create a deposit query

In STEAM, every operation starts with a Query. Define your deposit query with the owner, receiver, mode, input assets, and a unique salt.
Query memory depositQuery = Query({
    owner: address(this),
    receiver: address(this),
    mode: Mode.DEPOSIT,
    input: new Asset[](1),
    output: new Asset[](0),
    salt: bytes32(uint256(123)),
    data: ""
});
depositQuery.input[0] = Asset({
    asset: address(underlyingAsset),
    value: 100e18
});
5

Execute the deposit

The deposit follows the STEAM two-step lifecycle: create then unlock.First, approve the Vehicle to pull your assets, then create the query:
underlyingAsset.approve(address(vehicle), 100e18);
vehicle.create(depositQuery);
Because this is a synchronous adapter, create() immediately deposits the assets into the underlying vault and transitions the query to UNLOCKING.Then settle the query to receive your shares:
vehicle.unlock(depositQuery);
The query transitions to SETTLED, and you receive Vehicle shares.
6

Verify the deposit

Check your balance of Vehicle shares to confirm the deposit succeeded.
uint256 shares = vehicle.balanceOf(address(this));
// shares > 0 and matches expected amount based on vault exchange rate
7

Execute a redemption

To redeem, create a new query with Mode.REDEEM and provide your Vehicle shares as input.
Query memory redeemQuery = Query({
    owner: address(this),
    receiver: address(this),
    mode: Mode.REDEEM,
    input: new Asset[](1),
    output: new Asset[](0),
    salt: bytes32(uint256(456)),
    data: ""
});
redeemQuery.input[0] = Asset({
    asset: address(vehicle),
    value: shares
});

// Create and unlock in sequence
vehicle.create(redeemQuery);
vehicle.unlock(redeemQuery);
The Vehicle burns your shares, withdraws assets from the underlying vault, and transfers the underlying assets to your receiver address.
8

Verify the redemption

Confirm you received the underlying assets.
uint256 finalBalance = underlyingAsset.balanceOf(address(this));
// finalBalance increased by the amount withdrawn from the vault

Sync adapter state flow

In a synchronous adapter, the query lifecycle is straightforward:
EMPTY --[create()]--> UNLOCKING --[unlock()]--> SETTLED
There is no PROCESSING or WAITING state because the underlying protocol completes the operation immediately within the create() transaction.

What your adapter implements

When building a custom sync adapter (rather than using the built-in ERC4626Vehicle), you inherit from SingleAssetBaseVehicle and implement:
MethodPurpose
_create()Execute the deposit or redeem on your protocol. Return UNLOCKING for sync.
_unlock()Transfer output assets to the receiver. Return SETTLED.
_maxDeposit()Return the maximum depositable amount based on protocol constraints.
_maxRedeem()Return the maximum redeemable amount based on protocol liquidity.
_totalAssets()Return the total underlying assets managed by this adapter.

Next steps

Build a custom adapter

Handle protocols with withdrawal delays or cooldown periods.

Write adapter tests

Validate your adapter with the STEAM testing framework.