Cross-VM Tokens
Tenzro implements the Sei V2 pointer model for cross-VM token interoperability. Instead of bridging tokens between virtual machines (which introduces lock-and-mint risk and liquidity fragmentation), each VM exposes a pointer contract that reads and writes the same underlying native balance. A user holding 100 TNZO sees 100 TNZO whether they query from EVM, SVM, or DAML -- the balance is one, the views are three.
Architecture Overview
The cross-VM token architecture is built on the TnzoToken native layer, which stores balances in the unified state tree. Each VM has a thin pointer adapter that translates VM-specific token interfaces (ERC-20 on EVM, SPL Token on SVM, CIP-56 on DAML) into reads and writes against the same native balance. No tokens are locked, minted, or burned during cross-VM operations.
Why not bridge? Traditional cross-chain bridges lock tokens on one side and mint wrapped tokens on the other. This creates bridge risk (if the bridge is compromised, locked tokens are lost) and liquidity fragmentation (liquidity is split across multiple wrapped representations). The pointer model eliminates both problems because there is only ever one balance -- the native balance.
Tenzro-Specific Precompiles
The cross-VM token system is powered by five Tenzro-specific EVM precompiles. These are callable at reserved addresses and provide native access to token operations, cross-VM transfers, staking, and governance from within EVM smart contracts.
| Address | Precompile | Description |
|---|---|---|
0x1001 | TNZO Bridge | Wrap and unwrap native TNZO to/from VM representations |
0x1002 | Token Factory | Create new ERC-20 tokens and register them in the unified registry |
0x1003 | Cross-VM Bridge | Atomic token transfers between EVM, SVM, and DAML |
0x1004 | Staking | Stake and unstake TNZO from within EVM contracts |
0x1005 | Governance | Vote on proposals and query voting power from EVM |
wTNZO ERC-20 Pointer (EVM)
On EVM, TNZO is exposed as wTNZO, an ERC-20 pointer contract with 18 decimal places. The pointer contract does not hold a token supply of its own. Every balanceOf call reads the native TNZO balance, and every transfer call writes directly to the native state. ERC-20 approvals are stored in the unified token registry so that DeFi protocols (AMMs, lending, etc.) can interact with wTNZO using standard interfaces.
Function Selectors
| Function | Selector | Behavior |
|---|---|---|
balanceOf(address) | 0x70a08231 | Reads native TNZO balance (18 decimals) |
transfer(address,uint256) | 0xa9059cbb | Writes to native balance directly |
approve(address,uint256) | 0x095ea7b3 | Stores approval in unified token registry |
transferFrom(address,address,uint256) | 0x23b872dd | Checks approval, then writes to native balance |
totalSupply() | 0x18160ddd | Returns total native TNZO supply |
// Interact with wTNZO from Solidity
interface IwTNZO {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function totalSupply() external view returns (uint256);
}
// wTNZO pointer address (CREATE2-deployed by Token Factory precompile)
IwTNZO wtnzo = IwTNZO(0x7a4bcb13a6b2b384c284b5caa6e5ef3126527f93);
// Check balance -- reads from native TNZO layer
uint256 bal = wtnzo.balanceOf(msg.sender);
// Transfer -- writes to native TNZO layer
wtnzo.transfer(recipient, 100 ether); // 100 TNZO (18 decimals)ERC-7802 Cross-Chain Token Interface
wTNZO implements the ERC-7802 SuperchainERC20 interface, enabling standardized cross-chain minting and burning. When tokens arrive from an external chain via LayerZero, Chainlink CCIP, or deBridge, the bridge adapter calls crosschainMint. When tokens leave for another chain, crosschainBurn is called. Both operations update the native TNZO balance atomically.
Function Selectors
| Function | Selector | Behavior |
|---|---|---|
crosschainMint(address,uint256) | 0xf637f84d | Mints tokens from cross-chain transfer (15,000 gas) |
crosschainBurn(address,uint256) | 0xa609e02b | Burns tokens for cross-chain transfer (15,000 gas) |
// ERC-7802: Cross-Chain Token Interface (SuperchainERC20)
interface IERC7802 {
function crosschainMint(address to, uint256 amount) external;
function crosschainBurn(address from, uint256 amount) external;
event CrosschainMint(address indexed to, uint256 amount, address indexed sender);
event CrosschainBurn(address indexed from, uint256 amount, address indexed sender);
}ERC-7802 vs traditional bridging: Traditional bridges lock tokens on one chain and mint wrapped tokens on another. ERC-7802 standardizes the mint/burn interface so that any bridge adapter can interact with the same token contract. Combined with Tenzro's pointer model, this means cross-chain transfers update the single native balance -- no wrapped tokens, no fragmentation.
// Bridge adapter calls crosschainMint when tokens arrive
// from Ethereum via LayerZero
let calldata = [
selectors::CROSSCHAIN_MINT.to_vec(),
abi::encode_address(&recipient),
abi::encode_uint256(amount),
].concat();
let result = execute_tnzo_bridge(&token, ®istry, &calldata, 100_000)?;
assert!(result.success);wTNZO SPL Adapter (SVM)
On SVM, TNZO is exposed through the SPL Token Adapter, which maps SPL Token Program instructions to the native TnzoToken layer. Because SPL tokens use 9 decimal places (compared to the native 18), the adapter performs truncation on writes and scaling on reads. Associated Token Account (ATA) addresses are derived deterministically from the owner public key and the TNZO mint address.
Decimal Conversion (18 to 9)
When converting from the native 18-decimal representation to the SVM 9-decimal representation, the adapter divides by 10^9 and truncates. This means the smallest representable unit on SVM is 1 lamport = 10^9 wei (1 Gwei equivalent). Any sub-lamport dust is lost during conversion.
// Decimal conversion between EVM (18) and SVM (9)
// Native balance: 1,500,000,000,000,000,000 (1.5 TNZO in 18 decimals)
let native_balance: u128 = 1_500_000_000_000_000_000;
// Convert to SVM (9 decimals): divide by 10^9, truncate
let svm_balance: u64 = (native_balance / 1_000_000_000) as u64;
// Result: 1,500,000,000 (1.5 TNZO in 9 decimals)
// Convert back to native (18 decimals): multiply by 10^9
let restored_native: u128 = (svm_balance as u128) * 1_000_000_000;
// Result: 1,500,000,000,000,000,000 (no loss for this amount)
// Example with sub-lamport dust:
// Native: 1,500,000,000,123,456,789 (1.500000000123456789 TNZO)
// SVM: 1,500,000,000 (1.500000000 TNZO -- dust truncated)ATA Derivation
Associated Token Account addresses are derived using the standard Solana PDA derivation: PDA(ATA_PROGRAM, owner, TOKEN_PROGRAM, mint). The adapter automatically creates ATAs on first access and maps them back to the owner's native address for balance lookups.
use tenzro_vm::svm::SplTokenAdapter;
let adapter = SplTokenAdapter::new(tnzo_mint_address);
// Derive ATA for an owner
let ata = adapter.derive_associated_token_account(&owner_pubkey);
// Get balance in SPL format (9 decimals)
let spl_balance = adapter.get_balance(&owner_pubkey)?;
// Transfer in SPL units (9 decimals)
// Internally: amount * 10^9 -> native transfer -> truncate result
adapter.transfer(&from_pubkey, &to_pubkey, 1_500_000_000)?; // 1.5 TNZOCIP-56 DAML Token (Canton)
On Canton, TNZO is represented as a CIP-56 holding template. Canton's DAML execution model uses a two-step transfer flow: the sender creates a transfer proposal contract, and the recipient explicitly accepts or rejects it. This matches Canton's privacy model where both parties must consent to state transitions.
Two-Step Transfer Flow
// DAML template (conceptual)
template TnzoHolding
with
owner : Party
amount : Decimal -- DAML Decimal string formatting
custodian : Party
where
signatory owner, custodian
choice Transfer : ContractId TransferProposal
with
newOwner : Party
transferAmount : Decimal
controller owner
do
create TransferProposal with
sender = owner
receiver = newOwner
amount = transferAmount
template TransferProposal
with
sender : Party
receiver : Party
amount : Decimal
where
signatory sender
observer receiver
choice Accept : ContractId TnzoHolding
controller receiver
do
-- Updates native TNZO balance atomically
create TnzoHolding with owner = receiver, amount = amount, custodian
choice Reject : ()
controller receiver
do return ()Party-to-Address Mapping
Canton parties (e.g., Alice::1234abcd) are mapped to Tenzro native addresses for balance resolution. The adapter maintains a bidirectional mapping so that DAML contracts can reference native balances and native transfers can be reflected in DAML contract state.
DAML Decimal Formatting
DAML uses arbitrary-precision Decimal values represented as strings. The adapter converts the native u128 balance (18 decimals) to a DAML Decimal string by inserting a decimal point at the correct position. There is no precision loss in this direction since DAML Decimal can represent the full 18-decimal range.
// Native u128 to DAML Decimal string conversion
let native_balance: u128 = 1_500_000_000_000_000_000; // 1.5 TNZO
// Format as DAML Decimal string
let whole = native_balance / 1_000_000_000_000_000_000;
let frac = native_balance % 1_000_000_000_000_000_000;
let daml_decimal = format!("{}.{:018}", whole, frac);
// Result: "1.500000000000000000"Unified Token Registry
The Unified Token Registry provides a single catalog of all tokens across all VMs. It is backed by a DashMap for concurrent in-memory access and persisted to RocksDB via the CF_TOKENS column family. Every token -- whether created on EVM, SVM, or DAML -- receives a deterministic TokenId and is indexed by symbol, VM-specific address, and token ID.
TokenId Computation
Token IDs are computed deterministically as the SHA-256 hash of the creator address concatenated with a per-creator nonce. This ensures that the same creator deploying the same sequence of tokens will always produce the same token IDs, enabling reproducible deployments.
use tenzro_vm::token_registry::TokenId;
use sha2::{Sha256, Digest};
// Deterministic TokenId = SHA-256(creator || nonce)
let mut hasher = Sha256::new();
hasher.update(creator_address.as_bytes());
hasher.update(&nonce.to_le_bytes());
let token_id = TokenId(hasher.finalize().into());Cross-VM Address Mapping
Each token entry in the registry stores addresses for every VM where it has been deployed. When a token is created on EVM, the registry automatically generates corresponding SVM and DAML representations (pointer contracts) so the token is accessible from all VMs without manual bridging.
| Field | Type | Description |
|---|---|---|
token_id | TokenId (32 bytes) | SHA-256(creator + nonce) |
symbol | String | Token ticker (e.g., TNZO, USDC) |
evm_address | Option<Address> | ERC-20 pointer contract address |
svm_address | Option<Address> | SPL mint address |
daml_template_id | Option<DamlTemplateId> | CIP-56 holding template ID |
decimals | u8 | Native decimal precision (18 for TNZO) |
total_supply | u128 | Total supply in native units |
Creating Tokens
Tokens can be created through the Token Factory precompile (from EVM), the Rust SDK, the TypeScript SDK, the CLI, or the MCP server. All methods register the token in the unified registry and deploy pointer contracts on every VM.
Rust SDK
use tenzro_sdk::TenzroClient;
let client = TenzroClient::new("https://rpc.tenzro.network");
// Create a new token via RPC
let token = client.create_token(
"MyToken", // name
"MTK", // symbol
18, // decimals
1_000_000, // initial supply (in whole units)
&creator_address, // creator
).await?;
println!("Token ID: {}", token.token_id);
println!("EVM address: {:?}", token.evm_address);
println!("SVM address: {:?}", token.svm_address);
println!("DAML template: {:?}", token.daml_template_id);TypeScript SDK
import { TenzroClient } from "@tenzro/sdk";
const client = new TenzroClient("https://rpc.tenzro.network");
const token = await client.createToken({
name: "MyToken",
symbol: "MTK",
decimals: 18,
initialSupply: 1_000_000,
creator: creatorAddress,
});
console.log("Token ID:", token.tokenId);
console.log("EVM address:", token.evmAddress);
console.log("SVM address:", token.svmAddress);CLI
# Create a new token
tenzro-cli token create \
--name "MyToken" \
--symbol "MTK" \
--decimals 18 \
--supply 1000000
# Query token info by symbol
tenzro-cli token info --symbol MTK
# List all registered tokens
tenzro-cli token list
# List tokens filtered by VM type
tenzro-cli token list --vm evmMCP Server
The Tenzro MCP server exposes seven token tools. Any MCP client (Claude, agents, or custom applications) can create tokens, query the registry, and perform cross-VM transfers.
| Tool | Description |
|---|---|
create_token | Create ERC-20 token via factory, register in unified registry |
get_token_info | Lookup token by symbol, EVM address, or token ID |
list_tokens | List registered tokens with optional VM type filter |
cross_vm_transfer | Atomic cross-VM token transfer (pointer model) |
wrap_tnzo | Wrap native TNZO to VM representation (no-op in pointer model) |
deploy_contract | Deploy bytecode to EVM/SVM/DAML via MultiVmRuntime |
get_token_balance | Get TNZO balance across all VMs with decimal conversion |
Cross-VM Transfers
Cross-VM transfers move tokens between virtual machines atomically. Because of the pointer model, a cross-VM transfer is not a bridge operation -- it is a re-framing of the same native balance. The transfer updates the address mapping in the unified registry so that the recipient can access the balance from the target VM.
Rust SDK
use tenzro_sdk::TenzroClient;
use tenzro_types::VmType;
let client = TenzroClient::new("https://rpc.tenzro.network");
// Transfer TNZO from EVM to SVM
let receipt = client.cross_vm_transfer(
"TNZO", // token symbol
1_000_000_000_000_000_000, // 1 TNZO in 18 decimals
VmType::Evm, // source VM
VmType::Svm, // target VM
&sender_address, // sender (EVM address)
&recipient_address, // recipient (SVM address)
).await?;
println!("Transfer hash: {}", receipt.tx_hash);
println!("Source VM: {:?}", receipt.source_vm);
println!("Target VM: {:?}", receipt.target_vm);
// Verify balance on SVM (9 decimals)
let svm_balance = client.get_token_balance(
&recipient_address,
"TNZO",
Some(VmType::Svm),
).await?;
// Returns 1,000,000,000 (1 TNZO in 9 decimals)TypeScript SDK
import { TenzroClient, VmType } from "@tenzro/sdk";
const client = new TenzroClient("https://rpc.tenzro.network");
// Transfer from EVM to DAML
const receipt = await client.crossVmTransfer({
token: "TNZO",
amount: "1000000000000000000", // 1 TNZO (18 decimals, as string)
sourceVm: VmType.Evm,
targetVm: VmType.Daml,
sender: evmAddress,
recipient: cantonParty,
});
console.log("Transfer hash:", receipt.txHash);CLI
# Transfer TNZO from EVM to SVM
tenzro-cli token transfer \
--token TNZO \
--amount 1.0 \
--source-vm evm \
--target-vm svm \
--recipient <svm-address>
# Check balance across all VMs
tenzro-cli token balance --address <your-address>
# Wrap native TNZO to a specific VM (no-op in pointer model)
tenzro-cli token wrap --amount 10.0 --vm evmDecimal Conversion Reference
Each VM uses a different decimal precision for token amounts. The native TNZO balance always uses 18 decimals. Conversions happen at the adapter boundary when entering or leaving a VM context.
| VM | Decimals | 1 TNZO Representation | Smallest Unit | Precision Loss |
|---|---|---|---|---|
| Native | 18 | 1000000000000000000 | 1 wei | None (source of truth) |
| EVM | 18 | 1000000000000000000 | 1 wei | None (same precision) |
| SVM | 9 | 1000000000 | 1 lamport = 10^9 wei | 9 decimal digits truncated |
| DAML | Arbitrary | "1.000000000000000000" | 1 wei (string-encoded) | None (arbitrary precision) |
SVM truncation warning: When transferring from EVM or DAML to SVM, any sub-lamport amount (the lowest 9 decimal digits) is truncated and lost. For example, transferring 1.000000000999999999 TNZO to SVM results in 1.000000000 TNZO on SVM. The truncated 0.000000000999999999 TNZO remains in the native balance but is not visible from SVM. Transferring back to EVM will restore visibility of the full balance.
Wrapping TNZO
The wrap_tnzo operation is intentionally a no-op in the pointer model. Because all VM representations already point to the same native balance, there is nothing to wrap or unwrap. The operation exists for API compatibility with bridge-based architectures and returns immediately with the current balance.
// Wrapping is a no-op in the pointer model
let result = client.wrap_tnzo(
1_000_000_000_000_000_000, // 1 TNZO
VmType::Evm,
).await?;
// result.wrapped == true (always succeeds)
// result.amount == 1_000_000_000_000_000_000 (unchanged)
// No tokens were locked, minted, or burned