# Compliance & access control Source: https://docs.railnet.org/conduits/compliance Configure whitelisting, KYC gating, and role-based permissions for your Conduit Railnet uses **External Access Control (EAC)** — a flexible role system that supports global, scoped, and public roles. As a Conduit owner, you configure EAC to control who can operate strategies, execute STEAM queries, manage fees, and more. ## Role types Standard roles that apply across the entire protocol. If you grant an account the `VEHICLE_STEAM_DEPOSIT` role globally, they can create deposit queries on **all** yield sources. `VEHICLE_STEAM_REDEEM` works the same way for redeem operations. ```solidity Solidity theme={null} accessControl.grantRole(Roles.VEHICLE_STEAM_DEPOSIT, operatorAddress); accessControl.grantRole(Roles.VEHICLE_STEAM_REDEEM, operatorAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` Roles restricted to a **specific contract**. Grant the `VEHICLE_STEAM_DEPOSIT` role scoped to a single yield source, and the account can only create deposit queries on that source. ```solidity Solidity theme={null} accessControl.grantScopedRole( Roles.VEHICLE_STEAM_DEPOSIT, address(myVehicle), // scope operatorAddress ); // VEHICLE_STEAM_REDEEM can be granted independently accessControl.grantScopedRole( Roles.VEHICLE_STEAM_REDEEM, address(myVehicle), operatorAddress ); ``` ```typescript TypeScript theme={null} // Coming soon ``` Scoped roles are encoded as `keccak256(abi.encodePacked(role, scope))`. Roles effectively granted to everyone. When a role is public, `hasRole` checks always return `true` regardless of the account. Deposits and redeems can be opened independently. ```solidity Solidity theme={null} // Open deposits on a specific Vehicle accessControl.setScopedRolePublic( Roles.VEHICLE_STEAM_DEPOSIT, address(myVehicle), true ); // Open redeems on the same Vehicle accessControl.setScopedRolePublic( Roles.VEHICLE_STEAM_REDEEM, address(myVehicle), true ); ``` ```typescript TypeScript theme={null} // Coming soon ``` The `DEFAULT_ADMIN_ROLE` can never be made public. ## Deploy and configure access control Every Conduit needs an EAC contract. This section walks you through deploying one and granting the roles your product needs. Deploy the EAC contract with your initial admin. Set `initialDelay` to a non-zero value (e.g. 48 hours) for production deployments — this protects admin transfers with a time delay. ```solidity Solidity theme={null} AccessControlFactory.SpawnParams memory params = AccessControlFactory.SpawnParams({ initialDelay: 48 hours, // Time-delayed admin transfer for security initialDefaultAdmin: platformAdmin, // Your platform's admin address (use multisig) initialRoles: new IExternalAccessControl.RoleAttribution[](0), deploymentSalt: keccak256("platform-eac-v1") }); ExternalAccessControl eac = eacFactory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` Use a multisig wallet (e.g. Safe) as the `initialDefaultAdmin`. This is the most privileged role in the system — it can grant and revoke any role. Authorize your operators and asset managers for the specific strategies and yield sources they manage. ```solidity Solidity theme={null} // Grant STEAM access scoped to a specific Multi-Vehicle eac.grantScopedRole(Roles.VEHICLE_STEAM_DEPOSIT, address(multiVehicle), operator); eac.grantScopedRole(Roles.VEHICLE_STEAM_REDEEM, address(multiVehicle), operator); // Grant Multi-Vehicle operation roles — these are gated on the Sector Accounting Engine IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); eac.grantScopedRole(Roles.MULTI_VEHICLE_DEPOSIT, address(accounting), operator); eac.grantScopedRole(Roles.MULTI_VEHICLE_DISPATCH, address(accounting), operator); ``` ```typescript TypeScript theme={null} // Coming soon ``` Allow the strategy to interact with specific yield sources. ```solidity Solidity theme={null} // Vehicle authorization is gated on the Vehicle Manager IVehicleManager manager = MultiVehicle(multiVehicle).manager(); eac.grantScopedRole( Roles.MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION, address(manager), platformAdmin ); ``` ```typescript TypeScript theme={null} // Coming soon ``` ```solidity Solidity theme={null} // Allow updating fees eac.grantScopedRole(Roles.FEE_MANAGER_SET_FEES, address(feeManager), feeAdmin); // Allow distributing collected fees eac.grantScopedRole(Roles.FEE_MANAGER_DISPATCH_ERC20, address(feeManager), operator); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Key roles reference ### Yield source operations | Role | Allows | | --------------------------- | ---------------------------------------------------------------------- | | `VEHICLE_STEAM_DEPOSIT` | `create()`, `resume()`, `unlock()`, `recover()` on **deposit** queries | | `VEHICLE_STEAM_REDEEM` | `create()`, `resume()`, `unlock()`, `recover()` on **redeem** queries | | `VEHICLE_SET_INTERCEPTIONS` | Configure reward interception rules | | `VEHICLE_ALLOW` | Manage module allowlist | ### Strategy management | Role | Allows | | ----------------------------------------- | ------------------------------------- | | `MULTI_VEHICLE_DEPOSIT` | Direct deposits into accounting | | `MULTI_VEHICLE_DISPATCH` | Send assets to yield sources | | `MULTI_VEHICLE_MOVE` | Move assets or shares between sectors | | `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` | Authorize/deauthorize yield sources | | `MULTI_VEHICLE_SET_QUEUES` | Configure queue parameters | | `MULTI_VEHICLE_SET_THRESHOLDS` | Set operational thresholds | | `MULTI_VEHICLE_PROGRESS_QUERY` | Advance sub-query state | ### Fee management | Role | Allows | | -------------------------------- | ------------------------- | | `FEE_MANAGER_SET_FEES` | Update fee percentages | | `FEE_MANAGER_SET_FEE_RECIPIENTS` | Update fee recipients | | `FEE_MANAGER_DISPATCH_ERC20` | Distribute collected fees | ### Infrastructure | Role | Allows | | --------------------------------- | ---------------------------------- | | `FACTORY_SPAWN` | Deploy new contracts via factories | | `BEACON_UPGRADE` | Upgrade beacon implementations | | `BEACON_FREEZE` | Permanently freeze a beacon | | `BEACON_PAUSE` / `BEACON_UNPAUSE` | Pause/unpause beacons | ## Checking permissions ```solidity Solidity theme={null} // Check global role bool hasAccess = eac.hasRole(Roles.VEHICLE_STEAM_DEPOSIT, account); // Check scoped role bool hasScopedAccess = eac.hasScopedRole( Roles.VEHICLE_STEAM_DEPOSIT, address(myVehicle), account ); // Check either global or scoped bool hasAnyAccess = eac.hasRoleOrScopedRole( Roles.VEHICLE_STEAM_DEPOSIT, address(myVehicle), account ); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Admin transfer The `DEFAULT_ADMIN_ROLE` uses a **time-delayed transfer** for security: ```solidity theme={null} eac.beginDefaultAdminTransfer(newAdmin); ``` The configured delay (e.g., 48 hours) must pass. The new admin calls: ```solidity theme={null} eac.acceptDefaultAdminTransfer(); ``` Use scoped roles whenever possible. They provide granular control and limit the blast radius if an account is compromised. # Configure fees & revenue Source: https://docs.railnet.org/conduits/configure-fees Set fee structures and revenue distribution for your Conduit Your Conduit can have its own Fee Manager to charge distribution-layer fees — independent of any fees configured on the underlying Strategy. This guide covers how to design your fee model, deploy a Fee Manager for your Conduit, and configure recipient splits. For the full fee calculation reference (formulas, preview functions, view functions), see [Fee Manager reference](/developers/contracts/fee-manager). ## Fee types | Fee type | Typical use | When applied | | ------------------- | ------------------------------------------------------------ | -------------------------------------- | | **Management fee** | Platform distribution revenue — annualized, prorated by time | Before each new query (`onOperations`) | | **Performance fee** | Optional platform share of yield — earned on new gains | Before each new query (`onOperations`) | | **Deposit fee** | Optional — deducted from deposit amounts | During `unlock` (STEAM state) | | **Redeem fee** | Optional — deducted from redemption amounts | During `unlock` (STEAM state) | All fees are in **basis points** (bps), where 10,000 bps = 100%. ## Understanding fee layers Railnet supports fees at two independent levels. Each level has its own Fee Manager contract with its own rates, caps, and recipients. * **Strategy-level fees** handle revenue distribution for the strategy operator. If you followed [Use a Strategy](/conduits/use-a-strategy), you already configured these during deployment. * **Conduit fees** (this page) are your platform's additional distribution-layer fees. They are applied on top of the Strategy's fees. Users experience the combined effect of both fee layers in the Conduit's share price. Consider the total fee load when designing your Conduit-level fees. If the underlying Strategy already charges a 2% management fee, adding another 2% at the Conduit level may be uncompetitive. ## Design your fee model A typical Conduit fee structure is lighter than the Strategy's, since the asset manager is already compensated at the Strategy level: | Fee | Rate | Recipient | Purpose | | --------------- | ----------------- | --------- | ---------------------------------------------------- | | Management fee | 0–0.5% annualized | Platform | Distribution revenue, covers platform infrastructure | | Performance fee | 0% | — | Typically zero (AM compensated at Strategy level) | | Deposit fee | 0% | — | Usually zero for growth | | Redeem fee | 0–0.25% | Platform | Optional exit friction | **No fees at all?** Pass `address(0)` as the Fee Manager when deploying your Conduit. You can still earn revenue from the Strategy-level Fee Manager configured during [Use a Strategy](/conduits/use-a-strategy). ### Common configurations No Conduit-level fees — rely entirely on Strategy-level fee distribution. Pass `address(0)` as the Fee Manager during [Conduit deployment](/conduits/quick-start). Small management fee for distribution revenue. ```solidity Solidity theme={null} FeeManager.Fees({ performanceFeeBps: 0, managementFeeBps: 50, // 0.5% annual depositFeeBps: 0, redeemFeeBps: 0 }) ``` ```typescript TypeScript theme={null} // Coming soon ``` Management fee plus a small exit fee. ```solidity Solidity theme={null} FeeManager.Fees({ performanceFeeBps: 0, managementFeeBps: 50, // 0.5% annual depositFeeBps: 0, redeemFeeBps: 25 // 0.25% exit fee }) ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Deploy the Fee Manager Deploy a Fee Manager for your Conduit. This is separate from any Fee Manager attached to the underlying Strategy. **Requires:** `FACTORY_SPAWN` role scoped to the Fee Manager factory. ```solidity Solidity theme={null} // All Conduit-level fees go to the platform FeeManager.FeeRecipient[] memory recipients = new FeeManager.FeeRecipient[](1); recipients[0] = FeeManager.FeeRecipient({ target: platformTreasury, shareBps: 10000 // 100% to platform }); FeeManagerFactory.SpawnParams memory params = FeeManagerFactory.SpawnParams({ accessControl: externalAccessControl, initialFees: FeeManager.Fees({ performanceFeeBps: 0, // AM compensated at Strategy level managementFeeBps: 50, // 0.5% annual distribution fee depositFeeBps: 0, redeemFeeBps: 0 }), initialMaxFees: FeeManager.Fees({ performanceFeeBps: 1000, // 10% max (future flexibility) managementFeeBps: 200, // 2% max depositFeeBps: 100, // 1% max redeemFeeBps: 100 // 1% max }), initialRecipients: recipients, deploymentSalt: keccak256("conduit-fee-manager-v1") }); FeeManager conduitFeeManager = feeManagerFactory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` `initialMaxFees` are immutable caps. You can lower fees later but never exceed these maximums. Set them thoughtfully — leave room for future adjustments. Performance and management ceilings may go up to 10,000 bps; deposit and redeem ceilings cannot exceed 9,999 bps. ## Update fees You can adjust fee rates at any time (within the max caps). **Requires:** `FEE_MANAGER_SET_FEES` role scoped to the Fee Manager. ```solidity Solidity theme={null} FeeManager.Fees memory newFees = FeeManager.Fees({ performanceFeeBps: 0, managementFeeBps: 25, // Reduce management fee to 0.25% depositFeeBps: 0, redeemFeeBps: 0 }); feeManager.setFees(newFees); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Update recipients Change how collected fees are split. Distribute pending fees first to avoid loss. **Requires:** `FEE_MANAGER_SET_FEE_RECIPIENTS` role scoped to the Fee Manager. ```solidity Solidity theme={null} // IMPORTANT: Distribute pending fees before changing recipients feeManager.dispatchERC20(IERC20(usdc)); // Update the split: add a DAO treasury FeeManager.FeeRecipient[] memory newRecipients = new FeeManager.FeeRecipient[](2); // Targets must be sorted strictly ascending by address, // and shares must sum to 10,000 bps newRecipients[0] = FeeManager.FeeRecipient({ target: platformTreasury, shareBps: 8000 // 80% to platform }); newRecipients[1] = FeeManager.FeeRecipient({ target: daoTreasury, shareBps: 2000 // 20% to DAO }); feeManager.setFeeRecipients(newRecipients); ``` ```typescript TypeScript theme={null} // Coming soon ``` Changing recipients causes old recipients to lose access to uncollected fees. Always call `dispatchERC20` first. ## Fee collection workflow Ongoing fees (management and performance) accumulate as Conduit shares minted to the Fee Manager. The Fee Manager never redeems those shares itself — a Conduit is an ERC20 share token, so you dispatch the shares to the recipients and each recipient redeems on its own schedule. Transactional deposit and redeem fees arrive already denominated in the payout asset. Dispatch the Conduit share token like any other ERC20. Recipients receive Conduit shares, split by their `shareBps`; the last recipient absorbs the rounding remainder. **Requires:** `FEE_MANAGER_DISPATCH_ERC20` role. ```solidity Solidity theme={null} feeManager.dispatchERC20(IERC20(address(conduit))); ``` ```typescript TypeScript theme={null} // Coming soon ``` Send any underlying-asset balance — transactional deposit and redeem fees — to each recipient according to its share. **Requires:** `FEE_MANAGER_DISPATCH_ERC20` role. ```solidity Solidity theme={null} feeManager.dispatchERC20(IERC20(usdc)); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Role setup for fee management As the Conduit owner, you retain fee configuration roles on your Conduit's Fee Manager. If you also own the Strategy's Fee Manager, you manage those roles separately. | Role | Grant to | Purpose | | -------------------------------- | ------------------ | ------------------------- | | `FEE_MANAGER_SET_FEES` | Platform admin | Change fee rates | | `FEE_MANAGER_SET_FEE_RECIPIENTS` | Platform admin | Change recipient splits | | `FEE_MANAGER_DISPATCH_ERC20` | Operator or keeper | Distribute collected fees | See [Roles and permissions](/developers/contracts/roles) for the complete role setup guide. ## Next steps Process deposits and withdrawals through your Conduit. Full fee calculation formulas, preview functions, and view functions. # Deposits & withdrawals Source: https://docs.railnet.org/conduits/deposits-and-withdrawals Let users deposit, view balances, and withdraw through your Conduit Your users interact with Railnet through the Conduit you deployed. The Conduit provides a simplified deposit and withdrawal interface — the same experience regardless of whether the underlying strategy uses sync protocols (Aave), async protocols (Ethena), or a mix of both. ## Deposit through a Conduit Users deposit base assets (e.g., USDC) and receive Conduit shares representing their proportional ownership. ```solidity Solidity theme={null} IERC20(usdc).approve(address(conduit), amount); ``` ```typescript TypeScript theme={null} // Coming soon ``` ```solidity Solidity theme={null} // The Conduit binds the query id to the caller: query.salt MUST be // keccak256(abi.encode(msg.sender, sourceSalt)) or create() reverts InvalidQuerySalt. bytes32 sourceSalt = keccak256(abi.encode("deposit", nonce)); Query memory query = Query({ owner: address(conduit), receiver: address(conduit), input: Asset({asset: address(usdc), value: amount}), output: Asset({asset: address(conduit.getVehicle()), value: 0}), mode: Mode.DEPOSIT, salt: keccak256(abi.encode(msg.sender, sourceSalt)), data: "" }); // Create the deposit — auto-processes for sync Vehicles (Id queryId, State state) = conduit.create(query, userAddress, sourceSalt); ``` ```typescript TypeScript theme={null} // Coming soon ``` The query's `owner` and `receiver` are the Conduit itself — the `userAddress` argument is who receives the resulting shares. `output.value` is a minimum enforced at the **Vehicle** output; `0` disables the floor. It ignores Conduit fees and the cShare exchange rate, so size expected output with `estimate()` instead. For **sync** underlying strategies, the deposit settles in the same transaction — the user receives shares immediately. For **async** underlying strategies, the query enters `PROCESSING`. A keeper will call `process()` automatically when the operation is ready to settle. ## Withdraw through a Conduit Users burn Conduit shares and receive base assets. Use `createRedeemFromConduitShares` — it converts cShares to Vehicle shares at the current rate and builds the redeem query for you. ```solidity Solidity theme={null} // Same caller-bound salt rule as deposits bytes32 sourceSalt = keccak256(abi.encode("redeem", nonce)); // Minimum asset to receive; value 0 disables the floor Asset memory outputAsset = Asset({asset: address(usdc), value: minAssetsOut}); // Burns the caller's cShares and creates the redeem — auto-processes for sync Vehicles (Id queryId, State state) = conduit.createRedeemFromConduitShares( shareAmount, outputAsset, sourceSalt, userAddress ); ``` ```typescript TypeScript theme={null} // Coming soon ``` No `approve` is required. The Conduit burns the caller's shares internally, so withdrawals are a single transaction. `createRedeemFromConduitShares` is a wrapper over `create`: it converts the cShare amount to Vehicle shares and calls `create` for you. Call `create` with a REDEEM query directly only if you already know the Vehicle-share amount — see the [Conduit reference](/developers/contracts/conduit#redeeming-which-entrypoint). Same settlement behavior as deposits: sync strategies settle immediately, async strategies are settled automatically by keepers. ## Automated settlement For async operations, **keepers** call `process()` on your Conduit's active queries when the underlying protocol is ready to settle. This means: * Your platform doesn't need to build async monitoring infrastructure * Users never need to return to manually claim after cooldown periods * The experience is the same for sync and async strategies from the user's perspective ```mermaid theme={null} sequenceDiagram participant User participant Conduit participant Vehicle participant Keeper User->>Conduit: create(deposit) Conduit->>Vehicle: create(DEPOSIT) Vehicle-->>Conduit: PROCESSING (async) Note over Vehicle: Cooldown period... Keeper->>Conduit: process(query) Conduit->>Vehicle: unlock() Vehicle-->>Conduit: SETTLED Conduit-->>User: Shares minted ``` ## Preview operations Use `estimate()` to preview deposits and withdrawals including fees before executing: ```solidity Solidity theme={null} // Preview: how many cShares will the user get for 100 USDC? Asset memory estimated = conduit.estimate( Asset({asset: address(usdc), value: 100e6}), Mode.DEPOSIT, EstimationType.OUTPUT ); // estimated.value = cShares the user will receive (after fees) // Preview: how much USDC for burning 50 cShares? Asset memory redeemEstimate = conduit.estimate( Asset({asset: address(conduit), value: 50e18}), Mode.REDEEM, EstimationType.OUTPUT ); ``` ```typescript TypeScript theme={null} // Coming soon ``` `estimate()` includes fees and slippage. For pure share-to-asset conversion without fees, use `convert()` instead. # Distribute yield Source: https://docs.railnet.org/conduits/index Build your own yield experience with Conduits — one integration for fees, compliance, branding, and access to every strategy on the network A Conduit is how you build a unique earn experience for your users. They deposit, earn, and withdraw — without tracking async settlement states, managing cooldown periods, or dealing with protocol-specific flows. Keepers handle all of that behind the scenes, so deposits and withdrawals just work. One API gives you real-time positions, performance, and fee reporting across everything your Conduit connects to. Behind a single Conduit, you can connect to an allocation strategy that composes multiple yield sources, an advanced strategy running its own execution logic, or a single yield source directly. Compose these building blocks into something no one else offers — that's the experience your users see, not the protocols underneath. You set the fees, compliance rules, and branding. Want different pricing for different customer segments? Deploy a Conduit for your standard plan and another for your premium tier with lower fees and exclusive access — both backed by the same strategy, same liquidity. ## Who builds with Conduits Exchanges, neobanks, wallets, and fintechs use Conduits to add yield to their product. Deploy a Conduit with your own fee structure, KYC rules, and branded receipt token — your users see an "Earn" button, not a DeFi protocol. One integration gives you access to every strategy on the network. An AM with a sales team uses Conduits as distribution channels. Deploy one strategy, then create a Conduit for each platform or client segment — each with its own fees, compliance rules, and access control. Scale distribution without redeploying the same strategy. Offer multiple risk profiles — conservative, balanced, aggressive — each pointing at a different strategy. One Conduit per product, each with its own receipt token, compliance rules, and fee structure. One integration, multiple offerings. ## What you configure | Control | What it means | | ------------------------ | ------------------------------------------------------------------------------------------------------------- | | **Fee structure** | Management, performance, deposit, and redeem fees — all configurable with immutable caps | | **Revenue distribution** | Split collected fees among any combination of recipients — your platform treasury, partners, a DAO, or others | | **Compliance** | Allowlists, blocklists, and sanctions oracle integration via AccountList | | **Transfer policy** | Permissionless transfers, restricted to approved accounts, or mint-and-burn only | | **Receipt token** | Your Conduit issues its own ERC20 receipt token with a custom name and symbol | ## Two paths to a Conduit Point your Conduit at a managed strategy that allocates across multiple yield sources. You get diversification without operating the strategy yourself. Deploy a strategy you own, set the guardrails (authorized sources, fee caps, risk limits), and delegate day-to-day management to an asset manager. ## What you get One integration covers all current and future yield sources — Aave V3, Morpho Blue, Ethena, Lagoon, and any ERC-4626 or ERC-7540 vault. The Conduit abstracts away protocol-specific complexity. Each Conduit has its own fee structure (management, performance, deposit, redeem). Revenue is attributed per-Conduit, enabling custom rev-share arrangements. AccountList-based access control with allowlists, blocklists, and sanctions oracle integration. Transfer policies govern receipt token transferability. Keepers automatically settle async operations. Users never need to return to manually claim after cooldown periods — deposits and withdrawals just work. Real-time portfolio data through GraphQL — positions, performance, fee accrual, and transaction history. Your Conduit issues its own ERC20 receipt token with a custom name and symbol. Users hold your token, not the underlying strategy's. ## Guides Deploy your first Conduit in minutes. Select an existing strategy or deploy the infrastructure for a bespoke one. Define guardrails and delegate operations to an asset manager. Set fee structures and revenue distribution for your Conduit. Configure whitelisting, KYC gating, and role-based permissions. Let users deposit, view balances, and withdraw through your Conduit. # Mandate a strategy Source: https://docs.railnet.org/conduits/mandate-a-strategy Deploy a strategy you own, define guardrails, and delegate operations to an asset manager When you mandate a strategy, you deploy and own the infrastructure while delegating day-to-day operations to a professional asset manager. You define exactly what the asset manager can and cannot do — which yield sources they can use, how fees are split, and what roles they hold. The smart contracts enforce these boundaries, not trust. This guide walks you through the full journey: agreeing on terms, deploying the strategy, configuring guardrails, and delegating operations. ## What is a mandate? A mandate is your platform's request for an asset manager to operate a Strategy according to guardrails you define. You say: "manage this capital, but only using these yield sources, with these fee caps." The asset manager accepts and operates within those constraints. Three parties are involved: This differs from the "use an existing strategy" path where the asset manager owns the strategy and you simply point your Conduit at it. With a mandate, **you own and control the strategy** — the asset manager is your operator, not your counterparty. ## Define the terms of your mandate Before deploying anything, agree with your asset manager on what the mandate allows: | Term | What you decide | How it's enforced on-chain | | ---------------------------- | -------------------------------------------- | ----------------------------------------------------------------------- | | **Authorized yield sources** | Which protocols the AM can deploy capital to | Vehicle Manager — only explicitly authorized sources can receive assets | | **Allocation limits** | Maximum exposure per yield source | Queue configuration and allocation caps | | **Fee structure** | Fee rates, max caps, and recipient splits | Fee Manager — immutable max caps guarantee the boundaries | | **Operational scope** | Which on-chain roles the AM receives | EAC scoped roles — granular, per-contract permissions | ## Execute the mandate As the platform, you deploy and own the strategy infrastructure — the External Access Control, Fee Manager, and MultiVehicle. This gives you admin control over the entire system. Follow the deployment guide to set up: * **External Access Control (EAC)** — your central permission contract, with your platform admin as `DEFAULT_ADMIN_ROLE` * **Fee Manager** — configured with the fee structure you agreed on with the AM * **MultiVehicle** — the Strategy contract itself, along with its accounting and queue engines Step-by-step guide to deploying the strategy infrastructure as a Conduit owner. For more detail on each contract and parameter, see [Create a Strategy](/strategies/allocation/create). Decide which yield sources your strategy can use. This is the most important guardrail — the asset manager can only deploy capital to sources you have explicitly approved. Everything else is blocked at the contract level. You can add or remove authorized sources at any time. Only the owner (you) can change this list. ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(multiVehicle).manager(); // Authorize yield sources for your strategy manager.authorize(aaveVehicle); manager.authorize(compoundVehicle); ``` ```typescript TypeScript theme={null} // Coming soon ``` **Requires:** `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` role scoped to the Vehicle Manager. As the owner, grant this role to yourself — never to the asset manager. This is the formal delegation. You grant the asset manager scoped roles that allow them to operate the strategy — and nothing more. | Role category | What it allows | Scoped to | | --------------------------------------------- | -------------------------------------------------------------- | ------------------------ | | **Core operations** (deposit, move, dispatch) | Allocate capital and move it between sectors and yield sources | Sector Accounting Engine | | **Queue management** | Configure deposit and redeem allocation queues | Queue Strategy Engine | | **Query progression** | Advance async operations through their lifecycle | Sub Query Engine | | **Redemption queue** | Feed and retrieve assets from the redemption queue | Vehicle Manager | For the full code to grant all operational roles, see [Grant operational roles](/strategies/allocation/guardrails#grant-operational-roles). Never grant `DEFAULT_ADMIN_ROLE` to the asset manager. This is the most privileged role — it can grant and revoke any other role, including its own. Optionally grant the fee distribution role (`FEE_MANAGER_DISPATCH_ERC20`) to the AM or a keeper for routine fee distribution. See [Optional: grant fee collection roles](/strategies/allocation/guardrails#optional-grant-fee-collection-roles). Confirm the asset manager has exactly the right permissions — operational roles granted, admin roles retained by you. ```solidity Solidity theme={null} // Verify the AM has an operational role — MULTI_VEHICLE_DISPATCH is gated on the accounting engine ISectorAccountingEngine accounting = MultiVehicle(multiVehicle).manager().accountingEngine(); bool canDispatch = eac.hasRoleOrScopedRole( keccak256("MULTI_VEHICLE_DISPATCH"), address(accounting), assetManager ); // Verify the AM does NOT have admin roles bool hasAdmin = eac.hasRoleOrScopedRole( keccak256("DEFAULT_ADMIN_ROLE"), address(0), assetManager ); assert(!hasAdmin); // Must be false ``` ```typescript TypeScript theme={null} // Coming soon ``` **Mandate checklist:** * The AM has core operational roles (deposit, move, dispatch) * The AM has queue management and query progression roles * The AM does **not** have vehicle authorization (`MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION`) * The AM does **not** have fee configuration (`FEE_MANAGER_SET_FEES`, `FEE_MANAGER_SET_FEE_RECIPIENTS`) * The AM does **not** have `DEFAULT_ADMIN_ROLE` With the mandate in place and the asset manager operating your strategy, deploy a Conduit on top to let your users access it through your platform's interface. Deploy your first Conduit and connect it to your mandated strategy. ## Monitor the mandate Once the mandate is active, ongoing oversight is your responsibility as the strategy owner. Monitor your strategy's TVL, allocation breakdown, and operational activity through the [Railnet API](/developers/api): ```graphql GraphQL theme={null} query StrategyOverview($address: String!) { Vehicle(where: { address: { _ilike: $address } }) { supply assetSymbol assetDecimals } SectorBalance( where: { sector: { accountingEngine: { multiVehicle: { vehicle: { address: { _ilike: $address } } } } } } ) { asset value sector { name } } Query( where: { vehicle: { address: { _ilike: $address } } } order_by: { event: { tx: { block: { number: desc } } } } limit: 20 ) { mode state event { tx { block { timestamp } } } } } ``` See [API & reporting](/developers/api) for comprehensive monitoring queries. Every asset movement emits on-chain events. You can verify that the asset manager is deploying capital only to authorized yield sources and within any allocation limits you defined. The Fee Manager tracks all fee accrual and distribution on-chain. Fees flow to the configured recipients automatically, according to the splits you defined in the Fee Manager. See [Fee Manager reference](/developers/contracts/fee-manager) for details on how fees are collected and distributed. ## Adjust the mandate As the strategy owner, you can adjust mandate terms without redeploying: | Adjustment | How | Notes | | ----------------------------- | -------------------------------------------- | --------------------------------------------------------------- | | **Add a yield source** | Authorize via `vehicleManager.authorize` | Takes effect immediately — the AM can start allocating | | **Remove a yield source** | Deauthorize via `vehicleManager.unauthorize` | The AM must withdraw capital from that source first | | **Change fee rates** | Update via Fee Manager | Must stay within immutable max caps | | **Update fee recipients** | Update via Fee Manager | Dispatch pending fees before changing recipients | | **Tighten allocation limits** | Update queue configuration | Restricts future allocations; existing positions are unaffected | ## Offboard an asset manager Before revoking roles, ensure there are no pending queries that require the asset manager's roles to complete. ```graphql GraphQL theme={null} query PendingQueries($address: String!) { Query( where: { vehicle: { address: { _ilike: $address } } state: { _nin: ["SETTLED", "REJECTED"] } } order_by: { event: { tx: { block: { number: desc } } } } ) { id mode state owner event { tx { block { timestamp } } } } } ``` Allow time for async operations (like Ethena's 7-day cooldown) to settle before revoking roles. Pending operations will complete even after revocation, but new ones cannot be initiated. Once in-flight operations have settled, revoke the asset manager's roles. They will no longer be able to initiate any operations on the strategy. For the full revocation code, see [Revoke access](/strategies/allocation/guardrails#revoke-access). You can immediately delegate to a new asset manager by granting them the same set of scoped roles. Your strategy, guardrails, and fee structure remain unchanged — only the operator changes. See [Grant operational roles](/strategies/allocation/guardrails#grant-operational-roles) to onboard the new AM. ## Mandate vs. using an existing strategy Not sure which path is right? Here's how they compare: | | Use an existing strategy | Mandate a bespoke strategy | | ------------------------------ | ----------------------------- | ------------------------------------ | | **Who owns the strategy?** | The asset manager | You (the platform) | | **Who sets guardrails?** | The AM (self-imposed) | You | | **Fee structure** | The AM's terms | Your terms | | **Yield source authorization** | The AM decides | You decide | | **Can you switch AMs?** | Point to a different strategy | Revoke and re-delegate | | **Complexity** | Lower — just connect | Higher — deploy, configure, delegate | ## Next steps Configure distribution-layer fees on your Conduit and understand the two-layer fee model. Full technical reference for the delegation model and role-granting code. # Quick start Source: https://docs.railnet.org/conduits/quick-start Deploy your first Conduit in minutes A Conduit wraps an existing strategy (Multi-Vehicle or Vehicle) with your own ERC20 share token, fee structure, and access control. This guide walks you through deploying one using the ConduitFactory already deployed on each supported chain. ## Before you begin You need a strategy to wrap and an External Access Control (EAC) contract. Fee Manager and AccountList are optional. | Prerequisite | Required | How to get it | | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ | | **Strategy** (Multi-Vehicle or Vehicle) | Yes | [Use a Strategy](/conduits/use-a-strategy) | | **External Access Control (EAC)** | Yes | [Deploy an EAC](/conduits/compliance#deploy-and-configure-access-control) — your central permission contract | | **Fee Manager** | No | [Deploy a Fee Manager](/conduits/configure-fees#deploy-the-fee-manager) — or pass `address(0)` for no fees | | **AccountList** | No | [Set up compliance](/conduits/compliance) — or pass `address(0)` for permissionless access | **Already completed [Use a Strategy](/conduits/use-a-strategy)?** You already have an EAC and a Strategy-level Fee Manager deployed. You can optionally deploy a separate Fee Manager for your Conduit — see [Configure fees](/conduits/configure-fees). Skip to [Choose your configuration](#choose-your-configuration). **Starting fresh?** At minimum, deploy an EAC before proceeding — it's a single transaction. See [Compliance — Deploy and configure access control](/conduits/compliance#deploy-and-configure-access-control). ## Choose your configuration Before deploying, decide on three key parameters: Deploy a FeeManager to collect fees on your Conduit. You can configure: * **Management fee** — annualized, prorated by time elapsed * **Performance fee** — charged on gains above a high water mark * **Deposit fee** — deducted from shares received * **Redeem fee** — deducted from assets received Set to `address(0)` if you don't need fees. To deploy a Fee Manager, see [Configure fees](/conduits/configure-fees#deploy-the-fee-manager). Deploy an AccountList to control who can deposit, transfer, and hold shares. It combines an allow-list, a block-list, and an optional sanctions oracle, with one of three modes: | Mode | Behavior | | --------- | --------------------------------------------------------------------------------------- | | `OPEN` | Allow-list ignored — block-list and sanctions only | | `REGULAR` | Allow-list gates deposits; transfers only screened against the block-list and sanctions | | `STRICT` | Allow-list gates deposits **and** transfers — both parties must be listed | Precedence is sanctions, then block-list, then the allow-list mode. Redeems are the exception: any non-sanctioned holder can always exit, even if blocked or de-listed. Sanctions screening plugs into Chainalysis-style oracles via `ISanctionsList` and fails closed if the oracle reverts. Set to `address(0)` for permissionless access. To deploy an AccountList, see [Compliance](/conduits/compliance). Your Conduit's ERC20 shares are governed by a single `bool transferEnabled` latch, not a mode enum: * Spawn with `transferEnabled: false` for a mint-and-burn-only token. User-to-user cShare transfers, wrapped-query NFT transfers, and third-party-receiver creates are all denied; deposits, redeems, fee dispatch, and `forceRedeem` still work. * Spawn with `transferEnabled: true`, or flip it later with `enableTransfers()` (gated on `CONDUIT_SET_TRANSFER_ENABLED`). This is a **one-way latch**: calling it again reverts `StateUnchanged`, and transfers can never be turned back off. Both gates must pass for value to move: `transferEnabled` **and**, when an AccountList is set, `accountList.canTransfer(from, to)`. So a `STRICT` AccountList still restricts transfers to allow-listed parties even with the latch on. ## Deploy the Conduit Railnet deploys a ConduitFactory on each supported chain. Look up factory addresses in [Supported protocols](/developers/vehicles/supported-protocols). `spawn` is gated on the `CONDUIT_SPAWN` role held in the factory's own access control — Railnet's, not the EAC you pass in `params` — so have your deployer address granted that role first. ```solidity Solidity theme={null} ConduitFactory factory = ConduitFactory(CONDUIT_FACTORY_ADDRESS); ``` ```typescript TypeScript theme={null} // Coming soon ``` Define your Conduit's configuration. The `vehicle` parameter accepts any STEAM-compliant contract — a Multi-Vehicle or a Vehicle. The deposit asset is that Vehicle's own `asset()` and the initial deposit amount is read from the [AssetRegistry](/developers/contracts/asset-registry) — neither is passed here. ```solidity Solidity theme={null} ConduitFactory.SpawnParams memory params = ConduitFactory.SpawnParams({ name: "Platform USDC Yield", // Your branded share token name symbol: "pUSDCy", // Your branded share token symbol vehicle: IVehicle(MULTI_VEHICLE_ADDRESS), // The strategy to distribute feeManager: IFeeManager(feeManagerAddress), // address(0) for no fees accountList: IAccountList(accountListAddress), // address(0) for permissionless ownerRegistry: IOwnerRegistry(address(0)), // address(0) for no wrapped-query NFTs accessControl: accessControl, // Your EAC contract transferEnabled: false, // One-way latch, flip with enableTransfers() initialInterceptions: new Interceptor.Interception[](0), initialExpectedSupply: 1e6, // Minimum cShare supply after the seed deposit querySalt: bytes32(0), deploymentSalt: keccak256("platform-conduit-v1") }); ``` ```typescript TypeScript theme={null} // Coming soon ``` Look up the initial deposit amount in the AssetRegistry, approve the factory, then spawn the Conduit. The deployment salt travels inside `params`, so `spawn` takes a single argument. ```solidity Solidity theme={null} // Read the configured initial deposit from the AssetRegistry uint256 initialDeposit = assetRegistry.getInitialDepositAmount(USDC_ADDRESS); // Approve the factory to spend it IERC20(USDC_ADDRESS).approve(address(factory), initialDeposit); // Spawn the Conduit — requires CONDUIT_SPAWN IConduit conduit = factory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` The initial deposit protects against inflation attacks by bootstrapping the share supply. Once the seed query settles, the factory sends the shares it received to the burn address, checks `totalSupply() >= initialExpectedSupply`, and calls `enable()`. See [Asset registry](/developers/contracts/asset-registry) for initial deposit sizing guidance. The factory is the `msg.sender` of the seed deposit. If you pass an `accountList`, the factory address must itself satisfy `canDeposit` at spawn time (allow-listed under `REGULAR`/`STRICT`, never blocked or sanctioned), or the spawn reverts. For sync yield sources, the Conduit is enabled inside `spawn`. For async yield sources (or strategies wrapping async sources) the seed deposit cannot settle in the same transaction: the factory records a `PendingDeposit`, emits `PendingConduitDeposit`, and returns a Conduit that is not yet enabled. Wait for the deposit to settle, then finalize: ```solidity Solidity theme={null} // Burns the seed shares and enables the Conduit; reverts NoPendingDeposit if there is nothing pending factory.finalizeConduitDeposit(conduit); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Verify deployment After deployment, confirm the Conduit is operational: ```solidity Solidity theme={null} // Check the Conduit is enabled require(conduit.ready(), "Conduit not enabled"); // Verify the underlying strategy address vehicle = address(conduit.getVehicle()); require(vehicle == MULTI_VEHICLE_ADDRESS, "Wrong vehicle"); // Check the asset address asset = conduit.asset(); require(asset == USDC_ADDRESS, "Wrong asset"); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Next steps Process deposits and withdrawals through your Conduit. Configure roles and permissions for your Conduit. Set up fee structures and revenue distribution. Look up deployed factory and infrastructure addresses per chain. # Use a Strategy Source: https://docs.railnet.org/conduits/use-a-strategy Select an existing strategy or mandate a bespoke one for your Conduit This guide walks you through deploying a strategy for your Conduit. By the end, you will have a fully deployed strategy ecosystem that you own and control, with authorized yield sources ready for an asset manager to operate. If you are an asset manager deploying your own Strategy, see [Create an Allocation Strategy](/strategies/allocation/create) instead. ## Prerequisites * A wallet with funds — the deposit asset (e.g. USDC) for the initial deposit and ETH for gas * The `FACTORY_SPAWN` role on the Multi-Vehicle factory (or access to a public factory) * The factory must not be deprecated The EAC is the central permission contract for your entire strategy. As the Conduit owner, you are the initial admin — you control who can do what across all contracts. Set `initialDelay` to a non-zero value (e.g. 48 hours) for production deployments. This protects admin transfers with a time delay. ```solidity Solidity theme={null} AccessControlFactory.SpawnParams memory params = AccessControlFactory.SpawnParams({ initialDelay: 48 hours, // Time-delayed admin transfer for security initialDefaultAdmin: platformAdmin, // Your platform's admin address (use multisig) initialRoles: new IExternalAccessControl.RoleAttribution[](0), deploymentSalt: keccak256("platform-eac-v1") }); ExternalAccessControl eac = eacFactory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` Use a multisig wallet (e.g. Safe) as the `initialDefaultAdmin`. This is the most privileged role in the system — it can grant and revoke any role. Configure the fee structure for your strategy. You control fee rates, max caps, and recipient splits through the Fee Manager. ```solidity Solidity theme={null} FeeManagerFactory.SpawnParams memory params = FeeManagerFactory.SpawnParams({ accessControl: externalAccessControl, initialFees: FeeManager.Fees({ performanceFeeBps: 1000, // 10% performance fee managementFeeBps: 200, // 2% annual management fee depositFeeBps: 0, redeemFeeBps: 0 }), initialMaxFees: FeeManager.Fees({ performanceFeeBps: 2000, // 20% max managementFeeBps: 500, // 5% max depositFeeBps: 100, // 1% max redeemFeeBps: 100 // 1% max }), initialRecipients: recipients, // See "Configure fees" for recipient setup deploymentSalt: keccak256("platform-fee-manager-v1") }); FeeManager feeManager = feeManagerFactory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` `initialMaxFees` are immutable — they can never be increased. Set them high enough to allow future adjustments, but low enough to provide guarantees to users. See [Fee Manager reference](/developers/contracts/fee-manager) for detailed guidance on fee calculation formulas and recipient setup. Every strategy deployment requires an initial deposit as a security measure to prevent inflation attacks. The amount is configured per asset in the [AssetRegistry](/developers/contracts/asset-registry), and the shares minted from this deposit are sent to the burn address. ```solidity Solidity theme={null} uint256 initialDepositAmount = assetRegistry.getInitialDepositAmount(usdc); IERC20(usdc).approve(address(multiVehicleFactory), initialDepositAmount); ``` ```typescript TypeScript theme={null} // Coming soon ``` Deploy the full strategy ecosystem in a single transaction. The factory creates six interconnected contracts — all correctly linked and initialized. ```solidity Solidity theme={null} MultiVehicleFactory.SpawnParams memory params = MultiVehicleFactory.SpawnParams({ asset: IERC20(usdc), name: "Platform Yield Strategy", symbol: "pYLD", initialInterceptions: new Interceptor.Interception[](0), accessControl: externalAccessControl, feeManager: feeManager, modulesManager: modulesManager, salts: MultiVehicleFactory.Salts({ multiVehicle: keccak256("mv"), queryRedeemQueue: keccak256("qrq"), queueStrategyEngine: keccak256("qse"), sectorAccountingEngine: keccak256("sae"), subQueryEngine: keccak256("sqe"), vehicleManager: keccak256("vm"), initialDepositQuery: keccak256("idq") }), forbiddenAddresses: new address[](0), queryRegistry: queryRegistry }); MultiVehicleFactory.Contracts memory contracts = factory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` Decide which yield sources your strategy can use. Only authorized yield sources can receive assets from the strategy. This is one of the key guardrails you control as the Conduit owner. **Requires:** `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` role scoped to the Vehicle Manager. ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(contracts.multiVehicle).manager(); // Grant yourself the authorization role eac.grantScopedRole( keccak256("MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION"), address(manager), platformAdmin ); // Authorize yield sources for your strategy manager.authorize(aaveVehicle); manager.authorize(compoundVehicle); ``` ```typescript TypeScript theme={null} // Coming soon ``` The Vehicle Manager validates that a yield source uses the same base asset as the strategy, is a contract, and reports `ready()`. Confirm your strategy is correctly deployed and configured. ```solidity Solidity theme={null} address multiVehicle = address(contracts.multiVehicle); // Verify the asset address asset = MultiVehicle(multiVehicle).asset(); // Verify the total supply (should reflect initial deposit) uint256 totalSupply = MultiVehicle(multiVehicle).totalSupply(); // Verify the accounting engine link IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); ``` ```typescript TypeScript theme={null} // Coming soon ``` You can also verify via the [Railnet API](/developers/api): ```graphql theme={null} query VerifyDeployment($address: String!) { Vehicle(where: { address: { _ilike: $address } }) { address id name vehicleType symbol supply } } ``` ## What you deployed | Contract | Purpose | | ---------------------------- | ------------------------------------------------------------------------ | | **External Access Control** | Central permission system — you are the admin | | **Fee Manager** | Fee collection and distribution — you set the structure | | **Multi-Vehicle** | Main Strategy contract — users deposit and receive ERC-20 shares | | **Vehicle Manager** | Yield source authorization and configuration — you control the allowlist | | **Sector Accounting Engine** | Tracks all asset allocations across yield sources | | **Queue Strategy Engine** | Deposit and redeem priority manager | | **Sub Query Engine** | Yield source query executor | | **Query Redeem Queue** | Asynchronous redemption handler | ## The platform journey You just completed this step — your strategy is deployed with access control and fee infrastructure. Set up fee rates, max caps, and recipient splits for your strategy. You can also add Conduit-level distribution fees when you deploy your Conduit. [Configure Conduit fees](/conduits/configure-fees) · [Fee Manager reference](/developers/contracts/fee-manager) Define the mandate terms, grant operational roles, and set guardrails — while retaining control over yield source authorization, fees, and admin access. [Mandate a strategy](/conduits/mandate-a-strategy) Once your strategy is running, deploy a Conduit to let your users access it through your platform's interface. [Quick start](/conduits/quick-start) ## Next steps Set up fee structures and revenue distribution for your Conduit. Define guardrails and delegate operations to an asset manager. # API Source: https://docs.railnet.org/developers/api Railnet indexer GraphQL API — endpoint, query conventions, and core entities Railnet indexes its contracts and exposes the indexed state as a read-only GraphQL API. Use it to display balances, track deposits and redeems through the STEAM lifecycle, and build dashboards without running your own indexer. ## Endpoint ``` https://graphql-enriched.staging.railnet.org/query ``` The API is **read-only and currently unauthenticated** — send a GraphQL `query` as an HTTP `POST`. This is the **staging** endpoint. The production endpoint will be provided when available. Run these queries against the live endpoint in your browser — with schema autocomplete and docs. ## Query conventions The API follows Hasura's argument and filter grammar, not the subgraph conventions used by The Graph. ### Root fields Each entity gets two root fields, named exactly like the entity type in `PascalCase` — `Vehicle`, `Query`, `Conduit`. There is no pluralized or camelCase alias. | Root field | Returns | Arguments | | --------------- | ---------------------- | ----------------------------------------------------- | | `Vehicle` | list of matching rows | `where`, `order_by`, `limit`, `offset`, `distinct_on` | | `Vehicle_by_pk` | a single row or `null` | `id` | ```graphql GraphQL theme={null} query Roots($id: String!) { Vehicle(where: { enabled: { _eq: true } }, order_by: { name: asc }, limit: 10) { address name } Vehicle_by_pk(id: $id) { address name } } ``` Entity IDs are deterministic and chain-scoped (for example `8453_contract_0x6eea217d…`). Prefer filtering on `address` or `owner` over reconstructing an ID. ### Filtering `where` accepts one input object per entity, with a key per scalar field, per relationship, and the boolean combinators `_and`, `_or`, `_not`. | Operator | Applies to | Notes | | -------------------------------------- | ------------- | -------------------------------------------------------- | | `_eq`, `_neq` | all types | exact match | | `_in`, `_nin` | all types | list membership | | `_gt`, `_gte`, `_lt`, `_lte` | all types | numeric and lexicographic ordering | | `_is_null` | all types | `true` / `false` | | `_like`, `_ilike`, `_nlike`, `_nilike` | `String` only | SQL pattern match, case-insensitive in the `_i` variants | Addresses are stored **lowercase**. Filter them with `_ilike` so a checksummed address from a wallet or block explorer still matches: `where: { address: { _ilike: $address } }`. Relationships are filterable, so you can select through the graph instead of chaining round-trips: ```graphql GraphQL theme={null} query OpenRedeems($strategy: String!) { Query( where: { vehicle: { address: { _ilike: $strategy } } mode: { _eq: "REDEEM" } _not: { state: { _in: ["SETTLED", "REJECTED"] } } } ) { id state owner } } ``` ### Sorting and pagination `order_by` takes an object (or a list of objects for multi-key sorts) with `asc` or `desc`, and nested relationships are sortable. `limit` and `offset` work as expected; for large result sets, prefer passing the last `id` you saw as a cursor. ```graphql GraphQL theme={null} query VehiclePage($cursor: String!) { Vehicle(where: { id: { _gt: $cursor } }, order_by: { id: asc }, limit: 100) { id address name } } ``` ### Value types | Kind | Wire type | Representation | | -------------------------- | ---------------- | ----------------------------------------------------- | | addresses and byte strings | `String` | lowercase `0x`-prefixed hex | | token and share amounts | `numeric` | **decimal string** — `"3000000"`, never a JSON number | | prices and rates | `numeric` | decimal string — `"0.9996709120638021"` | | enums (`mode`, `state`, …) | lowercase scalar | uppercase string — `"DEPOSIT"`, `"SETTLED"` | ```graphql GraphQL theme={null} query ByState($states: [state!], $mode: mode!) { Query(where: { state: { _in: $states }, mode: { _eq: $mode } }) { id } } ``` ```json Variables theme={null} { "states": ["PROCESSING", "UNLOCKING"], "mode": "DEPOSIT" } ``` ## Core entities These are the entities you need for balances, product state, and operation tracking. The schema covers considerably more — engines, queues, roles, fees, and automation — which you can explore through [introspection](#explore-the-schema). ### Vehicle Every yield source and strategy is a `Vehicle` row. | Field | Type | Notes | | ----------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------- | | `address` | `String` | vehicle contract | | `name`, `symbol` | `String` | ERC-20 metadata | | `vehicleType` | `vehicletype` | discriminator string, e.g. `"MULTI_VEHICLE"` | | `asset` | `String` | base asset address | | `assetDecimals` | `Int` | decimals of `asset` | | `assetSymbol` | `String` | resolved symbol of `asset`, nullable | | `assetPriceUSD` | `numeric` | latest indexed price, nullable | | `supply` | `numeric` | share supply, always 18 decimals | | `enabled` | `Boolean` | accepting queries | | `weeklyYield`, `monthlyYield`, `inceptionYield` | `Yield` | `netApr` and `netApy` in percentage points (`4.25` = 4.25%), nullable until enough history exists | | `queries` | `[Query]` | every query against this vehicle | | `balances` | `[Balance]` | share balance per holder | ### Query One row per STEAM query — the unit of every deposit and redeem. `id` is the on-chain query id. | Field | Type | Notes | | ---------- | --------------- | ------------------------------------------------------------------------------------ | | `mode` | `mode` | `DEPOSIT` or `REDEEM` | | `state` | `state` | `EMPTY`, `PROCESSING`, `PAUSED`, `UNLOCKING`, `RECOVERING`, `REJECTED`, or `SETTLED` | | `owner` | `String` | account that opened the query | | `receiver` | `String` | account that receives the output | | `input` | `[AssetInput]` | assets committed on creation — `address` and `value` per asset | | `output` | `[AssetOutput]` | assets owed on settlement — `address` and `value` per asset | | `vehicle` | `Vehicle` | vehicle the query targets | `state` is updated in place, so the row always reflects the current state. `SETTLED` and `REJECTED` are terminal — treat everything else as in-flight. See the [STEAM standard](/developers/contracts/steam-standard) for the state machine. ### Balance Share balance of one holder in one vehicle: `owner`, `value` (18 decimals), `vehicle`. ### Conduit A distribution channel wrapping a vehicle with its own ERC-20 share token. | Field | Type | Notes | | -------------------------------------------------------- | ------------------ | ------------------------------------------------ | | `address` | `String` | conduit contract | | `name`, `symbol` | `String` | branded ERC-20 metadata | | `asset`, `assetDecimals`, `assetSymbol`, `assetPriceUSD` | — | underlying asset, same semantics as on `Vehicle` | | `supply` | `numeric` | conduit share supply | | `enabled`, `transferEnabled` | `Boolean` | open for deposits / share transfers | | `vehicle` | `Vehicle` | wrapped strategy | | `weeklyYield`, `monthlyYield`, `inceptionYield` | `Yield` | same semantics as on `Vehicle` | | `queries` | `[ConduitQuery]` | deposits and redeems through this conduit | | `balances` | `[ConduitBalance]` | share balance per holder | ### ConduitQuery and ConduitBalance | Entity | Fields | | ---------------- | ---------------------------------------------------------- | | `ConduitQuery` | `owner`, `conduit`, `query` → the underlying STEAM `Query` | | `ConduitBalance` | `owner`, `value`, `conduit` | ## Example queries ### Vehicle overview ```graphql GraphQL theme={null} query VehicleOverview($address: String!) { Vehicle(where: { address: { _ilike: $address } }) { address name symbol vehicleType asset assetSymbol assetDecimals assetPriceUSD supply enabled inceptionYield { netApr netApy } } } ``` ### Conduit overview ```graphql GraphQL theme={null} query ConduitOverview($address: String!) { Conduit(where: { address: { _ilike: $address } }) { address name symbol asset assetSymbol assetDecimals supply enabled transferEnabled vehicle { address name vehicleType } inceptionYield { netApr netApy } } } ``` ### Holder position across vehicles and conduits ```graphql GraphQL theme={null} query HolderPosition($owner: String!) { Balance(where: { owner: { _ilike: $owner }, value: { _gt: 0 } }) { value vehicle { address symbol assetSymbol assetDecimals } } ConduitBalance(where: { owner: { _ilike: $owner }, value: { _gt: 0 } }) { value conduit { address symbol assetSymbol assetDecimals } } } ``` ### One query's current state ```graphql GraphQL theme={null} query QueryState($id: String!) { Query_by_pk(id: $id) { id mode state owner receiver vehicle { address name assetDecimals } input { address value } output { address value } event { tx { hash block { number timestamp } } } } } ``` ### In-flight queries for a vehicle ```graphql GraphQL theme={null} query OpenQueries($address: String!, $states: [state!]) { Query( where: { vehicle: { address: { _ilike: $address } } state: { _in: $states } } order_by: { event: { tx: { block: { number: desc } } } } limit: 50 ) { id mode state owner input { address value } } } ``` ```json Variables theme={null} { "address": "0x6eea217dd6bdd3bcb4db0f20a846d66afd00963e", "states": ["PROCESSING", "PAUSED", "UNLOCKING", "RECOVERING"] } ``` ## Indexer health Check sync progress before trusting a result as current. ```graphql GraphQL theme={null} query IndexerHealth { _meta { chainId isReady progressBlock sourceBlock readyAt } } ``` `sourceBlock - progressBlock` is the indexing lag in blocks. `isReady: false` means the indexer is still backfilling and rows are incomplete. ## Explore the schema The endpoint has introspection enabled, so any GraphQL client can discover entities and fields beyond the core set above. It does not serve a browser playground itself — use the [Apollo Sandbox link](#endpoint) for that. ```graphql GraphQL theme={null} { __type(name: "Vehicle") { fields { name type { kind name ofType { name } } } } } ``` ## Next steps Combine on-chain calls with indexed data to build an earn experience. The state machine behind `Query.state` and `Query.mode`. # Access control and roles Source: https://docs.railnet.org/developers/contracts/access-control Role-based permissions and external access control in Railnet This page covers the smart contract implementation details. See [Glossary](/developers/glossary). External Access Control (EAC) is Railnet's central permission system. Based on OpenZeppelin's `AccessControlDefaultAdminRules`, it manages permissions across all vehicles and protocol components with granular, auditable control. ## Role types EAC supports three distinct types of roles to provide flexible permission management. ### Global roles Global roles are standard `bytes32` identifiers that apply across the entire protocol. When you grant an account a global role, it holds that permission for all protocol components that check for it. ```solidity Solidity theme={null} // Grant a global role eac.grantRole(VEHICLE_STEAM_DEPOSIT, aliceAddress); // Check a global role bool hasAccess = eac.hasRole(VEHICLE_STEAM_DEPOSIT, aliceAddress); // true for all vehicles // VEHICLE_STEAM_REDEEM works identically for redeem operations eac.grantRole(VEHICLE_STEAM_REDEEM, aliceAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Scoped roles Scoped roles are restricted to a specific contract address (the scope). This allows fine-grained permissions, such as granting an account the ability to manage a specific vehicle without giving it permissions over all vehicles. Internally, a scoped role is represented as `keccak256(abi.encodePacked(role, scope))`. ```solidity Solidity theme={null} // Grant VEHICLE_STEAM_DEPOSIT to Alice for a specific vehicle only eac.grantScopedRole(VEHICLE_STEAM_DEPOSIT, vehicleAddress, aliceAddress); // Alice has deposit access to this specific vehicle bool hasAccess = eac.hasScopedRole(VEHICLE_STEAM_DEPOSIT, vehicleAddress, aliceAddress); // true // But NOT to other vehicles bool hasGlobal = eac.hasRole(VEHICLE_STEAM_DEPOSIT, aliceAddress); // false // VEHICLE_STEAM_REDEEM works the same way for redeem operations eac.grantScopedRole(VEHICLE_STEAM_REDEEM, vehicleAddress, aliceAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Public roles Public roles are effectively granted to everyone. When you make a role public, `hasRole` and `hasScopedRole` checks for that role return `true` for any account. ```solidity Solidity theme={null} // Make VEHICLE_STEAM_DEPOSIT public for a specific vehicle eac.setScopedRolePublic(VEHICLE_STEAM_DEPOSIT, vehicleAddress, true); // Now anyone can deposit on that vehicle bool anyoneHasAccess = eac.hasScopedRole(VEHICLE_STEAM_DEPOSIT, vehicleAddress, bob); // true // VEHICLE_STEAM_REDEEM can be made public separately for redeem operations eac.setScopedRolePublic(VEHICLE_STEAM_REDEEM, vehicleAddress, true); ``` ```typescript TypeScript theme={null} // Coming soon ``` The `DEFAULT_ADMIN_ROLE` cannot be made public. While a role is public, `grantRole`, `revokeRole`, `renounceRole`, `grantScopedRole`, `revokeScopedRole`, and `renounceScopedRole` all revert with `PublicRoleAuthDenied` for it — make it private again before managing individual holders. ## Checking permissions EAC provides three methods for checking access: | Method | Checks | Use when | | ------------------------------------------- | -------------------------------------- | -------------------------------------- | | `hasRole(role, account)` | Global role only (or public) | You need protocol-wide access | | `hasScopedRole(role, scope, account)` | Scoped role only (or public for scope) | You need access to a specific contract | | `hasRoleOrScopedRole(role, scope, account)` | Either global OR scoped | Most common -- allows both patterns | Contracts do not call these views directly. They route through `AccessControlLib`, which wraps `hasRoleOrScopedRole` in two flavours: * `gatedCheckRole(role, scope, account)` is **fail-closed**: it reverts `ZeroAddress` when the contract has no access control set, then reverts `MissingRole` unless the account holds the role globally or scoped. Every operator-facing function uses this variant. * `ungatedCheckRole(role, account)` is **fail-open**: an unset access control means access control is disabled and the call passes. Vehicles reach it through `_onlyRoleWhenEnabled`, which additionally skips the check entirely until the Vehicle is enabled, so the factory can seed the initial deposit before roles exist. ## Admin management EAC implements a secure, time-delayed mechanism for transferring the default admin role: The current admin calls `beginDefaultAdminTransfer(newAdmin)` to start the transfer process. A configurable time delay must pass before the transfer can complete. The pending admin calls `acceptDefaultAdminTransfer()` to complete the transfer. The delay is the `initialDelay` (in seconds) passed to the constructor, and is changed later with `changeDefaultAdminDelay(uint48 newDelay)` — which is itself scheduled rather than immediate. The default admin role can never be dropped: `beginDefaultAdminTransfer(address(0))` and `renounceRole(DEFAULT_ADMIN_ROLE, ...)` both revert with `DefaultAdminCannotBeRenounced`. ## Role reference ### Factory roles | Role | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `FACTORY_SPAWN` | Spawn vehicles and ecosystem contracts (FeeManager, ModulesManager, AccountList, OwnerRegistry) via their factories | | `CONDUIT_SPAWN` | Spawn Conduits via the `ConduitFactory` | | `FACTORY_DEPRECATE` | Deprecate a factory to prevent future deployments | | `ASSET_REGISTRY_SET_ASSET` | Authorize, deauthorize, or reconfigure assets in the [AssetRegistry](/developers/contracts/asset-registry) | ### Beacon and proxy roles | Role | Description | | ---------------- | ---------------------------------------------- | | `BEACON_UPGRADE` | Upgrade the implementation address of a beacon | | `BEACON_FREEZE` | Permanently freeze a beacon (irreversible) | | `BEACON_PAUSE` | Pause beacon operations | | `BEACON_UNPAUSE` | Resume paused beacon operations | ### Vehicle roles | Role | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `VEHICLE_STEAM_DEPOSIT` | Interact with STEAM functions (`create`, `resume`, `unlock`, `recover`) on **deposit** queries | | `VEHICLE_STEAM_REDEEM` | Interact with STEAM functions (`create`, `resume`, `unlock`, `recover`) on **redeem** queries | | `VEHICLE_SET_INTERCEPTIONS` | Configure reward interception rules | | `VEHICLE_ALLOW` | Manage the allowlist of modules in the vehicle's ModulesManager | | `VEHICLE_EXEC` | Execute an allowed module on the vehicle | | `VEHICLE_PROCESS_QUEUE` | Submit a size-bounded request from an async vehicle's deposit or redeem queue (the unbounded `processRequest()` stays permissionless) | STEAM authorization is split across deposit and redeem so operators can gate each direction independently — for example, keeping deposits open while pausing redemptions during a strategy wind-down, or restricting deposits to KYC'd addresses while exits remain public. ### Conduit roles | Role | Description | | ------------------------------ | -------------------------------------------------------------------------- | | `CONDUIT_SET_INTERCEPTIONS` | Configure the Conduit's reward interception rules | | `CONDUIT_SET_TRANSFER_ENABLED` | Flip the Conduit's one-way `transferEnabled` latch via `enableTransfers()` | | `CONDUIT_FORCE_REDEEM` | Force-redeem (eject) blocked or sanctioned Conduit holders | | `CONDUIT_PROCESS` | Process Conduit queries on behalf of any owner (keepers) | | `ACCOUNT_LIST_MANAGER` | Manage AccountList membership, mode, and sanctions configuration | ### FeeManager roles | Role | Description | | -------------------------------- | ------------------------------ | | `FEE_MANAGER_SET_FEES` | Update fee percentages | | `FEE_MANAGER_SET_FEE_RECIPIENTS` | Update fee recipient addresses | | `FEE_MANAGER_DISPATCH_ERC20` | Distribute collected fees | ### Multi-Vehicle roles | Role | Description | | -------------------------------------------------- | ----------------------------------------------------------------------- | | `MULTI_VEHICLE_DEPOSIT` | Deposit assets into the accounting engine | | `MULTI_VEHICLE_MOVE` | Move assets or shares between sectors | | `MULTI_VEHICLE_DISPATCH` | Dispatch a deposit or redeem to a sub-vehicle | | `MULTI_VEHICLE_SET_QUEUES` | Configure the deposit and redeem queues | | `MULTI_VEHICLE_PROGRESS_QUERY` | Advance sub-query states in the SubQueryEngine | | `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` | Authorize, configure, or unauthorize sub-vehicles on the VehicleManager | | `MULTI_VEHICLE_SET_THRESHOLDS` | Set operational thresholds and the maximum total assets | | `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` | Feed assets to the query redeem queue | | `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` | Retrieve assets from the query redeem queue | ### ModulesManager roles | Role | Description | | ---------------- | ---------------------------------------------------- | | `MODULE_MANAGER` | Add or remove modules in the ModulesManager registry | Module registration and removal take effect immediately; there is no module timelock and no cancellation role. Authorizing a registered module on a specific target is a separate step gated by that target's `VEHICLE_ALLOW`, and executing it is gated by `VEHICLE_EXEC`. ### Keeper roles | Role | Description | | ------------------------ | ------------------------------------------ | | `JOB_LISTING_REGISTER` | Register new keeper jobs | | `JOB_LISTING_UNREGISTER` | Remove registered keeper jobs | | `JOB_LISTING_EXECUTE` | Execute registered jobs | | `KEEPER_ON_REPORT` | Forward job reports to the Keeper contract | ## FreezablePausableBeacon Vehicle and Conduit proxies read their implementation address from a `FreezablePausableBeacon`. The beacon holds the implementation; the EAC gates who may upgrade, freeze, pause, or unpause it. The beacon has two protective states: A **permanent and irreversible** state. Once frozen, the implementation address can never be upgraded again. This provides a "trustless" guarantee that the contract logic is immutable. ```solidity Solidity theme={null} // Permanently freeze the beacon (irreversible) // Requires BEACON_FREEZE, and the beacon must not be paused beacon.freeze(); ``` ```typescript TypeScript theme={null} // Coming soon ``` A **temporary and reversible** state. While paused, `implementation()` reverts with `EnforcedPause`, effectively disabling all proxies that rely on this beacon. `pauseFor` can only extend an active pause — shortening it reverts `CannotShortenPause`. ```solidity Solidity theme={null} // Pause indefinitely (BEACON_PAUSE) beacon.pause(); // Or pause for a specific duration (BEACON_PAUSE) beacon.pauseFor(7 days); // Resume operations (BEACON_UNPAUSE) beacon.unpause(); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Common permission patterns Use scoped roles to restrict the operator to a single Multi-Vehicle. Scope each role to the contract that carries the gated function, not to the Multi-Vehicle itself: `MULTI_VEHICLE_DISPATCH` is gated on the Sector Accounting Engine, while `MULTI_VEHICLE_SET_QUEUES` is gated on the Queue Strategy Engine. ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(multiVehicleAddress).manager(); address accounting = address(manager.accountingEngine()); address strategy = address(manager.queueStrategyEngine()); eac.grantScopedRole(MULTI_VEHICLE_DISPATCH, accounting, operatorAddress); eac.grantScopedRole(MULTI_VEHICLE_MOVE, accounting, operatorAddress); eac.grantScopedRole(MULTI_VEHICLE_SET_QUEUES, strategy, operatorAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` Make the STEAM roles public for a specific vehicle so any user can interact. Deposits and redeems can be opened independently: ```solidity Solidity theme={null} eac.setScopedRolePublic(VEHICLE_STEAM_DEPOSIT, vehicleAddress, true); eac.setScopedRolePublic(VEHICLE_STEAM_REDEEM, vehicleAddress, true); ``` ```typescript TypeScript theme={null} // Coming soon ``` Grant the roles needed to manage and distribute fees: ```solidity Solidity theme={null} eac.grantScopedRole(FEE_MANAGER_SET_FEES, feeManagerAddress, operatorAddress); eac.grantScopedRole(FEE_MANAGER_SET_FEE_RECIPIENTS, feeManagerAddress, operatorAddress); eac.grantScopedRole(FEE_MANAGER_DISPATCH_ERC20, feeManagerAddress, operatorAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` # Accounting and flow of funds Source: https://docs.railnet.org/developers/contracts/accounting How Railnet tracks assets across strategies with sector-based accounting In user-facing documentation, a MultiVehicle is referred to as a **Strategy**. See [Glossary](/developers/glossary). Multi-Vehicle implements rigorous double-entry accounting to track all asset movements through the system. Understanding the sector model and asset flow is essential for operating multi-vehicle deployments. ## Why sector-based accounting In a multi-protocol environment, assets are rarely static. They move between being idle in the vault, committed to a deposit query, held as shares in a sub-vehicle, or queued for redemption. Traditional balance-based accounting struggles to track these "in-flight" assets, leading to potential double-counting or inaccurate share pricing. Multi-Vehicle solves this by partitioning assets into logical **sectors** that represent their current operational state. This allows the protocol to: * Track the exact lifecycle stage of every asset * Handle asynchronous settlements without losing track of value * Provide an accurate `totalAssets()` calculation at any point in time ## The double-entry principle Every movement of assets within the system has an explicit source and destination sector. The SectorAccountingEngine: 1. Decrements the balance of the source sector 2. Increments the balance of the destination sector 3. Emits a `SectorTransfer` event for a clear audit trail This ensures total supply of accounted assets remains constant across internal transfers, making the system resistant to accounting leaks or "lost" assets. ## Core sectors | Sector | Type | Description | | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **ENTRY** | Virtual | Assets entering the system. Used as a source for initial deposits; not tracked in internal balances. | | **AVAILABLE** | Physical | Idle base assets (e.g., USDC), usable for both sub-vehicle allocation and redeem-queue fulfillment. Counted in both `totalAssets()` and `withdrawable()`. | | **RESERVED** | Physical | Base assets the operator has explicitly earmarked. Counted in `totalAssets()` but excluded from `withdrawable()`. Asset-only: share moves into RESERVED are rejected. | | **ALLOCATION** | Physical | Sub-vehicle shares from settled deposit queries, representing deployed capital across the portfolio. | | **EXIT** | Virtual | Assets leaving the system. Used as a destination for withdrawals; not tracked in internal balances. | `RESERVED` exists so that you can park liquidity neither the auto-fulfill path nor the QueueStrategyEngine can consume: both spend against `withdrawable()`, which reads only `AVAILABLE`, so a parked balance is invisible to them while still counting towards `totalAssets()`. Nothing reaches `RESERVED` implicitly — you must `move` assets in explicitly (`AVAILABLE → RESERVED` to park, `RESERVED → AVAILABLE` to unpark), or route a redeem's `settledDestination` there. ## Dynamic sectors Beyond the core sectors, the system creates dynamic sectors for per-vehicle operations: | Sector | Purpose | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Vehicle sector** | `SectorLib.toSector(vehicle)` — the `0x01` prefix followed by the vehicle address. Per-vehicle staging area between AVAILABLE/ALLOCATION and the sub-vehicle, where assets or shares accumulate before a STEAM query is created. | | **Query sector** | Temporary sector keyed by sub-query, used when recovering from a failed sub-query. Provides high-granularity tracking before funds return to a standard sector. | ## Deposit flow Assets flowing from users into sub-vehicles follow this path: The user deposits base assets into the Multi-Vehicle. Assets transfer from ENTRY (virtual) to the AVAILABLE sector. The QueueStrategyEngine processes the deposit queue and determines which sub-vehicle receives the assets. ```solidity Solidity theme={null} // QueueStrategyEngine processes deposit queue depositQueue = [{aave, 60k}, {morpho, 30k}] // Determines: allocate X to Aave vehicle ``` ```typescript TypeScript theme={null} // Coming soon ``` Assets move from AVAILABLE into the target vehicle's own sector, `SectorLib.toSector(vehicle)`, where they wait for a query to be created. `dispatch` creates the [STEAM query](/developers/contracts/steam-standard#the-state-machine) and sends the assets to the sub-vehicle, so they leave the vehicle sector and the accounting system in one step. Ephemeral accounting estimates the expected shares to keep `totalAssets` accurate during this gap. When the sub-vehicle query settles ([sync or async](/developers/contracts/sync-vs-async)), shares enter the accounting system. `ENTRY → ALLOCATION` (virtual). Ephemeral estimates are replaced with actual values. Between steps 4 and 5, assets exist outside the Multi-Vehicle's accounting system (they are held by the sub-vehicle). Ephemeral accounting tracks their estimated value during this gap to keep `totalAssets()` and share price accurate. ```mermaid theme={null} graph LR A[ENTRY] -->|"1. deposit"| B[AVAILABLE] B -->|"2. stage"| C["Vehicle sector"] C -->|"3. create query"| D["Sub-vehicle (outside accounting)"] E[ENTRY] -->|"4. settle"| F[ALLOCATION] ``` ## Redeem flow Assets flowing from sub-vehicles back to users follow the reverse path: The QueueStrategyEngine processes the redeem queue and determines which sub-vehicle to unallocate from. `ALLOCATION → vehicle sector`. `dispatch` creates a STEAM redeem query and sends the shares to the sub-vehicle, so they leave the vehicle sector and the accounting system. Ephemeral accounting estimates the expected base assets to keep `totalAssets` accurate. Base assets arrive from the sub-vehicle and land in the query's `settledDestination` — `AVAILABLE` by default, though you can route them to `RESERVED` or to another vehicle's sector instead. Ephemeral estimates are replaced with actual values. The user withdraws their base assets. `AVAILABLE → EXIT`. ```mermaid theme={null} graph LR A[ALLOCATION] -->|"1. unallocate"| B["Vehicle sector"] B -->|"2. create query"| C["Sub-vehicle (outside accounting)"] D[ENTRY] -->|"3. settle"| E[AVAILABLE] E -->|"4. withdraw"| F[EXIT] ``` ## Example: deposit cycle walkthrough A Multi-Vehicle starts with 100k in total assets. A user deposits 50k, which is allocated to an Aave sub-vehicle. | Step | Action | AVAILABLE | Aave vehicle sector | Ephemeral | ALLOCATION | totalAssets | | ---- | -------------------------------------- | --------- | ------------------- | ------------------- | ------------ | ----------- | | T0 | Initial state | 20k | — | — | 80k (shares) | 100k | | T1 | User deposits 50k | 70k | — | — | 80k | 150k | | T2 | Stage 50k into the Aave vehicle sector | 20k | 50k | — | 80k | 150k | | T3 | Query created, assets sent to Aave | 20k | — | \~50k (est. shares) | 80k | \~150k | | T4 | Settlement: shares received (ENTRY →) | 20k | — | — | 130k | \~150k | **Key insight:** `totalAssets` stays at \~150k through every step. At T2 the 50k is still accounted for, just staged in the vehicle sector. At T3 it has left the system entirely and the shares have not arrived yet (`ENTRY → ALLOCATION` has not happened); ephemeral accounting bridges this gap by estimating the expected share value. ## Asynchronous redemptions When immediate liquidity is insufficient, the QueryRedeemQueue handles fulfillment over time: 1. User requests a redemption that exceeds available liquidity 2. A **demand** is created in the QueryRedeemQueue for the unfulfilled portion 3. A keeper or operator provides liquidity by calling `feedQueryRedeemQueue` on the VehicleManager, which spends `withdrawable()` (the AVAILABLE balance) 4. FIFO position-based matching pairs demands with fulfillments as liquidity arrives 5. The user receives assets (full or partial) as liquidity becomes available ```solidity Solidity theme={null} // User redeems shares worth 2000 USDC // Immediate liquidity, withdrawable() over AVAILABLE: 500 USDC // Need: 1500 USDC more // 1. The immediate portion settles out of AVAILABLE // 2. Multi-Vehicle queues the rest: // redeemQueue.demand(remainingShares, 1500e6) // Later: operator provides liquidity from AVAILABLE // 3. manager.feedQueryRedeemQueue() // 4. The user unlocks again and receives the remaining 1500 USDC ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Ephemeral accounting One of the most critical challenges in async asset management is accounting for value that has been committed but not yet received. When a deposit to a sub-vehicle is PROCESSING, the Multi-Vehicle no longer has the base assets, but it does not yet have the shares. The SubQueryEngine manages **ephemeral accounting** to bridge this gap: ```solidity Solidity theme={null} // WITHOUT ephemeral accounting // T0: totalAssets = 100k (AVAILABLE: 20k, ALLOCATION: 80k in shares) // // User deposits 10k -> staged in the Aave vehicle sector -> query dispatched // Assets leave the system: vehicle sector -> sub-vehicle // Shares not yet received (ENTRY -> ALLOCATION hasn't happened) // // T1: totalAssets = 100k — the 10k left the system entirely! // Share price drops — new depositors get shares too cheaply ``` ```typescript TypeScript theme={null} // Coming soon ``` ```solidity Solidity theme={null} // WITH ephemeral accounting // T0: totalAssets = 100k (AVAILABLE: 20k, ALLOCATION: 80k in shares) // // User deposits 10k -> staged in the Aave vehicle sector -> query dispatched // Assets leave the system: vehicle sector -> sub-vehicle // Ephemeral: vehicle.estimate({usdc, 10k}, Mode.DEPOSIT, EstimationType.OUTPUT) ≈ 9,950 aUSDC // // T1: totalAssets = 100k + convert(9,950 aUSDC) ≈ 110k ✓ // // Settlement: actual 9,980 aUSDC received → ENTRY → ALLOCATION // Ephemeral cleared, real shares replace estimate // T2: totalAssets = 100k + convert(9,980 aUSDC) ≈ 110k ✓ ``` ```typescript TypeScript theme={null} // Coming soon ``` When a sub-query enters PROCESSING, the assets leave the vehicle sector and the accounting system altogether, and the system uses the vehicle's `estimate()` function to determine the expected output. This estimated value is tracked as ephemeral accounting against the query's `settledDestination`. On settlement, shares enter the system (`ENTRY → ALLOCATION`) and the ephemeral estimation is replaced with actual values. ## Exchange rate and share price Multi-Vehicle uses the ERC-4626 standard for pricing: ```solidity Solidity theme={null} shares = assets * totalSupply / totalAssets() ``` ```typescript TypeScript theme={null} // Coming soon ``` The `totalAssets()` function aggregates value across all sectors: ```solidity Solidity theme={null} totalAssets = AVAILABLE sector balance // idle base assets + RESERVED sector balance // operator-earmarked base assets + for each active vehicle: + assets in vehicle sector // awaiting deposit query creation + expected assets from PROCESSING redeems // ephemeral + estimateSingleAssetAssets( // redeem-output estimate, post-fee shares in ALLOCATION // settled + shares in vehicle sector // awaiting redeem query + expected shares from PROCESSING deposits // ephemeral ) ``` ```typescript TypeScript theme={null} // Coming soon ``` By including both settled and in-flight value, Multi-Vehicle ensures that its share price always reflects the true underlying value of the entire portfolio. `withdrawable()` is deliberately narrower: it reports only the `AVAILABLE` balance. `RESERVED`, vehicle-sector residuals and `ALLOCATION` shares are never counted as immediate liquidity, because reaching them requires an explicit `move` or `dispatch`. ## Monitoring Operators should track these key metrics: * **AVAILABLE** -- Idle base-asset liquidity, and everything `withdrawable()` reports * **RESERVED** -- Earmarked base assets, excluded from `withdrawable()` * **ALLOCATION** -- Total deployed capital, held as sub-vehicle shares * **Vehicle sectors** -- Assets and shares staged for the next query * **Ephemeral** -- Value in flight for PROCESSING queries * **Demand** -- Total pending redemptions in the QueryRedeemQueue * **Fulfillment** -- Available liquidity for matching * **Average wait time** -- User experience metric * **Per-vehicle** -- totalAssets, share price, query states * **Distribution** -- Actual vs target allocations across sub-vehicles ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); uint256 idle = accounting.getSectorBalance(SectorLib.AVAILABLE, IERC20(baseAsset)); uint256 parked = accounting.getSectorBalance(SectorLib.RESERVED, IERC20(baseAsset)); uint256 liquidity = accounting.withdrawable(); // equals `idle` uint256 staged = accounting.getSectorBalance(SectorLib.toSector(vehicle), IERC20(baseAsset)); // Per-vehicle detail ( uint256 sharesAfterUnlocks, // settled shares in ALLOCATION uint256 sharesBeforeCreates, // shares staged for new redeem queries uint256 expectedSharesAfterUnlocks, // ephemeral shares from in-flight deposits uint256 expectedAssetsAfterUnlocks, // projected assets from in-flight redeems uint256 assetsBeforeCreates // assets staged for new deposit queries ) = accounting.vehicleHoldings(vehicle); ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best practices:** * Monitor vehicle sectors for balances that linger, indicating slow query settlement * Maintain a sufficient AVAILABLE balance for immediate withdrawals * Regularly fulfill the QueryRedeemQueue to minimize user wait times * Validate sector balances for accounting integrity with regular audits ## Next steps How Multi-Vehicle orchestrates capital across sub-vehicles. The state machine interface that drives all deposit and redeem queries. Deploy an Allocation Strategy (MultiVehicle) ecosystem. Day-to-day operations including queue management and rebalancing. # Asset registry Source: https://docs.railnet.org/developers/contracts/asset-registry Centralized per-asset authorization and initial deposit sizing for factory spawns The **AssetRegistry** is a single contract that Vehicle and Conduit factories consult at spawn time. For each asset, it stores two things: * Whether the asset is authorized for use * The **initial deposit amount** the factory should pull and burn during deployment Registry-backed factories no longer accept an `initialDepositSize` in their `SpawnParams` — the registry is the source of truth. A caller with `FACTORY_SPAWN` cannot deploy through them for an unauthorized asset, and cannot override the initial deposit amount set by the registry admin. `ERC7540VehicleFactory` is the exception: it is not wired to a registry and still takes `initialDepositSize` in its `SpawnParams`. ## What it stores Each asset has an `AssetConfig`: ```solidity theme={null} struct AssetConfig { bool isAuthorized; uint256 initialDepositAmount; } ``` The registry exposes three reads: | Method | Returns | | -------------------------------- | -------------------------------------------------------------------------------- | | `getAssetConfig(asset)` | Full `AssetConfig` struct | | `isAssetAuthorized(asset)` | `true` if authorized | | `getInitialDepositAmount(asset)` | Configured initial deposit (reverts with `AssetNotAuthorized` if not authorized) | ## Why the initial deposit size matters Vehicles wrap underlying yield protocols (Aave V3, Morpho Blue, ERC-4626 vaults), creating two independent layers of integer share math. Each layer rounds in the protocol's favor (floor on deposit, ceil on withdraw), and because the Vehicle's share math and the underlying protocol's share math round independently, per-operation dust accumulates non-deterministically on the Vehicle's balance. Two outcomes per operation: * **Erosion** — the underlying protocol rounds harder against the Vehicle than the Vehicle rounds against the user. The Vehicle ends up with slightly less underlying value, dragging the share price down for all holders. * **Accretion** — the Vehicle rounds harder against the user. The Vehicle retains extra dust, lifting the share price. The per-operation impact is **inversely proportional** to the Vehicle's total underlying balance. A large enough initial deposit makes cumulative rounding losses economically insignificant relative to `totalAssets`. The shares minted from the initial deposit are burned to a permanent burn address, so the underlying buffer stays in the Vehicle forever, absorbing rounding losses without affecting any real user. Low-decimal assets are more sensitive to rounding than high-decimal assets because 1 wei represents a larger fraction of a unit. Size the initial deposit for USDC (6 decimals) more aggressively than for DAI (18 decimals). Configure it high enough that realistic operation volumes cannot meaningfully erode the Vehicle's balance. ## Authorization Managing registry entries requires the `ASSET_REGISTRY_SET_ASSET` role. | Role | Purpose | | -------------------------- | ------------------------------------------------------------- | | `ASSET_REGISTRY_SET_ASSET` | Authorize, deauthorize, or reconfigure assets in the registry | `setAssetConfig(asset, isAuthorized, initialDepositAmount)` is the only writer. Authorizing an asset requires a non-zero `initialDepositAmount`, otherwise the call reverts with `InvalidInitialDepositSize`; deauthorizing forces the stored amount back to `0`. Every write emits `AssetConfigSet(asset, isAuthorized, initialDepositAmount)`. When a factory is asked to spawn for an unauthorized asset, it reverts with `AssetNotAuthorized(address)` from `FactoryLib`. Constructing a factory with a zero or non-contract registry address reverts with `InvalidAssetRegistry`. Deauthorizing an asset does not affect Vehicles already deployed for that asset — it only prevents new spawns. Existing positions continue to operate normally. ## Deploy and configure The registry is typically deployed once per environment, pre-populated with the assets you expect to support. ```solidity Solidity theme={null} // Deploy with an initial set of authorized assets IAssetRegistry.InitialAssetConfig[] memory configs = new IAssetRegistry.InitialAssetConfig[](2); configs[0] = IAssetRegistry.InitialAssetConfig({ asset: USDC, initialDepositAmount: 1_000e6 // 1,000 USDC — sized for 6-decimal rounding sensitivity }); configs[1] = IAssetRegistry.InitialAssetConfig({ asset: DAI, initialDepositAmount: 1e18 // 1 DAI — 18 decimals, less sensitive }); AssetRegistry registry = new AssetRegistry(accessControl, configs); ``` ```typescript TypeScript theme={null} // Coming soon ``` Update a single asset later: ```solidity Solidity theme={null} // Authorize a new asset (requires ASSET_REGISTRY_SET_ASSET) registry.setAssetConfig(WETH, true, 0.1 ether); // Deauthorize an asset — initialDepositAmount is forced to 0 registry.setAssetConfig(oldAsset, false, 0); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## How factories use the registry The `AaveV3`, `ERC4626`, `Ethena`, `MorphoBlue`, `MultiVehicle`, `Wrapper`, and Conduit factories each hold an immutable `ASSET_REGISTRY` pointer set at construction, announced with the `AssetRegistryInitialized` event. On `spawn`: 1. The factory reads `getInitialDepositAmount(asset)` — reverts if the asset is not authorized. `ConduitFactory` uses the underlying Vehicle's `asset()`. 2. It pulls that amount from the caller and performs the initial deposit into the new Vehicle or Conduit. 3. It transfers the resulting shares to the burn address (`0x…dEaD`), permanently locking the deposit. Callers of those factories do not pass an initial deposit size anywhere in the spawn flow — they only need to hold the registry-specified amount and have approved it to the factory. ## Next steps Factory flow that reads the registry at spawn time. Walk through a MultiVehicle deployment end-to-end. # Conduits Source: https://docs.railnet.org/developers/contracts/conduit The distribution layer that connects yield strategies to platforms and their users Conduits are how platforms distribute yield strategies to their users. A platform deploys a Conduit on top of any Vehicle or Multi-Vehicle to create a branded entry point with custom shares, fees, and compliance — without building custom infrastructure for each DeFi protocol. Think of it this way: * **Vehicle / Multi-Vehicle** — The strategy (executes protocol-specific logic, manages allocations) * **Conduit** — The distribution channel (each platform deploys its own Conduit with custom fees, shares, and compliance on top of the same strategy) Users interact with Conduits. Conduits interact with Vehicles or Multi-Vehicles on their behalf. ## Why Conduits exist Asset managers build one strategy (a Multi-Vehicle) — they don't need to deploy a separate strategy for each distribution partner. Each platform deploys its own Conduit on top of the same Multi-Vehicle, with its own fee structure and access control. One strategy, N platforms. Conduits identify exactly where liquidity comes from. Because each platform has its own Conduit, the protocol knows which platform originated each deposit. This enables custom revenue-sharing deals and precise attribution for business development. Regardless of underlying protocol complexity — whether the strategy uses Aave (sync) or Ethena (async with cooldowns) — the deposit and redeem interface through a Conduit is identical. Platforms don't need to build different UX flows for different protocols. Keepers automatically process async operations on behalf of Conduit users. In standard DeFi, users must come back after a cooldown period to manually claim their assets. With Conduits, keepers call `process()` automatically — users deposit and receive their shares (or assets) without a second transaction. This is a fundamental UX improvement over standard DeFi vault patterns. ## The share token model A Conduit issues its own ERC20 shares (conduit shares) to represent ownership. Internally, the Conduit holds shares issued by the underlying Vehicle. ``` Conduit Shares <-> Vehicle Shares <-> Underlying Assets ``` As the underlying Vehicle earns yield, the Vehicle share value increases. This growth is reflected in the Conduit's `totalAssets()`, increasing the value of each conduit share. Users never touch Vehicle shares directly. ## The create/process lifecycle Conduits simplify the STEAM lifecycle into two user-facing operations. ### Create When a user calls `create(query, receiver, sourceSalt)`: The Conduit checks the receiver, ensures the Conduit is enabled, verifies the sender is allowed (via AccountList if configured), and requires the query salt to be caller-bound — `query.salt` must equal `keccak256(abi.encode(msg.sender, sourceSalt))`, otherwise the call reverts `InvalidQuerySalt`. Pulls the input from `msg.sender` -- underlying tokens for deposits; for redeems it burns the caller's conduit shares internally, so no approval is required. Approves the Vehicle and calls `vehicle.create()` to start the STEAM operation. Captures the current fee configuration from the FeeManager to ensure consistent fee application at settlement. If `receiver == msg.sender`, the Conduit automatically attempts to process the query immediately. ### Process The `process(query)` function advances the query through the STEAM state machine: 1. **Fee accrual** -- Accrues management and performance fees by minting shares to the FeeManager 2. **State check** -- Checks the current state of the query in the Vehicle 3. **PAUSED** -- Calls `vehicle.resume()` to continue 4. **UNLOCKING** -- Calls `vehicle.unlock()`, mints conduit shares (deposits) or transfers assets (redeems), and applies transactional fees 5. **RECOVERING** -- Calls `vehicle.recover()` to refund assets or restore shares ### Keeper automation For async Vehicles (Ethena, ERC-7540 vaults), the `process()` call doesn't happen immediately — the underlying protocol needs time to complete the operation. **Keepers** monitor active queries and call `process()` automatically when the operation is ready to settle. This means users never need to return to manually claim their assets. They deposit, and their shares appear once the operation settles — no second transaction required. ## Deposit flow ```solidity Solidity theme={null} // User deposits 1,000 USDC into Conduit // // 1. Conduit pulls 1,000 USDC from user // 2. Conduit approves Vehicle, calls vehicle.create(DEPOSIT) // // Synchronous Vehicle (e.g., Aave): // -> Query reaches UNLOCKING immediately // -> Conduit calls vehicle.unlock() // -> Conduit mints shares to user (minus deposit fee if any) // -> Done in one transaction // // Asynchronous Vehicle (e.g., Ethena): // -> Query enters PROCESSING // -> Keeper calls process() when ready (user doesn't need to act) // -> When ready: UNLOCKING -> unlock -> mint shares ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Withdrawal flow ```solidity Solidity theme={null} // User redeems 1,000 conduit shares // // 1. Conduit burns 1,000 shares from the user (no ERC20 approval needed) // 2. Conduit calls vehicle.create(REDEEM) // 3. Vehicle redeems underlying assets // 4. Conduit applies redeem fee (if any) // 5. Conduit transfers base assets to user ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Optional modules Conduits are extensible through optional modules set at deployment. Controls who can create queries and receive shares. * Implements allowlist or blocklist logic * Checked on every `create()` call and share transfer * Can integrate with external sanctions oracles (e.g., Chainalysis) via `ISanctionsList` Delegates query ownership to an external registry. * Enables wrapping queries into transferable ERC721 NFTs * Advanced use case for tokenized positions Handles all fee types: * **Management fee** -- Annualized, prorated by time elapsed * **Performance fee** -- Charged on gains above high water mark * **Deposit fee** -- Deducted from shares received * **Redeem fee** -- Deducted from assets received Fees are captured at query creation time and applied at settlement, ensuring consistency. ## Transfer policies cShare transfer compliance is enforced at the public ERC-20 entrypoints. Two independent gates must **both** pass for a user-initiated transfer: | Gate | Where | Behavior | | ----------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transferEnabled` | the Conduit itself | A one-way latch. While `false`, no user-to-user transfers occur. `enableTransfers()` flips it to `true` permanently — it can never be turned back off, so integrations relying on transferability cannot be bricked. | | `canTransfer(from, to)` | the AccountList | Screens both parties for sanctions, block-list membership, and the allow-list in `STRICT` mode. With no AccountList configured this gate is a no-op. | `isTransferable(from, to)` returns the combined result, and the create flow reuses it to screen a third-party `receiver`. Set the initial latch state with `transferEnabled` in the Conduit's init params. Flipping it later requires the `CONDUIT_SET_TRANSFER_ENABLED` role; calling `enableTransfers()` when it is already on reverts `StateUnchanged`. Conduit **system** moves — mint, burn, fee accrual, the redeem pull-for-burn, and `forceRedeem` — use internal ERC-20 primitives and never traverse `transfer`/`transferFrom`, so they pass unscreened by construction. The one external system caller is the FeeManager dispatching accrued cShare fees, which is exempt as a **sender** only: its recipient is still screened. ## Conduit vs direct Vehicle vs Multi-Vehicle | Aspect | Direct Vehicle | Conduit | Multi-Vehicle | | ------------------- | ------------------------- | -------------------------------------------- | -------------------------- | | **Share token** | Vehicle shares (internal) | Conduit ERC20 shares | Multi-Vehicle ERC20 shares | | **Fee management** | None | Full suite (mgmt, perf, deposit, redeem) | Full suite via FeeManager | | **Access control** | None | AccountList (allowlist/blocklist) | EAC role-based | | **Wraps** | Single protocol | Any Vehicle or Multi-Vehicle | Multiple sub-vehicles | | **User experience** | Manual query management | Simple create/process with keeper automation | Automated queue-based | | **Best for** | Protocol integrations | Platform distribution | Institutional strategies | ## Key interface summary | Method | Description | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `create(query, receiver, sourceSalt)` | Create a deposit or redeem query. Auto-processes if possible. A redeem query's `input` must be **Vehicle shares** (`input.asset == address(vehicle)`). | | `createRedeemFromConduitShares(conduitShares, outputAsset, sourceSalt, receiver)` | Redeem a **cShare** amount. Converts cShares to Vehicle shares, then calls `create` internally. | | `process(query)` | Advance a query through the STEAM lifecycle. | | `forceRedeem(user, amount, output)` | Redeem another account's shares on its behalf (permissioned). | | `holdings()` | Vehicle shares held by the Conduit, tracked internally. | | `totalAssets()` | Total underlying assets managed. | | `estimate(asset_, mode, estimationType)` | Preview the output `Asset` including fees. | | `convert(asset_, sharesToAssets)` | Pure conversion to a single `Asset`, without fees. | | `ready()` | Whether the Conduit is open for deposits. | | `getVehicle()` | The underlying Vehicle contract address. | | `asset()` | The underlying ERC20 asset address. | ### Redeeming: which entrypoint `createRedeemFromConduitShares` is a thin wrapper — it converts the cShare amount to Vehicle shares at the current ratio (previewing ongoing fees first so the ratio matches what `create` will burn at), assembles the REDEEM query, and calls the same public `create`. Both paths share one gating and pull routine, so neither needs an ERC20 approval: `create` recognizes a Vehicle-share input and burns the caller's cShares through the Conduit's internal ERC-20 primitives. Pick by the unit you hold: | You know | Call | Notes | | ---------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | A cShare amount (a user's balance) | `createRedeemFromConduitShares` | Does the cShare → Vehicle share conversion for you, and reverts `NothingToRedeem` when the amount floors to zero Vehicle shares. | | A Vehicle-share amount | `create` with a REDEEM query | You own the conversion. `input.asset` must be the Vehicle address, not the Conduit address. | A REDEEM query with `input.asset == address(conduit)` is invalid. `create` only treats a Vehicle-share input as a cShare burn; any other asset is pulled with `transferFrom` and forwarded to the Vehicle, which rejects it because a Vehicle's redeem input must be its own share token. ## Deployment Conduits are deployed via `ConduitFactory.spawn()`: Deploy the Conduit proxy via the Beacon pattern. Configure the Vehicle, FeeManager, AccountList, OwnerRegistry, access control, and the initial `transferEnabled` state. Make an initial deposit for anti-inflation protection. For sync Vehicles, the initial deposit settles immediately and the Conduit is enabled. For async Vehicles, call `finalizeConduitDeposit()` after the initial deposit settles. # Create a Conduit Source: https://docs.railnet.org/developers/contracts/conduit-deployment Build a shared entry point that aggregates flows from multiple Multi-Vehicles A **Conduit** wraps an existing Vehicle and issues its own ERC20 shares. This lets multiple Multi-Vehicles access the same yield source while the Conduit manages position aggregation, deposits, and redeems through the STEAM lifecycle. Platforms also deploy Conduits as distribution channels for existing strategies. See [Deploy a Conduit as a platform](/conduits/quick-start) for the platform-focused guide. ## When to use a Conduit Use a Conduit when: * Multiple Multi-Vehicles need access to the **same** underlying yield source * You want a **single shared position** instead of separate per-Multi-Vehicle positions * You need ERC20 transferable shares representing a pro-rata claim on the underlying Vehicle If only one Multi-Vehicle will ever use a yield source, a regular Vehicle is simpler. ## Prerequisites * A deployed STEAM Vehicle (e.g., an `ERC4626Vehicle`) * A deployed `CoreFactory`, an `AssetRegistry`, and a `FreezablePausableBeacon` holding the Conduit implementation * The `CONDUIT_SPAWN` role on the factory's [access control](/developers/contracts/access-control) — `spawn` is gated on it * The deposit asset authorized in the [AssetRegistry](/developers/contracts/asset-registry) — the factory reads the initial deposit size from there at spawn time * Familiarity with the [STEAM standard](/developers/contracts/steam-standard) ## Deploy a Conduit The `ConduitFactory` is responsible for spawning new Conduit instances. Its constructor pins the deployment plumbing and the trusted factories used to validate the optional modules you pass to `spawn`. ```solidity Solidity theme={null} ConduitFactory factory = new ConduitFactory( coreFactory, // Deterministic CREATE2 deployment conduitBeacon, // Beacon holding the Conduit implementation accessControl, // Gates CONDUIT_SPAWN and FACTORY_DEPRECATE assetRegistry, // Supplies the per-asset initial deposit amount FactoryBase(address(0)), // previousFactory — none for a first deployment feeManagerFactory, // Validates the feeManager param accountListFactory, // Validates the accountList param ownerRegistryFactory, // Validates the ownerRegistry param accessControlFactory // Validates the accessControl param ); ``` ```typescript TypeScript theme={null} // Coming soon ``` Define the Conduit's configuration. The deposit asset is the underlying Vehicle's own `asset()` and the initial deposit amount is read from the [AssetRegistry](/developers/contracts/asset-registry) — neither is passed here. ```solidity Solidity theme={null} ConduitFactory.SpawnParams memory params = ConduitFactory.SpawnParams({ name: "My Conduit", symbol: "mCON", vehicle: IVehicle(VEHICLE_ADDRESS), feeManager: IFeeManager(address(0)), // No fees for now accountList: IAccountList(address(0)), // No compliance screening ownerRegistry: IOwnerRegistry(address(0)), // No wrapped-query NFTs accessControl: accessControl, transferEnabled: false, // One-way latch, see below initialInterceptions: new Interceptor.Interception[](0), initialExpectedSupply: 1e18, // Minimum cShare supply after the seed deposit querySalt: bytes32(0), deploymentSalt: keccak256("my-conduit-salt") }); ``` ```typescript TypeScript theme={null} // Coming soon ``` `transferEnabled` is a one-way latch. Deploy with `false` to mint and burn only, then flip it once with `enableTransfers()` (gated on `CONDUIT_SET_TRANSFER_ENABLED`); it can never be turned back off. A user-to-user transfer needs both `transferEnabled` and — when an `accountList` is set — `accountList.canTransfer(from, to)`. Resolve the deposit asset from the Vehicle, look up the amount in the AssetRegistry, approve the factory, then spawn. The deployment salt travels inside `params`, so `spawn` takes a single argument. ```solidity Solidity theme={null} // The seed deposit uses the Vehicle's own asset address depositAsset = IVehicle(VEHICLE_ADDRESS).asset(); // Read the configured initial deposit from the AssetRegistry uint256 initialDeposit = assetRegistry.getInitialDepositAmount(depositAsset); // Approve the factory to spend it IERC20(depositAsset).approve(address(factory), initialDeposit); // Spawn the Conduit — requires CONDUIT_SPAWN IConduit conduit = factory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` The initial deposit protects against inflation attacks by bootstrapping the share supply. Once the seed query settles, the factory transfers the shares it received to the burn address, checks `totalSupply() >= initialExpectedSupply`, and calls `enable()` on the Conduit. The registry amount also dampens cumulative rounding losses from nested vault accounting — see [Asset registry](/developers/contracts/asset-registry) for sizing guidance. With an async underlying Vehicle the seed deposit does not settle inside `spawn`. The factory records a `PendingDeposit`, emits `PendingConduitDeposit`, and returns a Conduit that is **not yet enabled**. Call `factory.finalizeConduitDeposit(conduit)` once the query reaches `SETTLED` to burn the seed shares and enable it. The factory is the `msg.sender` of the seed deposit, so if you pass an `accountList` the factory address must itself satisfy `canDeposit` at spawn time. ## Make a deposit After deployment, deposit into the Conduit. The Conduit handles pulling assets and creating the STEAM query in the underlying Vehicle. ```solidity Solidity theme={null} // Approve the Conduit to spend your tokens IERC20(underlyingAsset).approve(address(conduit), 100e18); // The query salt must be caller-bound: `create` requires // `query.salt == keccak256(abi.encode(msg.sender, sourceSalt))`, otherwise it // reverts `InvalidQuerySalt`. This ties the query id to you so it cannot be front-run. bytes32 sourceSalt = keccak256("my-deposit"); // Define the deposit query Query memory depositQuery = Query({ owner: address(conduit), receiver: address(conduit), input: Asset({ asset: address(underlyingAsset), value: 100e18 }), output: Asset({ asset: address(conduit), value: 0 }), // 0 = no slippage floor mode: Mode.DEPOSIT, salt: keccak256(abi.encode(msg.sender, sourceSalt)), data: "" }); // Create and auto-process the deposit — `msg.sender` receives the Conduit shares conduit.create(depositQuery, msg.sender, sourceSalt); ``` ```typescript TypeScript theme={null} // Coming soon ``` After the deposit settles, check your shares: ```solidity Solidity theme={null} uint256 shares = conduit.balanceOf(msg.sender); uint256 assets = conduit.totalAssets(); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Redeem from a Conduit Call `createRedeemFromConduitShares()` with the amount of Conduit shares to burn. It converts that cShare amount to Vehicle shares and routes through the same `create` entrypoint as a deposit, so the gating and salt binding are identical. No ERC20 approval is needed — the Conduit burns your shares internally. ```solidity Solidity theme={null} // The salt is caller-bound, exactly as for a deposit bytes32 sourceSalt = keccak256("my-redeem"); // `value` is a minimum enforced at the Vehicle output; 0 disables the floor Asset memory outputAsset = Asset({ asset: address(underlyingAsset), value: 0 }); // Burn Conduit shares and send the underlying asset to `msg.sender` (Id queryId, State state) = conduit.createRedeemFromConduitShares( shares, outputAsset, sourceSalt, msg.sender ); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Next steps Add fee structures to your Conduit. Understand Conduit architecture in depth. # Fee management Source: https://docs.railnet.org/developers/contracts/fee-manager How fees are configured, collected, and distributed in Railnet This page covers the smart contract implementation details. See [Glossary](/developers/glossary). The FeeManager is an optional contract that you can attach to a Multi-Vehicle or Conduit at deployment time. It automates fee calculation and collection across the vehicle lifecycle. Once set during initialization, the FeeManager cannot be changed or removed, ensuring predictability for users. You can share a single FeeManager across multiple Multi-Vehicle contracts to maintain consistent fee behavior and centralize configuration changes. ## The 4 fee types Railnet supports four distinct fee types, each calculated and applied at different points in the vehicle lifecycle. ### Performance fee (on earnings) A fee charged on gains above the high water mark. You only collect when the strategy makes money. ```solidity Solidity theme={null} High Water Mark: $1,000,000 Current Assets: $1,100,000 Earnings: $100,000 // If performance fee = 20%: // Fee = $100,000 * 20% = $20,000 (minted as shares) // The high water mark moves to the post-operation total assets // when the vehicle calls onUpdate at the end of the operation. // Formula: // earnings = max(0, currentTotalAssets - lastTotalAssets) // performanceFeeAssets = earnings * performanceFeeBps / 10000 ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best for:** Hedge fund-style fee models, performance-aligned compensation. Users only pay when the strategy profits. If the strategy loses value, no performance fees are collected until the share price recovers past the previous high water mark. ### Management fee (time-based) An annualized fee prorated by the time elapsed since the last fee-cache update (`onUpdate`). Charged regardless of performance. ```solidity Solidity theme={null} Total Assets: 1,000,000 USDC Management Fee: 2% annual (200 bps) Time Elapsed: 30 days (2,592,000 seconds) // Fee = totalAssets * managementFeeBps * timeDelta / (10000 * 31536000) // Fee = 1,000,000 * 200 * 2,592,000 / (10000 * 31536000) // Fee = ~1,644 USDC worth of shares minted ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best for:** Covering ongoing operational costs (gas, infrastructure, monitoring). Provides a predictable revenue stream. Management fees accrue continuously based on `block.timestamp`. The first operation after a long idle period may mint substantial shares. ### Deposit fee (entry fee) A fee charged when users deposit assets. Deducted from shares received during the unlock phase. ```solidity Solidity theme={null} User Deposits: 10,000 USDC Share Price: $1.00/share Expected Shares: 10,000 shares // If deposit fee = 1%: // Fee = 10,000 * 1% = 100 shares // User Receives: 9,900 shares // FeeManager Gets: 100 shares ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best for:** Discouraging short-term "hot money" deposits, one-time onboarding costs, aligning incentives for long-term holders. ### Redeem fee (exit fee) A fee charged when users redeem shares. Deducted from assets received during the unlock phase. ```solidity Solidity theme={null} User Redeems: 10,000 shares Share Price: $1.00/share Expected Assets: 10,000 USDC // If redeem fee = 0.5%: // Fee = 10,000 * 0.5% = 50 USDC // User Receives: 9,950 USDC // FeeManager Gets: 50 USDC ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best for:** Discouraging short-term withdrawals, compensating for liquidity management costs, protecting remaining holders from withdrawal impact. ## When fees are applied | Fee type | Calculation point | Applied as | | --------------- | -------------------------------- | --------------------------- | | **Performance** | `onOperations` (during `create`) | Shares minted to FeeManager | | **Management** | `onOperations` (during `create`) | Shares minted to FeeManager | | **Deposit** | `applyFees` (during `unlock`) | Reduced shares paid to user | | **Redeem** | `applyFees` (during `unlock`) | Reduced assets paid to user | ## Fee recipients and distribution Fees can be distributed to multiple recipients with configured percentage splits: ```solidity Solidity theme={null} FeeRecipient[] memory recipients = new FeeRecipient[](3); recipients[0] = FeeRecipient({ target: operatorAddress, shareBps: 6000 // 60% to operator }); recipients[1] = FeeRecipient({ target: daoTreasuryAddress, shareBps: 3000 // 30% to DAO treasury }); recipients[2] = FeeRecipient({ target: developerAddress, shareBps: 1000 // 10% to developers }); // Must sum to exactly 10,000 (100%) and be sorted // strictly ascending by target address feeManager.setFeeRecipients(recipients); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Example configurations ```solidity Solidity theme={null} Fees({ performanceFeeBps: 2000, // 20% of profits managementFeeBps: 0, depositFeeBps: 0, redeemFeeBps: 0 }) ``` ```typescript TypeScript theme={null} // Coming soon ``` Aligned with user returns. You only charge on gains. ```solidity Solidity theme={null} Fees({ performanceFeeBps: 0, managementFeeBps: 50, // 0.5% annual depositFeeBps: 0, redeemFeeBps: 0 }) ``` ```typescript TypeScript theme={null} // Coming soon ``` Low-cost passive strategy with minimal management overhead. ```solidity Solidity theme={null} Fees({ performanceFeeBps: 1000, // 10% of profits managementFeeBps: 200, // 2% annual depositFeeBps: 50, // 0.5% entry redeemFeeBps: 50 // 0.5% exit }) ``` ```typescript TypeScript theme={null} // Coming soon ``` Professional active management with entry/exit barriers. ```solidity Solidity theme={null} Fees({ performanceFeeBps: 0, managementFeeBps: 0, depositFeeBps: 100, // 1% entry fee redeemFeeBps: 0 }) ``` ```typescript TypeScript theme={null} // Coming soon ``` Encourages committed capital with free exit. ## Configuration ### Prerequisites You need the following roles on your External Access Control, scoped to the **Fee Manager** contract: | Role | Purpose | | -------------------------------- | --------------------------------------- | | `FEE_MANAGER_SET_FEES` | Update fee percentages | | `FEE_MANAGER_SET_FEE_RECIPIENTS` | Update the list of fee recipients | | `FEE_MANAGER_DISPATCH_ERC20` | Distribute collected fees to recipients | ### Set fee percentages Configure the four fee rates. Values must not exceed the maximum limits set during Fee Manager deployment. ```solidity Solidity theme={null} FeeManager.Fees memory newFees = FeeManager.Fees({ performanceFeeBps: 1000, // 10% performance fee managementFeeBps: 200, // 2% annual management fee depositFeeBps: 0, // No deposit fee redeemFeeBps: 50 // 0.5% redeem fee }); feeManager.setFees(newFees); ``` ```typescript TypeScript theme={null} // Coming soon ``` Fee values must not exceed the `maxFees` ceiling defined at deployment. Attempting to set fees above the maximum reverts with `FeeTooHigh`. You can query the current maximums with `feeManager.maxFees()`. Performance and management ceilings may go up to 10,000 bps, but deposit and redeem ceilings are themselves capped at 9,999 bps: a 100% transactional fee is not reversible by `reverseFees`. ### Fee configuration IDs Every accepted `setFees` call derives a `configId` — `keccak256(abi.encode(fees))` — stores the `Fees` struct under it, and makes it the current configuration. `feeManager.fees()` returns `(configId, Fees)` for the current one, and `feeManager.fees(configId)` returns any past one. Passing `bytes32(0)` to the config-scoped functions resolves to the current configuration; an unknown id reverts with `NonExistingConfigId`. This versioning is what keeps asynchronous queries consistent: * **Transactional fees.** The vehicle pins the active `configId` when a query is created and reads it back on `unlock`, so a fee change mid-flight never applies retroactively to an in-flight query. Read it with `vehicle.feesConfigId(query)` — or `conduit.feesConfigId(query)` for a Conduit, which pins its own Fee Manager's id at `create` time. * **Ongoing fees.** `onUpdate` snapshots the current `configId` into the vehicle's `Cache` as `applicableConfigId`, alongside the high water mark and timestamp. The next `onOperations` uses that snapshot, so a new rate only starts accruing after the vehicle's next update. ### Fee calculation formulas The management fee is annualized and prorated by time elapsed: ``` managementFeeAssets = currentTotalAssets * managementFeeBps * timeDelta / (10000 * 31536000) ``` Where `timeDelta` is the seconds elapsed since the last update and `31536000` is the number of seconds in a year (365 days). The performance fee is applied to earnings above the high water mark: ``` earnings = max(0, currentTotalAssets - lastTotalAssets) performanceFeeAssets = earnings * performanceFeeBps / 10000 ``` The high water mark (`lastTotalAssets`) is refreshed when the vehicle calls `onUpdate` at the end of an operation, so you only pay performance fees on new gains. Transactional fees are deducted from the gross amount: ``` feeAmount = amount * feeBps / 10000 // rounded down netAmount = amount - feeAmount ``` For reverse calculations (determining gross amount from a desired net amount): ``` grossAmount = netAmount * 10000 / (10000 - feeBps) // rounded up feeAmount = grossAmount - netAmount ``` ### Configure fee recipients Fee recipients define how collected fees are split among addresses. The sum of all recipient shares must equal 10,000 bps (100%). ```solidity Solidity theme={null} FeeManager.FeeRecipient[] memory recipients = new FeeManager.FeeRecipient[](2); // Targets must be sorted strictly ascending by address, // and shares must sum to 10,000 bps // 70% to the operator recipients[0] = FeeManager.FeeRecipient({ target: operatorAddress, shareBps: 7000 }); // 30% to the DAO treasury recipients[1] = FeeManager.FeeRecipient({ target: daoTreasuryAddress, shareBps: 3000 }); feeManager.setFeeRecipients(recipients); ``` ```typescript TypeScript theme={null} // Coming soon ``` Updating recipients causes old recipients to lose access to any uncollected fees. Always call `dispatchERC20` to distribute pending fees **before** changing recipients. ### Collect accumulated fees Performance and management fees accumulate as vehicle shares held by the Fee Manager. The Fee Manager never redeems those shares itself: a vehicle is an ERC20 share token, so you dispatch the shares to the recipients and each recipient redeems on its own schedule. Check the FeeManager's share balance in the vehicle to see how much has been collected. ```solidity Solidity theme={null} uint256 accumulatedShares = multiVehicle.balanceOf(address(feeManager)); uint256 sharePrice = multiVehicle.totalAssets() / multiVehicle.totalSupply(); uint256 feeValue = accumulatedShares * sharePrice; ``` ```typescript TypeScript theme={null} // Coming soon ``` Because the vehicle is itself an ERC20 share token, dispatch it like any other asset. Recipients receive vehicle shares, split by their `shareBps`. **Requires:** `FEE_MANAGER_DISPATCH_ERC20` role. ```solidity Solidity theme={null} feeManager.dispatchERC20(IERC20(address(multiVehicle))); // Splits the Fee Manager's entire share balance across recipients; // the last recipient absorbs the rounding remainder ``` ```typescript TypeScript theme={null} // Coming soon ``` Transactional deposit and redeem fees reach the Fee Manager denominated in the payout asset rather than in shares. Dispatch each ERC20 balance the same way. **Requires:** `FEE_MANAGER_DISPATCH_ERC20` role. ```solidity Solidity theme={null} feeManager.dispatchERC20(IERC20(baseAsset)); // Distributes entire balance according to recipient shares ``` ```typescript TypeScript theme={null} // Coming soon ``` `dispatchERC20` reverts with `ZeroValue` when the Fee Manager holds none of the requested asset, so check the balance before dispatching. ### View current configuration Query the Fee Manager's current state at any time. ```solidity Solidity theme={null} // Get current fee configuration (bytes32 configId, FeeManager.Fees memory currentFees) = feeManager.fees(); // Get maximum fee limits FeeManager.Fees memory maxFees = feeManager.maxFees(); // Get current recipients FeeManager.FeeRecipient[] memory recipients = feeManager.feeRecipients(); // Get fee cache for a specific vehicle (high water mark, last update, etc.) FeeManager.Cache memory vehicleCache = feeManager.cache(address(vehicle)); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Preview fee impact Use the view functions to simulate fee calculations without executing transactions. ```solidity Solidity theme={null} // Preview performance + management fees that would be collected uint256 feeSharesToMint = feeManager.previewOnOperations( totalSupply, totalAssets, sharesDecimals, assetDecimals ); // Preview deposit or redeem fee deduction on a single asset (Asset memory netAsset, Asset memory feeAsset) = feeManager.previewApplyTransactionalFees( configId, inputAsset, Mode.DEPOSIT ); // Reverse calculation: find the gross asset needed for a target net asset (Asset memory grossAsset, Asset memory reverseFeeAsset) = feeManager.reverseFees( configId, targetNetAsset, Mode.DEPOSIT ); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Immutability and guardrails **Cannot change after deployment:** * The FeeManager address (set during Multi-Vehicle or Conduit initialization; there is no setter) * The `maxFees` ceiling (set during FeeManager deployment) **Plan carefully:** Choose `maxFees` conservatively. You can always charge less than the maximum, but you can never increase fees beyond it. For example, set `maxFees.performanceFeeBps = 3000` (30%) even if you start at 2000 (20%) to allow future flexibility. # Additional rewards Source: https://docs.railnet.org/developers/contracts/interceptors Capture and distribute extra yield from DeFi incentive programs This page covers the smart contract implementation details. See [Glossary](/developers/glossary). Additional rewards are incentive tokens distributed outside the normal yield flow by DeFi protocols. These rewards -- such as AAVE, COMP, MORPHO, or ENA tokens -- arrive via side channels and can significantly enhance user returns. The key characteristic is that these rewards do not flow through the normal deposit/redeem lifecycle. They are distributed directly to vehicle addresses by external reward contracts and must be captured and distributed separately from base yield. ## The interceptor standard The Interceptor standard is Railnet's solution for managing additional reward distribution. It provides a declarative on-chain configuration that off-chain indexing services read to correctly route rewards. ### How it works The Vehicle declares distribution rules via the `interceptions()` view function. Off-chain indexers (e.g., reward distributors, airdrop systems) read these rules to determine how to route rewards. The indexer applies the routing logic and distributes rewards according to the declared rules. By keeping distribution logic off-chain, Railnet achieves gas efficiency (no on-chain overhead for reward splitting), flexibility (rules can be updated without migrating assets), and cross-chain support (rules can specify `chainId` targets). ### Core structures ```solidity Solidity theme={null} struct Interception { address asset; // Reward token (address(0) = all assets) Recipient[] recipients; // Distribution rules } struct Recipient { address target; // Where rewards go uint256 shareBps; // Share in basis points (10000 = 100%) uint256 chainId; // Target chain (0 = all chains) } ``` ```typescript TypeScript theme={null} // Coming soon ``` **Key features:** * **Asset-specific rules** -- Different routing for different reward tokens * **Multi-recipient** -- Split rewards among multiple addresses * **Cross-chain support** -- Route rewards to different chains * **Pass-through** -- Unallocated rewards (where total `shareBps` \< 10,000) pass through to users * **Last-matching-rule semantics** -- Later rules override earlier ones for the same asset, enabling powerful override patterns ### Matching semantics Interceptors use **last-matching-rule** semantics, similar to CSS or firewall rules: 1. The indexer iterates through the `interceptions` array and selects the **last** entry that matches the reward token (either exactly or via the `address(0)` wildcard) 2. Within the selected rule, for each unique `target`, only the **last** valid entry matching the current `chainId` (or the `0` wildcard) is used This allows you to define a default rule for all tokens, then append overrides for specific high-value tokens. ## Distribution strategies All additional rewards flow directly to end users with no operator intervention. ```solidity Solidity theme={null} // Empty interceptions = 100% pass-through Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](0); multiVehicle.setInterceptions(interceptions); ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best for:** Community-first strategies, DAOs with community governance, maximum transparency setups. The operator takes a percentage of rewards; the rest passes through to users. ```solidity Solidity theme={null} // Take 5% fee on all additional rewards Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](1); interceptions[0] = Interceptor.Interception({ asset: address(0), // All reward tokens recipients: new Interceptor.Recipient[](1) }); interceptions[0].recipients[0] = Interceptor.Recipient({ target: operatorFeeAddress, shareBps: 500, // 5% chainId: 0 // All chains }); // Remaining 95% passes through to users multiVehicle.setInterceptions(interceptions); ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best for:** Professional strategy operators, sustainable long-term operations, institutional strategies. The operator intercepts all rewards, sells them for the base asset, and reinvests to increase share price for all holders. ```solidity Solidity theme={null} // Intercept 100% of all additional rewards Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](1); interceptions[0] = Interceptor.Interception({ asset: address(0), recipients: new Interceptor.Recipient[](1) }); interceptions[0].recipients[0] = Interceptor.Recipient({ target: operatorRewardVault, shareBps: 10000, // 100% chainId: 0 }); multiVehicle.setInterceptions(interceptions); ``` ```typescript TypeScript theme={null} // Coming soon ``` **Best for:** Yield aggregators, single-asset strategies, operators with trading infrastructure. ## Reinvestment mechanics When using the reinvestment strategy, the operator sells reward tokens and deposits the proceeds back into the Multi-Vehicle without minting new shares. This increases the share price for all holders. ```solidity Solidity theme={null} // Before reinvestment: // Total assets: 1,000,000 USDC // Total supply: 1,000,000 shares // Share price: 1.0 USDC/share // Rewards earned and sold: // 10,000 COMP -> 50,000 USDC // 5,000 AAVE -> 100,000 USDC // Total to reinvest: 150,000 USDC // Operator deposits without minting shares: USDC.approve(address(sectorAccountingEngine), totalUsdc); sectorAccountingEngine.deposit( totalUsdc, false // allocate=false: assets land in the AVAILABLE sector ); // After reinvestment: // Total assets: 1,150,000 USDC // Total supply: 1,000,000 shares (unchanged) // Share price: 1.15 USDC/share (+15%) ``` ```typescript TypeScript theme={null} // Coming soon ``` Calling `sectorAccountingEngine.deposit()` without minting shares increases `totalAssets` while keeping `totalSupply` constant. Since share price = `totalAssets / totalSupply`, all existing holders benefit proportionally. With `allocate=false` the proceeds stay in `SectorLib.AVAILABLE` as idle liquidity until you allocate them. ## Configuration ### Prerequisites * A deployed Multi-Vehicle or Vehicle * The `VEHICLE_SET_INTERCEPTIONS` role scoped to the Vehicle contract ### Asset-specific fees Apply different fee rates to different reward tokens. ```solidity Solidity theme={null} Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](3); // Default: 5% on all tokens interceptions[0] = Interceptor.Interception({ asset: address(0), recipients: new Interceptor.Recipient[](1) }); interceptions[0].recipients[0] = Interceptor.Recipient({ target: operatorFeeAddress, shareBps: 500, // 5% chainId: 0 }); // AAVE rewards: 10% fee interceptions[1] = Interceptor.Interception({ asset: address(AAVE_TOKEN), recipients: new Interceptor.Recipient[](1) }); interceptions[1].recipients[0] = Interceptor.Recipient({ target: operatorFeeAddress, shareBps: 1000, // 10% chainId: 1 // Ethereum mainnet only }); // COMP rewards: 3% fee interceptions[2] = Interceptor.Interception({ asset: address(COMP_TOKEN), recipients: new Interceptor.Recipient[](1) }); interceptions[2].recipients[0] = Interceptor.Recipient({ target: operatorFeeAddress, shareBps: 300, // 3% chainId: 0 }); multiVehicle.setInterceptions(interceptions); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Multi-recipient split Split fees between multiple addresses. ```solidity Solidity theme={null} // Split: 3% to operator, 2% to DAO treasury, 95% passes through to users Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](1); interceptions[0] = Interceptor.Interception({ asset: address(0), recipients: new Interceptor.Recipient[](2) }); interceptions[0].recipients[0] = Interceptor.Recipient({ target: operatorAddress, shareBps: 300, // 3% chainId: 0 }); interceptions[0].recipients[1] = Interceptor.Recipient({ target: daoTreasuryAddress, shareBps: 200, // 2% chainId: 0 }); // Remaining 95% passes through to users multiVehicle.setInterceptions(interceptions); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Hybrid fee + reinvestment Take a small operator fee and reinvest the rest. ```solidity Solidity theme={null} // 2% fee to operator, 98% reinvested for users Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](1); interceptions[0] = Interceptor.Interception({ asset: address(0), recipients: new Interceptor.Recipient[](2) }); interceptions[0].recipients[0] = Interceptor.Recipient({ target: operatorFeeAddress, shareBps: 200, // 2% fee chainId: 0 }); interceptions[0].recipients[1] = Interceptor.Recipient({ target: operatorRewardVault, shareBps: 9800, // 98% reinvest chainId: 0 }); multiVehicle.setInterceptions(interceptions); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Reinvestment workflow After rewards accumulate at the operator reward vault: Use a DEX aggregator (e.g. 1inch, Paraswap) to sell accumulated reward tokens for the Multi-Vehicle's base asset (e.g. USDC). Call `deposit()` on the Sector Accounting Engine with `allocate=false`. This increases `totalAssets` without minting new shares, raising the share price for all holders. **Requires:** `MULTI_VEHICLE_DEPOSIT` role scoped to the Sector Accounting Engine. ```solidity Solidity theme={null} // Approve and deposit reinvested rewards IERC20(usdc).approve(address(sectorAccountingEngine), totalRewards); sectorAccountingEngine.deposit(totalRewards, false); // allocate=false: assets land in SectorLib.AVAILABLE as idle liquidity ``` ```typescript TypeScript theme={null} // Coming soon ``` After reinvestment, `totalAssets` increases while `totalSupply` stays the same, resulting in a higher share price. ```solidity Solidity theme={null} // Before reinvestment: totalAssets = 1,000,000, totalSupply = 1,000,000 // Share price = 1.0 // After reinvesting 150,000 USDC from rewards: // totalAssets = 1,150,000, totalSupply = 1,000,000 // Share price = 1.15 (+15%) ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Advanced patterns Let users keep governance tokens while reinvesting yield tokens. ```solidity Solidity theme={null} // Default: Reinvest all interceptions[0] = Interceptor.Interception({ asset: address(0), recipients: reinvestRecipients // 100% to operator }); // COMP: Pass through to users (for governance voting) interceptions[1] = Interceptor.Interception({ asset: address(COMP_TOKEN), recipients: new Interceptor.Recipient[](0) // 100% pass-through }); // AAVE: Pass through to users interceptions[2] = Interceptor.Interception({ asset: address(AAVE_TOKEN), recipients: new Interceptor.Recipient[](0) }); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Strategy comparison | Strategy | User experience | Operator complexity | Share price impact | Governance rights | | ------------------ | ---------------------------- | ----------------------------- | ------------------ | ---------------------- | | **Pass-through** | Users claim multiple tokens | None | None | Users keep tokens | | **Fee collection** | Users claim reduced amounts | Low | None | Users keep most tokens | | **Reinvestment** | Simple (share price grows) | High (selling + redepositing) | Positive (+APY) | Operator controls | | **Hybrid** | Simple + small fee deduction | High | Positive | Operator controls most | ## Required role Updating interception rules requires the `VEHICLE_SET_INTERCEPTIONS` role: ```solidity Solidity theme={null} multiVehicle.setInterceptions(interceptions); // Caller must have Roles.VEHICLE_SET_INTERCEPTIONS ``` ```typescript TypeScript theme={null} // Coming soon ``` # Manage modules Source: https://docs.railnet.org/developers/contracts/modules Register, authorize, and execute modules on your Vehicle This page covers the smart contract implementation details. See [Glossary](/developers/glossary). Modules are external contracts that extend a Vehicle's capabilities — for example, distributing Merkl rewards or integrating custom logic. The Modules Manager is a shared registry of approved module addresses, and each Vehicle opts in to the modules it wants to run. As an asset manager, you typically execute modules that have already been registered and allowed. If you are operating a platform-owned Vehicle, the platform owner manages the module registry — you can execute allowed modules using the `VEHICLE_EXEC` role. ## How modules work The Modules Manager is a **registry** plus a **per-target allowlist**. It never executes a module itself: 1. **Registry** — `ModulesManager` stores approved module contract addresses. A module is identified by its address; there are no module IDs. 2. **Per-target allowlist** — each target contract must explicitly allow a registered module before that module can run on it. The allowlist is keyed `target → module → bool`. 3. **Execution on the target** — the Vehicle `delegatecall`s the module from its own `executeModule` function, so module code runs against the Vehicle's storage. `addModule` and `removeModule` take effect immediately — the Modules Manager has no timelock. Safety comes from the two-sided opt-in instead: a module must be registered by a `MODULE_MANAGER` **and** allowed by the target Vehicle before it can execute. ## Module lifecycle Every module goes through two steps: 1. **Registration** — an account with `MODULE_MANAGER` calls `addModule(module)` on the Modules Manager. The module must be a deployed contract and must not already be registered. 2. **Authorization** — the target contract calls `allowModule(module)` on the Modules Manager, which records the allowance for `msg.sender`. For a Vehicle, an account with `VEHICLE_ALLOW` calls `vehicle.allowModule(module, true)` and the Vehicle forwards the call. ## Prerequisites Each role is checked on the External Access Control of the contract that carries the gated function, either globally or scoped to that contract: | Role | Checked on | Purpose | | ---------------- | --------------- | ------------------------------------------ | | `MODULE_MANAGER` | Modules Manager | Add or remove modules from the registry | | `VEHICLE_ALLOW` | Vehicle | Allow or disallow a module on that Vehicle | | `VEHICLE_EXEC` | Vehicle | Execute an allowed module on that Vehicle | ## Add a module Adding a module is a two-step process: register it globally, then allow it on each Vehicle that should run it. Call `addModule` with the module's contract address. **Requires:** `MODULE_MANAGER` role. ```solidity Solidity theme={null} address moduleAddress = 0x...; // The module contract, implementing IModule modulesManager.addModule(moduleAddress); // Verify the module is now registered bool registered = modulesManager.isRegistered(moduleAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` Reverts with `ExistingModule` if the address is already registered, or `ZeroCode` if it is not a deployed contract. Each target contract must explicitly authorize the module before it can be executed there. For Vehicles, use the Vehicle's own `allowModule` wrapper, which calls the Modules Manager on the Vehicle's behalf. **Requires:** `VEHICLE_ALLOW` role on the Vehicle. ```solidity Solidity theme={null} // Allow the module on a specific vehicle vehicle.allowModule(moduleAddress, true); // Verify authorization bool isAllowed = modulesManager.allowed(address(vehicle), moduleAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` Reverts with `ModuleNotFound` if the module is not registered, or `AlreadyAllowed` if the Vehicle already allows it. A Vehicle exposes the manager it was deployed with through `vehicle.modulesManager()`. Vehicles are wired to a Modules Manager at spawn time via the `modulesManager` field of the factory's `SpawnParams`, and the factory checks that it was deployed by the trusted `ModulesManagerFactory`. ## Execute a module Once a module is registered and allowed on the target, execute it **on the target**, not on the Modules Manager. **Requires:** `VEHICLE_EXEC` role on the Vehicle. ```solidity Solidity theme={null} address moduleAddress = 0x...; // The module to run bytes memory data = abi.encode(...); // Encoded parameters for the module // Optionally forward native currency with the call vehicle.executeModule(moduleAddress, data); ``` ```typescript TypeScript theme={null} // Coming soon ``` The Vehicle checks the caller's role, requires `modulesManager.isRegistered(module)` and `modulesManager.allowed(address(this), module)`, then `delegatecall`s `IModule.exec(data)` and emits `ModuleExecuted(module, data, msg.value)`. It reverts with `ModuleNotFound`, `ModuleNotAllowed`, or `MissingModulesManager` when the Vehicle has no manager configured. The target contract must implement the `IModuleTarget` interface, whose only function is `executeModule(address module, bytes calldata data)`. All Railnet Vehicles implement it through `BaseVehicle`. Conduits are not module targets and have no Modules Manager. ## Replace a module There is no update function: a module is its address. To swap an implementation, remove the old address and register the new one. ```solidity Solidity theme={null} modulesManager.removeModule(oldModuleAddress); modulesManager.addModule(newModuleAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` Allowances are per module address, so they do not carry over. Every Vehicle that should run the replacement must call `allowModule(newModuleAddress, true)` again. ## Remove a module Removing a module from the registry stops it from executing anywhere immediately, because every execution re-checks `isRegistered`. **Requires:** `MODULE_MANAGER` role. ```solidity Solidity theme={null} // Remove the module (immediate effect) modulesManager.removeModule(moduleAddress); ``` ```typescript TypeScript theme={null} // Coming soon ``` Removal does not clear per-target allowances. Targets that had allowed the module keep a stale allowlist entry and should disallow it explicitly if the module is gone for good. ## Disallow a module on a target Revoke a module's authorization on a specific target without removing it from the registry. **Requires:** `VEHICLE_ALLOW` role on the Vehicle. ```solidity Solidity theme={null} // Disallow the module on a specific vehicle vehicle.allowModule(moduleAddress, false); // The module remains in the registry but can no longer execute on this vehicle ``` ```typescript TypeScript theme={null} // Coming soon ``` Reverts with `AlreadyDisallowed` if the Vehicle was not allowing the module. ## Next steps Set up fee structures for your Multi-Vehicle. Capture and distribute additional protocol rewards with interceptors. # Multi-Vehicle architecture Source: https://docs.railnet.org/developers/contracts/multi-vehicle How Multi-Vehicles aggregate yield across multiple strategies In user-facing documentation, a MultiVehicle is referred to as a **Strategy**. See [Glossary](/developers/glossary). A Multi-Vehicle is a sophisticated vault system built on the STEAM protocol. It manages multiple sub-vehicles (yield strategies) with advanced allocation and redemption capabilities, providing a single entry point for diversified DeFi strategies. ## Architecture overview A Multi-Vehicle is composed of 6 interconnected components: ```mermaid theme={null} graph TD MV[MultiVehicle] --> VM[VehicleManager] VM --> SAE[SectorAccountingEngine] VM --> QSE[QueueStrategyEngine] VM --> SQE[SubQueryEngine] VM --> QRQ[QueryRedeemQueue] VM ---|Authorization & caps| VM SAE ---|Double-entry accounting| SAE QSE ---|Priority queue allocation| QSE SQE ---|STEAM query management| SQE QRQ ---|Async redemption FIFO| QRQ ``` The MultiVehicle exposes only `manager()`. Every engine is reached through the VehicleManager: ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); IQueueStrategyEngine strategy = manager.queueStrategyEngine(); ISubQueryEngine subQueryEngine = manager.subQueryEngine(); IQueryRedeemQueue redeemQueue = manager.redeemQueue(); ``` ## The 6 components ### 1. MultiVehicle (main contract) The STEAM-compliant entry point that users interact with directly. **User operations:** * **Deposit assets** -- receive Multi-Vehicle shares * **Redeem shares** -- receive base assets back **Key responsibilities:** * User-facing STEAM operations (`create`, `resume`, `unlock`, `recover`) * Share minting and burning using ERC-4626-style exchange rates * Integration with QueryRedeemQueue for asynchronous redemptions ### 2. SectorAccountingEngine The central accounting system implementing double-entry bookkeeping principles. Every asset movement is recorded as a transfer between sectors, ensuring the total supply of accounted assets remains constant. | Sector | Type | Description | | --------------- | --------- | ------------------------------------------------------------------------------------- | | **ENTRY** | Virtual | Assets entering the system (untracked balance) | | **AVAILABLE** | Physical | Idle base assets; counted in `totalAssets()` and `withdrawable()` | | **RESERVED** | Physical | Operator-earmarked base assets; in `totalAssets()` but excluded from `withdrawable()` | | **ALLOCATION** | Physical | Sub-vehicle shares from settled deposits (deployed capital) | | **EXIT** | Virtual | Assets leaving the system (untracked balance) | | Vehicle sectors | Staging | `SectorLib.toSector(vehicle)` — per-vehicle staging area | | Query sectors | Ephemeral | Assets or shares committed to an in-flight sub-query | `RESERVED` is asset-only and reachable only through an explicit `move`, so neither auto-fulfill nor the Queue Strategy Engine can consume liquidity parked there. For a deep dive into how sectors work and how funds flow between them, see [Accounting and flow of funds](/developers/contracts/accounting). ### 3. QueueStrategyEngine Defines the allocation strategy through configurable priority queues. The asset manager sets up deposit and redeem queues that control how capital is distributed. #### Deposit queue A prioritized list of `{vehicle, target}` pairs: 1. Processes in order (index 0 = highest priority) 2. `target` = maximum shares to allocate to a vehicle before moving to the next entry 3. Stops when the vehicle reaches its target, hits `maxDepositable`, or assets are exhausted ```solidity Solidity theme={null} // Initial state depositQueue = [{aave, 60k}, {morpho, 30k}] currentHoldings = {aave: 50k, morpho: 20k} newDeposit = 20k shares // Algorithm execution: // 1. Check aave: 50k < 60k target // -> allocate min(20k available, 10k to reach target) = 10k to aave // -> aave now has 60k // 2. Remaining: 10k shares // 3. Check morpho: 20k < 30k target // -> allocate min(10k available, 10k to reach target) = 10k to morpho // -> morpho now has 30k // 4. Done: aave=60k, morpho=30k ``` ```typescript TypeScript theme={null} // Coming soon ``` The deposit queue does **not** enforce ongoing ratios. If a vehicle grows past its target from yield alone, new deposits skip it and go to the next queue entry. #### Redeem queue A prioritized list of `{vehicle, target}` pairs where `target` represents a **floor** (minimum shares to maintain): 1. Processes in order (index 0 = highest priority) 2. Redeems from a vehicle down to its floor 3. Moves to the next entry if more assets are needed ```solidity Solidity theme={null} // Initial state redeemQueue = [{aave, 20k}, {morpho, 10k}] currentHoldings = {aave: 60k, morpho: 30k} redeemRequest = 50k shares // Algorithm execution: // 1. Check aave: 60k holdings, 20k floor // -> redeem min(50k requested, 60k - 20k available) = 40k from aave // -> aave now has 20k (at floor) // 2. Remaining: 10k shares // 3. Check morpho: 30k holdings, 10k floor // -> redeem min(10k requested, 30k - 10k available) = 10k from morpho // -> morpho now has 20k // 4. Done: redeemed 50k total ``` ```typescript TypeScript theme={null} // Coming soon ``` ### 4. SubQueryEngine Manages the STEAM query lifecycle for operations dispatched to sub-vehicles. It tracks ephemeral accounting to prevent share price distortion during asynchronous operations. **Ephemeral accounting** ensures that `totalAssets()` remains accurate even when assets are in-flight: * When a sub-query enters PROCESSING, the system uses the vehicle's `estimate()` function to record expected outputs * As actual shares are received on settlement, the ephemeral estimate is replaced with real values * This prevents spikes or drops in the Multi-Vehicle's share price during async settlements ### 5. QueryRedeemQueue Handles asynchronous redemptions when immediate liquidity is insufficient: * **Demands** — user redemption requests created by `demand(amountIn, maxAmountOut)`. Each demand records the shares owed, carries a slippage ceiling, and is assigned a 1-indexed id. * **Fulfillments** — liquidity provisions created by `fulfill(amountInFilled, amountOutProvided)`, driven by a keeper or operator calling `feedQueryRedeemQueue` on the Vehicle Manager. Assets are distributed pro-rata over the demand/fulfillment position overlap. * **Partial fills** — a single demand can be filled across multiple rounds. `redeem` is callable repeatedly as new fulfillments arrive, and `pending(demandId)` reports what is still owed. * **Claiming** — once a demand is redeemable, the holder calls `redeem(demandId)`, which returns `(redeemedAssets, remainingShares)` and transfers the base assets. Check `redeemable(demandId)` first; the call reverts if the demand is not yet redeemable or if the assets would exceed the demand's `maxAmountOut`. * **Excess** — assets left over when fulfillments over-provide accumulate as `retrievable()`, pulled back by the Vehicle Manager via `retrieveQueryRedeemQueueAssets`. ### 6. VehicleManager The control plane for sub-vehicles and the contract the MultiVehicle points at. Every vehicle must be authorized here before it can receive capital. **Key responsibilities:** * Validate vehicle compatibility (a contract, `ready()`, matching base asset) * Store per-vehicle configuration: `VehicleConfig { VehicleMode mode; Target cap; }` * Hold the wiring to the engines and the redeem queue (`accountingEngine()`, `queueStrategyEngine()`, `subQueryEngine()`, `redeemQueue()`) * Own the strategy-level knobs: `setThresholds`, `setMaxTotalAssets`, `feedQueryRedeemQueue`, `retrieveQueryRedeemQueueAssets` * Enforce role-based access for authorization changes ## The role of the asset manager The asset manager configures and operates the Multi-Vehicle. Beyond setting initial parameters, they have four active levers. ### Rebalance capital There is no `rebalance` function. The asset manager composes one from `dispatch` and `move`, reusing a single `operationId` so off-chain consumers can group the steps: ```solidity Solidity theme={null} bytes32 opId = keccak256("rebalance-aave-to-morpho"); // 1. Redeem from the source vehicle, proceeds land in AVAILABLE accounting.dispatch( ISectorAccountingEngine.DispatchParams({ vehicle: fromVehicle, mode: Mode.REDEEM, amount: shareAmount, settledDestination: SectorLib.AVAILABLE, rejectedDestination: SectorLib.ALLOCATION, minOutput: minAssetsOut, data: "", operationId: opId }) ); // 2. Stage the freed assets for the destination vehicle accounting.move( ISectorAccountingEngine.MoveParams({ from: SectorLib.AVAILABLE, to: SectorLib.toSector(toVehicle), asset: baseAsset, amount: type(uint256).max, operationId: opId }) ); // 3. Dispatch the deposit into the destination vehicle ``` ```typescript TypeScript theme={null} // Coming soon ``` If the source vehicle is asynchronous, step 1 returns `PROCESSING`: the redemption must settle before the assets exist in `AVAILABLE`, so steps 2 and 3 run in a later transaction. ### Manage sub-vehicles Add or remove yield sources at any time: ```solidity Solidity theme={null} // Authorize a new sub-vehicle with the default config vehicleManager.authorize(newVehicle); // Or authorize with an initial mode and cap vehicleManager.authorizeAndConfigure(newVehicle, config); // Change mode or cap later vehicleManager.configure(existingVehicle, newConfig); // Remove a sub-vehicle (redeem all positions first) vehicleManager.unauthorize(vehicleToRemove); ``` ```typescript TypeScript theme={null} // Coming soon ``` Removing a sub-vehicle prevents new allocations but does not automatically redeem existing positions — unallocate first. ### Reconfigure queues Update allocation priorities without moving capital: ```solidity Solidity theme={null} // Set new deposit and redeem queues queueStrategyEngine.setQueues(newDepositQueue, newRedeemQueue); ``` ```typescript TypeScript theme={null} // Coming soon ``` New deposits and redemptions follow the updated order immediately. Existing positions are unaffected. ### Feed redemption liquidity When async redemptions are pending in the QueryRedeemQueue, feed liquidity to fulfill demands: ```solidity Solidity theme={null} vehicleManager.feedQueryRedeemQueue(); ``` ```typescript TypeScript theme={null} // Coming soon ``` Keepers typically automate this, but operators can trigger it manually if redemptions stall. ## Authorization and guardrails Every sub-vehicle must be explicitly authorized on the **VehicleManager** before it can receive capital. Authorization validates that the vehicle is a contract, reports `ready()`, and uses the same base asset as the Multi-Vehicle. ### Vehicle configuration Each authorized vehicle has a configuration with two parameters: | Parameter | Values | Description | | --------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Mode** | `Automatic` (default), `Manual` | Automatic vehicles participate in queue-based allocation. Manual vehicles only receive capital through an explicit `move` plus `dispatch`. | | **Cap** | Share target (default: unlimited) | Maximum allocation target for this vehicle. Limits exposure to a single yield source. | ```solidity Solidity theme={null} // Authorize with Automatic mode and a 100k share cap vehicleManager.authorizeAndConfigure( vehicle, VehicleManagerStore.VehicleConfig({ mode: VehicleMode.Automatic, cap: Target({value: 100_000e18, threshold: 0}) }) ); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### On-chain guardrails Authorization and caps create enforceable boundaries: * **Queue configuration** cannot route capital to unauthorized vehicles * **Allocation caps** limit exposure regardless of queue targets or manual moves * **Role-based access** (via EAC) controls who can authorize vehicles, set caps, and reconfigure queues — allowing platforms commissioning a strategy to set guardrails that asset managers cannot override This means strategies can run hands-off through queue automation while enforcing risk boundaries that neither automation nor operators can exceed. ## Sync and async sub-vehicles Multi-Vehicle orchestrates both synchronous and asynchronous sub-vehicles. **Synchronous** sub-vehicles (Aave V3, Morpho Blue, ERC-4626) complete in a single transaction: ```solidity Solidity theme={null} // Sync flow: single transaction // EMPTY -> UNLOCKING -> SETTLED ``` ```typescript TypeScript theme={null} // Coming soon ``` **Asynchronous** sub-vehicles (ERC-7540 vaults, Lagoon, Ethena) require multiple transactions: ```solidity Solidity theme={null} // Async flow: multiple transactions // EMPTY -> PROCESSING -> (PAUSED ->) UNLOCKING -> SETTLED ``` ```typescript TypeScript theme={null} // Coming soon ``` When interacting with async sub-vehicles: * Sub-queries remain in PROCESSING until the underlying protocol is ready * Ephemeral accounting tracks expected outputs to prevent share price distortion * The Keeper system automates monitoring and advancing nested operations * Operators should watch vehicle sector balances for high amounts indicating slow settlement ## Design principles 1. **Separation of concerns** -- Each component has a single, well-defined responsibility 2. **Double-entry accounting** -- All asset movements are tracked through sector transfers 3. **Asynchronous by design** -- STEAM handles both sync and async operations gracefully 4. **Queue-based strategy** -- Flexible, operator-controlled allocation and redemption logic 5. **Accurate pricing** -- Ephemeral accounting ensures share price accuracy during in-flight operations **Trade-offs to consider:** * **Gas costs** -- More automation means higher gas costs. Queue-based strategies execute more transactions. * **Complexity** -- More vehicles mean more monitoring and management overhead. Start simple and scale up. * **Async operations** -- Sub-vehicle operations may not settle immediately. Design your flows with async handling in mind. # Roles and permissions Source: https://docs.railnet.org/developers/contracts/roles Configure access control for asset managers, platforms, and automation This page covers the smart contract implementation details. See [Glossary](/developers/glossary). This guide covers practical role setup for common scenarios. For the conceptual overview of External Access Control (EAC), role types, and scoping mechanics, see [Access control and roles](/developers/contracts/access-control). ## Persona-based role sets ### Platform owner setup The platform retains strategic control while delegating operations. | Role | Scope | Purpose | | ----------------------------------------- | --------------------- | ------------------------------------------ | | `DEFAULT_ADMIN_ROLE` | Global | Grant/revoke any role, admin transfers | | `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` | Vehicle Manager | Control which yield sources the MV can use | | `FEE_MANAGER_SET_FEES` | Fee Manager | Control fee rates | | `FEE_MANAGER_SET_FEE_RECIPIENTS` | Fee Manager | Control revenue distribution | | `VEHICLE_SET_INTERCEPTIONS` | Vehicle/Multi-Vehicle | Control reward interception rules | ### Asset manager setup The AM receives operational roles scoped to the specific Multi-Vehicle they manage. | Role | Scope | Purpose | | -------------------------------------------------- | ------------------------ | ------------------------------------- | | `MULTI_VEHICLE_DISPATCH` | Sector Accounting Engine | Send assets to sub-vehicles | | `MULTI_VEHICLE_MOVE` | Sector Accounting Engine | Move assets or shares between sectors | | `MULTI_VEHICLE_SET_QUEUES` | Queue Strategy Engine | Configure allocation priorities | | `MULTI_VEHICLE_PROGRESS_QUERY` | Sub Query Engine | Advance async queries | | `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` | Vehicle Manager | Feed the redemption queue | | `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` | Vehicle Manager | Retrieve assets from redemption queue | ### Keeper/automation setup Keepers automate routine operations like redemption queue processing. | Role | Scope | Purpose | | -------------------------------------------------- | --------------- | --------------------------------- | | `JOB_LISTING_REGISTER` | Job Listing | Register automation jobs | | `JOB_LISTING_EXECUTE` | Job Listing | Execute registered jobs | | `KEEPER_ON_REPORT` | Keeper | Submit execution reports | | `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` | Vehicle Manager | Automate redemption queue feeding | | `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` | Vehicle Manager | Automate asset retrieval | ### Fee collector setup For an operator or bot that handles fee collection and distribution. | Role | Scope | Purpose | | ---------------------------- | ----------- | --------------------------------------- | | `FEE_MANAGER_DISPATCH_ERC20` | Fee Manager | Distribute collected fees to recipients | ## Role reference matrix | Operation | Required role | Typical holder | Scope | | ------------------------------------- | -------------------------------------------------- | -------------- | ------------------------ | | Deploy via factory | `FACTORY_SPAWN` | Platform | Factory | | Authorize vehicles | `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` | Platform | Vehicle Manager | | Move assets or shares between sectors | `MULTI_VEHICLE_MOVE` | AM | Sector Accounting Engine | | Dispatch to sub-vehicles | `MULTI_VEHICLE_DISPATCH` | AM | Sector Accounting Engine | | Deposit into accounting | `MULTI_VEHICLE_DEPOSIT` | AM | Sector Accounting Engine | | Set allocation queues | `MULTI_VEHICLE_SET_QUEUES` | AM | Queue Strategy Engine | | Set operational thresholds | `MULTI_VEHICLE_SET_THRESHOLDS` | Platform / AM | Vehicle Manager | | Progress sub-queries | `MULTI_VEHICLE_PROGRESS_QUERY` | AM / Keeper | Sub Query Engine | | Feed redemption queue | `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` | AM / Keeper | Vehicle Manager | | Retrieve from redemption queue | `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` | AM / Keeper | Vehicle Manager | | STEAM deposit operations | `VEHICLE_STEAM_DEPOSIT` | Users (public) | Vehicle / Multi-Vehicle | | STEAM redeem operations | `VEHICLE_STEAM_REDEEM` | Users (public) | Vehicle / Multi-Vehicle | | Set reward interceptions | `VEHICLE_SET_INTERCEPTIONS` | Platform | Vehicle | | Manage modules | `MODULE_MANAGER` | Platform | Modules Manager | | Execute modules | `EXEC` | AM / Keeper | Modules Manager | | Update fees | `FEE_MANAGER_SET_FEES` | Platform | Fee Manager | | Update fee recipients | `FEE_MANAGER_SET_FEE_RECIPIENTS` | Platform | Fee Manager | | Distribute fees | `FEE_MANAGER_DISPATCH_ERC20` | AM / Keeper | Fee Manager | ## Common workflows ### Onboard a new asset manager ```solidity Solidity theme={null} address am = 0x...; // New asset manager address IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); IQueueStrategyEngine strategy = manager.queueStrategyEngine(); ISubQueryEngine subQueryEngine = manager.subQueryEngine(); // Core operational roles eac.grantScopedRole(keccak256("MULTI_VEHICLE_DISPATCH"), address(accounting), am); eac.grantScopedRole(keccak256("MULTI_VEHICLE_MOVE"), address(accounting), am); eac.grantScopedRole(keccak256("MULTI_VEHICLE_SET_QUEUES"), address(strategy), am); eac.grantScopedRole(keccak256("MULTI_VEHICLE_PROGRESS_QUERY"), address(subQueryEngine), am); eac.grantScopedRole(keccak256("MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE"), address(manager), am); eac.grantScopedRole(keccak256("MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS"), address(manager), am); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Offboard an asset manager ```solidity Solidity theme={null} address am = 0x...; // Asset manager to remove // Same scopes used when onboarding IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); IQueueStrategyEngine strategy = manager.queueStrategyEngine(); ISubQueryEngine subQueryEngine = manager.subQueryEngine(); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_DISPATCH"), address(accounting), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_MOVE"), address(accounting), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_SET_QUEUES"), address(strategy), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_PROGRESS_QUERY"), address(subQueryEngine), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE"), address(manager), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS"), address(manager), am); ``` ```typescript TypeScript theme={null} // Coming soon ``` Check for in-progress queries before offboarding. Pending async operations may require the AM's roles to complete. ### Make STEAM operations public Allow anyone to deposit and redeem on a Vehicle or Multi-Vehicle. Deposits and redeems can be opened independently: ```solidity Solidity theme={null} // Open deposits to all users eac.setScopedRolePublic(keccak256("VEHICLE_STEAM_DEPOSIT"), address(multiVehicle), true); // Open redeems to all users eac.setScopedRolePublic(keccak256("VEHICLE_STEAM_REDEEM"), address(multiVehicle), true); ``` ```typescript TypeScript theme={null} // Coming soon ``` Making the STEAM roles public is typical for vaults open to all users. These roles only control who can create STEAM queries — they do not affect operational roles. Splitting deposit and redeem lets operators pause one direction (e.g. redemptions during a wind-down, or deposits while under compliance review) without affecting the other. ### Rotate DEFAULT\_ADMIN\_ROLE Transfer admin control using the time-delayed mechanism: ```solidity theme={null} eac.beginDefaultAdminTransfer(newAdmin); ``` The delay (set during EAC deployment) must pass before the transfer can complete. ```solidity theme={null} // Called by the new admin eac.acceptDefaultAdminTransfer(); ``` ## Decision guidance ### Scoped vs global roles | Use scoped roles when | Use global roles when | | -------------------------------------------------- | --------------------------------------------- | | You manage multiple MVs with different operators | A single operator manages everything | | You want to limit blast radius of compromised keys | Convenience outweighs granularity | | Different teams manage different vehicles | You are the sole operator of a personal vault | **Default recommendation:** Always use scoped roles unless you have a specific reason not to. ### Public roles | Scenario | Make public? | Consideration | | ------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------ | | User-facing vault (open deposits) | Yes — `VEHICLE_STEAM_DEPOSIT` and `VEHICLE_STEAM_REDEEM` | Required for any user to deposit/redeem. Grant independently to pause one direction. | | Internal strategy (restricted access) | No | Only whitelisted addresses can interact | | Keeper automation | No | Grant `JOB_LISTING_EXECUTE` to specific keeper addresses | ### Multisig for admin Use a multisig wallet for `DEFAULT_ADMIN_ROLE` in production. A compromised admin key can grant itself any role and drain the vault. ## Security checklist * [ ] **Verify scope addresses** before granting — wrong scope means the role won't work as intended * [ ] **Use multisig** for `DEFAULT_ADMIN_ROLE` in production * [ ] **Set non-zero `initialDelay`** on the EAC to protect admin transfers * [ ] **Regularly audit** active roles via the API or on-chain queries * [ ] **Plan for offboarding** — document which roles each operator holds * [ ] **Never grant `DEFAULT_ADMIN_ROLE`** to an asset manager * [ ] **Keep vehicle authorization** with the platform, not the AM ## Troubleshooting The most common cause is an incorrect **scope**. Each role must be scoped to the correct contract: * `MULTI_VEHICLE_DEPOSIT`, `MULTI_VEHICLE_MOVE`, `MULTI_VEHICLE_DISPATCH` → scope to the **Sector Accounting Engine** (not the Multi-Vehicle itself) * `MULTI_VEHICLE_SET_QUEUES` → scope to the **Queue Strategy Engine** * `MULTI_VEHICLE_PROGRESS_QUERY` → scope to the **Sub Query Engine** * `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION`, `MULTI_VEHICLE_SET_THRESHOLDS`, `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE`, `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` → scope to the **Vehicle Manager** * Fee roles → scope to the **Fee Manager** * STEAM roles → scope to the **Vehicle** or **Multi-Vehicle** Use `hasRoleOrScopedRole` to verify the grant: ```solidity theme={null} bool hasAccess = eac.hasRoleOrScopedRole(role, scope, account); ``` It depends on the operation: * **`VEHICLE_STEAM_DEPOSIT`** and **`VEHICLE_STEAM_REDEEM`** are scoped to the Vehicle or Multi-Vehicle contract * **`MULTI_VEHICLE_DEPOSIT`**, **`MULTI_VEHICLE_MOVE`** and **`MULTI_VEHICLE_DISPATCH`** are scoped to the **Sector Accounting Engine** * **`MULTI_VEHICLE_SET_QUEUES`** is scoped to the **Queue Strategy Engine** * **`MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION`** and the threshold and redemption-queue roles are scoped to the **Vehicle Manager** Check the [Role reference matrix](#role-reference-matrix) for the correct scope for each role. Query the EAC contract directly or use the [Railnet API](/developers/api): ```solidity Solidity theme={null} // Check a specific role bool hasRole = eac.hasRoleOrScopedRole(role, scope, account); ``` ```graphql GraphQL theme={null} query AccountRoles($address: String!) { RoleMember(where: { account: { _ilike: $address } }) { role { name hash isPublic } } ScopedRoleMember(where: { account: { _ilike: $address } }) { scope role { name hash isPublic } } } ``` A role with `isPublic: true` is granted to everyone, so an empty member list does not mean nobody holds it. # The STEAM standard Source: https://docs.railnet.org/developers/contracts/steam-standard A universal interface for DeFi yield operations This page covers the smart contract implementation details. See [Glossary](/developers/glossary). STEAM (State Transition Engine for Asset Management) is a standardized interface that defines how deposit and redemption operations work across all of Railnet. Smart contracts implementing STEAM are called **Vehicles**. ## Why STEAM exists Traditional vault standards like ERC-4626 assume synchronous operations: deposit assets, receive shares instantly. But many DeFi operations are inherently asynchronous: * **Cooldown periods** -- Ethena requires waiting before unstaking sUSDe * **Request queues** -- ERC-7540 vaults settle deposits and redemptions in curator-driven batches * **Multi-step strategies** -- Multi-Vehicles coordinate across multiple protocols ERC-4626 has no mechanism for tracking in-flight operations. Failed transactions simply revert, losing all context. STEAM solves this by shifting from balance tracking to **state transitions**. STEAM is more than an interface -- it is a framework for building reliable DeFi infrastructure. By standardizing how time, failure, and complex state are handled, STEAM enables Railnet to orchestrate assets across diverse yield sources under a single operational model. ## The Query model The core of STEAM is the **Query** -- a structured request representing a deposit or redemption operation. Each Query carries its own identity, ownership, and state. ```solidity Solidity theme={null} struct Query { address owner; // Who controls the query address receiver; // Who receives the output Asset input; // The asset supplied to the operation Asset output; // The asset expected back; its value is a minimum, enforced as 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; // Token address uint256 value; // Amount } ``` ```typescript TypeScript theme={null} // Coming soon ``` The Query ID is computed as `keccak256(abi.encode(chainId, vehicleAddress, query))` and wrapped in a `bytes32` user-defined type, `Id`. Including the Vehicle address gives otherwise-identical queries distinct IDs across Vehicles. The whole struct is hashed -- `salt` and `data` included -- so repeated operations with the same parameters still produce unique IDs, and the ID stays fixed for the query's entire lifecycle. ## The state machine Every Query moves through a defined set of states. This makes the status of any operation transparent and auditable at all times. ### States | State | Description | Terminal | | -------------- | -------------------------------------------------------- | -------- | | **EMPTY** | No query exists yet. Default state before creation. | No | | **PROCESSING** | Vehicle received assets, performing protocol operations. | No | | **PAUSED** | Awaiting an external condition (cooldown, oracle, KYC). | No | | **UNLOCKING** | Operation succeeded, output assets ready for claim. | No | | **RECOVERING** | Error occurred, assets being recovered. | No | | **REJECTED** | Query failed, assets returned to owner. | Yes | | **SETTLED** | Query complete, assets distributed to receiver. | Yes | ### State diagram ```mermaid theme={null} stateDiagram-v2 [*] --> EMPTY EMPTY --> PROCESSING: create() EMPTY --> UNLOCKING: create() PROCESSING --> PAUSED: (external) PROCESSING --> UNLOCKING: (external) PROCESSING --> RECOVERING: (external) PAUSED --> PROCESSING: resume() UNLOCKING --> SETTLED: unlock() UNLOCKING --> PROCESSING: unlock() partial RECOVERING --> REJECTED: recover() RECOVERING --> PROCESSING: recover() partial SETTLED --> [*] REJECTED --> [*] ``` ### Transition types State transitions are categorized by their trigger mechanism: * **Method-driven (synchronous)** -- Occur within the same transaction as a direct call to a Vehicle method (`create()`, `resume()`, `unlock()`, `recover()`) * **Protocol-driven (asynchronous)** -- Triggered by external protocol events, off-chain verification, or settlement delays ## The Query lifecycle Every operation follows three phases: You call `create()` with a Query struct. The Vehicle pulls the input asset from the owner and starts the operation. * For **sync protocols** (Aave V3, Morpho Blue, ERC-4626): the Query transitions directly to UNLOCKING * For **async protocols** (Ethena with cooldown active, ERC-7540 vaults): the Query enters PROCESSING -- see [Sync vs async operations](/developers/contracts/sync-vs-async) If `create()` fails for any reason, the entire transaction reverts. No assets are transferred, no query is created. The Vehicle interacts with the underlying protocol. For async operations, the Query may pass through PAUSED if an external condition must be met (cooldown period, oracle update). Once the condition is satisfied, `resume()` moves the Query back to PROCESSING, and eventually the protocol signals success (UNLOCKING) or failure (RECOVERING). You call `unlock()` to claim the output asset. The Query transitions to SETTLED and the asset or shares are distributed to the receiver. If the operation failed, you call `recover()` instead. The Query transitions to REJECTED and the input asset is returned. ## Lifecycle methods | Method | Valid from | Transitions to | Description | | ---------------- | ---------- | -------------------------------- | ----------------------------------------------------------------------- | | `create(query)` | EMPTY | PROCESSING or UNLOCKING | Initiates a new query. Pulls the input asset. Returns `State`. | | `resume(query)` | PAUSED | PROCESSING | Resumes after an external condition is met. Returns `State`. | | `unlock(query)` | UNLOCKING | SETTLED or PROCESSING (partial) | Distributes the output asset to the receiver. Returns `(State, Asset)`. | | `recover(query)` | RECOVERING | REJECTED or PROCESSING (partial) | Returns the input asset after failure. Returns `(State, Asset)`. | ## Events The [Query Registry](#the-query-registry) is the sole emitter of query lifecycle events. Vehicles never declare or emit them -- a Vehicle calls the Registry, and the Registry writes the state and emits. | Event | Signature | Emitted when | | --------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `Created` | `Created(address indexed vehicle, Id indexed qid, address indexed receiver, Query query, State initialState, State[] possibleNext)` | Inside `register()`, which the Vehicle calls during `create()`. Exactly once per query. | | `Updated` | `Updated(address indexed vehicle, Id indexed qid, State newState, State[] possibleNext)` | Every committed state transition, including the initial one emitted alongside `Created` | | `Wrapped` | `Wrapped(Id indexed qid, address indexed recipient, address transferGate)` | A query is wrapped into a transferable ERC-721 token | The `vehicle` field namespaces the `qid`, and `receiver` is indexed so integrators can filter by beneficiary. The full `Query` struct appears only in `Created` -- the Registry never stores it on-chain, so reconstruct it off-chain from that event. The `possibleNext` array signals whether the state may change without anyone calling the Vehicle: * **Empty array** -- The state is stable. No further transitions will occur until the owner calls a method (`unlock()`, `recover()`, `resume()`). * **Non-empty array** -- The state may change asynchronously via protocol-driven transitions. Off-chain systems should keep polling `state(query)` or monitoring `Updated` events. Every entry in `possibleNext` must itself be a legal transition out of the state being reported, so the hint can never point more than one step ahead. Use `Updated` events with non-empty `possibleNext` arrays to drive automation: an off-chain runner watches for them and calls the matching method once the protocol is ready. ## The Query Registry Vehicles hold the economic logic; a single shared **Query Registry** owns query state. Every Vehicle calls `register()` once inside `create()` and routes every later state change through `transition()`. The Registry validates the transition, commits it, and emits the events above. The Registry keeps one slim record per query, keyed by its `Id`: | Field | Meaning | | ----------------- | ---------------------------------------------------------------------------------- | | `vehicle` | The Vehicle that registered the query -- the only address allowed to transition it | | `storedState` | The last state committed on-chain | | `outcome` | `PENDING`, `SUCCESS`, or `FAILURE` -- the branch the query has locked into | | `transitionCount` | Monotonic count of committed transitions | | `transferGate` | Optional gate for the wrapped ERC-721, immutable once set | The `Query` itself is not stored on-chain. A query exists if and only if its record's `vehicle` is non-zero. ### State resolution and the storedState lag `storedState` is only what has been committed. The Vehicle's `state(query)` is the live -- possibly **virtual** -- state, and that is the authoritative one. For an async Vehicle the live state may lead `storedState` by one leg: a cooldown has expired on-chain, so `state()` already reports UNLOCKING while `storedState` still says PROCESSING. `transition()` therefore validates one of two paths: * **Single-leg** (`state(query) == storedState`) -- validate `storedState -> newState`, emit one `Updated`. * **Two-leg** (`state(query) != storedState`) -- validate `storedState -> state(query)`, then `state(query) -> newState`, emitting an extra `Updated` for the virtual catch-up leg with an empty `possibleNext`. Never treat `storedState` as the current state of an async query. Read `state(query)` on the Vehicle instead -- `storedState` is stale for the whole virtual-state window. ### The outcome lock `outcome` is what actually enforces path isolation. It stays `PENDING` until the query first enters UNLOCKING (locking it to `SUCCESS`) or RECOVERING (locking it to `FAILURE`). Any later transition into the opposite branch reverts `OutcomeLocked`. Same-branch round-trips through PROCESSING stay legal, which is what makes partial settlement work. ### The transition witness `transitionCount` increments on every committed transition -- by one on the single-leg path, by two on the two-leg path -- and never resets. `(Id, transitionCount)` is therefore a compact witness that a query has not moved since you last observed it: snapshot it, then require it unchanged at execution time so the owner cannot front-run you by unlocking in between. ### Transferable ownership A query may optionally be wrapped into an ERC-721 token with `tokenId = uint256(Id)`. `wrap()` must be called by `query.receiver` on a registered, non-terminal query. While wrapped, the token holder is the bearer owner: `effectiveOwner` and `effectiveReceiver` return the holder instead of `query.owner` and `query.receiver`, and the Vehicle routes the query accordingly. The token burns automatically when the query reaches a terminal state. The `transferGate` passed to `wrap()` is immutable. Passing `address(0)` against a Vehicle that gates access by role lets anyone bypass that gate by receiving the token, so install a gate matching the Vehicle's access policy. ## Transition flows ### Synchronous flow (Aave V3, Morpho Blue, ERC-4626) ```solidity Solidity theme={null} // Complete in a single transaction // EMPTY -> UNLOCKING -> SETTLED ``` ```typescript TypeScript theme={null} // Coming soon ``` The Vehicle completes the entire operation during `create()`, immediately reaching UNLOCKING. Call `unlock()` to finalize. ### Asynchronous flow (Ethena, ERC-7540) ```solidity Solidity theme={null} // Requires multiple transactions // EMPTY -> PROCESSING -> (PAUSED ->) UNLOCKING -> SETTLED ``` ```typescript TypeScript theme={null} // Coming soon ``` The Vehicle enters PROCESSING, waits for external conditions, and advances to UNLOCKING when ready. An off-chain automation runner typically drives this. ### Error recovery ```solidity Solidity theme={null} // Error path // PROCESSING -> RECOVERING -> REJECTED ``` ```typescript TypeScript theme={null} // Coming soon ``` If an operation fails, the Query enters RECOVERING. Call `recover()` to reclaim assets. ### Partial settlement When `unlock()` or `recover()` cannot fully distribute assets in a single call, the Query returns to PROCESSING instead of reaching a terminal state. ```solidity Solidity theme={null} // Partial claim cycle // UNLOCKING -> PROCESSING -> ... -> UNLOCKING -> SETTLED // Partial recovery cycle // RECOVERING -> PROCESSING -> ... -> RECOVERING -> REJECTED ``` ```typescript TypeScript theme={null} // Coming soon ``` This happens with protocols that release assets in batches. Each partial call returns the single `Asset` distributed in that step. The cycle repeats until all assets are fully claimed or recovered. ## Critical constraints These constraints are enforced by the STEAM standard and must never be violated: * **Terminality** -- SETTLED and REJECTED are final states. No further transitions are possible. * **Path isolation** -- Once a query enters UNLOCKING it can never reach RECOVERING, and vice versa, even by routing through PROCESSING. The Registry's outcome lock enforces this and reverts `OutcomeLocked`. Same-branch round-trips through PROCESSING remain legal -- that is exactly what partial settlement relies on. * **Atomic creation** -- `create()` can never produce REJECTED or RECOVERING. Any failure during creation reverts the entire transaction. * **No idle loops** -- Self-loops on UNLOCKING and RECOVERING are forbidden. Any action taken in those states must change the state. * **No virtual termination** -- SETTLED and REJECTED are only ever reached synchronously, through `unlock()` or `recover()`. A protocol-driven transition can never terminate a query. * **Owner enforcement** -- All method-driven transitions must verify that `msg.sender` is the query's effective owner: `query.owner`, or the ERC-721 holder when the query is wrapped. * **State stability** -- If the `possibleNext` array in the `Updated` event is empty, the state is stable until the next user-initiated method call. ## View methods Vehicles provide methods to inspect state and simulate operations: | Method | Returns | Description | | ---------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------- | | `state(query)` | State | Live -- possibly virtual -- state of a query. Returns EMPTY for unknown queries and 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 query that ended in REJECTED. Empty bytes otherwise. | | `asset()` | address | The base asset of the vehicle (e.g., USDC) | | `routes()` | (Route\[], Route\[]) | Deposit and redeem route combinations | | `totalAssets()` | uint256 | Total value of all managed assets | | `maxDeposit(account)` | Asset | Maximum deposit asset and value for an account | | `maxRedeem(account)` | Asset | Maximum redeemable shares for an account | | `ready()` | bool | Whether the vehicle accepts new queries | `estimate()` provides a best-effort preview, not a commitment. Always implement slippage protection when using estimates for transaction previews. ## Fee semantics The `estimate()` and `convert()` methods serve different purposes: * **`estimate()`** includes all applicable entry/exit fees, slippage, and operational costs * **`convert()`** excludes all fees, providing a pure share-to-asset conversion Transactional fees are taken out of the payout in `unlock()`, not when `create()` pulls the input. `create()` only *previews* them, so the `output.value` floor is checked against a fee-inclusive estimate. `recover()` charges no transactional fee, letting users reclaim assets after a failure without additional cost. ## Next steps How synchronous and asynchronous Vehicles differ in practice How assets move through Multi-Vehicles and Sectors # Sync vs async operations Source: https://docs.railnet.org/developers/contracts/sync-vs-async Understanding synchronous and asynchronous yield operations in Railnet In user-facing documentation, a Vehicle is referred to as a **Yield Source**. See [Glossary](/developers/glossary). A fundamental architectural decision in Railnet is whether a Vehicle operates synchronously or asynchronously. This distinction determines how Queries move through the STEAM state machine and what operational patterns you need to follow. ## The core distinction The difference between sync and async Vehicles lies in the Query lifecycle. For the complete state machine reference — all seven states, every valid transition, and the critical constraints — see [The STEAM standard](/developers/contracts/steam-standard). ### Synchronous Vehicles Sync Vehicles complete their primary operation within a single transaction. A Query transitions directly from EMPTY to UNLOCKING via `create()` and then to SETTLED via `unlock()`. There is no waiting period or external dependency. ```solidity Solidity theme={null} // Sync flow: single transaction // EMPTY -> UNLOCKING -> SETTLED ``` ```typescript TypeScript theme={null} // Coming soon ``` **Examples:** * **Aave V3 Vehicle** -- Supplies and withdraws assets from Aave V3 lending pools * **Morpho Blue Vehicle** -- Supplies to and withdraws from a Morpho Blue market * **ERC-4626 Vehicle** -- Wraps any standard ERC-4626 vault into a STEAM-compliant interface * **Wrapper Vehicle** -- 1:1 wrap of any ERC-20 for STEAM compatibility (utility, no yield) Sync Vehicles provide the best user experience with the lowest operational overhead. ### Asynchronous Vehicles Async Vehicles handle operations that cannot complete immediately due to protocol-level constraints. A Query enters PROCESSING after creation and remains there until external conditions are met. ```solidity Solidity theme={null} // Async flow: multiple transactions // EMPTY -> PROCESSING -> (PAUSED ->) UNLOCKING -> SETTLED ``` ```typescript TypeScript theme={null} // Coming soon ``` **Examples:** * **ERC-7540 Vehicle** -- Wraps any ERC-7540 async vault, driving its request queues through the STEAM lifecycle * **Lagoon Vehicle** -- An `ERC7540Vehicle` subclass for Lagoon's hybrid vaults. It picks the sync or async path per call and adds the closed-vault recovery leg. * **Ethena Vehicle** -- Handles sUSDe cooldown periods when active. Operates synchronously when cooldown is disabled. ## Why some operations must be async Asynchronicity is rarely a choice. It is a requirement imposed by the underlying protocol's design. Ethena's sUSDe protocol implements a `cooldownDuration`. When you want to unstake USDe, you must first initiate a cooldown. The assets are only available for withdrawal after this period elapses. The Ethena Vehicle tracks this period, keeping the Query in PROCESSING until the cooldown ends. When Ethena's `cooldownDuration` is set to zero, the Ethena Vehicle operates synchronously — no PROCESSING state, no Account Clone needed. An ERC-7540 vault does not settle a deposit or redemption on request. `requestDeposit` and `requestRedeem` only register a request; a curator settles it later, in batches, and only then can the result be claimed. The ERC-7540 Vehicle keeps its own demand queue, registering one demand per Query and batching them into a single vault request under the Vehicle's own address. Each Query holds a demand ID, and `state()` reports UNLOCKING once that demand becomes claimable. Lagoon vaults are hybrid. The Lagoon Vehicle reads `syncMode()` and `isTotalAssetsValid()` on every call and takes the synchronous `syncDeposit` or `syncRedeem` path when both allow it, falling back to the async request path when they do not — or when the vault's runtime gates reject the sync attempt. Complex yield strategies might involve multiple swaps, bridge operations, or liquidity provisioning steps that cannot safely be compressed into a single atomic transaction. ## The PROCESSING state The PROCESSING state is the hallmark of an asynchronous operation. * In **sync Vehicles**, `create()` moves the Query directly to UNLOCKING because assets are immediately deposited and ready to claim * In **async Vehicles**, `create()` moves the Query to PROCESSING, signaling that the operation is underway but not yet ready for settlement The transition from PROCESSING to UNLOCKING is detected through the Vehicle's `state()` view function, which checks the underlying protocol's status. If conditions are met (e.g., `block.timestamp >= cooldownEnd`), `state()` reports UNLOCKING. A Keeper or caller then calls `unlock()` to finalize, and the Query Registry commits the pending leg at that point — see [State resolution and the storedState lag](/developers/contracts/steam-standard#state-resolution-and-the-storedstate-lag). If an external condition must be met before processing can continue, the Query may enter PAUSED. Once the condition is satisfied, `resume()` moves it back to PROCESSING. ## Error handling: sync vs async How errors surface depends on the Vehicle type: * **Sync Vehicles** — if `create()` fails, the entire transaction reverts. No query is created, no assets are transferred. There is nothing to recover. * **Async Vehicles** — if the underlying protocol operation fails after entering PROCESSING, the Query transitions to RECOVERING. The owner calls `recover()` to reclaim input assets, and the Query terminates in REJECTED. This distinction matters operationally: sync errors are invisible (reverted transactions), while async errors require active monitoring and recovery. ## Account Clones: isolating async operations Some protocols restrict operations to the address that initiated them. Ethena's `cooldownShares` is address-specific: a new cooldown started from the same address overrides the previous one, so concurrent redemptions driven from a single Vehicle address would clobber each other. Railnet solves this with the **Account Clone** pattern, built on the minimal `Account` contract: The Vehicle clones `Account` through the `CoreFactory`, using the query ID as the salt, and calls `initialize(owner)` with itself as owner. The position (for Ethena, the sUSDe being cooled down) is transferred to that Account, and the Vehicle's accounting is decreased accordingly. The Vehicle drives the protocol through `performCall(target, value, data)`, which only the owner may call — for Ethena, `cooldownShares` on entry and `unstake` at settlement. The Vehicle records the Account's address in its own per-query storage. The Query Registry record holds no such field. This provides **per-query isolation**: each redemption gets its own identity on the underlying protocol, letting Railnet run many concurrent asynchronous requests without address conflicts. Account Clones are one tool among several, used where a protocol binds operations to an address. Today only the Ethena Vehicle needs them, on its async redeem leg. The ERC-7540 and Lagoon Vehicles instead batch per-Query demands through their own request queue under the Vehicle's own address. ## Impact on callers When interacting with async Vehicles, you follow a multi-step process: Call `create()`. The Query enters the PROCESSING state. Assets are transferred to the Vehicle (or its Account Clone). Poll `state(query)` to check the current state. The Vehicle evaluates underlying protocol conditions on each call. * On **success** (state is UNLOCKING): call `unlock()` to claim output assets. The Query transitions to SETTLED. * On **failure** (state is RECOVERING): call `recover()` to reclaim input assets. The Query transitions to REJECTED. In most deployments, a **Keeper** network automates steps 2 and 3. You create the Query and the Keeper handles monitoring and finalization. ## Multi-Vehicle handling of async sub-vehicles When a Multi-Vehicle dispatches operations to async sub-vehicles, the SubQueryEngine manages the nested STEAM lifecycle. If a sub-vehicle enters PROCESSING, the Multi-Vehicle's own Query may remain open until settlement completes. **Ephemeral accounting** tracks expected outputs during in-flight operations to prevent share price distortion. See [Accounting and flow of funds](/developers/contracts/accounting) for how this works. The Keeper system automates monitoring and advancing nested sub-queries. See [Multi-Vehicle architecture](/developers/contracts/multi-vehicle#sync-and-async-sub-vehicles) for the full operational picture. ## Choosing between sync and async When integrating a new protocol into Railnet, the choice is dictated by the protocol's deposit and withdrawal mechanics: | Factor | Sync Vehicle | Async Vehicle | | ------------------------ | ---------------------------------------- | -------------------------------------------------- | | **Operations** | Immediate entry and exit | Delays, queues, or per-address restrictions | | **User experience** | Best (single transaction) | Requires monitoring or Keeper automation | | **Operational overhead** | Lowest | Higher (queue or clone management, state tracking) | | **Use when** | Protocol allows instant deposit/withdraw | Protocol has cooldowns, queues, or address limits | Async is not a limitation -- it is what allows Railnet to provide institutional-grade access to the full spectrum of DeFi yield sources, regardless of their underlying complexity. # WrapperVehicle Source: https://docs.railnet.org/developers/contracts/wrapper-vehicle A Vehicle that wraps ERC20 tokens for STEAM compatibility This page covers the smart contract implementation details. See [Glossary](/developers/glossary). WrapperVehicle wraps an existing ERC20 token into a STEAM-compatible Vehicle without adding yield logic. Deposits mint shares and redeems burn them at an ERC-4626-style rate (`shares = assets * totalSupply / totalAssets`), which stays 1:1 in value with the underlying token as long as no fees are configured. Shares always carry 18 decimals, so the raw share amount equals the deposited token amount only when the wrapped token is also an 18-decimal token. ## How it works WrapperVehicle is always synchronous. Deposits and withdrawals complete in a single transaction with no cooldown periods or withdrawal queues: `create` transitions straight to `UNLOCKING`, and both `resume()` and `recover()` revert `Unimplemented` because a query can never park in `PROCESSING`. The contract holds the underlying ERC20 token and issues STEAM-compatible shares against it. `asset()` returns the wrapped token, and `totalAssets()` is tracked internally -- incremented on deposits, decremented on redeems -- rather than read from an external protocol. Because there is no yield source, the share price stays at 1:1 unless a FeeManager is configured: management and performance fees mint fee shares, diluting holders below 1:1. ## Use cases * **Access restrictions** -- bar specific addresses from owning or receiving queries via the immutable `forbiddenAddresses` list fixed at deployment, and gate the Vehicle's admin surface through an `ExternalAccessControl`. Allow-lists, block-lists, sanctions screening, and share-transfer screening are Conduit-level features -- wrap the Vehicle in a Conduit with an AccountList for those. * **Fee application** -- apply deposit, redeem, management, or performance fees to any ERC20 token via a FeeManager * **Integration testing** -- use WrapperVehicle as a predictable, zero-yield sub-vehicle when testing MultiVehicle or Conduit configurations * **STEAM compatibility** -- make any ERC20 token composable with Railnet infrastructure (MultiVehicles, Conduits) without building a custom Vehicle adapter ## Deployment WrapperVehicle instances are deployed via `WrapperVehicleFactory.spawn(SpawnParams)`, which requires the `FACTORY_SPAWN` role. The factory clones a pre-deployed implementation at a deterministic address, pulls the `AssetRegistry`-configured initial deposit from the caller, settles it synchronously, then burns the resulting shares as an inflation-attack guard before calling `enable()` and recording the deployment. `SpawnParams` carries the wrapped `asset`, the optional `accessControl`, `feeManager`, and `modulesManager` modules (each validated against its trusted factory), `forbiddenAddresses`, the `queryRegistry`, and the `querySalt` and `deploymentSalt` salts. ## Composability Because WrapperVehicle implements the full STEAM interface, it can be: * Registered as a sub-vehicle in a MultiVehicle * Wrapped by a Conduit for distribution with custom fees and branding * Combined with the vehicle-level modules: a `FeeManager` for deposit, redeem, management, and performance fees, a `ModulesManager` for delegatecall modules, and an `ExternalAccessControl` for role gating # Glossary Source: https://docs.railnet.org/developers/glossary Key terms and definitions used throughout Railnet documentation Quick reference for every protocol-specific term used across the Railnet documentation. For a guided introduction to the key concepts, start with [How it works](/overview/how-it-works). A lightweight contract clone deployed on-the-fly to handle protocols that bind an operation to the address that initiated it. The Vehicle clones an `Account` per Query — the query ID is the salt — transfers the position to it, and drives the protocol through the clone's `performCall`, so each Query gets its own identity on the underlying protocol. The clone inherits the Interceptor pattern, routing any reward that lands on it back to the Vehicle that owns it. This enables hundreds of concurrent asynchronous operations without address conflicts. A compliance module attached to a Conduit that maintains an allowlist and a blocklist of addresses, plus optional sanctions-oracle screening. It answers intent-specific predicates — `canDeposit`, `canRedeem`, `canTransfer`, `canReceive`, and `canForceRedeem` — with precedence sanctions > blocklist > allowlist. Its `AllowlistMode` decides how strictly the allowlist applies: `OPEN` ignores it, `REGULAR` gates deposits only, and `STRICT` gates deposits and transfers. See also: **Transfer Policy**. An Advanced Strategy enables asset managers to execute on protocols or functions not available as standard Railnet Vehicles — such as swaps, borrows, bridges, perps, or complex RWA operations. Implemented on-chain as a Specialized Vehicle that standardizes non-custodial custody (multi-chain treasury wallet), on-chain policy enforcement (contract/function/calldata whitelists), and NAV computation. Composable with the full Railnet stack: distributable via Conduits or usable as a sub-vehicle within a MultiVehicle (fund-of-funds model). See also: **Specialized Vehicle**, **Strategy**. The operator action of deploying capital from a Multi-Vehicle's AVAILABLE Sector into one or more sub-vehicles. It runs through `dispatch()` on the SectorAccountingEngine, which opens a deposit Query on the target sub-vehicle, and requires `MULTI_VEHICLE_DISPATCH`. Distinct from a user deposit — allocations are performed by the asset manager (or keeper) to put idle capital to work across yield sources. The persona responsible for deploying and operating a Multi-Vehicle. Asset managers select yield sources, configure sub-vehicles, set allocation strategies, and rebalance capital. They can operate both standard Strategies (MultiVehicle) and Advanced Strategies (Specialized Vehicle). They interact with Railnet through the build interface and earn fees for their management services. A Vehicle that requires multiple transactions to complete an operation due to protocol-level constraints such as cooldown periods (Ethena's sUSDe) or request queues (ERC-7540 vaults, including Lagoon). Queries enter the PROCESSING state and settle over time. See also: **Sync Vehicle**. The primary asset (e.g., USDC) that a Vehicle or Multi-Vehicle denominates in. All deposits and redemptions are expressed in the base asset, and the share price is calculated relative to it. Returned by the STEAM `asset()` method. A distribution channel that wraps any Vehicle or Multi-Vehicle with its own ERC20 share token, custom fees, and access control. Platforms deploy Conduits to distribute yield strategies to their users with custom fee structures, compliance, and branded shares — without building protocol-specific integrations. Keepers automate async operations so users never need to manually claim. A protocol-imposed delay between initiating and completing a withdrawal. During the cooldown, assets are locked and the Query remains in the PROCESSING state. Common in protocols like Ethena, where unstaking sUSDe requires a multi-day waiting period before assets can be claimed. Shares permanently locked at the burn address (`0x…dEaD`) when a Vehicle or Conduit is deployed. The factory makes an initial deposit — its size is configured per asset in the AssetRegistry — then transfers the resulting shares to the burn address. This keeps `totalSupply` from ever being zero, preventing the first-depositor share price manipulation that affects naive vault implementations. A STEAM operation where a user commits base assets (such as USDC) to a Vehicle or Multi-Vehicle in exchange for shares. The Query mode is set to `DEPOSIT`. A specialized contract inside a Multi-Vehicle that handles one operational concern. Each Engine has a single responsibility and is deployed as a separate contract: * **SectorAccountingEngine** — double-entry bookkeeping across all Sectors * **SubQueryEngine** — manages the sub-vehicle Query lifecycle and ephemeral accounting * **QueueStrategyEngine** — configurable priority queues for deposit and redeem allocation * **QueryRedeemQueue** — FIFO queue for processing async redemptions fairly Together the four Engines power all Multi-Vehicle operations while keeping each concern isolated and independently upgradeable behind its own beacon. The VehicleManager sits alongside them as the Multi-Vehicle's management contract — it is not an Engine. A mechanism used by the SubQueryEngine to track the estimated value of in-flight Queries. Prevents share price distortion by including expected outputs from PROCESSING queries in the `totalAssets()` calculation. Railnet's central role-based access control system built on OpenZeppelin's `AccessControlDefaultAdminRules`. EAC manages three types of roles: * **Global roles** apply protocol-wide across all contracts (e.g. `FACTORY_SPAWN` to deploy new Vehicles) * **Scoped roles** are restricted to a specific contract address (e.g. `VEHICLE_STEAM_DEPOSIT` granted only on one Vehicle) * **Public roles** are opened up by the admin with `setRolePublic` or `setScopedRolePublic`, after which every account passes the check without an explicit grant — enabling permissionless access where appropriate A single EAC contract governs access for the entire Railnet deployment, keeping permissions centralized and auditable. A contract that deploys Vehicles and Conduits with consistent configuration and anti-inflation protection. Each type has its own factory — `AaveV3VehicleFactory`, `ERC4626VehicleFactory`, `MultiVehicleFactory`, `ConduitFactory`, and so on — gated by `FACTORY_SPAWN` (`CONDUIT_SPAWN` for Conduits) and fed by the AssetRegistry, which holds per-asset authorization and initial deposit amounts. They all route the deployment itself through the shared CoreFactory, which wraps CreateX for deterministic `CREATE2` addresses and EIP-1167 minimal proxy clones. A composition pattern where a Strategy (MultiVehicle) includes other Strategies or Advanced Strategies (Specialized Vehicles) as sub-vehicles, creating a nested allocation structure. The outer MultiVehicle's sector-based accounting tracks the inner strategies' NAV like any other sub-vehicle. This works because MultiVehicle extends `BaseVehicle`, making it a valid sub-vehicle once authorized on another MultiVehicle's VehicleManager. An optional contract attached to a Vehicle or Multi-Vehicle that handles fee collection and distribution. Supports four fee types: performance, management, deposit, and redeem. Fees are configured in basis points with an immutable maximum ceiling. The highest `totalAssets()` value previously checkpointed for a Vehicle, Multi-Vehicle, or Conduit with performance fees enabled. The FeeManager charges the performance fee only on earnings above that checkpoint, ensuring the asset manager does not earn fees on recovering from a loss — only on generating net new value. A pattern for managing additional reward distribution. Vehicles declare interception rules via the `interceptions()` view function, which off-chain indexers read to route protocol incentives, airdrops, and yield-bearing tokens to the correct destinations. An off-chain automation system that watches on-chain conditions and submits the transactions that advance operations. Jobs are published on-chain — for example by the MultiVehicleJobListing — each with a status target the keeper polls for `POLL`, `EXEC`, or `STOP` and an exec target it calls once the job is ready; the shipped Multi-Vehicle jobs feed the redemption queue and retrieve assets back from it. Keepers also progress Queries on a user's behalf, such as processing Conduit queries under `CONDUIT_PROCESS`. An extension that adds auxiliary functionality to a Vehicle through the ModulesManager. Modules can perform tasks such as claiming and distributing external rewards. Enabling one takes two steps: an account holding `MODULE_MANAGER` registers the module with `addModule`, then the Vehicle itself opts in with `allowModule` before that module can execute on it. A meta-strategy vault that orchestrates positions across multiple underlying Vehicles. It distributes capital across sync and async yield sources through a single entry point, and enables rebalancing without users needing to interact with individual protocols. Powered by four Engines: * **SectorAccountingEngine** — tracks every asset movement with double-entry bookkeeping * **SubQueryEngine** — manages per-Vehicle Queries and ephemeral accounting * **QueueStrategyEngine** — determines allocation priority across Vehicles * **QueryRedeemQueue** — processes redemptions in FIFO order A VehicleManager sits alongside the Engines, holding sub-vehicle authorization and configuration. The Multi-Vehicle computes a real-time on-chain NAV across all positions via `totalAssets()`, ensuring accurate share pricing at all times. Multi-Vehicles can also include Specialized Vehicles as sub-vehicles, enabling a fund-of-funds model where Advanced Strategies sit alongside standard yield sources. The real-time on-chain valuation of total assets under management in a Vehicle or Multi-Vehicle. NAV is computed directly by the contracts via `totalAssets()` — no off-chain oracles or backends are involved. The calculation includes both settled positions and in-flight operations (through ephemeral accounting), ensuring the share price always reflects the true state of the vault. Share pricing follows the formula: `shares = assets × totalSupply / totalAssets()`. The persona responsible for distributing yield strategies to end users. Platforms deploy Conduits to wrap existing Vehicles or Multi-Vehicles with branded shares, custom fee structures, and access control — without building direct protocol integrations. Examples include wallets, neobanks, and DeFi aggregators. An on-chain governance layer within a Specialized Vehicle that enforces what operations the asset manager can execute. Capabilities include: contract whitelists (which contracts can be called), function-level permissions (which functions), calldata-level checks (which parameters are accepted), and guardian governance with timelock enforcement. Replaces opaque web2 policy systems (Fireblocks, ForDeFi) with fully transparent, auditable, on-chain rules. A structured request representing a deposit or redemption operation in the STEAM standard. Each Query carries an owner, receiver, input/output assets, mode (`DEPOSIT` or `REDEEM`), a unique salt, and optional protocol-specific data. Queries move through three phases: 1. **Creation** — assets are committed and the Query enters the state machine 2. **Execution** — the underlying protocol processes the operation (instant for sync, multi-step for async) 3. **Settlement** — output assets or shares are distributed to the receiver Each Query is independent — one failure never blocks others, and multiple Queries can be in-flight concurrently. The operator action of shifting capital between sub-vehicles within a Multi-Vehicle. There is no single rebalance call: the asset manager chains `dispatch()` calls on the SectorAccountingEngine — a REDEEM dispatch out of the source sub-vehicle, then a DEPOSIT dispatch into the destination — reusing one `operationId` so both legs stitch back into a single intent, with `move()` shifting balances between Sectors along the way. When every sub-vehicle involved settles synchronously the whole sequence fits in one multicall; an async redeem splits it across transactions. Rebalancing lets the asset manager respond to changing market conditions, optimize yield, or reduce risk without requiring users to withdraw and re-deposit. A STEAM operation where a user returns shares to a Vehicle or Multi-Vehicle in exchange for base assets. The Query mode is set to `REDEEM`. A supported input/output asset combination for a Vehicle. The STEAM view method `routes()` returns two arrays — the deposit routes and the redeem routes — letting integrators discover programmatically which assets a Vehicle accepts for deposits and which it returns on redemption. A logical partition in the Multi-Vehicle accounting system that represents the operational state of assets. Every asset movement has a source Sector and a destination Sector — assets can never be "lost" because the double-entry bookkeeping always balances. Five static Sectors track the main flow: * **ENTRY** — virtual source for assets entering the accounting system * **AVAILABLE** — idle base assets usable for both allocation and redemption fulfillment * **ALLOCATION** — sub-vehicle shares held from settled deposits * **RESERVED** — base assets the operator has earmarked; counted in `totalAssets()` but excluded from `withdrawable()` * **EXIT** — virtual destination for assets leaving the accounting system Dynamic Sectors are derived rather than declared: a **vehicle Sector** (`SectorLib.toSector(vehicle)`) stages assets and shares per sub-vehicle, and a **query Sector** isolates a single in-flight sub-query. The ratio of `totalAssets() / totalSupply()` for a Vehicle, Multi-Vehicle, or Conduit. Share price reflects the current value of one share in terms of the base asset. It rises as the underlying yield sources generate returns, and is used to calculate deposit and redemption amounts. The on-chain implementation of an Advanced Strategy. A Specialized Vehicle wraps a Lagoon vault interface with a standard Railnet Vehicle adapter, making it composable with MultiVehicles and Conduits. Unlike standard Vehicles that adapt a specific DeFi protocol, Specialized Vehicles give asset managers full execution flexibility — they can call any whitelisted contract and function. The three pillars standardized by Specialized Vehicles are: (1) custody via non-custodial multi-chain treasury wallets, (2) policy via on-chain engines with contract, function, and calldata whitelists, (3) accounting via standardized NAV computation and reporting. See also: **Advanced Strategy**, **Vehicle**. State Transition Engine for Asset Management. The standardized interface that all Vehicles implement. STEAM defines a state machine with seven states (EMPTY, PROCESSING, PAUSED, UNLOCKING, SETTLED, RECOVERING, REJECTED) for managing the full lifecycle of deposit and redemption operations. A Vehicle that is managed by a Multi-Vehicle rather than receiving direct user deposits. The Multi-Vehicle allocates capital into its sub-vehicles, manages their Query lifecycles, and aggregates their returns into a single share price. Each sub-vehicle has its own Sector for accounting purposes. A Vehicle that completes operations in a single transaction. Queries transition directly from EMPTY to UNLOCKING during `create()`. Examples include the Aave V3, Morpho Blue, and ERC-4626 vehicles. See also: **Async Vehicle**. The two gates a Conduit applies to a user-initiated share transfer; `isTransferable(from, to)` returns their combined result. The first is `transferEnabled`, a one-way latch flipped on by `enableTransfers()` under `CONDUIT_SET_TRANSFER_ENABLED` — it can never be turned back off, so ERC20 integrations cannot be bricked. The second is the attached AccountList's `canTransfer(from, to)`, which screens both parties against sanctions, the blocklist, and — in `STRICT` mode — the allowlist. Conduit-internal moves such as minting, burning, fee accrual, and `forceRedeem` bypass both gates by construction. A multi-EVM-chain programmable wallet deployed as part of a Specialized Vehicle. Fully non-custodial — no third party can recover private keys. Each Advanced Strategy gets its own Treasury Wallet instance per chain, controlled by the on-chain policy engine. The STEAM lifecycle method that finalizes a successful operation. Called when a Query is in the UNLOCKING state, it distributes output assets or shares to the receiver and transitions the Query to SETTLED. A partial unlock distributes only a portion of the expected output and transitions the Query back to PROCESSING for the remainder. A smart contract that implements the STEAM interface for a specific DeFi protocol. Each Vehicle wraps protocol-specific logic behind four standardized lifecycle methods: * `create()` — initiates a deposit or redemption operation * `resume()` — advances an async operation past a paused state * `unlock()` — finalizes a successful operation and distributes output * `recover()` — handles failures by returning assets to the sender Vehicles come in two flavors: * **Sync Vehicles** (Aave V3, Morpho Blue, ERC-4626) settle in a single transaction * **Async Vehicles** (Ethena, ERC-7540 vaults such as Lagoon) require multiple transactions with cooldown periods or request queues The management contract of a Multi-Vehicle. It authorizes and unauthorizes sub-vehicles (`authorize`, `authorizeAndConfigure`, `unauthorize`), stores each one's configuration with `configure`, sets operational thresholds and `maxTotalAssets`, and exposes `feedQueryRedeemQueue` and `retrieveQueryRedeemQueueAssets` for the redemption queue. `MultiVehicle.manager()` returns it, and it is also the accessor for the four Engines. A protocol-imposed mechanism where redemption requests are queued and filled as vault settlement or liquidity allows. Railnet's ERC-7540 vehicles — including the Lagoon vehicle — batch user demand in an `ERC7540VehicleQueue` that submits one vault request per batch: the permissionless `processRequest()` drains the full live backlog, while the bounded `processRequest(maxSubmission)` overload is gated by `VEHICLE_PROCESS_QUEUE` for keepers chunking a backlog the vault will not accept in one request. The Vehicle's Query remains in the PROCESSING state until its share of the request settles. A Vehicle that wraps an existing ERC20 token for STEAM compatibility without adding yield. Used for access control overlays, fee application, integration testing, or making tokens composable with Railnet infrastructure. Always synchronous — deposits and withdrawals complete in a single transaction. The underlying DeFi protocol that a Vehicle wraps and standardizes behind the STEAM interface. Examples include lending protocols (Aave V3, Morpho Blue), staking protocols (Ethena), and tokenized vaults (ERC-4626, and ERC-7540 vaults such as Lagoon). Each yield source has its own mechanics, but the Vehicle abstraction presents a uniform interface to depositors and Multi-Vehicles. See how these terms fit together in a guided walkthrough of Railnet's core concepts. # Overview Source: https://docs.railnet.org/developers/index API, smart contract reference, and technical resources for Railnet Everything you need to build on Railnet — from querying on-chain data to diving into smart contract internals. ## Tools GraphQL endpoint for querying indexed on-chain data — strategies, queries, sectors, fees, and automation Deploy and operate Conduits, Strategies, and fees from TypeScript — viem-based actions plus React hooks ## Smart contracts Detailed documentation for every Railnet smart contract — the STEAM standard, Vehicle architecture (standard Vehicles, MultiVehicles, and Specialized Vehicles), accounting, fees, access control, and more. Start with the STEAM standard — the state machine at the heart of Railnet ## Reference Protocol terminology and definitions Deployed Vehicle factories and supported vault standards ## Use these docs with an AI assistant The whole site is published in a plain-text format that LLMs and coding agents can ingest directly, so an assistant can answer from the current docs instead of guessing at the API. | File | Contents | | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | [`llms.txt`](https://docs.railnet.org/.well-known/llms.txt) | An index of every page with its description — small enough to paste into any context window. | | [`llms-full.txt`](https://docs.railnet.org/.well-known/llms-full.txt) | The full text of every page in one file, for tools that can take the whole corpus. | Point your assistant at `llms.txt` first and let it fetch the pages it needs, or hand it `llms-full.txt` when you want everything in one shot. # Integrate a Conduit Source: https://docs.railnet.org/developers/integrate-conduit Build an earn experience on top of Railnet — deposits, withdrawals, balances, and reporting This guide walks you through integrating a deployed Conduit into your platform. By the end, you'll be able to show balances, handle deposits and withdrawals, preview operations with fees, and monitor positions through the Railnet API. This guide assumes a Conduit has already been deployed for your platform. If you need to deploy one first, see [Create a Conduit](/developers/contracts/conduit-deployment). ## Prerequisites * **Conduit address** — the deployed Conduit contract on your target network * **RPC endpoint** — an Ethereum JSON-RPC provider (Alchemy, Infura, etc.) * **Railnet API endpoint** — `https://graphql-enriched.staging.railnet.org/query` (staging) ## Read Conduit state A Conduit exposes view functions you can call directly on-chain to display balances, share prices, and product status in your UI. ### Product info ```solidity Solidity theme={null} // The underlying asset (e.g., USDC) address asset = conduit.asset(); // The underlying Vehicle (strategy) the Conduit wraps IVehicle vehicle = conduit.getVehicle(); // Whether the Conduit is enabled and accepting operations bool isReady = conduit.ready(); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Balances and share price ```solidity Solidity theme={null} // Total underlying assets managed by the Conduit uint256 tvl = conduit.totalAssets(); // Total Conduit shares in circulation uint256 supply = conduit.totalSupply(); // A specific user's share balance uint256 userShares = conduit.balanceOf(userAddress); // Share price: assets per share (no fees applied) Asset memory shareValue = conduit.convert( Asset({asset: address(conduit), value: 1e18}), // 1 share true // shares → assets ); ``` ```typescript TypeScript theme={null} // Coming soon ``` ### Preview operations Use `estimate()` to show users what they'll receive **after fees** before they commit to a transaction. Use `convert()` for a pure conversion without fees (e.g., displaying portfolio value). ```solidity Solidity theme={null} // How many cShares will the user get for 100 USDC? (includes fees) Asset memory estimated = conduit.estimate( Asset({asset: address(usdc), value: 100e6}), Mode.DEPOSIT, EstimationType.OUTPUT ); // estimated.value = cShares the user will receive // How much USDC for burning 50 cShares? (includes fees) Asset memory redeemEstimate = conduit.estimate( Asset({asset: address(conduit), value: 50e18}), Mode.REDEEM, EstimationType.OUTPUT ); // redeemEstimate.value = USDC the user will receive ``` ```typescript TypeScript theme={null} // Coming soon ``` `estimate()` includes all fee types (deposit, redeem, management, performance). Use it for transaction previews. Use `convert()` for display-only share-to-asset conversions where fees don't apply. ## Handle deposits Users deposit base assets (e.g., USDC) and receive Conduit shares representing proportional ownership. The Conduit handles all interaction with the underlying strategy. The user approves the Conduit to spend their tokens. ```solidity Solidity theme={null} IERC20(usdc).approve(address(conduit), amount); ``` ```typescript TypeScript theme={null} // Coming soon ``` The Conduit binds each query id to its creator: `query.salt` must equal `keccak256(abi.encode(msg.sender, sourceSalt))`, otherwise `create` reverts `InvalidQuerySalt`. This stops anyone else from occupying the id your user's query would get. ```solidity Solidity theme={null} bytes32 sourceSalt = keccak256(abi.encode("deposit", nonce)); Query memory query = Query({ owner: address(conduit), receiver: address(conduit), input: Asset({asset: address(usdc), value: amount}), output: Asset({asset: address(conduit.getVehicle()), value: 0}), mode: Mode.DEPOSIT, salt: keccak256(abi.encode(msg.sender, sourceSalt)), data: "" }); // Create the deposit — auto-processes for sync strategies (Id queryId, State state) = conduit.create(query, userAddress, sourceSalt); ``` ```typescript TypeScript theme={null} // Coming soon ``` The query's `owner` and `receiver` are always the Conduit; the `userAddress` argument is who receives the minted cShares. `output.value` is a floor enforced at the **Vehicle** output only. It ignores Conduit fees and the cShare exchange rate, so it does not bound what the user finally receives — set it from `estimate()` if you need slippage protection, or `0` to disable it. For **sync** strategies (e.g., Aave, Compound), the deposit settles in the same transaction. The user receives shares immediately. For **async** strategies (e.g., Ethena, Syrup), the query enters `PROCESSING`. A keeper calls `process()` automatically when the underlying protocol is ready — the user doesn't need to take any further action. ## Handle withdrawals Users burn Conduit shares and receive the underlying asset. Call `createRedeemFromConduitShares` — it converts the cShare amount to Vehicle shares at the current ratio and hands the assembled REDEEM query to the same `create` entrypoint. Building the query yourself only makes sense if you already hold a Vehicle-share figure; `input.asset` would then have to be the Vehicle address, never the Conduit's. ```solidity Solidity theme={null} bytes32 sourceSalt = keccak256(abi.encode("redeem", nonce)); // Minimum asset to receive; 0 disables the floor Asset memory outputAsset = Asset({asset: address(usdc), value: minAssetsOut}); // Burns the caller's cShares — auto-processes for sync strategies (Id queryId, State state) = conduit.createRedeemFromConduitShares( shareAmount, outputAsset, sourceSalt, userAddress ); ``` ```typescript TypeScript theme={null} // Coming soon ``` No `approve` call is needed on either path. The Conduit recognizes a Vehicle-share input and burns the caller's cShares through its internal ERC-20 primitives, so a withdrawal is a single transaction. Same settlement behavior as deposits: sync strategies settle immediately, async strategies are settled automatically by keepers. For async strategies, **keepers** monitor active queries and call `process()` when the underlying protocol is ready to settle. Your platform doesn't need to build monitoring infrastructure, and users never need to return for a second transaction. The experience is identical for sync and async strategies from the user's perspective. ## Monitor with the Railnet API The Railnet GraphQL API provides indexed on-chain data for building dashboards, tracking operations, and generating reports. Use it alongside on-chain view calls for a complete picture. ``` https://graphql-enriched.staging.railnet.org/query ``` This is the **staging** endpoint. The production endpoint will be provided when available. See the [API reference](/developers/api) for query conventions and core entities. Addresses are indexed lowercase, so filter them with `_ilike`, and every `numeric` value is returned as a decimal string in raw on-chain units. ### Read Conduit configuration One query replaces most of the view calls above, and adds fee configuration, compliance mode, and yield. ```graphql GraphQL theme={null} query ConduitState($conduit: String!) { Conduit(where: { address: { _ilike: $conduit } }) { address name symbol asset assetSymbol assetDecimals assetPriceUSD supply enabled transferEnabled vehicle { address name vehicleType } feeManager { fees { feeType fee maxFee } } accountList { mode sanctionsEnabled } inceptionYield { netApr netApy } } } ``` ### Query a user's position `ConduitBalance` holds the share balance of every holder of your Conduit's token. ```graphql GraphQL theme={null} query UserPosition($conduit: String!, $owner: String!) { ConduitBalance( where: { conduit: { address: { _ilike: $conduit } } owner: { _ilike: $owner } } ) { value conduit { symbol assetSymbol assetDecimals } } } ``` ### Track operation lifecycle Each deposit or withdrawal is a `ConduitQuery` wrapping a STEAM `Query`. Read `query.state` for the current [STEAM state](/developers/contracts/steam-standard), and `query.input` / `query.output` for the assets involved. ```graphql GraphQL theme={null} query UserOperations($conduit: String!, $owner: String!) { ConduitQuery( where: { conduit: { address: { _ilike: $conduit } } owner: { _ilike: $owner } } order_by: { event: { tx: { block: { number: desc } } } } limit: 20 ) { id query { mode state input { address value } output { address value } } event { tx { hash block { number timestamp } } } } } ``` To surface a "pending operations" badge, filter out the two terminal states: ```graphql GraphQL theme={null} query PendingOperations($conduit: String!) { ConduitQuery( where: { conduit: { address: { _ilike: $conduit } } _not: { query: { state: { _in: ["SETTLED", "REJECTED"] } } } } order_by: { event: { tx: { block: { number: asc } } } } ) { id owner query { mode state } } } ``` ### Poll for updates Fetch position data on a regular interval to keep your UI current. ```typescript TypeScript theme={null} const ENDPOINT = "https://graphql-enriched.staging.railnet.org/query"; async function getPosition(conduit: string, owner: string) { const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: ` query ($conduit: String!, $owner: String!) { ConduitBalance(where: { conduit: { address: { _ilike: $conduit } } owner: { _ilike: $owner } }) { value conduit { symbol assetDecimals } } } `, variables: { conduit, owner }, }), }); const { data, errors } = await res.json(); if (errors) throw new Error(errors.map((e: { message: string }) => e.message).join("; ")); return data.ConduitBalance; } ``` GraphQL errors come back with HTTP `200` in the `errors` array — check it explicitly instead of relying on the status code. ## Next steps Set up management, performance, deposit, and redeem fees for your Conduit. Configure allowlists, blocklists, and sanctions screening. Full technical reference for the Conduit contract. # Supported protocols Source: https://docs.railnet.org/developers/vehicles/supported-protocols All DeFi protocols supported by Railnet — native adapters and ERC-4626 compatible vaults In Railnet smart contracts, a Yield Source is connected via a **Vehicle** adapter. See [Glossary](/developers/glossary) for all terminology. Railnet connects to DeFi protocols through Vehicle adapters. Some protocols have **native adapters** built into the protocol. Any protocol exposing an ERC-4626 vault can be connected through the **generic ERC-4626 Vehicle** without custom code. ## Vault standards Railnet supports multiple vault standards through its Vehicle abstraction layer. A "supported" standard means Railnet ships a base Vehicle implementation that handles the standard's mechanics — you inherit from it, and Railnet handles the STEAM lifecycle, accounting, and keeper automation. **ERC-4626 (synchronous vaults).** The most common DeFi vault standard. ERC-4626 vaults have immediate deposits and withdrawals, making them the simplest integration path. Railnet wraps these with sync Vehicles. **ERC-7540 (asynchronous vaults).** The emerging standard for vaults with delayed operations — withdrawal queues, cooldown periods, and multi-step settlement. Railnet wraps these with async Vehicles and automates settlement via keepers. The same async state machine handles off-chain settlement for real-world assets, so tokenized treasuries, private credit, and trade finance integrate through the same Vehicle model as on-chain protocols. ## Native Vehicle adapters These protocols have dedicated Vehicle implementations in the Railnet codebase, optimized for each protocol's specific mechanics. | Protocol | Vehicle type | Deposits | Withdrawals | Factory (Base Testnet) | | ------------------ | ------------------- | ------------- | ---------------- | -------------------------------------------- | | Aave V3 | `AaveV3Vehicle` | Sync | Sync | `0x422a30120F484545b13d45747c82F152b9163D5a` | | Morpho Blue | `MorphoBlueVehicle` | Sync | Sync | `0x0482823F8731650fdb15c5f9f6943eFe0B4Fe646` | | Any ERC-4626 vault | `ERC4626Vehicle` | Sync | Sync | `0xdc8D4C06E139084Fa3b6e3d3E7A3D492Da78c05B` | | Any ERC-7540 vault | `ERC7540Vehicle` | Async (queue) | Async (queue) | `0x5dB0755790F640D61b400986b39e9D2d24efc3ef` | | Lagoon | `LagoonVehicle` | Async (queue) | Async (queue) | `0xea863693c6A9dacc40DC9b1080d8C24A8889019c` | | Ethena (sUSDe) | `EthenaVehicle` | Sync | Async (cooldown) | — | Factory addresses shown are for **Base Testnet** (chain ID 8453). Mainnet addresses will be added here as deployments are finalized. The Ethena factory is not yet deployed on testnet. Railnet also includes a **Wrapper Vehicle** (`WrapperVehicleFactory: 0xb6Bce9ad06e26a7879dd6B6f1C39fA26E629dfa8`) that provides a 1:1 wrap of any ERC-20 token for STEAM compatibility. This is a utility adapter — it does not generate yield. ## ERC-4626 compatible protocols Any protocol that exposes an [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) tokenized vault can be connected to Railnet using the generic `ERC4626Vehicle` — no custom adapter code required. The following protocols are known to implement ERC-4626 and are theoretically compatible. ### Lending | Protocol | Example vaults | Notes | | -------------------------- | -------------------------------- | -------------------------------------------------- | | Morpho Vaults (MetaMorpho) | USDC, WETH, wstETH vaults | Curated lending vaults built on Morpho Blue | | Yearn V3 | V3 vaults across multiple assets | All Yearn V3 vaults are ERC-4626 native | | Euler V2 | Lending and borrowing vaults | Modular lending with ERC-4626 vault interface | | Spark (Sky/MakerDAO) | sDAI | DAI Savings Rate vault | | Gearbox | Passive lending pools | ERC-4626 pools for lending to leveraged strategies | | Silo V2 | Isolated lending markets | Risk-isolated lending with ERC-4626 shares | | Fluid (Instadapp) | fTokens (fUSDC, fETH) | Lending layer with ERC-4626 deposit tokens | | Sturdy Finance | Aggregated yield vaults | Yield aggregation on top of lending protocols | ### Yield aggregators | Protocol | Example vaults | Notes | | -------------- | ------------------------ | ----------------------------------- | | Sommelier | Cellar vaults | Actively managed strategy vaults | | Mellow Finance | LRT and restaking vaults | Permissionless vault infrastructure | ### Staking and liquid staking | Protocol | Example vaults | Notes | | --------------- | ---------------------- | -------------------------------------------------- | | Origin Protocol | wOETH, wOUSD | ERC-4626 wrapped yield-bearing stablecoins and ETH | | Frax Finance | sFRAX, sfrxETH | Staked FRAX and staked frxETH | | Angle Protocol | Savings vaults (stEUR) | Euro and multi-currency savings vaults | ### Real-world assets | Protocol | Example vaults | Notes | | ----------------- | -------------- | ------------------------------------------ | | Ondo Finance | OUSG, USDY | Tokenized US Treasuries and yield notes | | Mountain Protocol | wUSDM | Yield-bearing stablecoin backed by T-bills | | Backed Finance | bIB01, bCSPX | Tokenized ETFs and bonds | | Centrifuge | Pool tokens | Tokenized real-world credit | | OpenEden | TBILL | Tokenized T-bill vault | | Superstate | USTB | Tokenized short-term government bonds | This list is not exhaustive. Any vault implementing the ERC-4626 standard can be connected using the generic `ERC4626Vehicle`. Check a protocol's documentation to verify ERC-4626 compatibility before deploying. ## Custom adapter candidates Some protocols require async handling (withdrawal queues, cooldown periods, or per-address restrictions) and cannot use the generic ERC-4626 Vehicle. Supporting them takes a custom async adapter, following the same pattern as the native Ethena Vehicle. | Protocol | Category | Async pattern | | ------------------ | ------------------ | ------------------------- | | Lido (wstETH) | Liquid staking | Withdrawal queue | | Rocket Pool (rETH) | Liquid staking | Minipool exit queue | | EigenLayer | Restaking | Withdrawal delay | | Symbiotic | Restaking | Withdrawal delay | | Karak | Restaking | Withdrawal delay | | Pendle | Yield tokenization | Maturity-based settlement | # Introduction Source: https://docs.railnet.org/index The connectivity layer for DeFi & RWAs Railnet is an orchestration layer for asset managers & earn Platforms to build composable earn offering across any yield source and distribute them anywhere. Managing yield across multiple protocols generally means building custom infrastructure for every integration — separate accounting, permissions, and settlement logic. Railnet replaces all of that with three composable building blocks that connect yield sources, strategies, and distribution channels. * **Yield Sources** — Protocols connect once via a standard adapter. One integration makes them composable with every strategy on the network. * **Strategies** — Asset managers compose yield sources into managed products with real-time accounting and on-chain guardrails. Both allocation-based and advanced standalone strategies. * **Conduits** — Platforms deploy branded entry points with custom fees, compliance, and access control. One strategy can serve multiple platforms with different configurations. ## Get started You want to add yield to your product. Deploy a **Conduit** — a branded yield experience with your fees, compliance rules, and access to every strategy on the network. You want to build yield strategies. Start with an **Advanced Strategy** for standalone protocol execution, or compose multiple sources into an **Allocation Strategy** — with automated routing, real-time NAV, and on-chain guardrails. A neobank wants to offer 5% USDC yield. Walk through how every piece connects — from yield sources to strategy to shipped product. *** API, smart contract reference, and protocol terminology. Every new yield source, strategy, and conduit amplifies the value for everyone else on the network. [See how it works →](/overview/how-it-works) # Application example Source: https://docs.railnet.org/overview/application-example Understand how platforms and asset managers use railnet The best way to understand Railnet is to build something with it. Let's walk through a real scenario and meet every concept along the way. **The setup:** A neobank wants to offer yield on USDC to its users. They don't want an existing strategy, they want to build their own. They mandate an asset manager to run the strategy within clear guardrails: get the best yield rebalancing from Aave, Morpho, and Dynamic tokenized treasury bill Strategy run by this asset manager for their earn product. The requested strategy is an **Allocation Strategy**. This strategy will involve rebalancing with Railnet intent system, including an existing Advanced Strategy from the selected asset manager, the asset manager doesn't need to do this part for the request of the customer, he will focus on managing allocation from this platform's liquidity to their already running strategy within the mandate of the customer. ## Connecting the yield sources Each protocol — Aave, Morpho, the treasury bill fund — are **Yield Sources**: a protocol that generates yield. To plug into Railnet, each builds an adapter that implements the **STEAM** standard (State Transition Engine for Asset Management). STEAM extends ERC-4626 with a lifecycle that tracks every operation from creation through settlement or rejection. This matters because not all yield sources work the same way: * **Aave** settles instantly — deposit USDC, receive aUSDC in the same transaction. This is a **synchronous** operation. * **Dynamic tokenized treasury bill Strategy** settles on T+1 — you commit capital today, but shares aren't available until tomorrow, it's an Advanced Strategy already running operated by this same asset manager. This is an **asynchronous** operation. Ethena's staking cooldown and an ERC-7540 vault's request queue work the same way. STEAM handles both through the same interface. Every deposit or redemption — whether it takes one transaction or seven days — follows the same lifecycle: **create → process → settle**. Each individual operation is called a **Query**. A Query carries its own identity, amount, and state, and moves through the lifecycle independently. If an Advanced Strategy redemption fails while an Aave deposit succeeds, each resolves on its own — one failing Query never blocks another. ## Setting the rules The neobank doesn't give the asset manager a blank check. They issue a **Mandate** — a formal request for the asset manager to operate according to specific constraints called **Guardrails** enforced by on-chain **Policies**. Guardrails include: * **Authorized yield sources** — only Aave, Morpho, and the treasury bill fund * **Allocation caps** — a hard cap on how much can sit in any single yield source, plus a ceiling on the strategy's total assets * **Scoped permissions** — the asset manager holds only the roles for the operations they are meant to run, so no other function on the strategy is callable by them, reducing fat finger errors or wrong operations on the strategy These constraints are enforced at the smart contract level. Neither the asset manager nor automation can exceed them. Anyone can verify on-chain that the rules haven't been breached — the trust boundary is verifiable, not assumed. How platforms set guardrails and how asset managers operate within them on an Allocation Strategy. ## The asset manager operates the strategy With three yield sources connected, the asset manager creates an **Allocation Strategy** — he sets up and actively manages allocation across one or more yield sources. This single Strategy holds both synchronous sources (Aave, Morpho) and asynchronous sources (the Dynamic treasury bill Strategy), routing capital through configurable priority queues. The asset manager sets up a **deposit queue**: first 20M to Morpho, then 15M to Aave, then the treasury bill fund and no automated queues for the advanced strategy. As users deposit, capital flows down the queue automatically. A matching **redeem queue** works in reverse — each source has a minimum floor, and redemptions pull from the first source down to its floor. The asset manager doesn't need to manually fulfill deposit and redemption requests, it's automatically handled by Railnet. Combining highly liquid sources such as Morpho and Aave enables instant fulfillment of customer withdrawal requests, eliminating the need to hold idle liquidity within the strategy or wait for redemptions from the T+1 dynamic Treasury bill strategy. This approach preserves a strong user experience while maintaining high returns, all without requiring additional effort from the asset manager. The asset manager can also intervene directly: rebalance capital between sources, and reconfigure queue priorities — all without interrupting active operations. ### Keeping the books An Allocation Strategy can hold assets in many states at once — idle USDC awaiting deployment, shares earning yield in Morpho, capital mid-deposit into the treasury bill fund. Traditional balance tracking can't handle this. Railnet uses **Sectors** — logical partitions that represent where assets are in their operational lifecycle. Every asset movement is a transfer between sectors, following double-entry bookkeeping. Idle assets sit in `AVAILABLE`, and capital the manager deliberately parks aside sits in `RESERVED`. Deployed capital sits in `ALLOCATION`. Each yield source gets its own staging sector, and in-flight async operations are tracked separately again. Because every state is tracked, the Strategy knows exactly what it holds every block — real-time NAV without off-chain oracles or batch reconciliation. In Railnet smart contracts, an Allocation Strategy is called a **MultiVehicle**. See [MultiVehicle](/developers/contracts/multi-vehicle) for the full technical reference. For strategies requiring operations beyond standard adapters — swaps, borrows, bridges, or complex RWA logic — asset managers operate **Advanced Strategies** using Specialized Vehicles that standardize custody, policy enforcement, and NAV computation while remaining fully composable with the rest of the network. See [Advanced Strategies](/strategies/advanced) for details. ## The neobank ships the product The neobank deploys a **Conduit** on top of the Strategy — a branded entry point for their users with: * Its own **ERC20 share token** — users hold shares representing their position * A **0.5% management fee** — with configurable recipients and automated revenue distribution between the platform and the asset manager. Performance, deposit, and redeem fees are also available. * **KYC-gated access** — allowlists, blocklists, and sanctions oracle screening, with a strict mode that gates transfers as well as deposits * **Transfers disabled** — shares can't be moved between wallets, only minted on deposit and burned on withdrawal. The neobank can switch transfers on later, but that flip is permanent Users see a simple "Earn 5%" button in their app. They don't know about Strategies, Yield Sources, or STEAM. They deposit, they earn, they withdraw. ## Everything runs automatically Another platform wants to offer the same strategy — but with a 1% management fee on top. They deploy their own Conduit on top of the **neobank Strategy**. No capital fragmentation, no duplicate infrastructure. The asset manager rebalances once, and earn products benefit. But what about the Dynamic treasury bill fund's T+1 deposit and redeem? When a user deposits, the deposit query to the treasury fund enters a PROCESSING state. When it's ready to settle tomorrow, who advances it? **Railnet Keepers** do. A Keeper is an off-chain automation system that monitors active Queries and executes state transitions when conditions are met. When the treasury fund cooldown ends, the Keeper advances the Query. When a user's redemption is ready, the Keeper finalizes it. Keepers also manage redemption queues — when a Strategy doesn't have enough idle liquidity for an immediate withdrawal, the request is staged as a demand and fulfilled in FIFO order as liquidity becomes available. Users never need to return to the app and manually claim. Async operations settle as fast as the underlying protocols allow — automatically. ## How they connect Every yield source speaks STEAM. Every Strategy composes them with real-time accounting. Every Conduit distributes them with custom fees and compliance. Each layer is independently useful — together, they form a coordination network where every new participant amplifies the value for everyone else. ## Concepts reference Quick definitions for every term introduced above, with links to deep dives. | Concept | Definition | Deep dive | | ----------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------- | | **Yield Source** | Any protocol generating yield, connected via a STEAM adapter (Vehicle) | [STEAM standard](/developers/contracts/steam-standard) | | **STEAM** | State Transition Engine for Asset Management — the standard lifecycle interface | [STEAM standard](/developers/contracts/steam-standard) | | **Query** | A single deposit or redemption flowing through the STEAM lifecycle | [STEAM standard](/developers/contracts/steam-standard) | | **Allocation Strategy** | Managed allocation composing multiple yield sources (MultiVehicle) | [Allocation Strategies](/strategies/allocation) | | **Advanced Strategy** | Standalone strategy with full flexibility via Specialized Vehicles | [Advanced Strategies](/strategies/advanced) | | **Sector** | Logical accounting partition tracking asset lifecycle within a Strategy | [Accounting](/developers/contracts/accounting) | | **Guardrail** | On-chain constraint limiting what an asset manager can do | [Guardrails](/strategies/allocation/guardrails) | | **Mandate** | Platform's request for an AM to operate within guardrails | [Mandate a strategy](/conduits/mandate-a-strategy) | | **Conduit** | Distribution channel with fees, compliance, and branded shares | [Conduit](/developers/contracts/conduit) | | **Keeper** | Off-chain automation executing state transitions | — | | **Factory** | Deploys contracts with consistent config and anti-inflation protection | [Supported protocols](/developers/vehicles/supported-protocols) | | **EAC** | Unified permission layer — global, scoped, and public roles | [Access control](/developers/contracts/access-control) | # Ecosystem Source: https://docs.railnet.org/overview/ecosystem How asset managers, platforms, issuers, and protocols connect through Railnet Now that you've seen how the building blocks fit together, here's who uses them and why. Railnet is a multi-sided network. Its value grows as more participants join — each new yield source expands the composition space for every manager, and each new platform expands distribution for every strategy. Four types of participants connect through the protocol. ## Asset managers Asset managers are the cornerstone of the Railnet ecosystem. They operate [Strategies](/strategies/index) — allocating capital across yield sources, rebalancing positions, and managing deposit and redemption flows. This group includes crypto-native firms with demonstrated risk management at scale and forward-looking traditional asset managers looking to distribute their strategy onchain. **Why they participate:** * One interface for every yield source — DeFi and real-world assets speak the same [STEAM](/developers/contracts/steam-standard) language * On-chain books and records eliminate off-chain reconciliation * Built-in distribution — strategies reach every platform on the network via [Conduits](/conduits/index) * Focus on portfolio construction and risk management, not integration engineering Deploy and operate Strategies with step-by-step guides. ## Platforms Platforms include exchanges, wallets, custodians, fintechs, and fund allocators. They seek diversified yield beyond the crypto "risk-free rate" of staking. Railnet gives platforms access to managed strategies that combine DeFi and real-world assets within a single on-chain fund. Platforms deploy [Conduits](/conduits/index) to distribute strategies to their users with custom fees, compliance controls, and branded shares. **Why they participate:** * Access any strategy and yield source available on the railnet infrastructure without bilateral integrations * Custom fee structures with flexible revenue distribution * Built-in compliance: allowlists, blocklists, sanctions oracle support, and configurable transfer modes * Automated operations — keepers handle async settlement so end users get a seamless experience Deploy Conduits, configure fees, and distribute strategies. ## Real-world asset issuers Asset issuers — including tokenization platforms, exchanges entering tokenized securities, and traditional financial institutions — benefit from broader distribution of their assets across strategies and products. STEAM models off-chain lifecycle constraints on-chain: settlement windows, KYC gates, redemption cutoffs, and repayment events. This makes tokenized assets composable with DeFi protocols within the same Strategy. **Why they participate:** * Additional distribution channel for tokenized products * STEAM represents lifecycles of any length — the adapter holds a Query in `PROCESSING` or `PAUSED` until the underlying settlement window, cooldown, or KYC check clears, whether that takes a day, a quarter, or years * Reach asset managers and platforms across the network through a single adapter integration ## DeFi protocols Dominant protocols in each DeFi category build and maintain their own Railnet adapters. Protocols own their integration lifecycle — updating it as their core logic evolves, without depending on a central team. **Why they participate:** * One adapter integration reaches every asset manager and platform on the network * Railnet brings additional capital flow from managed strategies, not just individual depositors * Adapter ownership means the protocol controls its own integration ## How participants connect Each group amplifies the others: more yield sources mean more composition options for asset managers, better strategies attract more platforms, and more platforms expand distribution for every strategy. The result is a composable value chain where protocols, managers, platforms, and issuers each benefit from the others joining the network. # How it works Source: https://docs.railnet.org/overview/how-it-works How Railnet routes capital from yield sources to end users through composable layers ## The problem Managing yield across multiple protocols requires custom infrastructure for every integration — its own accounting, permissions, failure handling, and timing logic. Existing vault standards don't solve this: ERC-4626 handles synchronous operations but can't track async flows, and ERC-7540 added async requests but remains all-or-nothing with no way to compose multiple sources. ## Three composable layers Railnet standardizes yield management through three layers connected by [STEAM](/developers/contracts/steam-standard) — a standard interface that tracks every operation from creation through settlement, handling both instant and multi-day flows. ### Layer 1: Yield Sources Protocols wrap their yield source in a STEAM adapter. Whether the protocol completes in one transaction (Aave, Morpho) or takes days to settle (Ethena, tokenized bonds), every operation follows the same lifecycle: **create → process → settle**. One integration makes the source composable with every Strategy and Conduit on the network. The protocol owns its adapter and updates it independently. *On-chain implementation: [Vehicle](/developers/contracts/steam-standard)* ### Layer 2: Strategies Asset managers compose yield sources into managed allocations. A single Strategy can hold both synchronous and asynchronous sources — routing capital through priority queues, tracking every asset via [sector-based accounting](/developers/contracts/accounting), and enforcing [guardrails](/strategies/allocation/guardrails) set by the Strategy owner. For complex operations not available as standard adapters — swaps, borrows, bridges, perps — asset managers can use [Advanced Strategies](/strategies/advanced) via Specialized Vehicles. *On-chain implementation: [MultiVehicle](/developers/contracts/multi-vehicle)* ### Layer 3: Conduits Platforms deploy a Conduit on any Strategy — or directly on a single Yield Source — to create a branded entry point for their users. Each Conduit has its own share token, fee structure, compliance controls, and automated settlement via Keepers. One Strategy can serve many Conduits simultaneously. Each platform gets its own configuration while the underlying strategy is shared — scaling distribution without fragmenting liquidity. *On-chain implementation: [Conduit](/developers/contracts/conduit)* ## What this replaces Railnet serves the same function on-chain that fund administration platforms serve in traditional finance — standardized accounting, consolidated books and records, compliance enforcement, NAV calculation, and uniform operational workflows. | Without Railnet | With Railnet | | -------------------------------------------- | -------------------------------------------------------- | | Custom accounting per protocol | Real-time books via sector-based double-entry accounting | | Off-chain NAV calculation, delayed reporting | Contract-native NAV, computed every block | | Manual reconciliation across systems | On-chain single source of truth | | Per-integration permissions and compliance | Unified role-based access control across all layers | | Bespoke fee collection and invoicing | On-chain fee management — automatic, verifiable | | Custom vault infrastructure per venue | One STEAM integration per yield source | | End-of-day batch position updates | Real-time capital deployment observable on-chain | ## Network effects Each participant amplifies the others: * Every new **Yield Source** adapter expands the composition space for every asset manager * Every new **Strategy** gives platforms more products to offer their users * Every new **Conduit** expands distribution reach for every strategy — without additional work from asset managers * Greater capital flow incentivizes more protocols to integrate The value of joining Railnet increases with every new participant at every layer. ## What to read next How asset managers, platforms, issuers, and protocols connect through Railnet. How STEAM compares to ERC-4626, ERC-7540, and other vault approaches. # Railnet and other standards Source: https://docs.railnet.org/overview/railnet-and-other-standards How STEAM compares to ERC-4626, ERC-7540, and other vault approaches Railnet's STEAM standard was designed to address specific limitations in existing vault standards and approaches. This page provides a structured, fact-based comparison to help you evaluate where Railnet fits. ## Standards comparison ### ERC-4626 — synchronous vaults ERC-4626 established a widely adopted vault interface for DeFi. It handles atomic deposit-and-share operations in a single transaction. **Limitations:** * Synchronous only — no mechanism for cooldown periods, withdrawal queues, or multi-step flows * No operation tracking — once a transaction completes, there is no lifecycle to observe * Works well for simple DeFi protocols (Aave, Compound) but cannot represent assets with settlement delays ### ERC-7540 — asynchronous extension ERC-7540 extended ERC-4626 with asynchronous deposit and redeem requests. The specification itself acknowledges several limitations: 1. **No cancellation flow** — users cannot revoke a pending request once initiated 2. **No native timestamping** — ambiguity around how long a request can remain pending and when it should expire 3. **No defined state transition model** — no clear, enforceable path from "pending" to "claimable" 4. **Single logical action** — abstracts a two-step process into one, which breaks down for complex strategies 5. **All-or-nothing** — vaults must be either fully synchronous or fully asynchronous, with no support for partial fills, staggered liquidity, or mixed execution paths ### STEAM — stateful lifecycle STEAM provides a 7-state lifecycle with explicit, enforceable transitions: * Every operation ([Query](/developers/glossary)) has a unique ID with full lifecycle tracking * Supports both sync and async in the same architecture — a Strategy can hold sync and async yield sources (Aave and Ethena) simultaneously * Built-in error recovery ([RECOVERING](/developers/contracts/steam-standard) state) — assets can be reclaimed without impacting other operations * Explicit rejection — operations can be denied with assets returned via the REJECTED state * Per-operation isolation — a failure in one Query does not affect others ## Capability comparison | Capability | ERC-4626 | ERC-7540 | STEAM (Railnet) | | --------------------------- | -------- | ----------------------- | --------------------------- | | Sync operations | Yes | Yes | Yes | | Async operations | No | Yes | Yes | | Mixed sync + async | N/A | No (vault-level choice) | Yes (per-Vehicle) | | Operation lifecycle | None | Pending / Claimable | 7-state machine | | Concurrent operations | N/A | Limited | Yes (unique Query IDs) | | Rejection with asset return | No | No | Yes (RECOVERING → REJECTED) | | Error recovery | No | No | Yes (RECOVERING state) | | Multi-source composition | No | No | Yes (MultiVehicle) | | Distribution layer | No | No | Yes (Conduits) | | Real-time NAV | External | External | Contract-native | | Access control | External | External | Built-in (EAC) | | Fee management | External | External | Built-in (Fee Manager) | ## Approach comparison ### Single-protocol vault approaches Some vault platforms are designed around a single protocol or ecosystem. Morpho Vaults are ERC-4626 based and built primarily for Morpho Markets. Asset managers ("curators") construct strategies within the Morpho ecosystem. Morpho explicitly does not build adapters for competing protocols (Aave, Euler, etc.). This is a deliberate design choice rooted in avoiding dependency on upgradeable external protocols. Their focus is on native tokenized products built directly on the Morpho stack. Railnet takes a different approach: it wraps any yield source — including Morpho — through a common standard. Dependency risk is distributed across many protocols rather than concentrated in one, and asset managers can rebalance liquidity across Vehicles if an underlying protocol changes. Lagoon implements ERC-7540 with an epoch-based request model: requests accumulate in a pending silo and the operator settles a whole epoch at once, priced against a total-assets valuation pushed on-chain by a valuation manager. Key constraints: * Requests are batched per epoch and observable only as pending or claimable — there is no per-operation lifecycle to track * A controller can hold only one pending redemption request at a time — requesting again in a later epoch reverts * Valuation and settlement are operator-driven and off-chain-sourced rather than the result of on-chain state transitions Railnet gives every operation its own identity. Each Query carries its own amount, state, and lifecycle, so one user can hold many concurrent partial requests on the same yield source, mixed sync and async sources can sit inside a single Strategy, and every transition is committed on-chain through STEAM. Veda's BoringVault is a minimalist vault core that delegates strategy execution, pricing, and access control to specialized modules. Curators submit Merkle-tree-verified rebalance messages to deploy capital across DeFi protocols. Key constraints from an asset manager's perspective: * **One vault per partner** — distributing the same strategy to multiple platforms requires deploying a separate BoringVault for each, with its own fee configuration, access control, and accounting * **Operational overhead scales linearly** — rebalancing, rate updates, and Merkle tree management must be repeated across every vault instance * **No native distribution layer** — there is no built-in mechanism to share a single strategy across multiple channels with per-partner customization Railnet separates strategy management from distribution. An asset manager creates one Strategy, then wraps it into multiple Conduits — each with custom fees, compliance rules, and access control — without duplicating capital allocation. Rebalancing happens once at the Strategy level, and operational complexity stays flat as partners scale. Railnet also composes well with Veda. Asset managers already operating on Veda infrastructure can bring existing BoringVaults into a Railnet Strategy as a yield source by building a STEAM adapter for them, enabling a fund-of-funds model — diversified, multi-strategy products on top of the infrastructure they already know and trust. ### Railnet's approach Railnet is not competing with individual protocols or single-asset vaults. It operates at a different layer of the stack: * **Protocol-agnostic** — wraps any yield source (DeFi or real-world asset) through the STEAM standard * **Multi-source by design** — a single Strategy composes multiple yield sources across different protocols and asset types * **On-chain operations** — deposit, redemption, rebalancing, fee collection, and queue management all happen on-chain through standardized interfaces * **Connective infrastructure** — connects protocols, asset managers, and platforms into a unified network rather than competing with any one of them ## When to use what Simple, single-protocol synchronous vaults. Wrapping an Aave market or Compound pool where deposits and withdrawals complete atomically. Single async vault when you need basic async deposit/redeem without multi-source composition or mixed execution modes. Multi-protocol strategies, mixed sync/async operations, real-world assets with off-chain timing, or institutional requirements (compliance, risk analytics, auditable accounting). # Accounting — NAV proposals and settlement Source: https://docs.railnet.org/strategies/advanced/accounting Operational guide for running NAV update cycles — propose, settle, and verify ## Overview A NAV update cycle is the process that keeps the vault's share pricing current and settles pending deposits and redemptions. Each cycle has two steps: 1. **Propose** — The Valuation Manager proposes a new `totalAssets` value on-chain 2. **Settle** — The Strategy finalizes the NAV and processes pending request queues This page is the operational guide for running these cycles. For the underlying mechanics of how NAV updates and settlement work at the contract level, see [Vault 7540 mechanics](/strategies/advanced/vault). ## NAV update step-by-step ### Step 1 — Check NAV validity Call `isTotalAssetsValid()` on the vault: * **`true`** — NAV is still valid. Sync deposits and sync redemptions are enabled, as far as the current sync mode allows them. The Valuation Manager **cannot** propose a new NAV until the current one expires. * **`false`** — NAV has expired. The Valuation Manager can propose a new NAV. ### Step 2 — Force expiration (if needed) If you need to run a NAV cycle immediately but the NAV is still valid, the Strategy can call `expireTotalAssets()` to force expiration. This immediately disables sync operations and enables the Valuation Manager to propose. ### Step 3 — Prepare liquidity Before settling, ensure the Strategy wallet holds enough liquid assets to cover pending redemptions. The NAV will be confirmed and deposits will be settled even if the Strategy lacks liquidity for redemptions. Redemption settlement simply won't execute — it does not revert the transaction. ### Step 4 — Propose NAV The Valuation Manager calls `updateNewTotalAssets(newTotalAssets)` with the new total assets value. This function: * Snapshots all pending requests and creates an epoch boundary * Validates the proposed NAV against [guardrails](/strategies/advanced/vault#nav-guardrails) (if active) * Stages the value for the Strategy to confirm This call reverts if the NAV is still valid (`ValuationUpdateNotAllowed`) or if guardrails are active and the price-per-share change exceeds the configured bounds (`GuardrailsViolation`). If guardrails block a legitimate update during an extreme market event, the Security Council can call `securityCouncilUpdateTotalAssets()` to bypass them. **Operational requirement:** Your tooling must persist the exact `newTotalAssets` value used here — the Strategy must pass the identical value in the settlement call. ### Step 5 — Finalize NAV and settle The Strategy calls one of two settlement functions, passing the **exact same** `newTotalAssets` value that was proposed in Step 4. | Function | Settles deposits | Settles redemptions | When to use | | ------------------------------- | ---------------- | ------------------- | ------------------------------------------------- | | `settleDeposit(newTotalAssets)` | Yes | Yes (best-effort) | **Recommended** — handles both queues in one call | | `settleRedeem(newTotalAssets)` | No | Yes (best-effort) | Only when you want to skip deposit settlement | What happens during settlement: 1. NAV is finalized to the proposed value 2. Management and performance fees are taken (shares minted to fee receivers) 3. Current entry and exit fee rates are recorded for the epoch 4. Deposit queue is settled (if using `settleDeposit`) — assets move from the Silo to the Strategy 5. Redemption queue is attempted — assets move from the Strategy to the vault for claiming (only if the Strategy has enough liquidity) You cannot call settlement twice in the same cycle. Once the NAV is finalized, the staged value is cleared. If redemption settlement fails due to insufficient liquidity, you must wait for the next NAV cycle — you cannot retry without re-running the full propose-and-settle flow (which also re-runs fee logic). ### Step 6 — Post-settlement verification After settlement, verify: * `totalAssets()` equals the NAV you finalized * `isTotalAssetsValid()` returns `true` (sync window is now open) * If sync redeem is enabled, check that the Strategy still holds sufficient idle liquidity for potential sync redemptions during the lifespan window ## Sync redemption liquidity When sync redeem is enabled, each settlement opens a sync window (the `totalAssetsLifespan` period) during which shareholders can instantly redeem directly from the Strategy wallet — without waiting for the next settlement. This creates an additional liquidity planning requirement on top of async redemptions: * **Async redemptions** require liquidity at the **next settlement** — the Strategy has time to unwind positions if needed * **Sync redemptions** require liquidity **immediately and continuously** during the entire sync window — there is no grace period ### Emergency levers If the Strategy's idle balance runs low during the sync window: 1. **`expireTotalAssets()`** — immediately closes the sync window and forces all new redemptions to async. This also enables a new NAV cycle. 2. **`setSyncMode(SyncDeposit)` or `setSyncMode(None)`** — disables sync redeems while optionally keeping sync deposits open. Unlike `expireTotalAssets()`, this does not expire the NAV. 3. **Do nothing** — let the lifespan expire naturally. ## Using a third-party valuation service Instead of having the Asset Manager handle both the NAV proposal and the settlement, the valuation proposal can be delegated to a third-party NAV computation service that calculates the vault's net asset value and pushes it on-chain automatically. This decouples the two steps of the NAV cycle: | Step | Self-managed | With valuation service | | ----------------------------------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------- | | **NAV proposal** (`updateNewTotalAssets()`) | Asset Manager proposes | Valuation service pushes automatically | | **Settlement** (`settleDeposit()` / `settleRedeem()`) | Asset Manager settles | Asset Manager settles (unchanged) | | **Frequency** | Both tied to Asset Manager's schedule | NAV updates can be daily (automated), settlements at the Asset Manager's discretion | ### Key benefits * **Fresh valuation always staged** — an automated daily proposal keeps a current valuation ready for the next settlement. Note that a proposal alone does not reopen the sync window: only settlement refreshes the NAV expiration, and a proposal can only be made once the previous window has already expired * **Settlement can batch** — the Asset Manager can let multiple NAV updates pass before settling, processing all pending requests at the latest NAV * **Separation of concerns** — valuation and settlement are handled by different parties, reducing single-point-of-failure risk Each `updateNewTotalAssets()` call overwrites the previous staged value. Only the most recent staged NAV can be used for settlement. There is no timeout — the staged NAV remains until a settlement call consumes it. ### Setup 1. **Update the valuation manager** — The vault owner calls `updateValuationManager(serviceWalletAddress)` to set the service's wallet as the valuation manager 2. **No access control needed** — The `updateNewTotalAssets()` function is gated by the `onlyValuationManager` modifier, not by allowlist/blocklist checks. The service wallet does not need to be whitelisted ## What to read next How the vault handles deposits, redemptions, settlement, fees, and access control at the contract level. How the Policy Engine enforces on-chain permissions, spending limits, and guardian governance. Day-to-day operations — execute transactions through the policy engine. High-level overview of the Advanced Strategy architecture. # Advanced Strategies Source: https://docs.railnet.org/strategies/advanced/index Full-flexibility strategy execution with on-chain policy enforcement, and standardized NAV ## What is an Advanced Strategy This is where most asset managers start. An Advanced Strategy is a standalone strategy container that gives full protocol execution flexibility — lend, borrow, swaps, bridges, perpetual futures, RWA operations — while standardizing the four things that are otherwise left to bespoke, risky setups: investor lifecycle, non-custodial execution, on-chain guardrails, and accounting. Each Advanced Strategy is composed of: * A **7540 Vault** — the capital entry point that handles deposits, redemptions, share pricing, settlement, fees, and access control * A **Strategy** where the strategy funds are held and the operations are executed — no third party can recover your keys * An **on-chain policy engine** governing exactly which contracts, functions, and parameters the asset manager can use, defined by the quorum of the strategy smart contract * **Standardized NAV reporting** so share pricing is accurate and auditable You interact with any supported DeFi protocol directly. There are no adapters to build, no Railnet adapter to wait for. If the protocol has a smart contract, Railnet will whitelist required operations in the strategy's policy engine. ## Why Specialized Vehicles exist Today, traditional asset managers who need to operate beyond simple allocation strategy currently use their existing wallet infrastructure like Fireblocks or ForDeFi for custody & execution with web2 co-signer services for policy enforcement, and ad-hoc setups for NAV computation. This approach has fundamental problems: * **Strategy is not truly non-custodial** — owners of the wallet infrastructure providers can recover private keys, creating a trust dependency that undermines the premise of on-chain asset management * **Policy engines are black boxes** — web2 co-signer and policy services are partial not publicly auditable, not verifiable on-chain, and not standardized accross asset managers * **NAV computation is ad-hoc and error-prone** — incorrect NAV means incorrect share pricing, which means loss of funds for depositors. In fund-of-funds structures, NAV errors chain across layers — a bad Advanced Strategy NAV cascades into every Allocation Strategy that holds it * **Every strategy is bespoke** — each deployment requires custom integration work for custody, policy, and reporting, with no standardized framework Specialized Vehicles solve these problems by standardizing four pillars that every advanced strategy needs. ## The four pillars ### Vault The 7540 Vault is the capital entry point for every Advanced Strategy. It implements the ERC-7540 standard handling deposits, redemptions, share pricing, fee collection, and access control through a single contract. The vault supports both synchronous and asynchronous flows. When the NAV is valid, deposits settle instantly; when it has expired, they are kept pending and settled together at the next valuation cycle. Redemptions follow the same dual model — pending by default, with an optional instant liquidity buffer for immediate withdrawals. Pending funds are isolated in a dedicated escrow (Silo) until settlement. The vault collects management, performance, entry, exit, and haircut fees — all configurable per strategy. Access can be gated via allowlist, blocklist, or external sanctions lists. NAV guardrails protect against erroneous valuations by enforcing on-chain bounds on share price changes. For a deep dive, see [Vault 7540 mechanics](/strategies/advanced/vault). ### Strategy A multi-EVM-chain programmable set of smart contract (Safe + Guards + Policy Engine) deployed per strategy. The smart contract is fully non-custodial and can't execute asset management operations on its own or via the admin quorum — no workspace owner, no infrastructure provider, and no third party can recover the private keys. The strategy owners define the policy engine, changes in the policy can only be done with a delay timelock. The asset manager operates the strategy with its own logic, quorum and signing process on the attached policy engine, and the smart contract architecture ensures that all operations follow the on-chain guardrails or reject the operation. ### Policy A custom on-chain policy engine is deployed matching your strategy requirements relying on **Zodiac Roles Modifier v2** that governs what the asset manager can do: * **Contract whitelists** — which contracts can be called * **Function-level permissions** — which functions on those contracts are allowed * **Granular and on-chain parameter checks** — which parameter values are accepted (recipient addresses, spending limits, slippage bounds) * **Guardian governance** — oversight roles with timelock enforcement for policy changes Every rule is transparent, auditable, and verifiable on-chain. No dependency on a third-party back-end or co-signer. What gets executed on the strategy is only what was agreed in the strategy definition. Platforms and depositors can inspect the exact constraints governing a strategy before allocating capital. There are no off-chain co-signers, no black boxes, and no trust assumptions beyond the smart contracts themselves. For a deep dive, see [Policy engine](/strategies/advanced/policy-engine). ### Accounting Accurate NAV is critical — it determines share pricing for every depositor and, when a Specialized Vehicle is held inside an Allocation Strategy (MultiVehicle), incorrect NAV cascades through the fund-of-funds accounting. The valuation process and format is standardized across all Advanced Strategies, making it possible for platforms and risk managers to compare strategies on equal terms. ## Supported protocols The policy engine can whitelist any smart contract, but these protocols ship with pre-built permission sets — ready to use in your strategy with prebuilt constraints, spending limits, and risk classifications. | Category | Protocol | Chains | Description | | ------------------- | ----------- | ------------------- | ----------------------------------------------------------------------- | | **Lending** | Aave V3 | ETH, ARB, OPT, BASE | Supply, borrow, and manage collateral across the largest lending market | | **Lending** | Compound V3 | ETH, ARB, BASE | Single-asset lending markets with isolated risk | | **Lending** | Morpho Blue | ETH, ARB, BASE | Isolated lending markets with permissionless market creation | | **Lending** | Spark | ETH | Lending and savings products in the Sky (Maker) ecosystem | | **DEX / Swaps** | Uniswap V3 | ETH, ARB, OPT, BASE | Concentrated liquidity swaps and LP positions | | **DEX / Swaps** | CoW Swap | ETH, ARB, BASE | MEV-protected batch auction swaps via pre-signing | | **DEX / Swaps** | Balancer V2 | ETH, ARB, OPT, BASE | Weighted pool swaps and liquidity provisioning | | **Staking / Yield** | Lido | ETH | Liquid staking (stETH/wstETH) with withdrawal queue support | | **Staking / Yield** | Pendle | ETH, ARB | Yield tokenization — trade fixed and variable yield separately | | **Savings** | Sky DSR | ETH | DAI Savings Rate — earn yield on idle DAI | This list is growing. If a protocol you need is not listed, you can whitelist its contracts manually through the policy engine. Any EVM smart contract can be scoped with custom permissions. ## Use cases * **Custom RWA looping strategy with instant redeem liquidity** — recursive deposit/borrow against tokenized real-world assets, with a liquid reserve to serve immediate redemptions * **Multi-perps yield strategy** — allocate across perpetual futures protocols to capture funding rate yield, with on-chain policy constraints on position sizing and protocol exposure * **Cross-chain yield farming** — deploy treasury wallets on multiple EVM chains and allocate capital to yield sources across networks, with unified accounting and NAV reporting * **Custom protocol interactions** — any strategy requiring contract calls not yet available as standard Railnet Vehicles. The policy engine lets you whitelist exactly the contracts and functions you need ## Custom Vehicle patterns Looping (recursive deposit/borrow to amplify yield), leverage, and cross-chain bridging are patterns you build using the Specialized Vehicle's on-chain policy engine. For each pattern, you whitelist the specific contracts and functions your strategy needs: * A **looping strategy** whitelists a lending protocol's deposit and borrow functions, allowing recursive calls within the policy engine's constraints * A **leverage strategy** whitelists borrowing and swap contracts, with calldata-level checks constraining leverage ratios * A **bridging strategy** whitelists bridge contracts and destination-chain treasury wallets, enabling cross-chain capital movement under on-chain governance These are not built-in protocol features — they are custom patterns enabled by the policy engine's flexibility. The on-chain nature of the policy means every constraint is visible to depositors and auditors. ## How it connects to Railnet While Railnet is not fully live, the **distribution of Advanced Strategies is done directly from the Vault 7540**, as well as the fees management. A yield source wraps the 7540 vault interface of this Advanced Strategy with Railnet's standard Vehicle adapter — a `LagoonVehicle`, which extends the generic `ERC7540Vehicle` and routes each operation through the vault's sync or async path depending on `syncMode()` and `isTotalAssetsValid()`. This makes it fully composable with the Railnet stack: * **Distributed directly via a Conduit** — wrap the Specialized Vehicle with a [Conduit](/conduits) to offer it to end users, with your own fees, compliance rules, and branded shares * **Used as a sub-vehicle in an Allocation Strategy** — a [MultiVehicle](/developers/contracts/multi-vehicle) can hold a Specialized Vehicle alongside standard Vehicles, creating a fund-of-funds model where managed allocation and custom execution coexist * **Fees managed at the Railnet Vehicle level** — the standard [fee framework](/developers/contracts/fee-manager) applies, including detailed per-fee-type accounting, multi-recipient support, and flexible revenue distribution From the perspective of a Conduit or an Allocation Strategy, a Specialized Vehicle behaves like any other Vehicle — deposits, redemptions, and NAV reporting follow the same STEAM interface. ## Fund of funds An Allocation Strategy (MultiVehicle) can include Specialized Vehicles as sub-vehicles alongside standard Vehicles. The MultiVehicle's [sector-based accounting](/developers/contracts/accounting) tracks the Specialized Vehicle's NAV just like any other sub-vehicle — each sub-vehicle occupies a sector, and the Allocation Strategy computes its aggregate NAV from all sectors. This enables portfolio construction where some allocation goes to established yield sources (via standard Vehicles) and some goes to custom strategies (via Specialized Vehicles), all managed through a single Allocation Strategy with unified accounting, fee collection, and distribution. ## What to read next How the Policy Engine enforces on-chain permissions, spending limits, and guardian governance. Day-to-day operations — execute transactions, manage spending limits, update permissions. Compose multiple yield sources and Advanced Strategies into a managed portfolio with automated queues. Guardrails, allocation caps, and risk frameworks for strategy operations. # Operate an Advanced Strategy Source: https://docs.railnet.org/strategies/advanced/operate Route protocol actions through the on-chain policy engine — one ABI, one extra encode step In Railnet terminology, an Advanced Strategy is also called a **Specialized Vehicle**. The Policy Engine is a Zodiac Roles Modifier v2 contract enabled as a module on the Strategy's Safe wallet. This is the operator guide for teams running protocol actions on a Railnet Advanced Strategy — the on-chain container that holds the capital and enforces the mandate you were given. It assumes you already know how to craft a transaction for your target protocol (`supply`, `swap`, `mint`, whatever) and that you already have a production signer. What you don't yet know is the one extra step that routes your calldata through the Strategy's Policy Engine so your mandate is enforced for you on-chain. Good news for the PM: your current execution engine keeps doing everything it already does. You add one outer encode step and change the `to` address. That's the whole integration. Good news for the dev: it's one ABI fragment, one function call, six args. ## TL;DR ```text Pseudocode theme={null} inner = encode(protocol.someAction, args) # you already know how wrapped = encode(RolesModifier.execTransactionWithRole, [innerTo, 0, inner, 0, ROLE_KEY, true]) tx = { to: POLICY_ENGINE_ADDRESS, data: wrapped, value: 0 } signer.signAndBroadcast(tx) # your existing infra ``` Four lines. The first and last already exist in your codebase. Line 2 is the integration. ## Mental model * The **Advanced Strategy** is a Safe. It holds the capital. Shortened to "the Strategy" for the rest of this guide. * The **Policy Engine** is a Zodiac Roles Modifier v2 contract, enabled as a module on the Strategy. It is a **separate address** from the Strategy — the administrator gives you both. * Your **member signer** (EOA, hardware wallet, Fireblocks vault, Safe, KMS key, anything) is registered against a **role key** on the Policy Engine. That role defines exactly which functions, on which targets, with which argument shapes, you are allowed to push through. On-chain call flow: From the protocol's perspective `msg.sender == STRATEGY_ADDRESS`. No allowlist update, no integration change, nothing about your signer is visible to the target contract. ## What the administrator gives you | Name | Type | What it is | | ----------------------- | --------- | ------------------------------------------------------------------ | | `STRATEGY_ADDRESS` | `address` | The Safe that holds the capital and executes the protocol call | | `POLICY_ENGINE_ADDRESS` | `address` | The Roles Modifier v2 contract enabled as a module on the Strategy | | `ROLE_KEY` | `bytes32` | Identifier for your member role on the Policy Engine | These are independent values. There is no deterministic derivation between `STRATEGY_ADDRESS` and `POLICY_ENGINE_ADDRESS` — ask the admin for both. ## The role key Two encodings are in the wild: **Numeric index** — a `uint` left-padded to 32 bytes. Simplest single-role deployments use this. ``` 0x0000000000000000000000000000000000000000000000000000000000000001 ``` **ASCII label packed as bytes32** — an admin-chosen human-readable string, left-aligned and right-zero-padded to 32 bytes. Most production deployments use this because it's greppable in explorer traces. Example — `"aave_usdc"`: ``` 0x616176655f757364630000000000000000000000000000000000000000000000 ``` At runtime the value is opaque to you. The admin tells you the literal `bytes32`; you pass it through. If it's wrong, the Policy Engine reverts with `NoMembership`. ## The only ABI you need This is the one contract surface you must paste into your ABI registry. Everything else about the Policy Engine is invisible to the execution path. ```json theme={null} { "name": "execTransactionWithRole", "type": "function", "stateMutability": "nonpayable", "inputs": [ { "name": "to", "type": "address" }, { "name": "value", "type": "uint256" }, { "name": "data", "type": "bytes" }, { "name": "operation", "type": "uint8" }, { "name": "roleKey", "type": "bytes32" }, { "name": "shouldRevert", "type": "bool" } ], "outputs": [{ "name": "success", "type": "bool" }] } ``` Argument by argument: * **`to`** — the **protocol** target (Aave Pool, Uniswap Router, USDC token, ...), not the Strategy. * **`value`** — ETH to forward. Almost always `0`. * **`data`** — your already-crafted inner calldata, untouched. * **`operation`** — **always `0` (CALL)**. Never `1` (DELEGATECALL); that path is reserved for internal tooling and has no valid use from an operator member. * **`roleKey`** — the `bytes32` from the section above. * **`shouldRevert`** — set to `true`. Makes inner failures revert the whole transaction instead of silently returning `false`, so your ops pipeline sees real errors. ## The flow in pseudocode Language-agnostic. Steps 1, 3, and 4 are what you're already doing for direct protocol calls today; step 2 is the entire integration. ``` inner_to, inner_data, inner_value = craft_your_protocol_tx(...) ``` This is the one new step. ``` wrapped_data = abi_encode( ROLES_MODIFIER_EXEC_ABI, "execTransactionWithRole", [inner_to, inner_value, inner_data, 0, ROLE_KEY, True], ) ``` ``` tx = { "to": POLICY_ENGINE_ADDRESS, "data": wrapped_data, "value": 0, # nonce / gas / chainId filled in by your existing signer pipeline } ``` ``` signed = signer.sign(tx) tx_hash = rpc.send_raw_transaction(signed) receipt = rpc.wait_for_receipt(tx_hash) ``` That's the contract: encode one extra layer, change the `to` address, everything else stays. ## Reference implementation Generic version using TypeScript and viem. Replace the two marked variables with whatever your existing protocol-crafting code produces. ```typescript TypeScript theme={null} import { createWalletClient, http } from "viem" import { privateKeyToAccount } from "viem/accounts" import { mainnet } from "viem/chains" // your chain of choice const ROLES_ABI = [ { name: "execTransactionWithRole", type: "function", stateMutability: "nonpayable", inputs: [ { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "data", type: "bytes" }, { name: "operation", type: "uint8" }, { name: "roleKey", type: "bytes32" }, { name: "shouldRevert", type: "bool" }, ], outputs: [{ name: "success", type: "bool" }] }, ] as const const wallet = createWalletClient({ account: privateKeyToAccount(MEMBER_PRIVATE_KEY), chain: mainnet, transport: http(RPC_URL), }) // 1. You already produced these const INNER_TARGET: `0x${string}` = /* your protocol contract */ const INNER_DATA: `0x${string}` = /* your protocol calldata */ // 2. Push it through the Policy Engine const txHash = await wallet.writeContract({ address: POLICY_ENGINE_ADDRESS, abi: ROLES_ABI, functionName: "execTransactionWithRole", args: [INNER_TARGET, 0n, INNER_DATA, 0, ROLE_KEY, true], }) ``` ```solidity Solidity theme={null} // Coming soon ``` Swap `privateKeyToAccount` for your signer's viem-compatible account object — Fireblocks, Turnkey, AWS KMS, Ledger Connect, Dynamic, Privy, and most others ship one. The rest of the snippet is identical regardless of backend. ## Any signer, same bytes The Policy Engine does not care **how** a transaction was signed, only that the `from` address is a registered member of the role. Anything that can sign a standard EIP-1559 contract call to `POLICY_ENGINE_ADDRESS` works out of the box. As shown in the reference implementation above. Sign and broadcast directly with a private key using viem, ethers, or any web3 library. User approves on device. The outer transaction is a plain contract call, so the device shows `to = Policy Engine`, `function = execTransactionWithRole`. Create a contract-call transaction in the provider's SDK with `to = POLICY_ENGINE_ADDRESS`, `data = wrapped_data`. No custom integration on the provider side. Sign the raw transaction bytes exactly as you would any other contract call. The Policy Engine sees a standard `from` address — it does not inspect the signing backend. Covered in the next section because it has two extra steps. Every option above produces the same on-chain call. Pick whichever fits your existing ops posture and you are done. ## When the member signer is a Safe Some teams register a dedicated Safe as the Policy Engine member so signing authority is already multi-party before any transaction touches the Strategy. The flow adds two steps around what you already built above. 1. Build `wrapped_data` exactly as in step 2 of the pseudocode flow (targeting the Policy Engine). 2. Wrap it a **second time** as a Safe transaction: a SafeTx with `to = POLICY_ENGINE_ADDRESS`, `data = wrapped_data`, `value = 0`, `operation = 0`. 3. Collect owner signatures on the SafeTx hash via your existing Safe flow — Safe Transaction Service, in-house queue, whatever you already run. 4. Execute the SafeTx. The on-chain path becomes: ``` Member Safe --> Policy Engine --> Strategy Safe --> Protocol ``` Minimal wrap step with `@safe-global/protocol-kit` (the rest — proposal, confirmation collection, execution — is your existing Safe infra): ```typescript TypeScript theme={null} import Safe from "@safe-global/protocol-kit" const memberSafe = await Safe.init({ provider: RPC_URL, signer: OWNER_KEY, safeAddress: MEMBER_SAFE_ADDRESS, }) const safeTx = await memberSafe.createTransaction({ transactions: [{ to: POLICY_ENGINE_ADDRESS, value: "0", data: wrappedData, // from step 2 of the pseudocode flow operation: 0, }], }) // ...then sign, propose, and execute via your existing Safe flow ``` ```solidity Solidity theme={null} // Coming soon ``` Gas is paid by whichever owner executes the SafeTx, not the proposer. ## What to read next Understand how the on-chain policy engine governs what operators can do. Overview of Specialized Vehicles — custody, policy, and accounting. Risk frameworks and guardrails for strategy operations. # Policy engine Source: https://docs.railnet.org/strategies/advanced/policy-engine On-chain guardrails for Advanced Strategies — contract whitelists, function permissions, and calldata checks In Railnet Advanced Strategies, the policy engine is a **Zodiac Roles Modifier v2** contract enabled as a module on the Strategy. Every rule is transparent, auditable, and verifiable on-chain. The policy engine is the on-chain governance layer that defines what an operator can and cannot do. There are no off-chain co-signers, no black-box policy services. Every constraint is visible on-chain and verifiable by depositors, platforms, and auditors. Traditional wallet infrastructure relies on web2 co-signer services for policy enforcement. These systems are not publicly auditable, not verifiable on-chain, and not standardized. The policy engine replaces that trust assumption with deterministic, on-chain rule enforcement. ## How it works An operator (the "member signer") never calls the Strategy directly. Every transaction routes through the policy engine, which checks the operator's role and validates every parameter before forwarding the call. The operator builds their protocol calldata as usual (e.g., an Aave `supply` call), wraps it in a single `execTransactionWithRole` call, and sends it to the policy engine address with the associated role key. The policy engine validates the call against the operator's role permissions. If all checks pass, the policy engine forwards the call through the Strategy, which executes it on the target protocol. From the protocol's perspective, `msg.sender == STRATEGY_ADDRESS`. The target contract sees no difference between a call from the policy engine and a direct call from the Safe. The operator's signer address is never visible to the target protocol. The operator's existing execution pipeline stays unchanged. The integration is one extra ABI-encoding step and a change to the `to` address. See [Operate an Advanced Strategy](/strategies/advanced/operate) for the full integration guide. ## Permission layers The policy engine enforces four layers of constraints, each narrowing what an operator can do. Every layer is configured on-chain and readable by anyone. ### Contract whitelists The first layer controls which contracts the operator can interact with. Only explicitly whitelisted target addresses are allowed — any call to a non-whitelisted contract reverts. For example, an operator managing a lending strategy might have three whitelisted targets: the Aave V3 Pool, the USDC token contract (for approvals), and the Morpho Blue contract. Any attempt to call a contract outside this set fails at the policy engine before reaching the Strategy. ### Function-level permissions The second layer controls which functions on whitelisted contracts the operator can call. Each function is identified by its 4-byte selector. Functions are categorized by risk level, which determines their default state: | Risk level | Default state | Examples | | ---------- | ------------------------------ | --------------------------------------- | | Low | Enabled | `withdraw`, `repay`, `claimWithdrawals` | | Medium | Enabled (with spending limits) | `supply`, `borrow`, `swap` | | High | Disabled | `liquidate`, `absorb`, admin functions | Low-risk functions like `withdraw` and `repay` are enabled by default because they move funds back to the Strategy. Medium-risk functions like `supply` and `borrow` are enabled but prompt for spending limits. High-risk functions like `liquidate` and admin functions are disabled by default and require explicit opt-in. Disabling a function at the policy engine level means the operator cannot call it at all, regardless of other permissions. The function selector is simply not in the allowed set. ### Calldata-level checks The third layer inspects the actual parameter values in each function call. The policy engine defines condition trees using these operators: | Operator | What it checks | Example use | | ----------------- | ------------------------------------------- | ------------------------------------------------------------ | | `EqualTo` | Parameter must match an exact value | Restrict to specific tokens or pool IDs | | `EqualToAvatar` | Parameter must equal the Strategy address | Lock recipients so funds cannot leave the Strategy | | `GreaterThan` | Parameter must exceed a threshold | Enforce minimum slippage protection (`amountOutMinimum > 0`) | | `LessThan` | Parameter must be below a threshold | Cap individual transaction sizes | | `WithinAllowance` | Parameter must fit within a spending budget | Rate-limit operations with periodic refills | These operators combine to express precise constraints on every parameter. Three examples: The most critical constraint. Any parameter named `to`, `recipient`, `receiver`, `onBehalfOf`, or `owner` is constrained to `EqualToAvatar` — meaning it must equal the Strategy's own address. This prevents the operator from sending funds to any external address. This constraint is non-overridable. It applies automatically to every function that has a recipient-like parameter, and the operator cannot remove it. ``` supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) ^ must == Strategy address ``` The `asset` or `tokenIn` parameter is constrained to a specific set of approved tokens using `EqualTo`. The operator can only interact with tokens explicitly listed in the policy — for example, USDC and WETH but not any arbitrary ERC-20. ``` exactInputSingle(ExactInputSingleParams) tokenIn: EqualTo(USDC) OR EqualTo(WETH) // only approved tokens recipient: EqualToAvatar // locked to Strategy ``` Parameters like `amountOutMinimum` are constrained with `GreaterThan(0)` to prevent zero-slippage swaps that could be exploited by MEV bots. The operator can set a higher minimum, but cannot set it to zero. #### Non-overridable constraints Three calldata constraints are safety-critical and cannot be removed by anyone, including the guardian: 1. **Recipient = Strategy address** — prevents funds from being sent to external addresses 2. **Approve spender = known protocol contract** — prevents arbitrary token approvals 3. **delegatecall = false** — always disabled on all permissions, preventing code injection ### Spending limits and allowances The fourth layer rate-limits how much capital the operator can deploy within a time period. Spending limits use the `WithinAllowance` operator with a periodic refill mechanism. Each allowance is defined by: * **Refill amount** — how much budget is restored each period (e.g., 100,000 USDC) * **Period** — how often the budget refills (daily, weekly, or monthly) * **Max accrual** — the maximum budget that can accumulate (prevents rollover) When the operator executes a transaction, the amount parameter is deducted from the allowance balance. If the balance is insufficient, the transaction reverts. The balance refills automatically at the start of each period. Spending limits apply to inbound operations only — `supply`, `deposit`, `borrow`, and swap inputs. Exit operations like `withdraw`, `repay`, and `claim` are unconstrained because they move funds back to the Strategy. Allowances can be scoped per function, per protocol, or shared across multiple functions: | Scope | Allowance key example | Effect | | --------------- | ------------------------- | ------------------------------------------- | | Per-protocol | `aave_v3_daily` | Caps all Aave V3 operations combined | | Per-route | `eth_arb_usdc_daily` | Caps a specific bridge route | | Global outbound | `eth_outbound_usdc_daily` | Caps all outbound operations from one chain | When multiple allowances apply (e.g., a per-route cap and a global cap), both are deducted on each transaction. If either is exhausted, the transaction reverts. ## Cross-chain policies Advanced Strategies can operate across multiple EVM chains using the same Safe address, deployed deterministically via CREATE2. Each chain has its own Roles Modifier instance with its own set of permissions. ``` Same Safe address (0xSafe...) on all chains ============================================ Ethereum Arbitrum Base Optimism Safe + Roles Safe + Roles Safe + Roles Safe + Roles ``` ### How cross-chain scoping works The Roles Modifier on each chain controls **outbound** operations only — bridge calls, token transfers, and cross-chain messages originating from that chain's Safe. Inbound bridged funds are permissionless. Anyone can send tokens to the Safe address, and the funds arrive without any Roles configuration. For bridge operations, the policy engine enforces: * **Bridge contract whitelist** — only approved bridge contracts (e.g., Across SpokePool) can be called * **Recipient locked to Strategy address** — the `recipient` parameter in bridge calls must equal the Safe address, which is the same on all chains due to deterministic deployment * **Approved destination chains** — the operator can only bridge to explicitly allowed chain IDs * **Per-route spending caps** — each source-destination pair has its own daily allowance (e.g., `eth_arb_usdc_daily`) Because the Safe has the same address on all chains, locking the bridge recipient to `EqualToAvatar` guarantees funds arrive at the organization's own Safe on the destination chain. Even if an operator key is compromised, bridged funds cannot leave the organization's wallets. ### Per-chain, per-route spending caps Each bridge route has an independent allowance. An operator bridging from Ethereum can have separate daily caps for Ethereum-to-Arbitrum, Ethereum-to-Base, and Ethereum-to-Optimism routes. For stricter control, routes from the same source chain can share a single allowance key. This enforces a global outbound cap — the total bridged across all destinations from one chain cannot exceed the shared limit. ## Comparing guardrail models Railnet offers two guardrail architectures. Allocation Strategies use the External Access Control (EAC) system built into Railnet's smart contracts. Advanced Strategies use the Zodiac Roles Modifier as an external policy engine. | Aspect | Advanced Strategy | Allocation Strategy | | --------------- | ------------------------------------------ | ------------------------------ | | Engine | Zodiac Roles Modifier v2 | External Access Control (EAC) | | Scope | Any contract call on any chain | Railnet operational roles | | Granularity | Calldata-level parameter checks | Role-based access control | | Spending limits | Per-function allowances with period refill | Allocation queue targets | | Governance | Guardian timelock | Admin role retention | | Visibility | All rules on-chain, verifiable | All roles on-chain, verifiable | Both models enforce constraints on-chain with no off-chain trust assumptions. The choice depends on what the strategy needs to do: Allocation Strategies work within Railnet's standard yield source framework, while Advanced Strategies interact with arbitrary contracts across any EVM chain. ## What to read next Integration guide for routing transactions through the policy engine. What Advanced Strategies are, why they exist, and how they connect to Railnet. Role-based access control for standard Allocation Strategies. Portfolio-level risk parameters, circuit breakers, and monitoring. # Vault 7540 mechanics Source: https://docs.railnet.org/strategies/advanced/vault How the ERC-7540 vault handles deposits, redemptions, settlement, fees, and access control ## Overview The 7540 Vault is the capital entry point for every Advanced Strategy. It implements the [ERC-7540](https://eips.ethereum.org/EIPS/eip-7540) standard handling deposits, redemptions, share pricing, fee collection, and access control through a single contract. This page covers the vault's internal mechanics. For a high-level overview of how the vault fits into the Advanced Strategy architecture, see [Advanced Strategies](/strategies/advanced). ## Deposits The vault exposes two deposit paths and gates the sync one on **NAV validity**. After each settlement, the NAV is considered valid for a configurable time window (`totalAssetsLifespan`). While valid, the vault has a reliable exchange rate and can process instant operations. Once the window expires, sync deposits revert with `TotalAssetsExpired` and capital is queued through the async path, which is priced at the next valuation cycle. ### Sync deposits When the NAV is valid, investors can deposit and receive shares instantly in a single transaction by calling `syncDeposit(assets, receiver, referral)`. The share price is computed using the vault's current NAV — no waiting, no claim step. Sync deposits: * Transfer the deposited assets directly into the **Strategy wallet** (the Safe multisig that holds the fund's capital) * Mint shares to the investor at the current price * Deduct an entry fee (if configured) from the minted shares * Enforce the maximum deposit cap (if configured) ### Async deposits The async path is always open: `requestDeposit()` is not gated on NAV validity, so an investor can queue a deposit whether the NAV is fresh or expired. Once the NAV has expired it is the only path available. This is a multi-step flow: 1. **Request** — The investor calls `requestDeposit(assets, controller, owner)`. Assets move into the Silo escrow (a separate holding contract). No shares are minted yet, and the share price is not fixed. 2. **Wait** — The request stays pending until the next NAV cycle. The investor can cancel during this window by calling `cancelRequestDeposit()`, as long as the epoch hasn't rolled — the epoch rolls when the valuation manager proposes a new NAV, before settlement. 3. **Settlement** — The valuation manager proposes a NAV, the Strategy confirms and settles. The share price is locked at this point, shares become claimable, and the deposited assets move from the Silo into the **Strategy wallet**. 4. **Claim** — The investor calls `deposit()` or `mint()` to receive their shares. An entry fee is deducted using the rate that was active at settlement time — not the current rate — protecting investors from fee changes between settlement and claim. Each address can only have one pending deposit request at a time. If a previous request from an older epoch is already claimable, the vault auto-claims it before accepting a new request. ### Maximum deposit cap The vault can enforce a maximum deposit limit at the **vault level** via `updateMaxCap(newCap)` (Strategy-only). This is a global cap on total TVL — not per user. The check compares `totalAssets + new deposit + pending deposits in escrow` against the cap. It is enforced on both `syncDeposit()` and `requestDeposit()`. If a deposit would push the vault above the cap, it reverts. ## Redemptions Redemptions follow the same model as deposits: sync redemptions require a valid NAV and a sync mode that includes redemptions, while async redemption requests can be submitted at any time, whether the NAV is fresh or expired. ### Async redemptions Async redemptions are the default withdrawal path. The flow mirrors async deposits: 1. **Request** — The investor calls `requestRedeem(shares, controller, owner)`. Shares move into the Silo escrow. The redemption price is not fixed yet. 2. **Wait** — The request stays pending until the next NAV cycle. The controller — or an approved operator — can cancel it with `cancelRequestRedeem(controller)` while the request is still in the current epoch; once a NAV proposal has rolled the epoch, the call reverts with `RequestNotCancelable`. 3. **Settlement** — The Strategy confirms the NAV and settles. The redemption price is locked, the escrowed shares are burned, and the equivalent assets are pulled from the **Strategy wallet** into the vault — provided the Strategy holds enough liquid assets. 4. **Claim** — The investor calls `redeem()` or `withdraw()` to receive their assets from the vault. An exit fee is deducted using the rate recorded at settlement time. Partial claims are allowed. ### Sync redemptions — instant liquidity buffer Sync redemptions allow investors to exit the vault instantly in a single transaction, without going through the async request/settle/claim cycle. This creates an **instant liquidity buffer** for platforms and investors that need immediate withdrawals. When an investor calls `syncRedeem(shares, receiver, minimumAssets)`, the vault: 1. Deducts an **exit fee** from the shares (collected as fee shares, split with the protocol) 2. Applies a **haircut fee** on the remaining shares — these shares are **burned, not collected by anyone**. The vault deliberately subtracts the pre-haircut asset value from `totalAssets`, so the price-per-share is left unchanged at redemption time and the retained value is recognized for the remaining holders at the next valuation. It is an economic disincentive for instant withdrawals that benefits patient investors. 3. Converts the net shares to assets at the current price 4. Pulls those assets directly from the **Strategy wallet** and transfers them to the receiver The `minimumAssets` parameter provides slippage protection — if the computed output falls below this threshold, the transaction reverts. **Prerequisites for sync redemptions:** * The vault must be open (not closing or closed) * The sync mode must include redemptions * The NAV must be valid (not expired) * The Strategy must hold enough liquid assets to cover the redemption Unlike async redemption settlement (which silently defers when the Strategy lacks funds), sync redeem hard reverts if the Strategy doesn't have enough liquid assets. There is no graceful fallback. ### Sync vs async redemptions | Aspect | Sync redeem | Async redeem | | ---------------------- | -------------------------------------- | -------------------------------------------------- | | **Transactions** | 1 (instant) | 3+ (request, settle, claim) | | **Wait time** | None | At least one settlement cycle | | **Exit fee** | Yes (current rate) | Yes (rate recorded at settlement) | | **Haircut fee** | Yes (burned) | No | | **Strategy liquidity** | Required (hard revert if insufficient) | Not required at request time | | **Best for** | Urgency, small positions | Cost sensitivity, large positions, patient capital | ## NAV update — two-phase valuation The vault's Net Asset Value (NAV) determines share pricing for every operation. NAV updates follow a **two-phase commit** designed to separate the valuation responsibility from the settlement authority. ### Phase 1 — Valuation manager proposes The valuation manager calls `updateNewTotalAssets(newTotalAssets)` to propose a new NAV. This function: * Snapshots all pending deposit and redemption requests from the escrow (Silo) * Creates an epoch boundary — all requests submitted before this point are included in this settlement batch * Stores the proposed value for the Strategy to confirm This function can only be called when the current NAV has expired. If the NAV is still valid, the call reverts. The Strategy can force expiration at any time by calling `expireTotalAssets()`. If [NAV guardrails](#nav-guardrails) are active, the proposed value is validated against the price-per-share bounds before being accepted. ### Phase 2 — Strategy confirms and settles The Strategy confirms the proposed NAV by calling either `settleDeposit(newTotalAssets)` or `settleRedeem(newTotalAssets)`. The value passed must exactly match what the valuation manager proposed. This function: * Finalizes the NAV into the vault's `totalAssets` * Takes management and performance fees (minting fee shares) * Records the current entry and exit fee rates for the settlement epoch * Refreshes the NAV validity window — sync operations become available again until the new expiration Settlement cannot happen without a prior valuation proposal. The two-phase design ensures that no single role can both propose and finalize a NAV — the valuation manager proposes, and the Strategy confirms. ### NAV validity and sync window After settlement, the NAV remains valid for a configurable lifespan (`totalAssetsLifespan`). During this window, sync deposits and sync redemptions are available. Once the lifespan expires, the vault switches to async-only mode until the next NAV cycle. The Strategy can update the lifespan via `updateTotalAssetsLifespan(lifespan)`, or force immediate expiration via `expireTotalAssets()` to trigger async mode on demand. ## Settlement Settlement is the process of converting pending requests into claimable shares (for deposits) or claimable assets (for redemptions). It always happens as part of the NAV confirmation step — there is no way to settle without updating the NAV. ### Settling deposits — `settleDeposit()` When the Strategy calls `settleDeposit(newTotalAssets)`, the vault: 1. Finalizes the NAV and takes fees 2. Converts pending deposit assets into shares at the newly confirmed price 3. Mints those shares to the vault contract (held until users claim) 4. Moves the deposited assets from the Silo escrow to the Strategy 5. Attempts to settle pending redemptions as well (best-effort) After settlement, depositors can claim their shares via `deposit()` or `mint()`. The Strategy can also push shares to users directly by calling `claimSharesOnBehalf([controllers])`, which batch-claims for multiple depositors without requiring each user to submit a transaction. ### Settling redemptions — `settleRedeem()` When the Strategy calls `settleRedeem(newTotalAssets)`, the vault: 1. Finalizes the NAV and takes fees 2. Converts pending redemption shares into the equivalent asset amount at the confirmed price 3. Checks whether the Strategy holds enough liquid assets to cover the full redemption amount 4. If sufficient — burns the escrowed shares and pulls assets from the Strategy into the vault for claiming 5. If insufficient — the redemption settlement is skipped entirely (no partial fills) After settlement, redeemers can claim their assets via `redeem()` or `withdraw()`. The Strategy can also batch-claim via `claimAssetsOnBehalf([controllers])`. Redemption settlement is all-or-nothing. The Strategy must hold enough liquid assets to cover all pending redemptions at the confirmed price. If liquidity is insufficient, no redemptions are settled in that cycle — they carry over to the next NAV update. ### Which settlement function to use | Function | Settles deposits | Settles redemptions | When to use | | ----------------- | ---------------- | ------------------- | ---------------------------------------- | | `settleDeposit()` | Yes | Yes (best-effort) | Default — handles both queues | | `settleRedeem()` | No | Yes (best-effort) | When you only want to settle redemptions | In most cases, `settleDeposit()` is the right choice because it processes both queues in a single transaction. ## Fee framework The vault collects fees at settlement time and during sync operations. Every fee except the haircut is minted as shares and split on-chain between the vault's fee receiver and the protocol fee receiver read from the protocol-wide fee registry. ### Fee types | Fee | Max rate | When applied | Destination | | --------------- | -------------- | --------------------------------------------------- | ----------------------------------- | | **Management** | 10% (1000 bps) | On settlement — time-weighted based on total assets | Fee receiver + protocol | | **Performance** | 50% (5000 bps) | On settlement — charged only above high-water mark | Fee receiver + protocol | | **Entry** | 2% (200 bps) | On deposit (sync and async claim) | Fee receiver + protocol | | **Exit** | 2% (200 bps) | On redeem/withdraw (sync and async claim) | Fee receiver + protocol | | **Haircut** | 20% (2000 bps) | On sync redeem only | Burned (benefits remaining holders) | ### How fees are computed **Management fees** accrue based on total assets under management and time elapsed since the last settlement. They are proportional to AUM and the configured annual rate. **Performance fees** are charged only when the price-per-share exceeds the high-water mark (HWM) — ensuring fees are only taken on net new gains. After fees are collected, the HWM is updated if the current price-per-share is higher. The HWM never decreases under normal operation, but the Strategy can reset it via `resetHighWaterMark()` if enabled at initialization (useful for vault migrations). **Entry and exit fees** are recorded per settlement epoch. When an investor claims shares or assets, the fee rate used is the one that was active at the time of settlement — not the current rate. This protects investors from fee changes between settlement and claim. **Haircut fees** are unique to sync redemptions. The haircut shares are burned rather than collected, and the assets they represent stay with the Strategy — the gain accrues to remaining holders at the next NAV update rather than instantly. ### Fee split Fee shares are minted the moment a fee is taken and split on-chain in the same transaction. The protocol portion — the rate published by the protocol-wide fee registry, capped at 30% of the fee — is minted to the protocol fee receiver as **Railnet**'s revenue; the remainder is minted to the vault's fee receiver, the **Asset Manager**'s address. The registry rate is read at every collection, so the split does not have to be configured per vault. The Asset Manager's own distribution of its portion happens at the fee receiver address (the **Fee Splitter**), outside the vault. ## Access control The vault supports configurable access control to gate who can deposit, redeem, and transfer shares. ### Access modes | Mode | Behavior | | ------------- | -------------------------------------------------------------- | | **Allowlist** | Only explicitly approved addresses can interact with the vault | | **Blocklist** | All addresses are allowed except those explicitly blocked | The vault owner can switch between modes at any time via `switchAccessMode()`. ### Allowlist mode The whitelist manager adds or removes addresses using `addToWhitelist()` and `revokeFromWhitelist()`. Only whitelisted addresses can deposit and redeem. ### Blocklist mode The whitelist manager manages blocked addresses using `addToBlacklist()` and `revokeFromBlacklist()`. Blocklisted addresses cannot deposit, redeem, **or transfer vault shares** — both sending and receiving are restricted. ### External sanctions list The vault can integrate with an external sanctions oracle (e.g., Chainalysis OFAC sanctions list) via `setExternalSanctionsList()`. When configured, an address must pass **both** the internal access check and the external sanctions check to be allowed. The protocol fee receiver and the super operator are the two exceptions: they always pass, in every mode. ### What access control gates | Action | What is checked | | ------------------------------------------------ | --------------------------------------------- | | `requestDeposit()` | Owner, controller, and caller must be allowed | | `syncDeposit()` | Caller and receiver must be allowed | | `requestRedeem()` | Owner, controller, and caller must be allowed | | `syncRedeem()` | Caller and receiver must be allowed | | `transfer()` / `transferFrom()` (blocklist mode) | Sender and receiver must not be blocked | ## Vault admin The vault owner (admin) is set at initialization and has exclusive control over the vault's configuration. The admin manages roles, fee rates, access control, and vault lifecycle — but cannot move funds or execute strategy operations. ### Role management The admin assigns and updates the addresses for each vault role: | Function | What it sets | | -------------------------- | --------------------------------------------------------- | | `updateValuationManager()` | Who can propose NAV updates | | `updateWhitelistManager()` | Who can manage allowlist/blocklist entries | | `updateFeeReceiver()` | Where fee shares are sent | | `updateSafe()` | Which Strategy wallet the vault is connected to | | `updateSecurityCouncil()` | Who can set NAV guardrails and perform emergency bypasses | | `updateSuperOperator()` | Who can act on behalf of any user for claims and requests | ### Fee configuration The admin updates fee rates via `updateRates()`. Management and performance rates can be changed freely. Entry and exit rates can only be **decreased** after initial configuration — protecting investors from fee increases post-deployment. ### Vault lifecycle and operations | Function | What it does | | --------------------------------- | --------------------------------------------------------------------------------------------- | | `switchAccessMode()` | Toggle between allowlist and blocklist modes | | `activateAsyncOnly()` | Permanently and irreversibly disable all sync operations | | `pause()` | Halt all core operations (deposits, redemptions, claims) | | `unpause()` | Resume operations | | `initiateClosing()` | Start the vault closing process — the Strategy then finalizes it with `close(newTotalAssets)` | | `updateName()` / `updateSymbol()` | Update the vault's share token name and symbol | ### Roles recap | Role | Who sets it | Key responsibilities | | ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------- | | **Vault owner (Admin)** | Set at initialization | Assign roles, update fees, pause/unpause, switch access mode | | **Strategy (Safe)** | Vault owner | Confirm NAV, settle queues, update lifespan, set sync mode, manage deposit cap | | **Valuation manager** | Vault owner | Propose NAV updates | | **Whitelist manager** | Vault owner | Manage allowlist/blocklist entries, set external sanctions list | | **Security Council** | Vault owner | Set NAV guardrails, emergency NAV bypass | | **Super Operator** | Vault owner | Act on behalf of any user for claims and requests | | **Fee receiver** | Vault owner | Receives the Asset Manager portion of every fee share (the protocol portion is minted to the protocol fee receiver) | | **Delay Proxy Admin** | Set at deployment | Propose and execute vault upgrades (separate from vault owner) | ### Ownership transfer Ownership uses a two-step transfer pattern for safety — the current owner calls `transferOwnership(newOwner)`, and the new owner must explicitly call `acceptOwnership()` to complete the transfer. This prevents accidental transfers to incorrect addresses. ## NAV guardrails NAV guardrails protect the vault against erroneous or malicious NAV updates by enforcing on-chain bounds on price-per-share changes. ### How it works A **Security Council** role sets upper and lower bounds on annualized price-per-share changes via `updateGuardrails()`. When the valuation manager calls `updateNewTotalAssets()`, the vault: 1. Computes the current price-per-share and the proposed price-per-share 2. Annualizes the change based on time elapsed since the last update 3. Rejects the proposal if the annualized change exceeds the upper bound or falls below the lower bound The Security Council can activate or deactivate guardrails at any time via `updateActivated()`. The first NAV update after vault initialization is always exempt. ### Emergency bypass In extreme market scenarios where guardrails would block a legitimate NAV update, the Security Council can call `securityCouncilUpdateTotalAssets()` to propose a NAV **without guardrails checking**. This bypasses the price-per-share bounds entirely. ### Why guardrails matter * Prevent malicious NAV proposals from a compromised valuation manager * Protect against oracle manipulation or flash loan attacks on NAV pricing * Provide a circuit breaker for extreme market events while maintaining an emergency override ## Vault upgrades The 7540 Vault is an upgradeable proxy contract. A **Delay Proxy Admin** is deployed alongside each vault at creation, enforcing a mandatory timelock (up to 30 days) between proposing and executing any implementation upgrade. The Delay Proxy Admin owner can be different from the vault owner — typically a quorum consisting of Railnet and the Asset Manager — so that no single party can upgrade the vault unilaterally. New implementations must also be whitelisted in a protocol-wide Logic Registry before any upgrade can proceed. ## What to read next High-level overview of how the vault fits into the Advanced Strategy architecture. How the Policy Engine enforces on-chain permissions, spending limits, and guardian governance. Day-to-day operations — execute transactions, manage spending limits, update permissions. Guardrails, allocation caps, and risk frameworks for strategy operations. # Create an Allocation Strategy Source: https://docs.railnet.org/strategies/allocation/create Step-by-step guide to deploying an Allocation Strategy ecosystem In Railnet smart contracts, an Allocation Strategy is implemented as a **MultiVehicle**. See [Glossary](/developers/glossary) for all terminology. This guide walks you through creating an Allocation Strategy on Railnet. By the end, you will have a fully deployed ecosystem with its External Access Control, optional Fee Manager, and the Allocation Strategy itself (MultiVehicle). If you are a Conduit owner deploying a strategy you will own and delegate to an asset manager, see [Use a Strategy](/conduits/use-a-strategy) instead. ## Prerequisites * A wallet with funds — the deposit asset (e.g. USDC) for the initial deposit and ETH for gas * The `FACTORY_SPAWN` role on the Multi-Vehicle factory (or access to a public factory) * The deposit asset authorized in the [AssetRegistry](/developers/contracts/asset-registry) — the factory reads the initial deposit amount from there at spawn time * The factory must not be deprecated Every Allocation Strategy deployment requires an initial deposit. This is a security measure to prevent inflation attacks (zero-share vulnerability). The amount is configured in the [AssetRegistry](/developers/contracts/asset-registry) per asset. The shares minted from the initial deposit are sent to the burn address and can never be withdrawn. Railnet uses an External Access Control contract to provide Role-Based Access Control (RBAC) across all strategies you operate. You define roles and grant them to different addresses from this single contract. You must deploy at least one EAC before creating any Allocation Strategy. **Parameters:** * `initialDelay` — delay for admin operations (in seconds), set to `0` for immediate access * `initialDefaultAdmin` — the address granted `DEFAULT_ADMIN_ROLE` * `initialRoles` — array of initial role attributions (can be empty and configured later) * `deploymentSalt` — salt for deterministic CREATE2 deployment ```solidity Solidity theme={null} // Deploy External Access Control via the EAC Factory AccessControlFactory.SpawnParams memory params = AccessControlFactory.SpawnParams({ initialDelay: 0, initialDefaultAdmin: msg.sender, initialRoles: new IExternalAccessControl.RoleAttribution[](0), deploymentSalt: keccak256("my-eac-v1") }); ExternalAccessControl eac = eacFactory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` The Fee Manager contract handles all fee-related operations for your Allocation Strategy: performance fees, management fees, deposit fees, and redemption fees. You can configure initial fee rates, maximum fee caps, and fee recipients. If you are absolutely sure you will never need fees, you can pass the zero address (`0x0000...0000`) when deploying the Allocation Strategy, however we recommend deploying one even if you want to set fees to 0 for future proofing. **Parameters:** * `accessControl` — your EAC contract address * `initialFees` — fee rates in basis points (1 bps = 0.01%) * `initialMaxFees` — maximum fee caps that can never be exceeded * `initialRecipients` — fee recipients with their share in bps (must sum to 10,000) * `deploymentSalt` — salt for deterministic CREATE2 deployment ```solidity Solidity theme={null} FeeManagerFactory.SpawnParams memory params = FeeManagerFactory.SpawnParams({ accessControl: externalAccessControl, initialFees: FeeManager.Fees({ performanceFeeBps: 1000, // 10% performance fee managementFeeBps: 200, // 2% annual management fee depositFeeBps: 0, // No deposit fee redeemFeeBps: 0 // No redeem fee }), initialMaxFees: FeeManager.Fees({ performanceFeeBps: 2000, // 20% max performance fee managementFeeBps: 500, // 5% max management fee depositFeeBps: 100, // 1% max deposit fee redeemFeeBps: 100 // 1% max redeem fee }), initialRecipients: recipients, // Array of FeeRecipient structs deploymentSalt: keccak256("my-fee-manager-v1") }); FeeManager feeManager = feeManagerFactory.spawn(params); ``` ```typescript TypeScript theme={null} // Coming soon ``` Set `initialMaxFees` carefully. These caps are immutable and define the absolute ceiling for each fee type. You can always lower fees later, but you can never exceed the max. When creating an Allocation Strategy, you select a single deposit asset (e.g. USDC). The factory will pull the initial deposit from your wallet at spawn time using the amount configured for that asset in the [AssetRegistry](/developers/contracts/asset-registry). Read the configured amount and approve the Multi-Vehicle Factory to spend it. ```solidity Solidity theme={null} // Read the registry-configured initial deposit for the chosen asset uint256 initialDepositAmount = assetRegistry.getInitialDepositAmount(usdc); // Approve the Multi-Vehicle Factory IERC20(usdc).approve(address(multiVehicleFactory), initialDepositAmount); ``` ```typescript TypeScript theme={null} // Coming soon ``` Deploy the full Allocation Strategy ecosystem in a single transaction. The factory creates the MultiVehicle, Sector Accounting Engine, Queue Strategy Engine, Sub Query Engine, Query Redeem Queue, and Vehicle Manager — all correctly linked and initialized. **Parameters:** * `asset` — the underlying deposit asset (e.g. USDC) * `name` — the ERC-20 token name for Allocation Strategy shares * `symbol` — the ERC-20 token symbol for Allocation Strategy shares * `initialInterceptions` — interceptor rules for reward distribution (can be empty) * `accessControl` — your EAC contract address * `feeManager` — Fee Manager address (or zero address for no fees) * `modulesManager` — Modules Manager address (or zero address) * `salts` — collection of deployment salts for all underlying contracts * `forbiddenAddresses` — addresses barred from holding Allocation Strategy shares (can be empty) * `queryRegistry` — the STEAM Query Registry for this deployment The initial deposit amount itself is read from the AssetRegistry — it is not passed here. ```solidity Solidity theme={null} MultiVehicleFactory.SpawnParams memory params = MultiVehicleFactory.SpawnParams({ asset: IERC20(usdc), name: "Diversified USDC", symbol: "dUSDC", initialInterceptions: new Interceptor.Interception[](0), accessControl: externalAccessControl, feeManager: feeManager, modulesManager: modulesManager, salts: MultiVehicleFactory.Salts({ multiVehicle: keccak256("mv"), queryRedeemQueue: keccak256("qrq"), queueStrategyEngine: keccak256("qse"), sectorAccountingEngine: keccak256("sae"), subQueryEngine: keccak256("sqe"), vehicleManager: keccak256("vm"), initialDepositQuery: keccak256("idq") }), forbiddenAddresses: new address[](0), queryRegistry: queryRegistry }); MultiVehicleFactory.Contracts memory contracts = factory.spawn(params); // contracts.multiVehicle — main Allocation Strategy contract // contracts.sectorAccountingEngine — Sector Accounting Engine // contracts.queueStrategyEngine — Queue Strategy Engine // contracts.subQueryEngine — Sub Query Engine // contracts.queryRedeemQueue — Query Redeem Queue // contracts.vehicleManager — Vehicle Manager ``` ```typescript TypeScript theme={null} // Coming soon ``` After deployment, verify your Allocation Strategy's status and configuration by querying the Railnet API or reading the contract state directly. ```solidity Solidity theme={null} // Read the deployed MultiVehicle state address multiVehicle = address(contracts.multiVehicle); // Verify the asset address asset = MultiVehicle(multiVehicle).asset(); // Verify the total supply (should reflect initial deposit) uint256 totalSupply = MultiVehicle(multiVehicle).totalSupply(); // Verify the engine wiring — the MultiVehicle only exposes its Vehicle Manager, // and the engines hang off the manager IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); ``` ```typescript TypeScript theme={null} // Coming soon ``` You can also query the [Railnet API](/developers/api) using GraphQL: ```graphql theme={null} query VerifyDeployment($address: String!) { Vehicle(where: { address: { _ilike: $address } }) { address id name vehicleType symbol supply } } ``` ## What you deployed Your Allocation Strategy ecosystem now consists of six interconnected contracts: | Contract | Purpose | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **MultiVehicle** | Main Allocation Strategy contract. Users deposit and receive ERC-20 shares. | | **Sector Accounting Engine** | Tracks all asset allocations across yield sources and idle sectors. | | **Queue Strategy Engine** | Defines deposit/redeem priority queues for automated allocation. | | **Sub Query Engine** | Executes deposit and redeem queries on yield sources. | | **Query Redeem Queue** | Handles asynchronous redemption processing. | | **Vehicle Manager** | Holds vehicle authorization, per-vehicle configuration, and thresholds. The MultiVehicle exposes it via `manager()`, and every engine is reachable from it. | ## Next steps Authorize yield sources, configure allocation queues, and operate your Allocation Strategy. Set up fee structures for your Allocation Strategy. # Guardrails Source: https://docs.railnet.org/strategies/allocation/guardrails How guardrails constrain what an asset manager can do within an Allocation Strategy — authorized sources, allocation caps, and RBAC In Railnet smart contracts, an Allocation Strategy is implemented as a **MultiVehicle**. See [Glossary](/developers/glossary) for all terminology. This page covers guardrails for Allocation Strategies, which use role-based access control via the External Access Control (EAC) contract. For Advanced Strategy guardrails, see [Policy engine](/strategies/advanced/policy-engine). Guardrails define the trust boundary between the party that owns an Allocation Strategy (typically the platform deploying the Conduit) and the asset manager who operates it day-to-day. The owner sets the rules. The asset manager executes within them. ## The delegation model The owner retains admin control while granting the asset manager scoped operational roles. The asset manager can execute the strategy within the boundaries the owner defines — they cannot change the rules. ## What the owner controls These controls remain exclusively with the Allocation Strategy owner and form the guardrails: | Guardrail | Role (retained by owner) | Why | | ---------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------- | | **Authorized yield sources** | `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION`, scoped to the Vehicle Manager | The owner decides which yield sources the strategy can use | | **Fee rates** | `FEE_MANAGER_SET_FEES` | The owner controls the economics | | **Fee recipients** | `FEE_MANAGER_SET_FEE_RECIPIENTS` | The owner controls revenue distribution | | **Role assignments** | `DEFAULT_ADMIN_ROLE` | The owner controls who has access | | **Infrastructure upgrades** | `BEACON_UPGRADE` | The owner controls contract upgrades | The asset manager operates within these boundaries. They can allocate capital, rebalance positions, and manage queues — but they cannot authorize new yield sources, change fees, or grant roles to others. ## Grant operational roles Grant the asset manager scoped roles on the appropriate contracts. Sector operations are scoped to the **Sector Accounting Engine**, queue configuration to the **Queue Strategy Engine**, query progression to the **Sub Query Engine**, and the redemption-queue roles to the **Vehicle Manager**. ```solidity Solidity theme={null} address am = 0x...; // Asset manager address IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); IQueueStrategyEngine strategy = manager.queueStrategyEngine(); ISubQueryEngine subQueryEngine = manager.subQueryEngine(); // Core operational roles (scoped to Sector Accounting Engine) eac.grantScopedRole(keccak256("MULTI_VEHICLE_MOVE"), address(accounting), am); eac.grantScopedRole(keccak256("MULTI_VEHICLE_DISPATCH"), address(accounting), am); // Queue management (scoped to Queue Strategy Engine) eac.grantScopedRole(keccak256("MULTI_VEHICLE_SET_QUEUES"), address(strategy), am); // Query progression (scoped to Sub Query Engine) eac.grantScopedRole(keccak256("MULTI_VEHICLE_PROGRESS_QUERY"), address(subQueryEngine), am); // Redemption queue (scoped to Vehicle Manager) eac.grantScopedRole(keccak256("MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE"), address(manager), am); eac.grantScopedRole(keccak256("MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS"), address(manager), am); ``` ```typescript TypeScript theme={null} // Coming soon ``` Always verify the **scope** parameter matches the correct contract. Granting a role with the wrong scope will not authorize the intended operation. ## What the asset manager can do With the roles above, the asset manager can: | Operation | Role | Scope | | ------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------ | | Move assets and shares between sectors | `MULTI_VEHICLE_MOVE` | Sector Accounting Engine | | Dispatch assets to yield sources | `MULTI_VEHICLE_DISPATCH` | Sector Accounting Engine | | Rebalance between yield sources (composed of `move` and `dispatch` steps) | `MULTI_VEHICLE_MOVE`, `MULTI_VEHICLE_DISPATCH` | Sector Accounting Engine | | Configure allocation queues | `MULTI_VEHICLE_SET_QUEUES` | Queue Strategy Engine | | Progress sub-queries | `MULTI_VEHICLE_PROGRESS_QUERY` | Sub Query Engine | | Feed the redemption queue | `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` | Vehicle Manager | | Retrieve redemption queue assets | `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` | Vehicle Manager | ## Optional: grant fee collection roles You may want the asset manager (or a keeper) to handle routine fee collection: ```solidity Solidity theme={null} // Allow the AM to trigger fee distribution (but NOT change fee rates or recipients) eac.grantScopedRole(keccak256("FEE_MANAGER_DISPATCH_ERC20"), address(feeManager), am); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Monitor your asset manager Track your Allocation Strategy's performance and the asset manager's operations via the [Railnet API](/developers/api): ```graphql GraphQL theme={null} query MultiVehicleStatus($address: String!) { Vehicle(where: { address: { _ilike: $address } }) { supply assetSymbol assetDecimals } SectorBalance( where: { sector: { accountingEngine: { multiVehicle: { vehicle: { address: { _ilike: $address } } } } } } ) { asset value sector { name } } Query( where: { vehicle: { address: { _ilike: $address } } } order_by: { event: { tx: { block: { number: desc } } } } limit: 20 ) { mode state event { tx { block { timestamp } } } } } ``` See the [API reference](/developers/api) for comprehensive monitoring queries and dashboards. ## Revoke access To offboard an asset manager, revoke all scoped roles. Pending operations will complete, but the asset manager cannot initiate new ones. ```solidity Solidity theme={null} address am = 0x...; // Asset manager to offboard IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); IQueueStrategyEngine strategy = manager.queueStrategyEngine(); ISubQueryEngine subQueryEngine = manager.subQueryEngine(); // Revoke all operational roles eac.revokeScopedRole(keccak256("MULTI_VEHICLE_MOVE"), address(accounting), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_DISPATCH"), address(accounting), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_SET_QUEUES"), address(strategy), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_PROGRESS_QUERY"), address(subQueryEngine), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE"), address(manager), am); eac.revokeScopedRole(keccak256("MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS"), address(manager), am); // Revoke fee collection roles if granted eac.revokeScopedRole(keccak256("FEE_MANAGER_DISPATCH_ERC20"), address(feeManager), am); ``` ```typescript TypeScript theme={null} // Coming soon ``` Before offboarding, ensure there are no in-progress queries that require the asset manager's roles to complete. Check the query state via the [API](/developers/api). ## Next steps Manage allocations, rebalance across yield sources, and handle operations. Complete guide to role setup, scoping, and common permission patterns. # Allocation Strategies Source: https://docs.railnet.org/strategies/allocation/index Compose multiple yield sources into a managed allocation with automated routing and on-chain guardrails An Allocation Strategy is the composition layer of Railnet. Once you have multiple yield sources — [Advanced Strategies](/strategies/advanced), standard protocol adapters, or both — you compose them into a single managed allocation with automated deposit and redeem routing, sector-based accounting, and on-chain guardrails. ## When to use an Allocation Strategy You need an Allocation Strategy when you want to: * **Compose multiple yield sources** into a single product — combine lending protocols, staking, RWA, and Advanced Strategies under one allocation * **Automate capital routing** — configurable deposit and redeem priority queues distribute capital across sources without manual intervention * **Build fund-of-funds** — include Advanced Strategies as sub-vehicles alongside standard protocol adapters, creating a portfolio that mixes custom execution with established DeFi yield * **Distribute through Conduits** — a single Allocation Strategy can back multiple [Conduits](/conduits), each with their own fees, compliance rules, and branded shares ## How it works When you deploy an Allocation Strategy, you get six interconnected contracts: | Contract | Purpose | | -------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **MultiVehicle** | Main strategy contract. Users deposit and receive ERC-20 shares; it owns the STEAM lifecycle entry points. | | **VehicleManager** | Authorizes and configures yield sources, manages thresholds, and owns the redeem queue operations. | | **SectorAccountingEngine** | Tracks every asset and share across sectors with double-entry bookkeeping. | | **QueueStrategyEngine** | Defines deposit and redeem priority queues for automated allocation. | | **SubQueryEngine** | Creates and advances the STEAM sub-queries on each yield source, including ephemeral accounting for in-flight value. | | **QueryRedeemQueue** | Handles asynchronous redemption processing, matching demands against liquidity in FIFO order. | Assets flow through distinct sectors — idle base assets in `AVAILABLE`, liquidity you deliberately earmark in `RESERVED`, deployed yield-source shares in `ALLOCATION`, plus a staging sector per authorized yield source and temporary query sectors for recoveries. Every movement follows double-entry bookkeeping, giving you real-time NAV without off-chain reconciliation. ## Allocation Strategy vs Advanced Strategy | | Allocation Strategy | Advanced Strategy | | ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------- | | **Purpose** | Compose multiple yield sources into one managed allocation | Execute custom logic on any protocol with full flexibility | | **Contract** | MultiVehicle | Specialized Vehicle (Safe + Zodiac Roles) | | **Best for** | Diversified portfolios, automated routing, fund-of-funds | Standalone strategies, bespoke DeFi execution, RWA operations | | **Guardrails** | Role-based access control (EAC) | On-chain policy engine (Zodiac Roles Modifier v2) | | **Typical journey** | Built after you have multiple strategies or yield sources to compose | Where most asset managers start | The two are composable: an Advanced Strategy can serve as a sub-vehicle inside an Allocation Strategy, and both can be distributed to users via [Conduits](/conduits). ## Guides Deploy the Allocation Strategy ecosystem — EAC, Fee Manager, and MultiVehicle — in a single transaction. Understand the trust boundary between platforms and asset managers. Manage allocations, rebalance across yield sources, and handle day-to-day operations. Evaluate yield sources, manage concentration risk, and handle emergencies. # Operate an Allocation Strategy Source: https://docs.railnet.org/strategies/allocation/operate Manage allocations, rebalance across yield sources, and handle day-to-day operations In Railnet smart contracts, an Allocation Strategy is implemented as a **MultiVehicle**. See [Glossary](/developers/glossary) for all terminology. This guide covers how to authorize yield sources, configure allocation queues, and run day-to-day operations for your Allocation Strategy — moving assets between sectors, dispatching to yield sources, rebalancing allocations, handling redemptions, and integrating with keepers for automation. The MultiVehicle exposes only its manager. Reach every engine through it: ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ISectorAccountingEngine accounting = manager.accountingEngine(); IQueueStrategyEngine strategy = manager.queueStrategyEngine(); ISubQueryEngine subQueryEngine = manager.subQueryEngine(); IQueryRedeemQueue redeemQueue = manager.redeemQueue(); ``` ## Prerequisites * A deployed Allocation Strategy ecosystem (see [Create an Allocation Strategy](/strategies/allocation/create)) * Your External Access Control (EAC) contract address * At least one authorized yield source * The following roles on your EAC: * `MULTI_VEHICLE_MOVE` and `MULTI_VEHICLE_DISPATCH`, scoped to the **Sector Accounting Engine** * `MULTI_VEHICLE_SET_QUEUES`, scoped to the **Queue Strategy Engine** ## Operating a platform-owned Allocation Strategy If a Conduit owner deployed the Allocation Strategy and invited you to manage it, be aware of the guardrails: * **Yield source authorization** is controlled by the owner. You manage allocations within the set of sources they have authorized. * **Fee structure** is set by the owner. You earn through the configured performance fee share. * **Admin access** remains with the owner. You cannot grant roles to others or change the access control configuration. Your operational roles are scoped to specific contracts — you can move capital, dispatch, and manage queues, but cannot change the strategic boundaries. See [Guardrails](/strategies/allocation/guardrails) for details. ## Authorize yield sources Before your Allocation Strategy can allocate assets to a yield source, the source must be authorized on the **Vehicle Manager**. The Vehicle Manager validates that a yield source uses the same base asset as the Allocation Strategy, is a contract, and reports `ready()`. **Requires:** `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` role scoped to the Vehicle Manager. Authorization is performed on the Vehicle Manager. Read it from the MultiVehicle, or look it up via the [Railnet API](/developers/api). ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(multiVehicle).manager(); ``` ```typescript TypeScript theme={null} // Coming soon ``` Call `authorize` for the default configuration, or `authorizeAndConfigure` to set the vehicle's mode and cap in the same transaction. ```solidity Solidity theme={null} // Authorize an Aave V3 vehicle with the default config address aaveVehicle = 0x...; // The sub-vehicle address manager.authorize(IVehicle(aaveVehicle)); // Authorize a Morpho Blue vehicle with an explicit cap address morphoVehicle = 0x...; manager.authorizeAndConfigure( IVehicle(morphoVehicle), VehicleManagerStore.VehicleConfig({ mode: VehicleMode.Automatic, cap: Target({value: 250_000e6, threshold: 0}) }) ); ``` ```typescript TypeScript theme={null} // Coming soon ``` Change a cap or mode later with `manager.configure(vehicle, config)`. To remove a yield source, first redeem all assets out of it, then unauthorize it. ```solidity Solidity theme={null} // Unauthorize a vehicle (ensure no funds remain allocated) manager.unauthorize(IVehicle(vehicleToRemove)); ``` ```typescript TypeScript theme={null} // Coming soon ``` Unauthorizing a yield source does not automatically redeem existing positions. Withdraw all funds from the source before removing it. `syncVehicleActivationStatus` is not an authorization call. It takes a single vehicle argument, is callable only by the SubQueryEngine, and keeps the accounting engine's internal *active vehicles* list in sync as positions and in-flight queries appear or clear. ## Configure allocation queues The Queue Strategy Engine determines how assets are distributed across authorized yield sources using deposit and redeem queues. ### Target semantics In the deposit queue, `target.value` acts as a **ceiling** — the share holdings a yield source is filled up to before the queue moves to the next entry. * The queue processes in order: the first entry is filled first, up to its target, then the second entry, and so on. * `type(uint256).max` means no limit (allocate all available to this source). * `target.threshold` is a tolerance band: the entry is skipped when holdings are already within `threshold` of the target, so tiny top-ups don't churn gas. * The queue does **not** enforce ongoing ratios. If yields diverge across sources, allocations will drift. **Example:** With deposit queue `[{Aave, target: 80000e18}, {Morpho, target: 20000e18}]`: 1. First 80,000 shares go to Aave 2. Next 20,000 shares go to Morpho 3. Any additional shares overflow to subsequent entries In the redeem queue, `target.value` acts as a **floor** — the share holdings a yield source is drained down to before the queue moves on. * The queue processes in order: the first entry is drained first, down to its target, then the second entry. * `0` means no minimum (the source can be fully drained). * `target.threshold` widens the floor: the entry is skipped when holdings are already within `threshold` above the target. **Example:** With redeem queue `[{Morpho, target: 0}, {Aave, target: 0}]`: 1. Withdrawals come from Morpho first, down to 0 shares 2. Then Aave is drained if more is needed ### Set queues **Requires:** `MULTI_VEHICLE_SET_QUEUES` role scoped to the Queue Strategy Engine. ```solidity Solidity theme={null} IQueueStrategyEngine strategy = MultiVehicle(multiVehicle).manager().queueStrategyEngine(); // Define deposit queue: Aave first (up to 80,000 shares), then Morpho (unlimited) IQueueStrategyEngine.QueueEntry[] memory depositQueue = new IQueueStrategyEngine.QueueEntry[](2); depositQueue[0] = IQueueStrategyEngine.QueueEntry({ vehicle: IVehicle(aaveVehicle), target: Target({value: 80_000e18, threshold: 0}) }); depositQueue[1] = IQueueStrategyEngine.QueueEntry({ vehicle: IVehicle(morphoVehicle), target: Target({value: type(uint256).max, threshold: 0}) // unlimited }); // Define redeem queue: Morpho first, then Aave IQueueStrategyEngine.QueueEntry[] memory redeemQueue = new IQueueStrategyEngine.QueueEntry[](2); redeemQueue[0] = IQueueStrategyEngine.QueueEntry({ vehicle: IVehicle(morphoVehicle), target: Target({value: 0, threshold: 0}) }); redeemQueue[1] = IQueueStrategyEngine.QueueEntry({ vehicle: IVehicle(aaveVehicle), target: Target({value: 0, threshold: 0}) }); // Both queues are replaced atomically strategy.setQueues(depositQueue, redeemQueue); ``` ```typescript TypeScript theme={null} // Coming soon ``` A common pattern is to set the redeem queue in reverse order of the deposit queue. This ensures that the last yield source to receive deposits is the first to be drained during redemptions. ## How asset flow works The Sector Accounting Engine tracks every asset and share in a **sector**, a `bytes32` identifier. Five are fixed, plus one staging sector per authorized yield source: | Sector | Holds | Counted in | | ----------------------------- | --------------------------------------------- | ------------------------------------ | | `ENTRY` | virtual source for assets entering the system | neither (untracked) | | `AVAILABLE` | idle base-asset liquidity | `totalAssets()` and `withdrawable()` | | `RESERVED` | base assets you have explicitly parked aside | `totalAssets()` only | | `SectorLib.toSector(vehicle)` | assets or shares staged for one yield source | `totalAssets()` | | `ALLOCATION` | vehicle shares from settled deposits | `totalAssets()` | | `EXIT` | virtual sink for assets leaving the system | neither (untracked) | `RESERVED` exists so you can hold liquidity back: it counts toward NAV but neither auto-fulfill nor the Queue Strategy Engine can consume it. Getting liquidity in or out of `RESERVED` always takes an explicit `move`. The typical operator workflow is: **move** assets from `AVAILABLE` into a vehicle's sector, then **dispatch** to execute the deposit into the yield source. ## View holdings and allocations Before operating, check the current state of your Allocation Strategy's sectors and allocations. ```solidity Solidity theme={null} ISectorAccountingEngine accounting = MultiVehicle(multiVehicle).manager().accountingEngine(); // Total assets across all sectors and sub-vehicles uint256 totalAssets = accounting.totalAssets(); // Idle base-asset liquidity (the AVAILABLE sector; RESERVED is excluded) uint256 withdrawable = accounting.withdrawable(); // One sector's balance for one token uint256 idleUsdc = accounting.getSectorBalance(SectorLib.AVAILABLE, IERC20(usdc)); uint256 stagedUsdc = accounting.getSectorBalance(SectorLib.toSector(IVehicle(aaveVehicle)), IERC20(usdc)); // Full holdings breakdown for one yield source ( uint256 sharesAfterUnlocks, // settled shares in ALLOCATION uint256 sharesBeforeCreates, // shares staged for new redeem queries uint256 expectedSharesAfterUnlocks, // ephemeral shares from in-flight deposits uint256 expectedAssetsAfterUnlocks, // projected assets once in-flight redeems unlock uint256 assetsBeforeCreates // assets staged for new deposit queries ) = accounting.vehicleHoldings(IVehicle(aaveVehicle)); // Headroom before the vehicle's cap or its own limits bind uint256 depositable = accounting.maxDepositable(IVehicle(aaveVehicle)); uint256 redeemable = accounting.maxRedeemable(IVehicle(aaveVehicle)); ``` ```typescript TypeScript theme={null} // Coming soon ``` You can also query the [Railnet API](/developers/api): ```graphql GraphQL theme={null} query SectorBalances($address: String!) { SectorBalance( where: { sector: { accountingEngine: { multiVehicle: { vehicle: { address: { _ilike: $address } } } } } } ) { asset value sector { name sectorId } } } ``` ## Move assets to a yield source Use `move` to shift the base asset from `AVAILABLE` into a yield source's sector. This stages the assets for dispatch. **Requires:** `MULTI_VEHICLE_MOVE` role scoped to the Sector Accounting Engine. Moving assets does not deposit them into the yield source yet. You must call `dispatch` afterward to execute the deposit. ```solidity Solidity theme={null} ISectorAccountingEngine accounting = MultiVehicle(multiVehicle).manager().accountingEngine(); // Move 10,000 USDC from idle liquidity into the vehicle's staging sector accounting.move( ISectorAccountingEngine.MoveParams({ from: SectorLib.AVAILABLE, to: SectorLib.toSector(IVehicle(targetVehicle)), asset: usdc, amount: 10_000e6, // USDC has 6 decimals operationId: keccak256("top-up-aave-2026-08") }) ); ``` ```typescript TypeScript theme={null} // Coming soon ``` Set `amount` to `type(uint256).max` to move the entire sector balance. `operationId` is a free-form tag echoed into the emitted event — reuse one value across the steps of a multi-step operation so dashboards and indexers can group them. ## Dispatch assets to a yield source After staging assets in a yield source's sector, call `dispatch` to create the deposit query on that source. This executes the actual deposit. **Requires:** `MULTI_VEHICLE_DISPATCH` role scoped to the Sector Accounting Engine. ```solidity Solidity theme={null} ISectorAccountingEngine accounting = MultiVehicle(multiVehicle).manager().accountingEngine(); (Query memory query, State dispatchState) = accounting.dispatch( ISectorAccountingEngine.DispatchParams({ vehicle: IVehicle(targetVehicle), mode: Mode.DEPOSIT, amount: 10_000e6, settledDestination: SectorLib.ALLOCATION, // shares land here on success rejectedDestination: SectorLib.AVAILABLE, // assets return here on failure minOutput: 9_950e18, // slippage floor; 0 accepts any output data: "", operationId: keccak256("top-up-aave-2026-08") }) ); ``` ```typescript TypeScript theme={null} // Coming soon ``` `dispatchState` is the state the sub-query reached in this call — `SETTLED` for a sync source, `PROCESSING` for an async one. `amount: type(uint256).max` dispatches the whole sector balance and auto-limits to the vehicle's cap instead of reverting, but it requires `minOutput: 0`. A slippage bound may only bind to an explicit amount, never to the execution-time sector balance. You can batch `move` and `dispatch` operations: stage assets into several yield source sectors first, then dispatch to each source in sequence. ## Rebalance between yield sources There is no single `rebalance` call. You compose one from the same primitives, reusing one `operationId` so the steps read as a single logical operation off-chain: Dispatch a REDEEM for the source vehicle's shares, sending the proceeds to `AVAILABLE`. ```solidity Solidity theme={null} bytes32 opId = keccak256("rebalance-aave-to-morpho"); (, State redeemState) = accounting.dispatch( ISectorAccountingEngine.DispatchParams({ vehicle: IVehicle(sourceVehicle), mode: Mode.REDEEM, amount: 5_000e18, // shares to redeem settledDestination: SectorLib.AVAILABLE, rejectedDestination: SectorLib.ALLOCATION, minOutput: 4_950e6, // minimum base assets back data: "", operationId: opId }) ); ``` Once the redeem settles, move the freed assets into the destination vehicle's sector. ```solidity Solidity theme={null} accounting.move( ISectorAccountingEngine.MoveParams({ from: SectorLib.AVAILABLE, to: SectorLib.toSector(IVehicle(targetVehicle)), asset: usdc, amount: type(uint256).max, // everything that arrived operationId: opId }) ); ``` ```solidity Solidity theme={null} accounting.dispatch( ISectorAccountingEngine.DispatchParams({ vehicle: IVehicle(targetVehicle), mode: Mode.DEPOSIT, amount: type(uint256).max, settledDestination: SectorLib.ALLOCATION, rejectedDestination: SectorLib.AVAILABLE, minOutput: 0, // required with the max sentinel data: "", operationId: opId }) ); ``` If the source's redemption is asynchronous, step 1 returns `PROCESSING` and the assets are not in `AVAILABLE` yet. Wait for the sub-query to settle before running steps 2 and 3 — check `vehicleHoldings` or the indexed query state. ## Handle user deposits When users deposit into the Allocation Strategy, assets flow through the STEAM lifecycle. With allocation queues configured, deposits are automatically distributed to yield sources based on the deposit queue priority. ```solidity Solidity theme={null} // User creates a deposit query Query memory query = Query({ owner: depositor, receiver: depositor, input: Asset({ asset: address(usdc), value: 500e6 }), // `output.value` is a minimum (slippage floor); 0 disables the floor output: Asset({ asset: address(multiVehicle), value: 0 }), mode: Mode.DEPOSIT, salt: keccak256("deposit-001"), data: "" }); // STEAM lifecycle: create → unlock multiVehicle.create(query); multiVehicle.unlock(query); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Handle redemptions Redemptions follow the STEAM lifecycle. The Queue Strategy Engine processes redeems according to the redeem queue priority. ```solidity Solidity theme={null} // User creates a redeem query Query memory query = Query({ owner: depositor, receiver: depositor, input: Asset({ asset: address(multiVehicle), value: 1_000e18 }), // `output.value` is a minimum (slippage floor); 0 disables the floor output: Asset({ asset: address(usdc), value: 0 }), mode: Mode.REDEEM, salt: keccak256("redeem-001"), data: "" }); multiVehicle.create(query); // For async redemptions, the query progresses through the redeem queue // Once assets are available: multiVehicle.unlock(query); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Keeper integration Keepers are off-chain bots that automate routine operations. As an operator, understanding keeper integration helps you decide what to automate vs manage manually. ### What keepers automate | Operation | Description | | ------------------------ | ----------------------------------------------------------------------- | | Redemption queue feeding | Provides liquidity to the redemption queue when there is pending demand | | Asset retrieval | Retrieves settled assets from the redemption queue back to accounting | | Fee share redemption | Progresses redemption queries for fee shares | | Sub-query progression | Advances async sub-queries (e.g. Ethena withdrawals) | ### What remains manual | Operation | Why | | ---------------------------- | ------------------------------ | | Capital allocation (`move`) | Requires strategic judgment | | Dispatching to yield sources | Depends on allocation strategy | | Rebalancing | Market-driven decision | | Queue configuration changes | Strategic decision | ### Check keeper status If your Allocation Strategy is registered with the keeper system, verify automation is running: ```graphql GraphQL theme={null} query KeeperJobs($address: String!) { Job(where: { execTarget: { _ilike: $address } }) { id status execTarget event { tx { hash block { timestamp } } } } } ``` `status` is `STARTED`, `DONE`, or `CANCELLED`. There is no last-execution timestamp field — read it from `event.tx.block.timestamp`. If keepers are not processing redemptions, you can handle them manually (see [Troubleshooting](#troubleshooting) below). ## Update queue priorities You can change the allocation strategy at any time by updating the deposit and redeem queues. ```solidity Solidity theme={null} IQueueStrategyEngine strategy = MultiVehicle(multiVehicle).manager().queueStrategyEngine(); Target memory unlimited = Target({value: type(uint256).max, threshold: 0}); Target memory drainFully = Target({value: 0, threshold: 0}); // New deposit queue: prioritize Morpho, then Aave IQueueStrategyEngine.QueueEntry[] memory newDepositQueue = new IQueueStrategyEngine.QueueEntry[](2); newDepositQueue[0] = IQueueStrategyEngine.QueueEntry({vehicle: IVehicle(morphoVehicle), target: unlimited}); newDepositQueue[1] = IQueueStrategyEngine.QueueEntry({vehicle: IVehicle(aaveVehicle), target: unlimited}); // New redeem queue: drain Aave first, then Morpho IQueueStrategyEngine.QueueEntry[] memory newRedeemQueue = new IQueueStrategyEngine.QueueEntry[](2); newRedeemQueue[0] = IQueueStrategyEngine.QueueEntry({vehicle: IVehicle(aaveVehicle), target: drainFully}); newRedeemQueue[1] = IQueueStrategyEngine.QueueEntry({vehicle: IVehicle(morphoVehicle), target: drainFully}); // Replaces both queues; every vehicle must still be authorized and ready strategy.setQueues(newDepositQueue, newRedeemQueue); ``` ```typescript TypeScript theme={null} // Coming soon ``` ## Troubleshooting If redemptions are not progressing (e.g. due to keeper failure or insufficient liquidity): **1. Feed the redeem queue** — if withdrawable assets are available. Requires `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` scoped to the Vehicle Manager: ```solidity Solidity theme={null} IVehicleManager manager = MultiVehicle(multiVehicle).manager(); manager.feedQueryRedeemQueue(); ``` **2. Progress stuck sub-queries** — if a sub-query is stuck in a non-terminal state. Requires `MULTI_VEHICLE_PROGRESS_QUERY` scoped to the SubQueryEngine: ```solidity Solidity theme={null} ISubQueryEngine subQueryEngine = manager.subQueryEngine(); subQueryEngine.progressQuery(subQuery, query); ``` **3. Dispatch staged assets** — if assets sit in a yield source's sector with no active query: ```solidity Solidity theme={null} accounting.dispatch( ISectorAccountingEngine.DispatchParams({ vehicle: IVehicle(targetVehicle), mode: Mode.DEPOSIT, amount: type(uint256).max, settledDestination: SectorLib.ALLOCATION, rejectedDestination: SectorLib.AVAILABLE, minOutput: 0, data: "", operationId: bytes32(0) }) ); ``` If a deposit query is stuck in `PROCESSING` state, the yield source may require manual progression (common with async protocols like Ethena): ```solidity Solidity theme={null} ISubQueryEngine subQueryEngine = MultiVehicle(multiVehicle).manager().subQueryEngine(); subQueryEngine.progressQuery(subQuery, query); ``` Check the yield source's STEAM state to understand what transition is needed. If `dispatch` reverts, common causes include: * **No staged assets** — check the yield source's sector has a non-zero balance with `getSectorBalance` * **Source not authorized** — verify the yield source is still authorized: `manager.isAuthorized(vehicle)` * **Missing role** — confirm you hold `MULTI_VEHICLE_DISPATCH` scoped to the Sector Accounting Engine * **Slippage bound** — `minOutput` must be `0` when `amount` is `type(uint256).max` * **Cap reached** — `maxDepositable(vehicle)` returns 0 when the vehicle is at its configured cap or paused * **Source rejection** — the yield source's `create` may be reverting (check the underlying protocol's status) Remember that queues use **ceiling** (deposit) and **floor** (redeem) semantics: * Deposit queue targets are maximums, not ratios — allocations will drift with yield * If all yield sources have reached their ceiling, new deposits stay idle in `AVAILABLE` * Redeem queue targets are minimums — a source won't be drained below its target Update queue targets as your strategy evolves. See [Configure allocation queues](#configure-allocation-queues). ## Next steps Evaluate yield sources, manage concentration risk, and handle emergencies. Set up performance, management, and transactional fee structures. # Strategies Source: https://docs.railnet.org/strategies/index Build a strategy once, automate operations, distribute it everywhere A strategy is a managed investment vehicle on Railnet. Each is defined with a set of allocation rules & onchain guardrails, you operate it — Railnet handles fees accounting, compliance enforcement, and distribution. Build a strategy once, track revenue from single place, and distribute it to every platform connected via [conduits](/conduits). ## Why asset managers choose Railnet Building yield strategies today means stitching together protocol integrations, accounting, policy enforcement, and distribution channels from scratch — and rebuilding it all for every distribution platform you work with. Railnet changes this: * **On-chain guardrails** — allocation rules, spending limits, and permissions are enforced at the smart contract level — transparent and verifiable by depositors, platforms, and regulators. See the [policy engine](/strategies/advanced/policy-engine) and [allocation guardrails](/strategies/allocation/guardrails) * **Automated NAV** — net asset value is computed on-chain, removing manual reconciliation. Allocation Strategies get fully automated NAV tracking; Advanced Strategies can bring their own NAV provider or use Railnet's reporting service * **Built-in distribution** — your strategy is immediately accessible to every platform on the network via [Conduits](/conduits), without bilateral integrations * **Composability** — strategies can serve as building blocks inside other strategies, enabling automated fund-of-funds construction where managed allocation and bespoke execution coexist ## Two ways to structure your strategy Pick the structure that matches your investment approach. **Design a controlled liquidity allocation layer across multiple yield sources.** An Allocation Strategy composes multiple yield sources — lending protocols, staking, RWA adapters, and Advanced Strategies — into a single managed portfolio. Operations are intent-based: you express "move capital from A to B" and the system handles routing, accounting, and settlement. No smart protocol specific expertise required, simplified operations. * **Intent-based operations** — allocate and rebalance by expressing what you want to achieve, not by crafting transactions on each protocols * **Automated Allocations Strategy** — define allocation queue to automaticly allocate user deposit and fill user withdraw on selected yield sources * **Automated NAV** — net asset value computed on-chain every in real time with no manual reconciliation * **Fee management** — configurable management, performance, deposit, and redeem with automatic accouting * **Revenue-share distribution** — split fees among multiple recipients (your treasury, partners, a DAO) **Example: Flagship Diversified USDC Lending** A multi-allocation strategy with risk-weighted rebalancing and partner revenue-share, managed by "AlphaCapital" for Platform XYZ. | Source | Allocation | Type | | ----------------- | ---------- | ---------------- | | Morpho Blue | 40% | DeFi lending | | Aave V3 | 35% | DeFi lending | | Advanced Strategy | 25% | Custom execution | Compose multiple yield sources into a managed allocation with automated routing and on-chain guardrails. **Bring any strategy onchain — from delta-neutral arbitrage to cross-chain RWA operations.** An Advanced Strategy gives you full protocol execution flexibility. You build your own logic, craft and sign transactions, and interact with any smart contract directly — lending, swaps, bridges, perpetual futures, RWA operations. Railnet wraps this with an on-chain policy engine and standardized NAV so your strategy can be distributed and composed like any other yield source. * **Advanced operations** — interact with any smart contract on any supported chain. If a protocol exists onchain, you can build a strategy around it * **Bring your own setup** — asset managers bring their own setup, automations, transaction crafting and signing infrastructure to operate the strategy * **On-chain policy engine** — contract whitelists, function permissions, and calldata-level checks, all transparent and auditable * **Standardized distribution** — despite custom execution logic, your strategy plugs works with any distribution partner * **Full composability** — serve as a standalone product or as a sub-strategy inside an Allocation Strategy **Powering strategies like** Diversified BTC yield combining onchain lending, RWA exposure, and delta-neutral funding arbitrage across DEXs and Perp DEXs. Dynamic DeFi Lending and Alloy Series blending DeFi lending, tokenized real-world assets, and private credit. Operate your strategy fully onchain with execution guardrails, and standardized NAV and simplified accounting & distribution. Both types are composable: an Advanced Strategy can serve as a sub-strategy within an Allocation Strategy, creating a fund-of-funds model. Both can be distributed to users via [Conduits](/conduits). ## Where are you starting? Start with an Advanced Strategy. Deploy a non-custodial treasury wallet with on-chain policy enforcement and execute on any protocol. Build an Allocation Strategy. Combine multiple strategies and yield sources into a managed portfolio with automated routing and real-time accounting. The platform deployed the strategy and set the guardrails. Manage allocations within the boundaries they defined. ## Guides Non-custodial custody, on-chain policy engine, and standardized NAV. Full flexibility for standalone strategy execution. How the Zodiac Roles Modifier enforces contract whitelists, function permissions, and calldata checks. Route protocol actions through the policy engine — one ABI, one extra encode step. Compose multiple yield sources into a managed allocation with automated routing and on-chain guardrails. Deploy the Allocation Strategy ecosystem — access control, fee manager, and strategy contracts — in a single transaction. Understand the trust boundary between platforms and asset managers. Manage allocations, rebalance across yield sources, and handle day-to-day operations. Evaluate yield sources, manage concentration risk, and handle emergencies. # Risk management Source: https://docs.railnet.org/strategies/risk-management Evaluate yield sources, manage concentration risk, and handle emergencies In Railnet smart contracts, an Allocation Strategy is implemented as a **MultiVehicle**, an Advanced Strategy as a **Specialized Vehicle**, and a Yield Source is connected via a **Vehicle** adapter. See [Glossary](/developers/glossary) for all terminology. Railnet provides several mechanisms to manage risk across yield strategies — from on-chain exposure limits and operational guardrails to portfolio-level analytics and compliance enforcement. ## Risk parameters Strategies support configurable caps that limit exposure to individual protocols. The [Vehicle Manager](/developers/contracts/multi-vehicle) stores per-source authorization and allocation caps, allowing operators to set a maximum allocation per yield source. These limits are enforced on-chain — the contracts reject operations that would breach them. This is not advisory risk management; it is deterministic, rules-based enforcement at the smart contract level. ## Exposure limits and diversification Allocation [queues](/strategies/allocation/operate) define target caps per yield source. When a source reaches its target allocation, new deposits are automatically routed to the next source in the queue. This provides deterministic diversification: * **Deposit queues** distribute incoming capital according to configured priorities (e.g., first 20M to Morpho, next 10M to Aave, remainder to a tokenized T-bill yield source) * **Redeem queues** define the order in which capital is withdrawn from yield sources * **Over-allocation prevention** is enforced at the contract level — the accounting engine checks a vehicle's cap in the Vehicle Manager before every allocation ## Operational guardrails Railnet's role-based [access control](/developers/contracts/access-control) (EAC) enforces strict separation of duties: * **Asset managers** can only operate within their authorized yield sources and allocation ranges. Permissions are scoped to specific contracts, preventing over-broad access. * **Keepers** can only advance query states, not move assets arbitrarily. Their role is limited to automating state transitions. * **Factory controls** prevent deployment of unauthorized adapter types. Only approved factories can create new Vehicle adapters. * **Module timelocks** require a mandatory waiting period between requesting a module change and activating it. This prevents sudden logic changes and gives stakeholders time to review. * **Beacon freeze** provides an irreversible guarantee that contract logic cannot be changed. Once a beacon is frozen, the implementation is provably immutable — providing a strong assurance to depositors and regulators. ## Error isolation The [STEAM standard](/developers/contracts/steam-standard) ensures that a failure in one Query does not affect other operations: * Each Query is independent with its own state machine and unique ID * The **RECOVERING** state allows assets to be reclaimed from a failed operation without impacting the broader system * The **REJECTED** state provides explicit denial with assets returned to the sender * A bug or failure in one yield source does not propagate to other yield sources in the same Strategy This per-operation isolation is critical when combining assets with fundamentally different risk profiles and timing characteristics in a single strategy. ## Circuit breakers and pause mechanisms Railnet uses a beacon proxy architecture that supports two independent safety mechanisms: Operations can be temporarily halted and resumed. This is useful during security incidents, market dislocations, or when investigating unexpected behavior. While paused, no new deposits, redemptions, or allocations can be executed. Once frozen, contract logic can never be upgraded again. This provides a provable guarantee to depositors, counterparties, and regulators that the rules governing their assets cannot change. Freezing is an irreversible, one-way action — it cannot be undone. These mechanisms operate independently. A contract can be paused without being frozen, and vice versa. Together they provide both emergency response capability and long-term immutability guarantees. ## Portfolio risk analytics [Sector-based accounting](/developers/contracts/accounting) provides real-time visibility into every capital state across the portfolio: * **Idle capital** in the deposit sector * **Allocated capital** deployed to each yield source * **In-flight capital** mid-deposit or mid-redemption through ephemeral sectors * **Pending redemptions** queued for processing This on-chain position data enables portfolio-level risk calculations: exposure per protocol, concentration risk across yield sources, liquidity profiles (how quickly capital can be withdrawn from each source), and in-flight asset tracking for accurate NAV under async conditions. The [GraphQL API](/developers/api) provides queryable access to Strategy positions, Query lifecycle events, fee accrual, and sector accounting data — enabling custom dashboards and monitoring systems. ## Monitoring and observability Railnet emits events on every state transition, enabling real-time monitoring and alerting: * **Keeper automation** monitors yield source health and operation status through the on-chain Job Listing registry * **Event-driven architecture** — every deposit, redemption, allocation, rebalancing, and fee collection emits on-chain events that external systems can subscribe to * **Job Listing registry** provides on-chain visibility into automation status — which operations are pending, which keepers are active, and where attention is needed ## Audit and compliance Every capital flow is recorded on-chain with block-level timestamps, creating a complete and independently verifiable audit trail: * **Permission history** — role-based access control creates an auditable record of who was authorized to do what, and when permissions were granted or revoked * **Fee transparency** — fee collection and distribution is fully on-chain and verifiable * **Compliance enforcement** — [AccountList](/conduits/compliance) integration supports allowlists, blocklists, and sanctions oracle connections * **Transfer modes** — configurable controls determine how shares can move: permissionless transfers (ALLOW\_TRANSFER), restricted to approved accounts (ACCOUNT\_LIST), or mint-and-burn only with no secondary transfers (BLOCK\_TRANSFER) * **On-chain books and records** — sector-based double-entry accounting provides continuously reconciled books that auditors can verify independently, without relying on the asset manager's internal records