Tenzro Testnet is live. Get testnet TNZO

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.

AddressPrecompileDescription
0x1001TNZO BridgeWrap and unwrap native TNZO to/from VM representations
0x1002Token FactoryCreate new ERC-20 tokens and register them in the unified registry
0x1003Cross-VM BridgeAtomic token transfers between EVM, SVM, and DAML
0x1004StakingStake and unstake TNZO from within EVM contracts
0x1005GovernanceVote 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

FunctionSelectorBehavior
balanceOf(address)0x70a08231Reads native TNZO balance (18 decimals)
transfer(address,uint256)0xa9059cbbWrites to native balance directly
approve(address,uint256)0x095ea7b3Stores approval in unified token registry
transferFrom(address,address,uint256)0x23b872ddChecks approval, then writes to native balance
totalSupply()0x18160dddReturns 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

FunctionSelectorBehavior
crosschainMint(address,uint256)0xf637f84dMints tokens from cross-chain transfer (15,000 gas)
crosschainBurn(address,uint256)0xa609e02bBurns 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, &registry, &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 TNZO

CIP-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.

FieldTypeDescription
token_idTokenId (32 bytes)SHA-256(creator + nonce)
symbolStringToken ticker (e.g., TNZO, USDC)
evm_addressOption<Address>ERC-20 pointer contract address
svm_addressOption<Address>SPL mint address
daml_template_idOption<DamlTemplateId>CIP-56 holding template ID
decimalsu8Native decimal precision (18 for TNZO)
total_supplyu128Total 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 evm

MCP 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.

ToolDescription
create_tokenCreate ERC-20 token via factory, register in unified registry
get_token_infoLookup token by symbol, EVM address, or token ID
list_tokensList registered tokens with optional VM type filter
cross_vm_transferAtomic cross-VM token transfer (pointer model)
wrap_tnzoWrap native TNZO to VM representation (no-op in pointer model)
deploy_contractDeploy bytecode to EVM/SVM/DAML via MultiVmRuntime
get_token_balanceGet 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 evm

Decimal 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.

VMDecimals1 TNZO RepresentationSmallest UnitPrecision Loss
Native1810000000000000000001 weiNone (source of truth)
EVM1810000000000000000001 weiNone (same precision)
SVM910000000001 lamport = 10^9 wei9 decimal digits truncated
DAMLArbitrary"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