Skip to content

Hub Contracts

The hub module is the pool-manager control plane. Pools, share classes, holdings, accounting, and prices are administered here; the results propagate to spoke chains as verified messages. All hub contracts are unmodified Centrifuge V3 (see the fork framing).

Contract Role
Hub Central control plane; entry point for all pool-manager actions
HubRegistry Canonical registry of pools, assets, managers, and dependencies
ShareClassManager Share-class lifecycle, issuance tracking, share prices
Accounting Per-pool double-entry journal
Holdings Per-pool asset bookkeeping and cross-chain snapshot tracking
HubHandler Inbound handler for hub-bound cross-chain messages

Hub

Hub coordinates everything: share-class operations delegate to ShareClassManager, holdings operations to Holdings with immediate accounting reconciliation, and every cross-chain notification is dispatched through the messaging layer. Its wired dependencies include the holdings, accounting, registry, share-class manager, outbound dispatcher, multi-adapter, gateway, and an optional feeHook.

Access control tiers

  1. Ward (auth) — system contracts only: file, createPool, and direct accounting-amount updates.
  2. Hub manager — per-pool administrators. Almost every pool method requires hubRegistry.manager(poolId, msgSender()). A hub manager has full rights over all actions of its pool. The manager set is maintained via updateHubManager. Note the use of msgSender() rather than msg.sender — the Hub supports batched multicalls, and msgSender() resolves the true caller through the batching context.
  3. Request manager — requestCallback is gated to the pool's registered hub-side request manager (see BatchRequestManager).

Accounting discipline

Every accounting mutation follows a strict cycle:

accounting.unlock(poolId) → addDebit(...) / addCredit(...) → accounting.lock()

lock() reverts unless total debits equal total credits, so no Hub operation can leave the books unbalanced. Holding amount changes map to Asset/Equity accounts (Expense/Liability for liabilities); valuation re-marks map to Asset versus Gain/Loss accounts.

Fee hook extension point

After price updates, the Hub calls feeHook.accrue(poolId, scId) if a fee hook is configured. This is the extension point that ArkonixFeeManager targets — it can also be used directly as the share-price-update wrapper. See Arkonix Extensions.

Function surface

  • System: file, createPool.
  • Pool notify/config: notifyPool, notifyShareClass, notifySharePrice, notifyAssetPrice, updateRestriction, updateVault, updateContract, setRequestManager, updateBalanceSheetManager, updateHubManager, setAdapters, share metadata, price-age limits. Every cross-chain notify emits locally and then dispatches the message with an explicit refund address.
  • Holdings config: initializeHolding/initializeLiability, updateHoldingValue/updateHoldingValuation/updateHoldingIsLiability, setHoldingAccountId.
  • Accounting: createAccount, setAccountMetadata, updateJournal.
  • Views: holdingAccounts, liabilityAccounts, pricePoolPerAsset.

HubRegistry

The canonical registry (ward = Hub). State: asset decimals, pool metadata, the pool's base currency (which doubles as the pool-exists flag), the hub-manager set per pool, named per-pool dependencies, and each pool's hub-side request manager per spoke chain.

Invariants: assets and pools register exactly once; a pool must exist before it can be updated.

ShareClassManager

Share-class lifecycle and issuance tracking (ward = Hub). State highlights:

  • Salts: globally unique deployment salts for share tokens; a salt must be prefixed with its pool ID.
  • Issuance: totalIssuance per share class, plus issuancePerNetwork per spoke chain — per-chain issuance/revocation reconciliation with a guard against negative issuance.
  • Prices: pricePoolPerShare per share class stores the price with its computation timestamp; future timestamps are rejected.
  • Metadata: names 1–128 bytes, symbols 1–32 bytes.
  • IDs: share-class IDs derive from the pool ID plus an incrementing per-pool counter, with preview views for the next ID.

Accounting — double-entry bookkeeping

Accounting maintains a per-pool journal under a lock/unlock discipline:

  • unlock(poolId) → addDebit(account, amount) / addCredit(...) / addJournal(debits[], credits[]) → lock().
  • lock() reverts Unbalanced unless total debits equal total credits — balance is enforced at the contract level, not by convention.
  • Accounts are debit-normal or credit-normal; an account's value is the absolute difference of total debits and credits, signed by its normal side.
  • Account types: Asset, Equity, Loss, Gain, Expense, Liability.
  • Transient storage tracks in-flight journal IDs, so interleaved entries within one transaction share a journal.

Why this matters for auditability

Every fund movement, issuance, and valuation change in a pool produces balanced journal entries in a conventional double-entry ledger, on-chain. An auditor or due-diligence reviewer can reconstruct a pool's books directly from contract state and events — assets, equity, gains, losses, and liabilities all tie out by construction, because unbalanced entries cannot be committed.

Holdings

Per-pool asset bookkeeping plus snapshot tracking. For each (pool, shareClass, asset), Holdings stores the valuation source, a liability flag, the asset amount, and its value; account-ID mappings link each holding to its accounting accounts.

Key mutators (all ward-gated): initialize, increase/decrease (amounts convert to values via PricingLib.convertWithPrice; decrease clamps at zero), update (re-mark via the holding's valuation, returning a signed delta for accounting), updateValuation, and setSnapshot (nonce-guarded).

Snapshots — knowing when a spoke is in sync

Per (pool, shareClass, spokeChain), Holdings records a snapshot flag with a nonce: whether that chain's reported assets and shares are fully in sync with the hub. The flag is set when a spoke's BalanceSheet queues fully drain, and it is what makes price-per-share derivation safe — NAV computed from holdings is only meaningful when no deltas are still in flight. A snapshot hook (onSync) fires when a chain drains its queues, which the NAV automation managers consume.

HubHandler

The inbound message handler for the hub side (counterpart to the Spoke's handlers). It processes:

  • registerAsset — records assets announced by spoke chains.
  • request — routes investment requests to the pool's hub-side request manager.
  • updateHoldingAmount — applies a spoke's queued holding delta: holding increase/decrease, accounting entries, and snapshot update.
  • updateShares — applies issuance/revocation deltas with snapshot update.
  • initiateTransferShares — coordinates burn-on-origin / mint-on-target cross-chain share transfers.