Overview

The multi-token layer covers two templates deployed through the same MultiTokenFactory:
  • CommodityBatchToken — an ERC-1155 contract where each token ID is a distinct commodity batch (warehouse receipt lot), fungible within the batch.
  • PoolVault — an ERC-20 multi-asset fund. It is not an ERC-1155 contract itself (it extends AnkaraChainBaseToken, the same ERC-20 base as the Phase 1 templates), but it’s grouped here because it’s deployed through MultiTokenFactory alongside CommodityBatchToken and depends on the IAnkaraOracle price-feed interface introduced for this layer.

AnkaraMultiToken

Abstract base inherited by ERC-1155 templates (currently just CommodityBatchToken). The key architectural difference from the ERC-20/ERC-721 bases: identity verification is per-token-ID, not per-contract — each registered ID can have its own IIdentityVerifier, or none (open transfers for that ID).

Token ID registry

Before minting, every ID must be registered with a definition:
Minting (mint, mintBatch, both MINTER_ROLE) reverts with TokenIdNotRegistered if the ID hasn’t been registered, and with MaxSupplyExceeded if maxSupply > 0 and the mint would exceed it. Manual per-ID supply tracking (_idTotalSupply) backs the totalSupply(id) view since ERC-1155 doesn’t track this natively.

Transfer hook

_update() checks the per-ID verifier (if any) for both sender and receiver, mirroring the ERC-20/ERC-721 bases’ pattern but looped over every ID in a batch transfer:

CommodityBatchToken

One contract per warehouse/operator; one ERC-1155 token ID per commodity batch (lot). Tokens are fungible within a batch, representing fractional warehouse-receipt shares.
  • registerBatch(id, meta) (MANAGER_ROLE) registers the batch’s token ID with maxSupply = quantityKg (1 token = 1 kg).
  • updateBatchValuation(), expireBatch() (MANAGER_ROLE) — administrative controls.
  • mergeBatches(fromId, toId, amount, holder) (MANAGER_ROLE) — consolidates two compatible batches (same commodity type + grade, neither expired) by burning from fromId and minting into toId. This bypasses the holder’s ERC-1155 approval since it’s a manager-authorised operation, not a holder-initiated transfer.
  • isExpired(id) / activeBatchCount() — natural (timestamp) expiry is separate from administrative expireBatch() (status-based); both are checked via _isExpiredById().

IAnkaraOracle / ManualOracle

Pluggable USD price-feed interface, used by PoolVault for NAV pricing:
All prices are USD with 18-decimal precision (1e18 = $1.00). ManualOracle is the reference implementation — MANAGER_ROLE sets prices directly via setPrice(token, priceUSD), and a price is considered stale if it was never set or if stalenessThreshold seconds (default recommendation: 24 hours) have elapsed since the last update. It’s a plain AccessControl contract, not upgradeable — deploy a new instance to change logic. Intended for testing, MVP deployments, and illiquid asset classes with no on-chain feed; a Chainlink or AI-oracle implementation of the same interface can be swapped in later without touching PoolVault.

PoolVault

An on-chain multi-asset fund. The vault itself is an ERC-20 token — pool tokens represent proportional ownership. Investors deposit accepted ERC-20 tokens and receive pool tokens priced at current NAV; withdrawals burn pool tokens and return a proportional basket of the underlying assets.

Deposit

Reverts if the token isn’t in the accepted list, the amount is below the per-token minimum, no oracle is configured, or the oracle’s price for that token is stale. Pool tokens minted are computed by _calculateMintAmount():
  • First depositor (empty pool): 1 pool token per $1 of deposit value (NAV bootstrap).
  • Subsequent depositors: valueUSD × totalSupply / totalAUM — proportional to current NAV.

Withdrawal

Computes the proportional basket of every accepted token before burning, then burns the caller’s pool tokens (checks-effects-interactions — burn happens before any transfer), then transfers each token’s proportional share.

Management fee

accrueManagementFee() is callable by anyone and mints newly-accrued fee tokens directly to feeRecipient, based on elapsed time since the last accrual:
Up to MAX_TOKENS_PER_VAULT = 50 ERC-20 tokens can be whitelisted per vault via addAcceptedToken() / removeAcceptedToken() (MANAGER_ROLE). Removing a token doesn’t affect existing vault holdings — they remain and are still returned proportionally on withdrawal.

MultiTokenFactory

Deploys both templates via ERC1967Proxy, following the same registration pattern as TokenFactory and NFTFactory:
deployCommodityBatchToken(...) and deployPoolVault(...) both call the shared internal _deploy(), which collects the optional deployment fee, deploys the proxy, and emits:
CommodityBatchToken deploys pass bytes32(0) for assetId since it has no single asset identifier (it’s inherently multi-token). Registry views: totalDeployedMultiTokens(), getDeployerMultiTokens(address).

SDK Integration

TokenFactory.deployCommodityBatchToken() and TokenFactory.deployPoolVault() wrap the factory calls. See the CLI’s ankara deploy-batch and ankara deploy-pool commands, plus register-batch, batch-mint, deposit, withdraw, pool-status, and oracle-set for post-deploy operations.

Running the Multi-Token Tests Locally