In Railnet smart contracts, a Yield Source is connected via a Vehicle adapter. See Glossary for all terminology.
When you need a custom adapter
You must build a custom async adapter if your protocol has any of the following:- Cooldown periods — the protocol requires a waiting period before assets can be claimed (e.g., Ethena’s
sUSDeunstaking cooldown) - Withdrawal queues — the protocol processes withdrawals in batches or FIFO order (e.g., Syrup/Maple Finance)
- Per-address restrictions — the protocol limits active requests to one per wallet address
Async vs sync state flow
The key difference is thePROCESSING state. In a sync adapter, create() transitions directly to UNLOCKING. In an async adapter, create() transitions to PROCESSING, where the query remains until the external condition is met.
Synchronous flow:
PROCESSING to UNLOCKING is protocol-driven — it happens when the underlying protocol’s conditions are met (e.g., block.timestamp > cooldownEnd). The adapter’s state() view function checks these conditions and reports the updated state.
The Account Clone pattern
Protocols with per-address restrictions create a problem: if all redemptions flow through a single adapter address, only one withdrawal can be active at a time. Railnet solves this with the Account Clone pattern. When an async redemption starts:- The adapter deploys a lightweight
Accountcontract clone. - Assets (e.g., pool tokens) transfer to this specific Account.
- The Account initiates the protocol interaction (e.g.,
requestRedeemorcooldownShares). - The query stores the Account address for later retrieval.
Implementation guide
Inherit from SingleAssetBaseVehicle
Your async adapter inherits from the same base as sync adapters. The difference is in how you implement the lifecycle methods.
Implement create with PROCESSING state
For deposits, your
_create() may still return UNLOCKING if the protocol accepts deposits synchronously. For redemptions, return PROCESSING to signal that the operation requires time.Implement state checking
Override the
_state() function to check whether the external condition is met. When it is, return UNLOCKING to signal that the query is ready for settlement.Implement unlock for async redeems
When the query reaches
UNLOCKING, the _unlock() function claims assets from the Account clone and transfers them to the receiver.Monitoring async queries
To track the progress of an async redemption, poll thestate() view function:
Protocol-specific patterns
Ethena (cooldown pattern)
Ethena (cooldown pattern)
Ethena’s
sUSDe requires a cooldown period before unstaking. The adapter:- Deploys an Account clone and calls
cooldownShares()on the Ethena contract. - The query stays in
PROCESSINGuntilblock.timestamp > cooldownEnd. - The
state()function checks the timestamp and transitions toUNLOCKINGwhen ready. - On
unlock(), the Account claims the unstaked USDe and transfers it to the receiver.
Syrup (queue pattern)
Syrup (queue pattern)
Syrup (Maple Finance) processes withdrawals in batches through a withdrawal queue. The adapter:
- Deploys an Account clone and calls
requestRedeem()on the Syrup pool. - The query stays in
PROCESSINGuntil the pool manager processes the batch. - The
state()function checks the withdrawal manager for completion. - On
unlock(), the Account claims the assets and transfers them to the receiver.
Keeper integration
In production, you do not expect users to manually pollstate() and call unlock(). Instead, a keeper bot automates these transitions:
- The keeper monitors all active queries by polling
state(). - When a query transitions to
UNLOCKING, the keeper callsunlock(). - When a query transitions to
RECOVERING, the keeper callsrecover().
Next steps
Set up keeper automation
Automate state transitions for your async adapter.
Write adapter tests
Test both sync and async paths with the STEAM testing framework.