Documentation

Accounting and Flow of Funds

MultiVehicle implements rigorous double-entry accounting principles to track all asset movements through the system. Understanding the sector accounting model and asset flow is essential for operators managing multi-vehicle deployments.

Sector Accounting Model

All asset movements follow double-entry bookkeeping, where every transfer is recorded as a movement between sectors.

Core Sectors

SectorPurposeCounted in
ENTRYAssets entering the system. Carries negative balances, so it is a flow marker rather than a holding.Neither (virtual)
AVAILABLEIdle base assets awaiting allocation.totalAssets() and withdrawable()
RESERVEDBase assets the operator has earmarked. Asset-only — share moves into it are rejected on-chain.totalAssets() only
ALLOCATIONSub-vehicle shares from settled deposits (deployed capital).totalAssets()
EXITAssets leaving the system.Neither (virtual)

RESERVED is not a rename of an old “REDEEM” sector: it holds operator-earmarked base assets that still count toward totalAssets() but are deliberately excluded from withdrawable().

Dynamic Sectors

SectorFormatPurpose
Vehicle (staging) Sector0x01 + 11 zero bytes + the vehicle addressPer-vehicle staging area, where value waits for a query to be created for it
Query Sector0x02-prefixedEphemeral, used only while recovering a failed sub-query

Static sector ids are the sector name as right-aligned ASCII in a 32-byte word — so AVAILABLE is 0x0000…617661696c61626c65. There are no “pending” sectors: value in flight sits in the relevant vehicle’s staging sector.

Deposit Flow

Assets flowing from users into sub-vehicles follow this path:

User Wallet
    │
    ├─ User initiates deposit
    │
    ▼
┌─────────────────┐
│  MultiVehicle   │
│   (Contract)    │
└────────┬────────┘
         │
         ├─ 1. Transfer assets from user
         │
         ▼
    ┌─────────┐
    │  ENTRY  │ ← Virtual sector (accounting marker)
    └────┬────┘
         │
         ├─ 2. Record inflow
         │
         ▼
   ┌───────────┐
   │ AVAILABLE │ ← Idle assets, awaiting allocation
   └─────┬─────┘
         │
         ├─ 3. QueueStrategyEngine determines allocation
         │
         ▼
┌──────────────────┐
│  Vehicle Sector  │ ← Staging: 0x01 + 11 zero bytes + vehicle address
│  (e.g., Aave)    │    Value waits here for a query, and stays here
└────────┬─────────┘    while that query is in flight
         │
         ├─ 4. Dispatch creates a STEAM query
         ├─ 5. Query progresses: PROCESSING → UNLOCKING → SETTLED
         │
         ▼
   ┌────────────┐
   │ ALLOCATION │ ← Sub-vehicle shares (deployed capital)
   └────────────┘

Deposit Flow Details

Step 1-2: Asset Reception

// A deposit pulls the assets and books them into AVAILABLE, crossing the
// virtual ENTRY marker on the way in. Pass allocate=true to run the strategy
// engine and dispatch the resulting allocations in the same call.
accountingEngine.deposit(amount, false)

Step 3: Allocation Decision

// QueueStrategyEngine processes deposit queue
depositQueue = [{aave, 60k}, {morpho, 30k}]

// Determines: allocate X to Aave vehicle

Step 4-5: Query Dispatch

// AVAILABLE → the vehicle's staging sector.
accountingEngine.move(ISectorAccountingEngine.MoveParams({
    from: SectorLib.AVAILABLE,
    to: vehicle.toSector(),
    asset: address(baseAsset),
    amount: amount,
    operationId: operationId
}));

// Create the STEAM query. The value stays in the vehicle's staging sector
// while the query is in flight; settled shares land in settledDestination.
accountingEngine.dispatch(ISectorAccountingEngine.DispatchParams({
    vehicle: vehicle,
    mode: Mode.DEPOSIT,
    amount: type(uint256).max,   // everything staged, cap-limited
    settledDestination: SectorLib.ALLOCATION,
    rejectedDestination: SectorLib.AVAILABLE,
    minOutput: 0,                // required when amount is the max sentinel
    data: "",
    operationId: operationId     // same id, so the steps correlate
}));

Redeem Flow

Assets flowing from sub-vehicles back to users follow this path:

   ┌────────────┐
   │ ALLOCATION │ ← Sub-vehicle shares
   └─────┬──────┘
         │
         ├─ 1. QueueStrategyEngine determines unallocation
         │
         ▼
┌──────────────────┐
│  Vehicle Sector  │ ← Staging for redeem; the in-flight query's value
└────────┬─────────┘    stays here until it settles
         │
         ├─ 2. Dispatch creates the redeem query
         ├─ 3. Query progresses and settles
         │
         ▼
   ┌───────────┐
   │ AVAILABLE │ ← Base assets, now withdrawable
   └─────┬─────┘
         │
         ├─ 4. Record outflow
         │
         ▼
    ┌─────────┐
    │  EXIT   │ ← Virtual sector (accounting marker)
    └────┬────┘
         │
         ├─ 5. Transfer to user
         │
         ▼
┌─────────────────┐
│  MultiVehicle   │
│   (Contract)    │
└────────┬────────┘
         │
         ▼
    User Wallet

Redeem Flow Details

Step 1: Unallocation Decision

// A redemption is a STEAM query in REDEEM mode, not a redeem() method.
multiVehicle.create(query)   // query.mode = Mode.REDEEM

// QueueStrategyEngine processes redeem queue
redeemQueue = [{morpho, 10k}, {aave, 20k}]

// Determines: unallocate from Morpho first, then Aave if needed

Step 2-3: Query Dispatch

// ALLOCATION → the vehicle's staging sector. The asset is the vehicle
// itself, because what moves here is that vehicle's shares.
accountingEngine.move(ISectorAccountingEngine.MoveParams({
    from: SectorLib.ALLOCATION,
    to: vehicle.toSector(),
    asset: address(vehicle),
    amount: shares,
    operationId: operationId
}));

// Dispatch the redeem. Settled base assets land in settledDestination;
// a rejected redeem returns the shares to ALLOCATION.
accountingEngine.dispatch(ISectorAccountingEngine.DispatchParams({
    vehicle: vehicle,
    mode: Mode.REDEEM,
    amount: type(uint256).max,
    settledDestination: SectorLib.AVAILABLE,
    rejectedDestination: SectorLib.ALLOCATION,
    minOutput: 0,
    data: "",
    operationId: operationId
}));

Step 4-5: Asset Return

// Unlocking the query pays the receiver, booking the outflow across the
// virtual EXIT marker.
multiVehicle.unlock(query)   // returns (newState, paidAsset)

Asynchronous Redemptions

When immediate liquidity is insufficient, the QueryRedeemQueue handles asynchronous fulfillment:

User Redeem Request
    │
    ├─ Insufficient immediate liquidity
    │
    ▼
┌──────────────────┐
│ QueryRedeemQueue │
│                  │
│ Demand created   │ ← User's pending redemption
└────────┬─────────┘
         │
         │ ... waiting for liquidity ...
         │
         ├─ Keeper/Operator calls VehicleManager.feedQueryRedeemQueue()
         │
         ▼
┌──────────────────┐
│  Fulfillment     │ ← Liquidity provision
└────────┬─────────┘
         │
         ├─ Binary search matching
         │
         ▼
    User receives assets (full or partial)

Exchange Rate & Share Price

MultiVehicle uses the ERC4626 standard for pricing:

shares = assets × totalSupply / totalAssets()

Total Assets Calculation

The totalAssets() function aggregates value across all sectors:

totalAssets =
    AVAILABLE sector (idle base assets)
  + RESERVED sector (operator-earmarked base assets)
  + ALLOCATION sector (sub-vehicle shares, converted to base asset)
  + ephemeral accounting (expected outputs from in-flight queries)

Why Ephemeral Accounting Matters

Without ephemeral accounting, in-flight queries would create pricing anomalies:

Without Ephemeral Accounting (❌ Incorrect):

Time T0: totalAssets = 100k
User deposits 10k → query dispatched
Time T1: totalAssets = 100k (10k staged in the vehicle sector, not counted)
         Share price drops incorrectly!

With Ephemeral Accounting (✅ Correct):

Time T0: totalAssets = 100k
User deposits 10k → query dispatched
Time T1: totalAssets = 110k (10k expected output tracked)
         Share price remains accurate!

Query Lifecycle States

Sub-vehicle queries progress through STEAM protocol states:

Standard Flow (Synchronous)

EMPTY → UNLOCKING → SETTLED

Asynchronous Flow

EMPTY → PROCESSING → (PAUSED →) UNLOCKING → SETTLED

Error Recovery

PROCESSING → RECOVERING → REJECTED

State Responsibilities

StateAccounting ImpactOperator Action
EMPTYNo query exists-
PROCESSINGAssets received, vehicle performing operations. Ephemeral accounting activeWait for vehicle
PAUSEDPaused for external condition (cooldown, oracle)Call resume()
UNLOCKINGProcessing complete, output ready for claimCall unlock
SETTLEDQuery complete, assets in final sector-
RECOVERINGError state, assets frozenInvestigate and recover
REJECTEDQuery failed, assets returnedHandle failure

Sector Transfer Examples

Example 1: Simple Deposit

// User deposits 1000 USDC
// Current state: AVAILABLE=50k, ALLOCATION=100k

1. ENTRY ← 1000 (virtual inflow)
2. AVAILABLE ← 1000 (from ENTRY)
   // Now: AVAILABLE=51k

3. vehicleSector ← 1000 (move, from AVAILABLE)
   // Now: AVAILABLE=50k

4. dispatch(DEPOSIT) creates the query
   // Value stays in vehicleSector while in flight
   // Ephemeral: +1000 expected shares

5. ALLOCATION ← 1000 shares (query settles)
   // Now: ALLOCATION=101k shares
   // Ephemeral cleared

Example 2: Redeem with Queue

// User redeems shares worth 2000 USDC
// Immediate liquidity: 500 USDC
// Need: 1500 USDC more

1. ALLOCATION → vehicleSector (move, asset = the vehicle's shares)
2. dispatch(REDEEM) creates the query
3. AVAILABLE ← 500 USDC (query settles, immediate portion)

4. QueryRedeemQueue.createDemand(1500 USDC)
   // User partially fulfilled: 500 USDC
   // Demand: 1500 USDC pending

// Later: Operator provides liquidity. feedQueryRedeemQueue lives on the
// VehicleManager now, and takes NO arguments — it feeds whatever the queue
// needs from what is available. To pull assets back out of the queue, use
// VehicleManager.retrieveQueryRedeemQueueAssets(amount).
5. vehicleManager.feedQueryRedeemQueue()
6. AVAILABLE → EXIT → user receives the remaining 1500 USDC

Monitoring & Observability

Operators should monitor these key metrics:

Sector Balances

DEPOSIT:    Amount of idle capital
ALLOCATION: Total deployed capital
REDEEM:     Liquidity ready for withdrawal
Pending:    Assets in flight

Queue Health

QueryRedeemQueue.demand:      Total pending redemptions
QueryRedeemQueue.fulfillment: Available liquidity
Average wait time:            User experience metric

Vehicle Health

Per-vehicle: totalAssets, share price, query states
Distribution: Actual vs target allocations

Best Practices

  1. Monitor vehicle staging sectors: High staged balances indicate slow query settlement
  2. Maintain Redeem Liquidity: Keep sufficient DEPOSIT/REDEEM for immediate withdrawals
  3. Regular Queue Fulfillment: Minimize QueryRedeemQueue demand wait times
  4. Validate Sector Balances: Ensure accounting integrity with regular audits
  5. Track Ephemeral Accounting: Verify share price accuracy during high query volume

Summary

The MultiVehicle accounting system provides:

  • Accurate pricing through ephemeral accounting
  • Complete auditability via double-entry bookkeeping
  • Flexible allocation through queue-based strategies
  • Async support for real-world DeFi protocols
  • Operator transparency with comprehensive sector tracking