# Conduits \[The wrapper that turns any Vehicle into a distributor's own branded product]

A Conduit wraps any Vehicle for distribution. The Vehicle holds the yield position; the Conduit issues your own ERC20 share token on top of it and adds what a distributor needs: fees, compliance screening, reward interception, keeper-automated settlement. The wrapped Vehicle can be a single yield source or a MultiVehicle composing several, and the Conduit behaves the same either way. Several Conduits can share one Vehicle, each with its own fees and holder set, all aggregating into one position.

Users hold Conduit shares; the Conduit holds Vehicle shares. As the Vehicle earns, its share value rises, the Conduit's `totalAssets()` rises with it, and every Conduit share is worth more. Users never touch Vehicle shares.

## Create

`create(query, receiver, sourceSalt)` opens a deposit or redeem:

1. Checks the receiver, that the Conduit is enabled, and that the sender passes the AccountList when one is configured.
2. Requires a caller-bound salt: `query.salt` must equal `keccak256(abi.encode(msg.sender, sourceSalt))`, otherwise it reverts `InvalidQuerySalt`. The query ID cannot be front-run.
3. Pulls the input. Deposits pull the underlying token; redeems burn the caller's conduit shares through internal ERC-20 primitives, so no approval is needed.
4. Approves the Vehicle and calls `vehicle.create()`.
5. Pins the FeeManager's current `configId`, so a fee change mid-flight never applies to this query.
6. Auto-processes when `receiver == msg.sender`.

## Process

`process(query)` advances the query through the STEAM state machine. It accrues management and performance fees by minting shares to the FeeManager, reads the query's state in the Vehicle, then:

* `PAUSED`: calls `vehicle.resume()`
* `UNLOCKING`: calls `vehicle.unlock()`, mints conduit shares (deposit) or transfers assets (redeem), and applies transactional fees
* `RECOVERING`: calls `vehicle.recover()` to refund assets or restore shares

Behind a sync Vehicle, `create` and `process` complete in one transaction. Behind an async one, a keeper holding `CONDUIT_PROCESS` calls `process()` once the underlying protocol settles, so the user does not come back for a second transaction. The owner can always call it themselves.

## Redeeming: which entrypoint

Both paths go through the same gating and pull routine; pick by the unit you hold.

| You know | Call | Notes |
|---|---|---|
| A cShare amount (a user's balance) | `createRedeemFromConduitShares` | Converts cShares to Vehicle shares at the current rate, previewing ongoing fees first. 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. |

:::warning
A REDEEM query with `input.asset == address(conduit)` is invalid. `create` treats only a Vehicle-share input as a cShare burn; any other asset is pulled with `transferFrom` and forwarded to the Vehicle, which rejects it.
:::

## Transfer policies

Two independent gates must both pass for a user-initiated cShare transfer:

| Gate | Where | Behavior |
|------|-------|----------|
| `transferEnabled` | the Conduit | A one-way latch. While `false`, no user-to-user transfers occur. `enableTransfers()` (role `CONDUIT_SET_TRANSFER_ENABLED`) flips it on permanently, so integrations relying on transferability cannot be bricked; calling it twice reverts `StateUnchanged`. |
| `canTransfer(from, to)` | the AccountList | Screens both parties for sanctions, block-list membership, and the allow-list in `STRICT` mode. A no-op when no AccountList is set. |

`isTransferable(from, to)` returns the combined result, and `create` reuses it to screen a third-party receiver. [Compliance](/conduits/compliance) covers the policy side.

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. The FeeManager dispatching accrued cShare fees is exempt as a sender only; its recipient is still screened.

## Interface summary

| Method | Description |
|--------|-------------|
| `create(query, receiver, sourceSalt)` | Create a deposit or redeem query. Auto-processes when possible. |
| `createRedeemFromConduitShares(conduitShares, outputAsset, sourceSalt, receiver)` | Redeem a cShare amount. |
| `process(query)` | Advance a query through the STEAM lifecycle. |
| `forceRedeem(user, amount, output)` | Eject a blocked or sanctioned holder (role `CONDUIT_FORCE_REDEEM`). |
| `holdings()` | Vehicle shares held by the Conduit. |
| `totalAssets()` | Total underlying assets managed. |
| `estimate(asset_, mode, estimationType)` | Preview the output `Asset`, including fees. |
| `convert(asset_, sharesToAssets)` | Pure conversion, without fees. |
| `ready()` | Whether the Conduit is open for deposits. |
| `getVehicle()` / `asset()` | The underlying Vehicle and ERC20 asset. |

## Deployment

`ConduitFactory.spawn(params)` deploys the proxy behind the Conduit beacon and initializes it with the Vehicle, FeeManager, AccountList, OwnerRegistry, access control, initial interceptions and the initial `transferEnabled` state. Module addresses are validated against the trusted factories pinned in the factory's constructor.

The seed deposit is not a parameter: the factory resolves the asset from the Vehicle's `asset()`, reads the amount from the [AssetRegistry](/developers/contracts/asset-registry), and pulls it from the caller. Once the seed query settles it burns the shares received, checks `totalSupply() >= params.initialExpectedSupply`, and calls `enable()`.

Behind an async Vehicle the seed 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`. The factory is that deposit's `msg.sender`, so with an AccountList configured it must itself satisfy `canDeposit`.

`spawn` is gated on `CONDUIT_SPAWN`, which the Railnet team holds today.
