OmniFi Tranche docs
Covers the single-chain and the multichain version of the Senior/Junior tranche vault: structure, how they work, how to use them, functions and events.
Introduction
OmniFi Tranche splits one yield-source pool into two layers and sells them as products with different risk appetites. Senior takes a fixed annual return first; Junior takes everything left over but absorbs losses first.
There are two versions. Single-chain version keeps the tranche structure and settlement entirely inside one chain. Multichain version extends the same tranche structure into a hub-spoke layout, so one product is sold on several chains. Whichever chain you join from, it is the same tranche at the same price; the ledger and settlement live on the hub chain.
The user surface is the same ERC-7540 async vault in both versions. The sections below on what a tranche is and on the ERC-7540 async vault apply to both.
What a tranche is
A tranche is one yield pool divided into layers of differing priority. This system uses two tranches, Senior and Junior.
| Senior | Junior | |
|---|---|---|
| Return | Takes a fixed annual APR first | Takes everything left after Senior is paid (residual) |
| Loss | Only takes a loss once Junior is fully wiped out | Absorbs losses first (first-loss) |
| Character | Fixed return · principal protection first | Leveraged variable return |
Waterfall allocation
At every settlement the product total valuation (product NAV) is divided across the tranches. Senior NAV is min(Senior target, product NAV) — the target is principal plus accrued fixed APR. Junior NAV is everything else. Gains fill the Senior target and the whole remainder goes to Junior; losses eat into Junior NAV first.
In the implementation the Senior target accrues at each settlement as target += target × APR × elapsed ÷ 1 year. Interest is applied to the previous target again, so it compounds at the settlement interval. A deposit raises the target by the principal added; a redemption lowers it by the fraction of shares burned.
The waterfall in numbers
Say 1,000 is raised as Senior 800 · Junior 200, the Senior APR is 5%, and the product runs for a year. After a year the Senior target is 840. Whatever the product NAV turns out to be, the rule is the same —everything up to 840 is Senior's, everything above it is Junior's.
| Product NAV | Total return | Senior NAV | Senior return | Junior NAV | Junior return |
|---|---|---|---|---|---|
| 1,100 | +10% | 840 | +5% | 260 | +30% |
| 1,040 | +4% | 840 | +5% | 200 | 0% (break-even) |
| 1,000 | 0% | 840 | +5% | 160 | −20% |
| 840 | −16% | 840 | +5% | 0 | −100% |
| 800 | −20% | 800 | 0% | 0 | −100% |
| 700 | −30% | 700 | −12.5% | 0 | −100% |
One return on one pool of assets splits into two completely different payoff curves. Senior is untouched down to −16% and only starts losing below that; Junior has to clear +4% to make anything, but past that it takes the entire gain. Junior is only 200 of the 1,000, so from Junior's side this is 5x leverage.
Share price
The share price of each tranche is tranche NAV ÷ shares outstanding for that tranche. A deposit gives you deposit ÷ price shares, and a redemption returns share count × price in assets. Deposit principal that has not settled yet is excluded from NAV even once it is deployed, so new money cannot move the price for existing holders.
Risk model
Tranching does not create return — it only re-divides one P&L into two claims ordered by priority. Senior is a threshold structure: capped at a fixed APR, losing nothing until the attachment point is crossed. Junior is leveraged by total ÷ Junior's share of it.
Three conclusions matter. ① Senior's risk is non-linear in α (the Senior share): raising α from 80% to 90% multiplies the loss probability by roughly 9. ② Even with no asset loss, if the yield source pays less than α·r the Junior buffer drains at a fixed rate. ③ α here is endogenous — deposits set it — so a Senior investor should watch the current tranche NAV ratio rather than the fixed APR.
ERC-7540 async vault
In both versions the user entry point is a TrancheVault following the ERC-7540 async vault standard. Deposits and redemptions do not fill immediately; they run in three steps, request → settle → claim.
- Request —
requestDeposit/requestRedeem. Assets or shares move into escrow and the request becomes Pending. - Settle — the operator fixes NAV and sets the price. The request becomes Claimable.
- Claim — the user calls
deposit/mint(receive shares) orwithdraw/redeem(receive assets). Any time.
No price is needed at request time. The price is fixed at settlement, and claiming is a local operation that picks up the fixed value. That separation is what lets the multichain version absorb cross-chain round-trip latency.
The share token is an ERC-20 separate from the vault (ERC-7575), with ERC-1404 transfer restrictions, so only whitelisted addresses can hold or transfer it. The vault is only an entry point and has no mint authority; only the Manager can mint or burn shares.
Glossary
| Glossary | Meaning |
|---|---|
| share | Tranche ownership token. Senior and Junior shares are separate ERC-20s. |
| escrow | Where assets and shares sit between request and settlement. The Manager owns it. |
| waterfall | The rule that divides the product NAV, Senior first and the remainder to Junior. |
| Yield-source adapter | A contract wrapping one yield-source protocol (Compound, Morpho, and so on). Handles deployment, withdrawal and valuation. |
| settle / settlement | The procedure that fixes NAV and approves pending requests. In multichain it runs in rounds (settlementId). |
| requestId | Identifier for one deposit or redeem request. Issued by the Manager. |
| settlementId | Settlement round number. Increments by 1 on every successful settlement. |
| NAV | Net asset value. Multichain qualifies the layer — adapter NAV (reported by an adapter) → chain NAV (one chain's sum, observed) → product NAV (fixed after adjustment) → tranche NAV (after the waterfall). |
| hub / spoke | The multichain role split. The hub handles the ledger, NAV and allocation decisions; spokes handle the user surface and execution. |
| CCCP | Cross-chain infrastructure that carries a message and assets together. Relayers forward them. |
| pallet precompile | A fixed-address interface for calling the hub chain runtime (pallet) from EVM. It is the source of truth for the multichain ledger. |
Definition
In the single-chain version a whole tranche set is self-contained on one chain. User entry, deploying into yield sources, computing NAV and settlement are all contract calls on the same chain. It is the starting point for the multichain version, and the core (Manager · Vault · ShareToken) shares the same lineage across both.
Product policy lives in Valuation. Tranche count, Senior APR, settlement mode and yield-source weights are all Valuation deploy parameters and settings; the core (Manager · Vault · ShareToken) is reused regardless of product.
Structure and components
share mint/burn
price · allocation
Behind each Vault sits one ShareToken (ERC-1404) per tranche. Arrows are call delegation; assets flow user → Manager (escrow) → Valuation → Adapter → yield source.
| Component | Count | Role |
|---|---|---|
| TrancheVault | 1 per tranche | ERC-7540/7575/4626 surface entry point. Holds no state and no mint rights — everything is delegated to the Manager. |
| ShareToken | 1 per tranche | ERC-20 + ERC-1404 transfer-restricted share. A stricter regulated form (ERC-3643 surface) is also available. |
| TrancheManager | 1 per product | Escrow custody, request state, requestId issuance, share mint/burn, claim handling. A general-purpose core that knows nothing about any particular yield source. |
| Valuation | 1 per product | NAV computation, waterfall, share price fixing (settle), targetWeights allocation, adapter management. This is where product policy lives. |
| Yield-source adapter | 1 per yield source | Deploy (requestSupply), withdraw (requestWithdraw), value (totalAssets). Adding or swapping a yield source is a matter of attaching or detaching one adapter. |
| WhitelistHook | Shareable | The whitelist that the ShareToken 1404 hook queries. The vault deposit gate uses the same check. |
Every contract is deployed behind an ERC-1967 proxy and can be upgraded. Vault and share are separate (ERC-7575), so replacing the vault logic does not touch holder balances.
Settlement modes — SYNC / ASYNC
Settlement speed is decided at two layers. Adapters declare, separately for deposits and withdrawals, what their yield source actually supports (SYNC = fills immediately, ASYNC = request then wait). Valuation sets, through its deploy parameters (depositMode / redeemMode), the settlement behavior the product promises the user.
There is one constraint: Valuation cannot be faster than its adapters. You cannot promise the user an immediate settlement (SYNC) when the money cannot leave the yield source immediately (ASYNC). With several adapters, the slowest one sets the bound.
In an ASYNC flow, fixing the redemption price and starting the yield-source withdrawal both happen at the operator's settle. If the yield source is ASYNC, two settles are needed — the first fixes the price and starts the withdrawal, and the second, once the withdrawal is ready, makes the request claimable.
How deposits and redemptions work
Deposit
- The user calls
requestDeposit(assets, controller, owner)— after the 1404 whitelist check, assets move into Manager escrow and, in the same transaction, Valuation deploys them into the adapters. If targetWeights is set it splits by those weights; if not, the whole amount goes to thedepositTargetadapter. - The operator calls
settle(reqId)— the price is fixed at the NAV of that moment and the share count is set. - The user calls
deposit/mintto claim the shares. There is also akeeperClaimpath where a keeper claims on the user's behalf (shares always go to the controller).
Redemption
- The user calls
requestRedeem(shares, controller, owner)— the shares are locked in escrow (not burned). - Operator settle — fixes the price and runs the adapter withdrawal plan, splitting the withdrawal across adapters. The SYNC portion realizes immediately; the ASYNC portion issues tickets.
- Once every ASYNC ticket is ready, a batch claim — the user calls
withdraw/redeemto receive the assets. The escrowed shares are burned at that point.
Cancellation
User cancellation is not supported. In both versions, calling cancelDepositRequest /
cancelRedeemRequest reverts with CancelNotSupported. Once accepted, a request can only be ended by settlement.
The functions are kept so the ERC-7540 surface matches, but they do nothing, so a front end should not show a cancel button. Reversing a request that has already crossed chains can desynchronize the two ledgers, so multichain blocked it first and single-chain was aligned to the same rule.
A request orphaned by a lost cross-chain message is cleared by the operator with adminClearRequest — from the ledger only (compensating the assets is a separate procedure).
How to use it
Users
- Get whitelisted (the operator registers you on the WhitelistHook).
- Approve the base asset (USDC) to the vault with
approve. - Call
requestDepositon the Senior or the Junior vault. - After settle, claim the shares with
deposit(assets, receiver).maxDeposit(controller)greater than 0 means there is something to claim. - Redemption is the same order —
requestRedeem→ wait for settle →redeem.
Operators
pendingRequests()orpendingDeposits()/pendingRedeems()return the pending requests.isSettleable(reqId)filters them; then callsettleorsettleBatch.- Change weights with
setTargetWeights; move funds between protocols withrebalance. Both of those, plus adapter management and drain, are owner-only; the operator account can only settle.
Demo: Single-chain products →
Functions
TrancheVault — user surface (ERC-7540/7575/4626)
| Functions | Description |
|---|---|
requestDeposit(assets, controller, owner) | Request a deposit. Assets move into escrow. |
requestRedeem(shares, controller, owner) | Request a redemption. Shares are locked in escrow. |
deposit / mint | Claim the shares of a settled deposit. Full claims only (no partial claim). |
withdraw / redeem | Claim the assets of a settled redemption. |
cancelDepositRequest / cancelRedeemRequest | Attempts to cancel. In the current configuration it always reverts (see Cancellation below). |
keeperClaim(controller) | A keeper performs the claim on the user's behalf. The proceeds always go to the controller. |
setOperator(operator, approved) | 7540 operator approval. Only an approved operator can request on another controller's behalf. |
pendingDepositRequest / claimableDepositRequest etc. | Request state queries (same shape on the redeem side). |
convertToShares / convertToAssets / maxDeposit etc. | 4626 surface. The preview family deliberately reverts, since the vault is async. |
Valuation — operator surface
| Functions | Description |
|---|---|
settle / settleBatch / isSettleable | Settle requests. A batch reverts entirely if an unready request is mixed in, so filter before calling. |
pendingRequests / pendingDeposits / pendingRedeems | List every pending request (off-chain view only). |
setTargetWeights(bps[]) | Set adapter allocation weights (summing to 10,000). If unset, the whole amount goes to the single depositTarget adapter. Adding or removing an adapter resets the weights, so set them again. |
addAdapter / removeAdapter / forceRemoveAdapter | Register or deregister a yield-source adapter. force removes an unresponsive adapter by writing off its remaining assets. |
rebalance | Move funds between adapters (valuation unchanged). |
initiateDrain / completeDrain / supplyIdle | Pull funds out of an ASYNC adapter → hold idle → redeploy. |
setOperator | Designate a settlement-only operations account. Only settle/settleBatch can be called from that account; weight changes, adapter management and drain are owner-only (the owner can settle too). |
Other
| Functions | Description |
|---|---|
Manager setPaused(bool) | Blocks requests and claims entirely. true can be set by the owner or the keeper; clearing it (false) is owner-only. |
Manager setKeeper / setVault | Replace the keeper account or the vault. |
Adapter requestSupply / requestWithdraw / isReady / receiveReady / totalAssets | IAdapter surface. A SYNC adapter returns a final value at request time. ASYNC tickets are finalized with receiveReady (batched: receiveBatch). |
Events
It follows the standard surface events as they are, so it works with general-purpose indexers.
- ERC-7540 —
DepositRequest/RedeemRequest, operator approvalOperatorSet. No cancel event is emitted, since cancellation is unsupported. The standard event'srequestIdis always 0 and cannot distinguish requests — for a per-request join key use the Manager'sDepositRequested(bytes32 requestId, …)andDepositsApproved/RedeemsApproved. - ERC-4626 — on claim,
Deposit/Withdraw. - ERC-7575 — on the share side,
VaultUpdate(vault replacement notice). - ERC-20 / 1404 — share
Transfer(including mint/burn); whitelist changes are hook-contract events. - Operations — adapter registration, weight changes, rebalance, drain, pause and the other main operations each have a matching event.
Definition
The multichain version sells one tranche product across several chains. A tranche is a product-global concept, so whichever chain you join from it is the same Senior and Junior at one price. A chain is only a route in.
The roles split in two. The hub (Bifrost testbed) handles ledger records, NAV aggregation and allocation decisions — only the place where every chain's valuation lands can decide price and allocation. Spokes handle the user surface (vaults) and execution (deploying into and withdrawing from yield sources). The hub can hold vaults and yield sources too, so the hub is sometimes an entry chain itself.
CCCP handles chain-to-chain communication. It carries the message and the assets (USDC) in one request, so there is no separate token bridge. The ledger's source of truth is the pallet in the hub runtime, and contracts read and write it through pallet precompiles at fixed addresses.
Structure and components
Hub and spoke are symmetric — both hold a Manager, vaults and yield sources. Spoke messages ride the CCCP bridge; a request originating on the hub is handled in the same transaction with no bridge. An off-chain relayer watches send events and executes them on the receiving chain.
| Component | Location | Role |
|---|---|---|
| TrancheVault / ShareToken | per issuing chain | The same user surface as single-chain. One vault + one share per tranche. |
| MultichainTrancheManager | 1 per chain × product | Escrow, requestId issuance, share mint/burn, CCCP message send and receive handlers. The multichain edition of the single-chain Manager. |
| MultichainAdapter | 1 per chain × product | Fan-out to that chain's yield-source adapters and aggregation of their valuations. Holds the local allocation weight cache (LocalAllocation). |
| Orchestrator(Hub) | 1 global on the hub | The hub's CCCP endpoint. Routes inbound messages to the right product's Valuation and proxies outbound sends. |
| ValuationDiamond | 1 per product on the hub | The settlement engine — aggregates chain NAVs, runs the waterfall, fixes prices, produces the allocation plan and is the caller that writes it to the pallet. |
| 3 precompiles | fixed hub addresses | TrancheSystem(0x200) product/tranche/adapter configuration · Investments(0x201) request, approval and settlement ledger · TranchePermissions(0x202) source of truth for investor eligibility. |
| settle keeper | off-chain | Calls tryUpdateNAV once per settle interval to trigger the settlement cycle. |
| Relayers | off-chain | Watches CCCP events → signs → executes on the receiving chain. |
ID scheme
productId — uint64
The top 32 bits are the product class prefix, the low 32 bits the sequence within that class. Always written in hex.
| prefix | Class | Example |
|---|---|---|
0x00000001 | single-chain | 0x0000000100000002 |
0x00000002 | multichain | 0x0000000200000009 (current product) |
requestId — bytes32
requestId = productId (64 bits) | chainId (64 bits) | seq (128 bits)
Issued by the Manager. Cutting off the top 64 bits gives the full productId including its prefix; the next 64 bits are the EVM chainId of the requesting chain, and the low 128 bits are that Manager's local incrementing sequence. The (productId, chainId) prefix guarantees global uniqueness.
settlementId — uint256
The settlement round number. It increments by 1 on every successful settlement and is the join key for settlement-family events and records. A request that arrives while a settlement is collecting rolls over to the next round.
Tranche key — (chainId, vault)
The pallet identifies a tranche by the pair (chain ID, vault address). There is no separate tranche index field. For the same tranche type the share price is identical across chains; only the supply differs per vault.
Deposit flow
The principle is to centralize allocation decisions on the hub. Requested funds are sent to the hub in full, and each chain receives its own portion back and deploys it according to the plan the hub set.
Previously the requesting chain deployed its own portion first, which mixed funds that had not become shares yet into that chain's NAV and inflated that round's price by other people's money (fixed 2026-08-21). Now the requesting chain goes through the hub like every other chain to get its portion — one more bridge hop, but an accurate price.
- Request (entry chain, user tx) —
requestDeposit→ whitelist check → Manager escrow → requestId issued. The requested amount, in full, is sent to the hub as a DEPOSIT_REQUEST (kind 1) message with the assets attached. (Each product has a minimum request amount; below it the call reverts withBelowMinRequest.) - Record and allocate (hub) — the Orchestrator receives it and routes it to Valuation. It writes the pallet ledger with
record_investment_request, computes the allocation plan, deploys the hub's portion immediately, and propagates each spoke's portion as an asset-carrying ADAPTER_SUPPLY (kind 7). A deposit that arrives while a settlement is collecting is held by the Orchestrator and deployed only after that round's price is fixed. - Deploy (each spoke) — on receipt each chain deploys its portion into local yield sources. That includes the requesting chain.
- Settle · claim — share issuance and price fixing happen in the settle cycle, and the user can claim any time after that.
Joining from the hub delivers message 1 directly inside the same transaction, with no bridge — the event structure is identical.
Redeem flow
Symmetrically with deposits, withdrawal starts as soon as the request is made. That absorbs an ASYNC yield source's lead time (request → ready → claim) into the gap between request and settlement. The start is immediate; the asset movement is batched at settlement.
- Request (entry chain) —
requestRedeem→ shares are locked in escrow → the expected payout is computed at the last fixed price → the local portion's withdrawal starts immediately → reported to the hub as REDEEM_REQUEST (kind 2), including the amount started. - Delegate withdrawal (hub) — after writing the ledger, it sends ADAPTER_WITHDRAW (kind 8, flag=1) to every spoke except the requesting chain to start their portion's withdrawal. Each receiving chain computes its own localShareBps of the total expected payout. The hub's portion and any remainder start locally, with no message.
- Realized amounts collected (at settlement) — each chain sends what it realized to the hub as assets attached to SETTLEMENT_RESPONSE (kind 4).
- Payout (settlement fixed) — per-request payouts are set at the fixed price, and the per-chain payout allocation is sent down attached to SETTLEMENT_FINALIZE (kind 5). The receiving Manager credits it to the payout reserve (payoutReserve) and flips the requests to claimable.
- Claim — the user calls
withdraw/redeemto receive them. The escrowed shares are burned when the user claims.
withdraw was called — request order is not enforced.settle cycle
The keeper calls the hub Valuation's tryUpdateNAV once per settle interval to start the cycle (if the interval has not elapsed it returns without doing anything). Redemption withdrawals have already started outside this cycle, at request time. settle covers price fixing, share distribution, redemption payouts, and deploying deposits that arrived and were held during collection.
- Collection starts (hub) — snapshots the hub's own valuation and sends SETTLEMENT_COLLECT (kind 3) to every registered spoke. The work for this round is fixed to the pending queue as of that moment.
- Response (each spoke) — aggregates its yield-source adapter valuations and replies with the chain NAV and a per-source breakdown (AdapterValuation) in SETTLEMENT_RESPONSE (kind 4). Assets realized from redemption withdrawals are attached here too.
- Finalize (hub) — collect every chain's response → adjust out unsettled deposit principal → waterfall → compute the fixed price per tranche. It increments the round number first (0 is genesis; real settlements start at 1) and records the adapter breakdown, the tranche settlement and the per-request approvals to the pallet under that number.
- Distribute (hub → each spoke) — sends the approval list and the fixed price down in SETTLEMENT_FINALIZE (kind 5), chunked if the list is large. The receiving Manager flips those requests to claimable.
Hub state becomes Claimable the moment the approval is recorded (optimistically). If spoke delivery fails, the state is not rolled back — the message is re-sent until it converges, and receive handling is idempotent.
CCCP messages
Every message payload is one CccpPayload struct passed through abi.encode. It decodes with the same ABI regardless of kind, and unused fields are 0 or empty. The second field is always uint64 productId, so any message can be identified by kind and product from a (uint8, uint64) decode alone.
struct CccpPayload {
uint8 kind; // 1~10
uint64 productId; // uint32 prefix | uint32 seq
bytes32 requestId; // 0 outside the request family
uint256 settlementId; // 0 outside the settlement family
uint8 flag; // kind 8 usage · kind 9 grant/revoke
address account; // investor · permission subject
address vault;
uint256 amount;
uint256 aux;
address[] addrs;
uint256[] nums;
bytes data; // nested struct arrays (kind 4·5 only, 2-stage encoding)
}
| kind | Name | Direction | Attached assets | Role |
|---|---|---|---|---|
| 1 | DEPOSIT_REQUEST | spoke→hub | full request amount | Deposit request report |
| 2 | REDEEM_REQUEST | spoke→hub | — | Redeem request report (includes the locally started amount) |
| 3 | SETTLEMENT_COLLECT | hub→spoke | — | Settlement collection instruction (carries the last fixed price) |
| 4 | SETTLEMENT_RESPONSE | spoke→hub | realized withdrawals | Chain NAV + per-source breakdown reply |
| 5 | SETTLEMENT_FINALIZE | hub→spoke | payout allocation (first chunk) | Distribute the approval list and the fixed price |
| 6 | REBALANCE_BRIDGE | spoke→hub | withdrawn amount | Rebalance withdrawals collected at the hub |
| 7 | ADAPTER_SUPPLY | hub→spoke | deployment amount | Asset delivery → deployed on receipt |
| 8 | ADAPTER_WITHDRAW | hub→spoke | — | Withdrawal instruction — flag 0 = rebalance · 1 = redemption funding |
| 9 | WHITELIST_SYNC | hub→spoke | — | Propagate investor eligibility (nonce ordering guaranteed) |
| 10 | ALLOCATION_SYNC | hub→spoke | — | Propagate allocation weights (as soon as a weight changes) |
At both the send and the receive end, CccpSent / CccpReceived events record the full original message, so an indexer can trace every cross-chain leg from those two events alone. If the declared send amount differs from the amount actually received, that is the bridge fee gap, and the fee sponsor covers it.
Whitelist · rebalance · allocation propagation
Whitelist
The source of truth for investor eligibility is the hub's TranchePermissions (0x202). When an admin makes one grant_permission call, the pallet records it and calls the Orchestrator directly to send WHITELIST_SYNC (kind 9) to the target vault's chain. Once the receiving chain's WhitelistHook is updated, share transfers and the vault deposit gate open. Revoke takes the same path.
CCCP does not guarantee delivery order, so messages carry a propagation nonce. The receiver compares it against the latest nonce per (vault, target address) and discards stale messages — a revoke that arrives before its grant still ends in the right final state.
Rebalance
User ledgers and shares are untouched; only fund capital moves. Moving between chains goes ADAPTER_WITHDRAW (kind 8, flag=0) to the overweight chain → the withdrawn amount collects at the hub via REBALANCE_BRIDGE (kind 6) → redeployed to the underweight chain with ADAPTER_SUPPLY (kind 7).
Allocation weight propagation
Each spoke caches only its own weights (localShareBps and the local adapter split). To change them the operator calls pushAllocation (onlyOwner) on the hub, which propagates ALLOCATION_SYNC (kind 10), and a version counter (weightsVersion) detects staleness. Even if a spoke deployed at the old weights, the hub knows the gap between target and actual and corrects it at the next rebalance.
How to use it
Users
- Pick an entry chain — hub (testbed) or spoke (Sepolia); it is the same product either way.
- Get test assets (USDC) from the faucet, and get whitelisted.
- Call
requestDepositon the vault — from there it is the same 7540 surface as single-chain. - Once a settlement round comes around (a few minutes when entering from a spoke), call
depositto claim the shares. - Redemption is the same —
requestRedeem→ wait for settlement →withdraw.
cancelDepositRequest /
cancelRedeemRequest reverts. Atomically cancelling a request that has already spread across chains is needed first, so it is deferred to v2.Operations
- Create the product — register the Valuation address, the tranche layout (APR · priority · vault) and the chain and adapter weights with the pallet's
create_product. - Grant eligibility — one
grant_permissioncall both records the source of truth and propagates to every chain. - Settlement is triggered by the keeper on an interval. Watch progress through events and pallet queries.
- Weight changes and rebalancing are
set_multichain_adapters(replacing the pallet source of truth) followed by starting a rebalance.
Demo: Multichain products → — entry-chain selection, deposit and redeem, a cross-chain tracker, a faucet and an operations panel.
Functions
User surface
The same TrancheVault (ERC-7540/7575/4626) as single-chain — requestDeposit /
requestRedeem / deposit / redeem/ view functions are all identical. The only difference is that cancellation is unsupported.
MultichainTrancheManager
| Functions | Description |
|---|---|
requestDeposit / requestRedeem | Handles requests delegated from the vault — escrow, requestId issuance, starting local deployment and withdrawal, sending kinds 1 and 2. |
receiveMessage(...) | CCCP receive entry point. Branches by kind — applying a settlement, deploying, starting a withdrawal, applying a whitelist change, and so on. |
sweepDust(to, amount) | Recovers only residue outside the accounting (bridge rounding and the like). User assets (poolCash, the payout reserve) are protected by a cap. |
setWhitelistHook / setFeeSponsorHook / setPaused etc. | Operational wiring. Every state-changing function emits an event. |
ValuationDiamond
| Functions | Description |
|---|---|
tryUpdateNAV() | Settlement cycle entry point — called by the keeper. Advances the stages from starting collection through finalizing and distributing. |
onDepositReq / onRedeemReq / onNavResponse etc. | The message handlers the Orchestrator routes to. Write the ledger and execute the allocation and withdrawal plans. |
rebalanceCrossChain(...) | Start a cross-chain rebalance. |
lastPriceOf(tranche) / settlementId() | Read the fixed price and the round — the price source for the UI and indexers. |
Pallet precompiles (hub)
| Address | Main functions |
|---|---|
0x200 TrancheSystem | create_product · set_multichain_adapters · get_tranches · get_multichain_adapters — the source of truth for product configuration. |
0x201 Investments | record_investment_request / approval · record_adapter_valuations · record_settlement · get_last_settlement · get_pending_requests — the ledger. Only a product's own Valuation may write to it. |
0x202 TranchePermissions | grant_permission / revoke_permission — the source of truth for investor eligibility, plus the cross-chain propagation trigger. |
Every product_id parameter is uint64 (converted 2026-08-18).
Events
Every external state-changing function emits an event, and an indexer can reconstruct the whole system state from events alone. The request lifecycle joins on requestId (standardized as the first indexed parameter); the settlement lifecycle joins on settlementId.
Wire chokepoints
| Events | Description |
|---|---|
CccpSent(dstChainId, to, amount, message) | Every outbound leg. message = the raw payload. |
CccpReceived(srcChainId, sender, declared, received, message) | Every inbound leg. declared ≠ received means a fee gap. |
Requests · claims
| Events | Emitted by | Description |
|---|---|---|
DepositRequested / RedeemRequested | entry-chain Manager | Request accepted — the amount, including the locally deployed and started portions. |
DepositQueued / RedeemQueued | hub Valuation | Arrival at the hub and ledger write — the round it belongs to, whether it rolled over, and the allocation and funding chain list (adapterChainIds — guaranteed one entry per chain). |
InvestmentRequested / InvestmentApproved | pallet 0x201 | Ledger write and approval. |
DepositsApproved / RedeemsApproved | hub Valuation | One array per settlement — per-request entries of (requestId, amount in, amount out, fixed price). |
DepositReceived / RedeemReceived | claiming chain's Manager | User claim (productId uint64 indexed). It sums everything claimable at that moment, so join with Approved for per-request values. |
Deployment · withdrawal
| Events | Description |
|---|---|
Supplied / SupplyReceived | Deployed into a yield source (requestId=0 means a deployment not tied to a request). |
WithdrawRequested | Withdrawal started — the requested amount and the amount realized immediately. |
PayoutRealized | ASYNC withdrawal realized and received — waiting to be attached to the next settlement response. |
Settle
| Events | Description |
|---|---|
SettleStarted | Round started — hub NAV, the chains to collect from and the chains scheduled for distribution. |
NavReported / NavReceived | Chain NAV reply (sent by a spoke / received by the hub). |
Settled | Round finalized — product NAV and per-tranche NAV (after the settlement is applied). |
SettleApplied | Applied on the spoke — the number of deposits and redemptions processed. |
RedeemDeferred | A redemption rolled over to the next round because the funds were not realized. |
Eligibility · allocation · operations
| Events | Description |
|---|---|
WhitelistRequested / WhitelistApplied / WhitelistStale | Eligibility propagation sent, applied, and discarded when out of order. Carries the nonce. |
AllocationPushed / AllocationSynced | Allocation weight propagation and application. |
RebalanceStarted / RebalanceCollected | Rebalance started and collected. |
DustAccrued / Sponsored / DustSwept | Message fee funding accrued · fee gap covered · residue recovered. |
Full signatures and the per-action emission order are in docs/multichain/events.md in the repository (the source of truth handed to the indexer and node teams).
Single vs multichain
| single-chain | multichain | |
|---|---|---|
| User surface | ERC-7540 interface + 1404 share (institutional products use a 3643 share and SYNC deposits) | ERC-7540 async + 1404 share |
| Tranche · waterfall | Identical — Senior's fixed APR first, Junior residual and first-loss | |
| Ledger · price fixing | Valuation (price) + TrancheLedger (ledger, same ABI as the 0x201 pallet) | Hub pallet (source of truth) + ValuationDiamond (engine) |
| Settlement trigger | Operator settle (per request or batched) | Keeper-driven rounds on an interval (settlementId) |
| Asset deployment | Split across adapters on the same chain | Two-stage split — chain weight, then adapter weight within the chain; propagated as soon as the request lands |
| Redemption funding | SYNC products withdraw at request time; only ASYNC redemptions withdraw at settlement | Each chain starts at request time → moved in one batch at settlement |
| Whitelist | Set directly on the WhitelistHook (institutional products use the 3643 IdentityRegistry and Compliance) | Hub source of truth (0x202) → propagated to every chain (nonce ordering guaranteed) |
| Cancellation | Unsupported (CancelNotSupported) | Unsupported (CancelNotSupported) |
| productId | prefix 0x00000001 | prefix 0x00000002 |
Current deployments
Four products are running at once. Two are single-chain and two are multichain.
| Structure | Character | productId | |
|---|---|---|---|
| A | Multichain — hub (Bifrost testbed) + spoke (ETH Sepolia) | Plain USDC tranche, 60/40 allocation | 0x0000000200000009 |
| B | Single-chain — ETH Sepolia | JPYC collateral, two yield sources in parallel (USDC rewards + Morpho Blue) | 0x0000000100000004 |
| C | Single-chain — Bifrost testbed | Institutional lending, ERC-3643 whitelist | 0x0000000100000005 |
| D | Multichain — hub (Bifrost testbed) + spoke (Robinhood testnet) | Mirror basket of the top 8 Robinhood QQQ names | 0x000000020000000b |
A — multichain USDC
Hub testbed (49088) · spoke Sepolia (11155111), 60/40 allocation. This generation was cleanly redeployed with a SettleFacet that reflects settlementId numbering (0 is genesis; real settlements start at 1). The Orchestrator is reused as it is, since the CCCPClient interface did not change (deployed and activated 2026-08-28).
| Component | Chain | Address |
|---|---|---|
| Orchestrator | The hub | 0xb90933d2707EE5452A87888Ae1b5c353c391a261 (deployed 2026-08-27, still shared by A and D) |
| ValuationDiamond | The hub | 0xDeE470bA416AeA3E08829467169b03fdF7056F9d |
| Manager | The hub | 0x71B6Fd1f500D2350e2ebBa79b9dAC7C0993194cc |
| Vault SR / JR | The hub | 0x8917815653085A8A00C07FEB39e139C2B819a110 / 0xB5d830Abd8902Efc66DBeAD8cb29feCa00BE86ee |
| Manager | Spokes | 0xD38C81f57796c1C9269294118Ee55c3ACd91A314 |
| Vault SR / JR | Spokes | 0x5c1F2eAAA1c20f7babdA63dCFB944B9542FA5362 / 0xAa573F5fB545Aab1f3750655Ae8819AB7E6D9DE0 |
| Precompile | The hub | TrancheSystem 0x…0200 · Investments 0x…0201 · TranchePermissions 0x…0202 |
The full address book is in the multichain/deployments/ deployment records in the repository.
D — multichain basket
Hub testbed (49088) · spoke Robinhood testnet (46630), 60/40 allocation. The structure is the same hub-spoke pattern as A (the Orchestrator is reused); only the Robinhood spoke adapter is swapped for RobinhoodBasketAdapter so that at settlement only the net inflow/outflow difference is traded in one batch on Uniswap V3 (0.05% tier, full-range liquidity) into a mirror basket of the top 8 QQQ names (NVDA, AAPL, GOOGL, MSFT, MU, AMZN, AMD, TSLA) — deployed, pool seeded and registered with the pallet on 2026-08-31. The full address book is in the multichain/deployments/ deployment records in the repository.
minRequest) on deposit assets and redeem shares, and the CCCP maxTxFee is 1 USDC (applied 2026-09-03). Allocation weights split a request into several legs, and this guard keeps even the smallest leg above the bridge fee cap.B — single-chain JPYC
ETH Sepolia (11155111), base asset JPYC (18 decimals). Two yield-source adapters run in parallel at 50/50 — an in-house source that pays USDC rewards, and Morpho Blue. Deposit and redeem are both SYNC, so a request becomes claimable immediately.
C — single-chain institutional lending
Hub testbed (49088), base asset USDC. One institutional lending escrow adapter (APR 7%, Senior fixed 3%). Deposits are SYNC and redemptions ASYNC (FIFO, in the order loan repayments arrive). Investor eligibility is filtered by an ERC-3643 IdentityRegistry/Compliance pair rather than a WhitelistHook.
The full address book for the two single-chain products is in singlechain/deployments/.