Overview
The multi-token layer covers two templates deployed through the sameMultiTokenFactory:
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 extendsAnkaraChainBaseToken, the same ERC-20 base as the Phase 1 templates), but it’s grouped here because it’s deployed throughMultiTokenFactoryalongsideCommodityBatchTokenand depends on theIAnkaraOracleprice-feed interface introduced for this layer.
AnkaraMultiToken
Abstract base inherited by ERC-1155 templates (currently justCommodityBatchToken). 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: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 withmaxSupply = 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 fromfromIdand minting intotoId. 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 administrativeexpireBatch()(status-based); both are checked via_isExpiredById().
IAnkaraOracle / ManualOracle
Pluggable USD price-feed interface, used byPoolVault for NAV pricing:
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
_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
NAV and AUM
Management fee
accrueManagementFee() is callable by anyone and mints newly-accrued fee tokens directly to feeRecipient, based on elapsed time since the last accrual:
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 viaERC1967Proxy, 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.