Tongo Documentation

Welcome to the official Tongo documentation. Tongo is a confidential payment system for ERC20 tokens on Starknet, providing privacy-preserving transactions while maintaining auditability and compliance features. Tongo is heavily based on this paper.

What is Tongo?

Tongo wraps any ERC20 token with ElGamal encryption, enabling private transfers while maintaining full auditability. Built on zero-knowledge proofs and homomorphic encryption over the Stark curve, Tongo enables users to transact with hidden amounts while preserving the ability to verify transaction validity.

Key Features:

  • No Trusted Setup: Built entirely on elliptic curve cryptography
  • Hidden Amounts: All transfer amounts are encrypted
  • Flexible Compliance: Global auditor support and selective disclosure

Getting Started

If you're a developer looking to integrate Tongo into your application, start with the SDK Quick Start.

If you want to understand the protocol and cryptography, begin with the Protocol Introduction.

What's new in v2

Tongo v2 is the current protocol. This page summarizes what changed from v1 (the legacy, single‑contract design) so you can port an integration quickly. The full legacy docs remain available through the version switcher in the top bar (v1 · legacy).

TL;DR — v2 splits custody from bookkeeping (Vault + Tongo Ledger), adds External Transfers between ledgers, and introduces a Relayer so users can operate with no StarkNet account (gasless via the AVNU paymaster). The cryptography and account model are unchanged.

At a glance

Areav1v2
Contract layoutOne Tongo contract holds funds and stateVault holds the ERC20 reserve + config; Tongo Ledger holds encrypted state and verifies proofs
Deploying a ledgerDeploy the Tongo contract directlyVault.deploy_tongo(owner, tag, auditorKey?)
Cross‑ledger transferNot possibleExternal Transfer between ledgers of the same Vault
Gas / accountsCaller needs a funded StarkNet accountRelayer + AVNU paymaster: sign with your Tongo key, no account needed
Operation feestransfer / withdraw / ragequit carry a fee_to_sender (in Tongos, converted via rate)
SDK surfaceAccount operations onlyAdds RelayerAccount and the Relaying flow

1. Vault + Tongo split

In v1 a single contract both custodied the wrapped ERC20 and tracked encrypted balances. In v2 those responsibilities are separated:

  • Vault — holds the ERC20 reserve for a single asset, stores the shared configuration (ERC20, rate, bit_size, tongo_class_hash), and deploys Tongo Ledger instances.
  • Tongo Ledger — keeps the encrypted balances, pending balances and audit data, and verifies the zero‑knowledge proof of each operation. It no longer custodies ERC20 directly; funding/withdrawing forward the token movement to the Vault.

From a user's point of view nothing changes: you still call fund / withdraw on your Tongo instance.

New Tongo ledgers are created through the Vault:

#![allow(unused)]
fn main() {
// v2 — deploy a ledger through its Vault
vault.deploy_tongo(owner, tag, auditorKey /* Option<PubKey> */);
// `tag` is unique per Vault (used as the deploy salt); tag_to_address(tag) resolves it back.
}

2. External Transfers

The Vault/ledger split enables a new confidential transfer between two Tongo Ledgers deployed by the same Vault — an External Transfer. Both ledgers must approve the interaction (bidirectional, owner‑gated), and the two ledgers run the identical, Vault‑deployed class, so the receiving ledger can trust an incoming transfer that the sending ledger has already proven. See Tongo instances for the approval flow.

3. Relayer — operate without a StarkNet account

Every Tongo operation is a StarkNet transaction, which normally means the caller needs a funded account to pay gas. The Relayer removes that requirement:

  • It executes the operation on the user's behalf via SNIP‑9 outside execution and pays gas through the AVNU paymaster, then reimburses itself in ERC20 from the operation's fee.
  • The user authorizes the operation by signing it with their Tongo key — not a StarkNet account key.
  • Replay is prevented by a SNIP‑9 nonce derived from the sender's current Tongo account nonce, so each operation has exactly one valid nonce.
  • The sender address is committed inside every operation's zero‑knowledge proof, so a relayed proof is only valid when executed through the exact Relayer the user chose while building it.

The relayable operations — transfer, withdraw, ragequit — carry a fee_to_sender paid to the Relayer. Rollover has no fee of its own and is bundled with a paying operation when it needs relaying. The Relayer only executes a bundle when the collected fees cover the gas it pays plus a margin, so it never sponsors at a loss.

See the SDK Relaying guide for the client‑side flow.

4. Building an operation — v1 vs v2

The account model is unchanged, but v2 operations take relayer/fee data. Toggle the versions below:

// v2 — relayed transfer: no StarkNet account, gas paid by the Relayer
import { Account, RelayerAccount } from "@fatsolutions/tongo-sdk";

const account = new Account(tongoPrivateKey, tongoAddress, provider);
const relayer = new RelayerAccount(tongoAddress, relayerAddress, paymasterUrl, provider);

const op = account.transfer(to, amount, { feeToSender }); // fee reimburses the relayer
const est = await relayer.estimateFee(op);
const prepared = relayer.buildTransactionToSign(op /* w/ est */, snip9Nonce);
const signature = account.signMessage(prepared);          // sign with the Tongo key
await relayer.execute(prepared, signature);
// v1 — the caller needs a funded StarkNet account to pay gas
import { Account } from "@fatsolutions/tongo-sdk";

const account = new Account(tongoPrivateKey, tongoAddress, provider);
const op = account.transfer(to, amount);
await starknetAccount.execute(op.toCalldata());           // StarkNet account pays gas

Migration checklist

  • Point at a Vault‑deployed ledger. Resolve/deploy your Tongo instance through the Vault (deploy_tongo / tag_to_address) instead of deploying the Tongo contract directly.
  • Add fees to relayable ops. Populate fee_to_sender on transfer / withdraw / ragequit when relaying.
  • Adopt the Relayer flow (RelayerAccount) if you want the no‑account UX; otherwise the direct StarkNet‑account path still works.
  • Regenerate ABIs. The contract set changed — see the updated Contract ABI (it has a v1/v2 toggle).

Everything under SHE Cryptography is identical between v1 and v2.

Instances

The canonical v2 Tongo ledgers deployed on Starknet mainnet. To start, point the SDK's Account at the ledger for the asset you want — that address is the only thing you need.

Vault, ERC20, decimals/rate, Sepolia, and the class hashes live on the full Tongo instances page.

Instantiate through the SDK

import { Account } from "@fatsolutions/tongo-sdk";
import { RpcProvider } from "starknet";

// Canonical v2 Tongo ledgers — Starknet mainnet
const TONGO_MAINNET = {
  USDC: "0x00b32618c475b2fb50b0facd2c49136be6e77281834dc86bdae652680faad4d3",
  STRK: "0x07e3601b8a5123d601df41bdaba953c0baf6072dca7b9b877901b10a674c5691",
  ETH:  "0x04bd49a293fd461996bbb6c3fb121372368e084381180a090f32ee5d9bd91ab7",
  USDT: "0x00f7caef0285a79f7771c6ba5212cd60566f5bf8422f82d0e92da66904a82c94",
  WBTC: "0x01c20dc3b0881cb7c2841fe28007273e3562db0f50b6fe3547576ac3711917e1",
} as const;

const provider = new RpcProvider({ nodeUrl: "https://your-starknet-rpc" });

// Open an account on, e.g., the USDC ledger
const account = new Account(tongoPrivateKey, TONGO_MAINNET.USDC, provider);

// read the encrypted state, then build operations
const state = await account.state();

Want gasless operations (no Starknet account)? Pair the same ledger with the mainnet Relayer and use RelayerAccount — see Relaying.

import { RelayerAccount } from "@fatsolutions/tongo-sdk";

const RELAYER_MAINNET = "0x038aa8efb4e76b524c4a49b92284187b229174de5f431f288bbd7c8e0e441c12";

const relayer = new RelayerAccount(
  TONGO_MAINNET.USDC,
  RELAYER_MAINNET,
  "https://paymaster-endpoint", // AVNU paymaster
  provider,
);

Mainnet ledgers

AssetTongo ledgerdecimalsrate
USDC (native)0x00b32618c475b2fb50b0facd2c49136be6e77281834dc86bdae652680faad4d361e3
STRK0x07e3601b8a5123d601df41bdaba953c0baf6072dca7b9b877901b10a674c5691181e16
ETH0x04bd49a293fd461996bbb6c3fb121372368e084381180a090f32ee5d9bd91ab7181e12
USDT0x00f7caef0285a79f7771c6ba5212cd60566f5bf8422f82d0e92da66904a82c9461e3
WBTC0x01c20dc3b0881cb7c2841fe28007273e3562db0f50b6fe3547576ac3711917e181e1

Need Sepolia, the Vault/ERC20 addresses, or want to deploy your own ledger through the Vault? See Tongo instances and Vault Architecture.

Introduction to Tongo

Tongo is a confidential payment system for ERC20 tokens on Starknet, providing privacy-preserving transactions while maintaining auditability and compliance features. Built on ElGamal encryption and zero-knowledge proofs, Tongo enables users to transact with hidden amounts while preserving the ability to verify transaction validity. Tongo is heavily based in this paper.

What Makes Tongo Different

No Trusted Setup

Unlike many ZK systems, Tongo requires no trusted ceremony. All cryptography is built on the discrete logarithm assumption over the Stark curve, with no hidden trapdoors or setup parameters.

Native Starknet Integration

Tongo leverages Starknet's native elliptic curve operations, making verification extremely efficient (~120K Cairo steps per transfer) compared to other privacy solutions that require expensive proof verification.

Flexible Compliance

The protocol supports multiple compliance models:

  • Global auditor: All transactions encrypted for regulatory oversight
  • Selective disclosure: Optional viewing keys per transaction
  • Ex-post proving: Retroactive transaction disclosure without revealing keys

How It Works

1. Key Generation

Each user generates a keypair \((x, y = g^x)\) where \(g\) is the Stark curve generator. The public key \(y\) serves as their account identifier.

2. Encrypted Balances

Balances are stored as ElGamal ciphertexts:

$$\text{Enc}[y](b, r) = (g^b y^r, g^r)$$

The encryption is additively homomorphic, allowing on-chain balance updates without decryption. Each Tongo account has two balances: the current balance and the pending balance.

The current balance stores the amount of Tongos the account can use to perform Transfers/Withdraw operations. Zero-Knowledge proofs are check againts this balance and only the owner of the Tongo account can modify this balance thought Fund/Rollover operations.

The pending balance stores the amount of Tongos that the account has received through Transfer operations. To use this balance the account has to transform the pending balance in current balance. This is done by a Rollover operation.

Core Operations

Tongo has four user operation needed to operate a Tongo account. All these operations requires some kind of Zero-Knowledge proof to be validated by te contract. The operations are:

Funding

Convert standard ERC20 tokens to encrypted balances: In this operation some amount of ERC20 are send to the Tongo contract. The contract the mint for the given Tongo account some amount of tongo based on the ERC20-Tongo rate defined in the same contract. At this stage the amount sent is public, so the contract creates a encryption with a fixed random and adds the newly minted Tongos to the user account. This operation can only be performed by the owner of the Tongo account.

Transfers

Performs confidential transfers between accounts: In this operation some amount of Tongos are sent to the given receiver. The sender creates a encryption of the amount for the receiver and a encryption of the same amount for themself. These encryption are added to the receiver and subtracted from the receiver respectively. The sender also provides a ZK proof that shows:

  • Ownership of the sender account.
  • Both encrpytion are valid encryptions for the same amount under the correct public keys.
  • The amount encrypted in positive.
  • The sender has enough balance to perform the operation.

Rollover

In this operation the pending balance is added to the current balance of a given Tongo account and then emptied.

This operation can only be performed by the owner of the Tongo account.

Withdrawals

Convert back to standard ERC20 tokens: In this operation some amount of Tongo are converted back to ERC20 and sent to the given starknet account. The whitdrawn amount is public, so the contract creates a encryption of the amount for the user public key and subtract it from the user balance. The user has to provide a ZK proof that shows:

  • Ownership of the Tongo account.
  • The account has enough balance to perform the operation.

Security Measures

No double usage of proofs

In Tongo, all operation are validated by a ZK proof. If a ZK proof from a given operation is valid, the operation is performed. For this reason, to preven reusage of previous valid proofs (or some parts of a valid proof) some data is included and signed in each ZK proof. This includes:

  • The chain_id: A valid proof in sepolia is not valid in mainet.
  • The tongo contract address: A valid proof constructer for a particular instance of Tongo is not valid for another one.
  • A user account nonce: ZK proof are construted for a given Tongo account nounce. With each performed operation the Tongo account nonce is increased. So a valid proof will no be valid in the future.

Whitelisting of Tx sender

When constructing the ZK proof the tongo account owner choses the straknet account that will execute the tx. This addres is incorporated and signed in the ZK proof. The contract checks this againts the caller address. This guarantees that the ZK proof will be valid only if it is executed by the starknet account chosed by the tongo account owner.

Balance Integrity

Each time a balance is going to be modified by adding/subtracting an encryption, the encrpytion has to pass a ZK proof that shows:

  • The encryption is a valid ElGamal encryption
  • The amount encrypted is positive
  • The encryption is made for the correct public key

Use Cases

Individual Privacy

  • Personal transactions: Hide transfer amounts from public view
  • Salary payments: Confidential payroll systems

Compliance

  • Optional Compliance for Institutions: Deployer can chose weather to deploy with or without auditor keys
  • Treasury Management: Confidential transfers with auditability for stakeholders

Potential Integrations

  • Private AMM trading: Hidden trade sizes
  • Neo-Bank Confidential Payments: By design tongo can support payment procesors required speeds
  • DAO governance: Confidential voting systems

Getting Started

To start building with Tongo, proceed to the SDK Documentation for installation and usage guides.

To understand the cryptographic foundations, continue to the Encryption System chapter.

Encryption System

Tongo uses ElGamal encryption over elliptic curves to maintain confidential balances while enabling homomorphic operations on-chain.

ElGamal Encryption

Each user's balance is encrypted using a public key derived from their private key. ElGamal encryption of an amount \(b\) under a public key \(y\) is a pair point of the group of elliptic curves points \(G\). The encryption function is defined as:

$$\begin{aligned} \text{Enc}[y]&: [0, b_{\max}) \times \mathbb{F}_p \rightarrow G^2 \\ \text{Enc}[y]&\left(b,r\right) = (L, R) = (g^b y^r, g^r) \end{aligned}$$

Where:

  • \(y = g^x\) is the user's public key (derived from private key \(x\))
  • \(g\) is the generator of the Stark curve
  • \(b\) is the balance amount in the range \([0, b_{\max})\)
  • \(r\) is a random blinding factor
  • \(p\) is the curve order

Additive Homomorphism

The key property of this encryption is additive homomorphism. Given two encryptions under the same public key, their product is a valid encryption of the sum:

$$\text{Enc}[y]\left(b,r\right) \cdot \text{Enc}[y]\left(b',r'\right) = (g^{b+b'} y^{r+r'}, g^{r+r'}) = \text{Enc}[y]\left(b+b', r+r'\right)$$

This allows the contract to:

  • Add encrypted amounts without decryption
  • Subtract encrypted amounts homomorphically
  • Update balances while maintaining privacy

Balance Decryption

To read their balance, a user recovers \(g^b\) using their private key \(x\):

$$\frac{L}{R^x} = \frac{g^b y^r}{(g^r)^x} = \frac{g^b (g^x)^r}{g^{rx}} = g^b$$

Since \(b\) is bounded by \([0, b_{\max})\), the discrete logarithm \(b\) can be brute-forced. The time requiered to decript a balance depends on \(b_{\max}\). Tongo is parametrized in this variable that we call bit_size the current implementation of Tongo uses bit_size = 32. A naïve JavaScript implementation can decrypt ~100k units per second, while optimized algorithms handle the full 32-bit range much faster. The common algorithms used for this kind of decryption are:

  1. Brute force: Iterate \(g^i\) for \(i = 0, 1, 2, \ldots\) until matching \(g^b\)
  2. Baby-step Giant-step: More efficient \(O(\sqrt{n})\) algorithm
  3. Pollard's rho: Probabilistic algorithm with similar complexity

Storage Architecture

For each Tongo account, the Tongo contract maintains multiple encrypted representations of each balance. Here we have a description of them.

Current Balance

ElGamal encryption of the balance that the owner can freely use. Only the owner can modify this balance through different operations (Fund/Withdraw/Transfer/Rollover). Zero-knowledge proofs are checked against this balance. We just call this balance the user's balance.

Pending Balance

ElGamal encryption of incoming transfers. This serves as a buffer to hold all transfers an account has received. We just call this balance the user's pending.

Upon owner's request, this balance is added to the user's balance. The separation between current and pending balances is needed because zero-knowledge proofs are checked against the current balance. If an incoming transaction were able to modify it, a malicious actor could render all ZK proofs of a user invalid by spamming transfers to the account.

Autenticated Encrypted Balance (Hint)

XChaCha12 encryption of the amount encrypted in the user's balance. This encryption uses a key derived from the user's private key, allowing instant balance recovery without discrete log computation. This is only intended to be used as a hint for fast decryption of th user's balance. This ae_balance is not cryptographically enforced by the protocol, since there is no way to prove that the user provided the correct hint. It is purely a convenience feature. We just call this encryption hint.

Audit Balance

If the Tongo instance has an auditor, the audit_balance is an ElGamal encryption that the owner creates in each operation, encrypting the state of the user's balance under the auditor’s key. In each case, the owner provides a Zero-Knowledge proof showing that the amount encrypted for the auditor is the same as that encrypted in the current balance.

Audit Hint

This is an XChaCha12 encryption of the audit_balance using a key symmetrically derived from the owner's and auditor's key. This is a hint for the auditor, equivalent to the ae_balance for the owner. It is not cryptographically enforced by the protocol.

Vault Architecture (v2)

In v2 the protocol is split into two kinds of contracts. Vaults and Tongo Ledgers. This new architecture allow us to have a new kind of tranfser called External Transfer. This operation can make a confidential tranfer from a Tongo Ledger onwer by some owner/auditor to another one.

From the user's point of view nothing changes: they still call fund / withdraw on their Tongo instance. Internally the Tongo instance forwards the ERC20 movement to its Vault, which is the contract that actually holds the reserve.

Vault

It holds the ERC20 reserves for a single asset, stores the global configuration and deploys Tongo ledger instances. Here are stored the parameters shared by every Tongo instance it deploys.

The Vault stores the parameters shared by every Tongo instance it deploys. These are exposed through get_vault_config, which returns a VaultConfig:

  • ERC20 — the token this Vault wraps.
  • rate — the fixed ERC20-to-Tongo conversion rate (ERC20_amount = Tongo_amount * rate).
  • bit_size — the maximum bit size supported by the range proofs.
  • tongo_class_hash — the class hash of the Tongo contract this Vault deploys.

Tongo

It keeps the encrypted balances, pending balances and audit data described in Storage Architecture and verifies the zero-knowledge proofs of each operation. A Tongo instance no longer custodies any ERC20 itself: on funding and withdrawing it calls the Vault to move the underlying tokens. Tongo contracts have a set owner (and an optional auditor) that have full control of the contract.

Tongo instances are deployed through the Vault rather than deployed directly. The function is called deploy_tongo and requires

Deploying a Tongo instance

New Tongo ledgers are created through the Vault rather than deployed directly:

  • owner — the StarkNet address that owns the resulting Tongo instance.
  • tag — a unique identifier for the instance. It is used as the salt of the deploy syscall, so it must be unique per Vault; tag_to_address(tag) resolves a tag back to its deployed address.
  • auditorKey — an optional auditor public key. If provided, the instance runs with the auditing features described in Auditing & Compliance; if omitted, it runs without an auditor and this cannot be changed later.

The Vault records every instance it deploys and emits a TongoDeployed event carrying the tag, address, ERC20, rate, bit_size and auditor key.

Relayer Architecture (v2)

Every Tongo operation is a StarkNet transaction, so it normally requires the caller to hold a funded account to pay for gas. The Relayer contract leverages the AVNU paymaster to let users operate their Tongo Ledgers without a StarkNet account. It executes the transaction (using SNIP-9 outside execution) on the user's behalf and pays the gas fee, then gets reimbursed in ERC20 from the operation's fee.

The user authorizes the operation by signing it with their Tongo key, not a StarkNet account key. No StarkNet account is needed on the user side.

Fees

The relayable operations — transfer, withdraw and ragequit — carry a fee_to_sender amount that is paid to the Relayer. It is denominated in Tongos and converted to ERC20 through the rate. Rollover has no fee of its own; when it needs relaying it is bundled with one of the paying operations.

The Relayer only executes a bundle when the fees it collects cover the gas it pays plus a configurable margin, so it never sponsors an operation at a loss.

Security

From a Tongo Account's perspective the Relayer is the caller. As described in the Introduction, the sender address is committed inside the zero-knowledge proof of every operation, so a relayed proof is only valid when it is executed through the exact Relayer the user chose while building it. On top of this cryptographic binding, the Relayer keeps whitelists of the Tongo Ledgers, ERC20 assets and forwarders it is willing to serve.

Replay is prevented by the SNIP-9 nonce, which is derived from the sender's current Tongo account nonce. Since that nonce increases with every operation, each relayed operation has exactly one valid nonce and cannot be replayed.

The Relayer has an owner that manages whitelists, the accepted entrypoints and the fee margins, and can withdraw the collected fees.

Auditing & Compliance

Tongo provides flexible auditing mechanisms that enable compliance without sacrificing user privacy. Through viewing keys and ex-post proving, regulators can verify transaction details while preserving confidentiality for all other parties.

Global Auditor

The Tongo contract can designate a global auditor with public key \(y_a\), the owner of the Tongo instance can rotate the auditor key anytime. If a Tongo instance was deployed without an auditor, it cannot be added after.

Auditor Encryptions

  1. Each time the balance of an account is modified, the owner of the account must provide a encryption of the new balance for the auditor public key. A Zero-Knowledge proof that shows the encryption is correct and is indeed encrypting the new balance must be provided.

  2. Each time a Transfer operation is made, the sender has to also provide an encryption of the transfered amount for the auditor public key. A Zero-Knowledge proof must also be provided.

Theese two kind of encryptions allow the auditor to reconstruct all transactional values while keeping those values confidential to third parties.

Multi-Signature Auditing

For enhanced security, auditor keys can be distributed across multiple parties:

$$y_a = g^{a_1 + a_2} = g^{a_1} \cdot g^{a_2} = y_{a_1} \cdot y_{a_2}$$

Individual auditors can compute partial decryptions:

  • Auditor 1: \(R^{a_1} = (g^r)^{a_1}\)
  • Auditor 2: \(R^{a_2} = (g^r)^{a_2}\)

The balance is recovered by combining: \(g^b = L_a / (R^{a_1} \cdot R^{a_2})\)

This prevents any single auditor from unilaterally accessing transaction data.

Ex-Post Proving & Viewing Keys

After a transfer is completed, participants may need to prove a specific transaction detail to a third party without revealing their private keys. Ex-post proving enables this through cryptographic proofs. These proofs can be created for diferent viewings keys if the user desires so.

Protocol

Consider a completed transfer with ciphertext \((TL, TR) = (g^{b_0} y^{r_0}, g^{r_0})\). To prove the transfer amount to a third party with public key \(\bar{y}\). The sender must creates a new encryption of the transfer amount for \(\bar{y}\):

$$(\bar{L}, R) = \text{Enc}[\bar{y}](b, r)$$

The sender must provide a comprehensive proof \(\pi_{\text{ExPost}}\) demonstrating:

1. Ownership Proof

Prove knowledge of private key \(x\) such that \(y_s = g^x\). This proof can only be constructed with knowledge of the private key \(x\).

2. Same Encryption Proof

Prove that the given encryption is a correct ElGamal encryption under \(\bar{y}\). It also shows that this encryption and \((TL, TR)\) are encrypting the same amount.

Off-Chain Verification

Ex-post proofs require no on-chain interaction:

  • Transaction data is retrieved from chain state
  • Proofs are generated and verified off-chain
  • Only requires the original transaction hash as reference

Regulatory Compliance

AML/KYC Integration

Tongo supports various compliance frameworks:

Real-Time Monitoring

  • Global auditor receives all transaction encryptions
  • Automated threshold detection (encrypted amounts)
  • Pattern analysis on transaction graphs

Selective Disclosure

  • Users can voluntarily encrypt for compliance officers
  • Jurisdiction-specific reporting requirements
  • Time-limited viewing key access

Retroactive Investigation

  • Ex-post proving enables transaction reconstruction
  • User cooperation required for private key revelation
  • Court-ordered disclosure mechanisms

Advanced Features

Threshold Auditing

Multiple auditors with threshold decryption:

$$y_a = \sum_{i=1}^n w_i \cdot y_{a_i}$$

Where \(w_i\) are threshold weights and \(t\) out of \(n\) auditors are required for decryption.

Zero-Knowledge Compliance

Prove compliance properties without revealing amounts:

  • Range compliance: Prove transfer amount below threshold
  • Velocity limits: Prove cumulative amounts within bounds
  • Whitelist compliance: Prove recipient authorization

These advanced features demonstrate Tongo's flexibility in balancing privacy and regulatory requirements across diverse jurisdictions and use cases.

Tongo Instances

Here we list the Tongo instances deployed on mainnet and sepolia. Use the selector to switch between the current v2 deployments and the legacy v1 ones.

In v2 each asset has a Vault that custodies the ERC20 and a canonical Tongo ledger deployed by that Vault. See Vault Architecture for the details, and note that anyone can deploy further Tongo ledgers through the Vault with deploy_tongo.

The class hashes of the v2 contracts are

Mainnet

Sepolia

Relayers

The relayer contracts used for Relaying are

The class hash of the v1 version of Tongo is

Mainnet

Sepolia

Deployment

In v1 you deploy a Tongo instance directly with your own set of parameters. The constructor of the contract is

#![allow(unused)]
fn main() {
    #[constructor]
    fn constructor(
        ref self: ContractState,
        owner: ContractAddress,
        ERC20: ContractAddress,
        rate: u256,
        bit_size: u32,
        auditor_key: Option<PubKey>,
    ) {
        self.owner.write(owner);
        self.ERC20.write(ERC20);
        self.rate.write(rate);

        assert!(bit_size <= 128_u32, "Bit size should be 128 at max");
        self.bit_size.write(bit_size);

        if let Some(key) = auditor_key {
            self._set_auditor_key(key);
        }
    }
}

Contract ABI

Use the selector to switch between the current v2 ABI and the legacy v1 one.

In v2 the protocol is split between a Vault (custody + factory) and the Tongo ledgers it deploys. See Vault Architecture for the design. The Tongo contract implements ITongo and the Vault contract implements IVault.

Tongo ABI

#![allow(unused)]
fn main() {
#[starknet::interface]
pub trait ITongo<TContractState> {
    /// Returns the complete Setup of this Tongo instance
    fn get_tongo_config(self: @TContractState) -> TongoConfig;

    /// Returns the address of the Vault that deployed this Tongo instance
    fn get_vault(self: @TContractState) -> ContractAddress;

    /// Returns the Tag this contract is registered with.
    fn get_tag(self: @TContractState) -> felt252;

    /// Returns the contract address of the ERC20 that is wrapped
    fn ERC20(self: @TContractState) -> ContractAddress;

    /// Returns the rate of conversion between the wrapped ERC20 and tongo:
    ///
    /// ERC20_amount = Tongo_amount*rate
    ///
    /// The amount variable in all operation refers to the amount of Tongos.
    fn get_rate(self: @TContractState) -> u256;

    /// Returns the bit_size set for this Tongo contract.
    fn get_bit_size(self: @TContractState) -> u32;

    /// Returns the contract address of the owner of the Tongo account.
    fn get_owner(self: @TContractState) -> ContractAddress;

    // User operations:
    /// Funds a tongo account. Callable only by the account owner
    ///
    /// Emits FundEvent
    fn fund(ref self: TContractState, fund: Fund);

    /// Funds a tongo acount. Can be called without knowledge of the pk.
    ///
    /// Emits OutsideFundEvent
    fn outside_fund(ref self: TContractState, outsideFund: OutsideFund);

    /// Withdraw Tongos and send the ERC20 to a starknet address.
    ///
    /// Emits WithdrawEvent
    fn withdraw(
        ref self: TContractState, withdraw: Withdraw, withdraw_options: Option<WithdrawOptions>,
    );

    /// Withdraw all the balance of an account and send the ERC20 to a starknet address. This proof
    /// avoids the limitations of the range prove that are present in the regular withdraw.
    ///
    /// Emits RagequitEvent
    fn ragequit(
        ref self: TContractState, ragequit: Ragequit, ragequit_options: Option<RagequitOptions>,
    );

    /// Transfer Tongos from the balance of the sender to the pending of the receiver
    ///
    /// Emits TransferEvent
    fn transfer(
        ref self: TContractState, transfer: Transfer, transfer_options: Option<TransferOptions>,
    );

    /// Moves to the balance the amount stored in the pending. Callable only by the account owner.
    ///
    /// Emits RolloverEvent
    fn rollover(ref self: TContractState, rollover: Rollover);

    // State reading functions
    /// Returns the current stored balance of a Tongo account
    fn get_balance(self: @TContractState, y: PubKey) -> CipherBalance;

    /// Returns the current pending balance of a Tongo account
    fn get_pending(self: @TContractState, y: PubKey) -> CipherBalance;

    /// Return, if the Tongo instance allows, the current declared balance of a Tongo account for
    /// the auditor
    fn get_audit(self: @TContractState, y: PubKey) -> Option<CipherBalance>;

    /// Returns the current nonce of a Tongo account
    fn get_nonce(self: @TContractState, y: PubKey) -> u64;

    /// Returns the current state of a Tongo account.
    fn get_state(self: @TContractState, y: PubKey) -> State;

    // Auditor handling
    /// Returns the current auditor public key.
    fn auditor_key(self: @TContractState) -> Option<PubKey>;

    /// Rotates the current auditor public key.
    fn change_auditor_key(ref self: TContractState, new_auditor_key: PubKey);

    // External Transfers
    /// Receive an encrypted transfer from another Tongo contract deployed by the same Vault.
    /// The interaction between these contract has to be approved by the owners.
    ///
    /// Emits ReceivedExternalTransfer
    fn receive_external_transfer(ref self: TContractState, external: ExternalTransfer);

    /// Approve a Tongo instance deployed by the same Vault to interact with
    /// this contract with the External Transfer mechanism.
    fn approveTongo(ref self: TContractState, address: ContractAddress);

    /// Revoke a previously approved Tongo instance to interact with
    /// this contract with the External Transfer mechanism.
    fn revokeTongo(ref self: TContractState, address: ContractAddress);
}
}

Vault ABI

The Vault custodies the ERC20 reserve and deploys Tongo ledgers:

#![allow(unused)]
fn main() {
#[starknet::interface]
pub trait IVault<TContractState> {
    /// Returns the global setup of the Vault.
    fn get_vault_config(self: @TContractState) -> VaultConfig;

    /// Returns the class hash of the Tongo this contract will work with.
    fn get_tongo_class_hash(self: @TContractState) -> ClassHash;

    /// Returns the contract address of the ERC20 that Tongo will wrap.
    fn ERC20(self: @TContractState) -> ContractAddress;

    /// Returns the rate of conversion between the wrapped ERC20 and Tongo.
    fn get_rate(self: @TContractState) -> u256;

    /// Returns the bit size Tongo will work with.
    fn get_bit_size(self: @TContractState) -> u32;

    /// Returns true if the address is a Tongo contract deployed by this Vault.
    fn is_known_tongo(self: @TContractState, address: ContractAddress) -> bool;

    /// Returns the address of a given tag if a Tongo contract was deployed with that tag.
    fn tag_to_address(self: @TContractState, tag: felt252) -> Option<ContractAddress>;

    /// Deploys a Tongo instance for the given owner and tag with the given auditor.
    ///
    /// Emits TongoDeployed event.
    fn deploy_tongo(
        ref self: TContractState, owner: ContractAddress, tag: felt252, auditorKey: Option<PubKey>,
    ) -> ContractAddress;

    /// Pulls ERC20 from the caller. The caller can only be a Tongo instance deployed by this Vault.
    fn deposit(ref self: TContractState, amount: u256);

    /// Sends ERC20 to the caller. The caller can only be a Tongo instance deployed by this Vault.
    fn withdraw(ref self: TContractState, amount: u256);
}
}

Storage Structure

Unlike v1, a Tongo instance no longer custodies ERC20. It records the vault that deployed it, its tag, and the set of approvedTongo instances allowed to send it external transfers.

#![allow(unused)]
fn main() {
#[storage]
struct Storage {
    /// The contract address that is owner of the Tongo instance.
    owner: ContractAddress,
    /// The Vault contract this Tongo instance interacts with for ERC20 custody.
    vault: ContractAddress,
    /// The tag this contract is registered with.
    tag: felt252,
    /// The contract address of the ERC20 that Tongo is wrapping.
    ERC20: ContractAddress,
    /// The conversion rate between the wrapped ERC20 and tongo:
    ///
    /// ERC20_amount = Tongo_amount*rate
    rate: u256,
    /// The bit size this contract will work with. This limits the values that can be proven
    /// by a range proof.
    bit_size: u32,
    /// The encrypted balance for the given pubkey.
    balance: Map<PubKey, CipherBalance>,
    /// The encrypted pending balance for the given pubkey. The pending balance is the sum of
    /// incoming transfers. The user executes a rollover to convert this to usable balance.
    pending: Map<PubKey, CipherBalance>,
    /// The nonce of the given pubkey. Nonce is increased in every user operation.
    nonce: Map<PubKey, u64>,
    /// Hint to fast decrypt the balance of the given pubkey.
    ae_balance: Map<PubKey, AEBalance>,
    /// The balance of the given pubkey encrypted for the auditor key.
    audit_balance: Map<PubKey, CipherBalance>,
    /// Hint to fast decrypt the audited balance of the given pubkey.
    ae_audit_balance: Map<PubKey, AEBalance>,
    /// The auditor pubkey. If the contract was deployed without auditor this is None.
    auditor_key: Option<PubKey>,
    /// The increasing number that identifies the public key.
    key_number: u128,
    /// Whitelist of Tongo instances (deployed by the same Vault) allowed to interact with this
    /// contract through the external_transfer mechanism. Managed by the owner.
    approvedTongo: Map<ContractAddress, bool>,
}
}

Events

v2 keeps the v1 events and adds OutsideFundEvent, ReceivedExternalTransfer, TongoApproved / TongoRevoked, and the Vault's TongoDeployed. Note that FundEvent now also carries from, and TransferEvent carries the receiver's Tongo instance toTongo.

#![allow(unused)]
fn main() {
/// Event emitted in a Fund operation.
#[derive(Drop, starknet::Event)]
pub struct FundEvent {
    #[key]
    pub to: PubKey,
    #[key]
    pub nonce: u64,
    #[key]
    pub from: ContractAddress,
    pub amount: u128,
}

/// Event emitted in an OutsideFund operation.
#[derive(Drop, starknet::Event)]
pub struct OutsideFundEvent {
    #[key]
    pub to: PubKey,
    #[key]
    pub from: ContractAddress,
    pub amount: u128,
}

/// Event emitted in a Rollover operation.
#[derive(Drop, starknet::Event)]
pub struct RolloverEvent {
    #[key]
    pub to: PubKey,
    #[key]
    pub nonce: u64,
    pub rollovered: CipherBalance,
}

/// Event emitted in a Withdraw operation.
#[derive(Drop, starknet::Event)]
pub struct WithdrawEvent {
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub amount: u128,
    pub to: ContractAddress,
}

/// Event emitted in a Transfer operation.
#[derive(Drop, starknet::Event)]
pub struct TransferEvent {
    #[key]
    pub to: PubKey,
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub toTongo: ContractAddress,
    pub transferBalance: CipherBalance,
    pub transferBalanceSelf: CipherBalance,
    pub hintTransfer: AEBalance,
    pub hintLeftover: AEBalance,
}

/// Event emitted when an External Transfer is received.
#[derive(Drop, starknet::Event)]
pub struct ReceivedExternalTransfer {
    #[key]
    pub to: PubKey,
    #[key]
    pub from: PubKey,
    #[key]
    pub fromTongo: ContractAddress,
    pub nonce: u64,
    pub transferBalance: CipherBalance,
    pub hintTransfer: AEBalance,
}

/// Event emitted in a Ragequit operation.
#[derive(Drop, starknet::Event)]
pub struct RagequitEvent {
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub amount: u128,
    pub to: ContractAddress,
}

/// Event emitted when users declare their balances to the auditor.
#[derive(Drop, starknet::Event)]
pub struct BalanceDeclared {
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub auditorPubKey: PubKey,
    pub declaredCipherBalance: CipherBalance,
    pub hint: AEBalance,
}

/// Event emitted when users declare a transfer to the auditor.
#[derive(Drop, starknet::Event)]
pub struct TransferDeclared {
    #[key]
    pub from: PubKey,
    #[key]
    pub to: PubKey,
    #[key]
    pub nonce: u64,
    pub auditorPubKey: PubKey,
    pub declaredCipherBalance: CipherBalance,
    pub hint: AEBalance,
}

/// Event emitted when the owner sets a public key for the auditor.
#[derive(Drop, starknet::Event)]
pub struct AuditorPubKeySet {
    #[key]
    pub keyNumber: u128,
    pub AuditorPubKey: PubKey,
}

/// Event emitted when an owner approves a Tongo instance for external transfers.
#[derive(Drop, starknet::Event)]
pub struct TongoApproved {
    #[key]
    pub address: ContractAddress,
}

/// Event emitted when an owner revokes a previously approved Tongo instance.
#[derive(Drop, starknet::Event)]
pub struct TongoRevoked {
    #[key]
    pub address: ContractAddress,
}

/// Event emitted by the Vault when a Tongo contract is deployed.
#[derive(Drop, starknet::Event)]
pub struct TongoDeployed {
    #[key]
    pub tag: felt252,
    pub address: ContractAddress,
    pub ERC20: ContractAddress,
    pub rate: u256,
    pub bit_size: u32,
    pub auditor_key: Option<PubKey>,
}
}

Operations

The operations verify the same ZK proofs as v1. The difference is custody: instead of holding the ERC20 itself, a Tongo instance forwards token movements to its Vault via deposit / withdraw. The relayable operations (transfer, withdraw, ragequit) also pay an optional fee_to_sender to the caller.

#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Funds a tongo account. Callable only by the account owner
fn fund(ref self: ContractState, fund: Fund) {
    verify_fund(/* public inputs */, proof);

    // pull the ERC20 from the owner and forward it to the Vault reserve
    self._transfer_from_caller(amount);
    self._send_to_vault(amount);

    let cipher = CipherBalanceTrait::new(to, amount, 'fund');
    self._add_balance(to, cipher);
    self.emit(FundEvent);

    if self.auditor_key.is_some() {
        self._handle_audit(auditPart);
    }
}
}
#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Transfer Tongos from the balance of the sender to the pending of the receiver
fn transfer(ref self: ContractState, transfer: Transfer, options: Option<TransferOptions>) {
    // if relayed, pull the fee from the Vault and pay it to the caller (relayer)
    if let Some(relay) = relayData {
        self._withdraw_from_vault(relay.fee_to_sender);
        self._transfer_to(get_caller_address(), relay.fee_to_sender);
        self._subtract_balance(from, CipherBalanceTrait::new(from, relay.fee_to_sender, 'fee'));
    }

    verify_transfer(/* public inputs */, proof);

    self._subtract_balance(from, transferBalanceSelf);
    self._add_pending(to, transferBalance);
    self.emit(TransferEvent);

    if self.auditor_key.is_some() {
        self._handle_audit(auditPart);
    }
}
}
#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Withdraw Tongos and send the ERC20 to a starknet address.
fn withdraw(ref self: ContractState, withdraw: Withdraw, options: Option<WithdrawOptions>) {
    // relay fee handling, as in transfer
    if let Some(relay) = relayData { /* pay fee_to_sender to caller */ }

    verify_withdraw(/* public inputs */, proof);

    self._subtract_balance(from, CipherBalanceTrait::new(from, amount, 'withdraw'));

    // pull the ERC20 from the Vault reserve and send it to the recipient
    self._withdraw_from_vault(amount);
    self._transfer_to(to, amount);

    self.emit(WithdrawEvent);

    if self.auditor_key.is_some() {
        self._handle_audit(auditPart);
    }
}
}
#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Moves the pending balance into the usable balance. Callable only by the account owner.
fn rollover(ref self: ContractState, rollover: Rollover) {
    verify_rollover(/* public inputs */, proof);
    self._pending_to_balance(to);
    self.emit(RolloverEvent);
}
}

Two more operations are new in v2:

  • outside_fund — funds a Tongo account without knowledge of its private key (no ZK proof from the account owner). Emits OutsideFundEvent.
  • receive_external_transfer — receives a confidential transfer coming from another Tongo instance deployed by the same Vault. Both owners must have approved the interaction (approveTongo / revokeTongo). Emits ReceivedExternalTransfer.

The main Tongo contract implements the ITongo interface and manages all confidential payment operations:

#![allow(unused)]
fn main() {
#[starknet::interface]
pub trait ITongo<TContractState> {
    // Tongo general setup:
    /// Returns the contract address that Tongo is wraping.
    fn ERC20(self: @TContractState) -> ContractAddress;

    /// Returns the rate of conversion between the wrapped ERC20 a tongo:
    ///
    /// ERC20_amount = Tongo_amount*rate
    ///
    /// The amount variable in all operation refers to the amount of Tongos.
    fn get_rate(self: @TContractState) -> u256;

    /// Returns the bit_size set for this Tongo contract.
    fn get_bit_size(self: @TContractState) -> u32;

    /// Returns the contract address of the owner of the Tongo account.
    fn get_owner(self: @TContractState) -> ContractAddress;

    // User operations:
    /// Funds a tongo account. Callable only by the account owner
    ///
    /// Emits FundEvent
    fn fund(ref self: TContractState, fund: Fund);

    /// Withdraw Tongos and send the ERC20 to a starknet address.
    ///
    /// Emits WithdrawEvent
    fn withdraw(ref self: TContractState, withdraw: Withdraw);

    /// Withdraw all the balance of an account and send the ERC20 to a starknet address. This proof
    /// avoids the limitations of the range prove that are present in the regular withdraw.
    ///
    /// Emits RagequitEvent
    fn ragequit(ref self: TContractState, ragequit: Ragequit);

    /// Transfer Tongos from the balanca of te sender to the pending of the receiver
    ///
    /// Emits TransferEvent
    fn transfer(ref self: TContractState, transfer: Transfer);

    /// Moves to the balance the amount stored in the pending. Callable only by the account owner.
    ///
    /// Emits RolloverEvent
    fn rollover(ref self: TContractState, rollover: Rollover);

    // State reading functions
    /// Returns the curretn stored balance of a Tongo account
    fn get_balance(self: @TContractState, y: PubKey) -> CipherBalance;

    /// Returns the current pending balance of a Tongo account
    fn get_pending(self: @TContractState, y: PubKey) -> CipherBalance;

    /// Return, if the Tongo instance allows, the current declared balance of a Tongo account for
    /// the auditor
    fn get_audit(self: @TContractState, y: PubKey) -> Option<CipherBalance>;

    /// Returns the current nonce of a Tongo account
    fn get_nonce(self: @TContractState, y: PubKey) -> u64;

    /// Returns the current state of a Tongo account.
    fn get_state(self: @TContractState, y: PubKey) -> State;

    // Auditor handling
    /// Returns the current auditor public key.
    fn auditor_key(self: @TContractState) -> Option<PubKey>;

    /// Rotates the current auditor public key.
    fn change_auditor_key(ref self: TContractState, new_auditor_key: PubKey);
}
}

Storage Structure

#![allow(unused)]
fn main() {
#[storage]
struct Storage {
    /// The contract address that is owner of the Tongo instance.
    owner: ContractAddress,
    /// The contract address of the ERC20 that Tongo is wrapping.
    ERC20: ContractAddress,
    /// The conversion  rage between the wrapped ERC20 a tongo:
    ///
    /// ERC20_amount = Tongo_amount*rate
    rate: u256,
    /// The bit size this contract will work with. This limites the values that cant be proven
    /// by a range proof. If is set to 32 that means that range proof will only work for values
    /// between 0 and 2**32-1.
    /// Note: The computational cost of verifying a tranfers operation (the most expensive one)
    /// is about (30 + 10*n) ec_muls and (20 + 8n) ec_adds, where n is the bit_size
    bit_size: u32,
    /// The encrypted balance for the given pubkey.
    balance: Map<PubKey, CipherBalance>,
    /// The encrypted pending balance for the given pubkey. The pending balance is the sum of
    /// incoming transfer. User has to execute a rollover operation to convert this to usable
    /// balance.
    pending: Map<PubKey, CipherBalance>,
    /// The nonce of the given pubkey. Nonce is increased in every user operation.
    nonce: Map<PubKey, u64>,
    /// Hint to fast decrypt the balance of the given pubkey. This encrypts the same amount that
    /// is stored in `balance`. It is neither check nor enforced by the protocol, only the the
    /// user can decrypt it with knowledge of the private key and it is only usefull for
    /// attempting a fast decryption of `balance.
    ae_balance: Map<PubKey, AEBalance>,
    /// The balance of the given pubkey enrypted for the auditor key.
    ///
    /// If the contract was deployed witouth an auditor, the map is empty and all keys return
    /// the Default CipherBalance {L: {x:0, y:0}, R:{x:0,y:0}};
    audit_balance: Map<PubKey, CipherBalance>,
    /// Hint to fast decrypt the audited balance of the given pubkey. This encrypts the same
    /// amount that is stored in `audit_balance`. It is neither check nor enforced by the
    /// protocol, only the auditor can decrypt it with knowledge of the auditor private key and
    /// it is only usefull for attempting a fast decryption of `audit_balance`.
    ae_audit_balance: Map<PubKey, AEBalance>,
    /// The auditor pubkey. If the contract was deployed without auditor this will be an
    /// Option::None without a way to change it.
    auditor_key: Option<PubKey>,
    /// The increasing number that identifies the public key
    key_number: u128,
}
}

Events

The contract emits events for all operations to enable off-chain monitoring:

#![allow(unused)]
fn main() {
/// Event emited in a Fund operation.
///
/// - to: The Tongo account to fund.
/// - nonce: The nonce of the Tongo account.
/// - amount: The ammount of tongo to fund.
#[derive(Drop, starknet::Event)]
pub struct FundEvent {
    #[key]
    pub to: PubKey,
    #[key]
    pub nonce: u64,
    pub amount: u128,
}

/// Event emited in a Rollover operation.
///
/// - to: The Tongo account to rollover.
/// - nonce: The nonce of the Tongo account.
/// - rolloverred: The cipherbalance of the rolloverred amount.
#[derive(Drop, starknet::Event)]
pub struct RolloverEvent {
    #[key]
    pub to: PubKey,
    #[key]
    pub nonce: u64,
    pub rollovered: CipherBalance,
}


/// Event emited in a Withdraw operation.
///
/// - from: The Tongo account to withdraw from.
/// - nonce: The nonce of the Tongo account.
/// - amount: The ammount of tongo to withdraw.
/// - to: The starknet contract address to send the funds to.
#[derive(Drop, starknet::Event)]
pub struct WithdrawEvent {
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub amount: u128,
    pub to: ContractAddress,
}


/// Event emited in a Transfer operation.
///
/// - to: The Tongo account to send tongos to.
/// - from: The Tongo account to take tongos from.
/// - nonce: The nonce of the Tongo account (from).
/// - transferBalance: The amount to transfer encrypted for the pubkey of `to`.
/// - transferBalanceSelf: The amount to transfer encrypted for the pubkey of `from`.
/// - hintTransfer: AE encryption of the amount to transfer to `to`.
/// - hintLeftover: AE encryption of the leftover balance of `from`.
#[derive(Drop, starknet::Event)]
pub struct TransferEvent {
    #[key]
    pub to: PubKey,
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub transferBalance: CipherBalance,
    pub transferBalanceSelf: CipherBalance,
    pub hintTransfer: AEBalance,
    pub hintLeftover: AEBalance,
}


/// Event emited in a Ragequit operation.
///
/// - from: The Tongo account to withdraw from.
/// - nonce: The nonce of the Tongo account.
/// - amount: The ammount of tongo to ragequit (the total amount of tongos in the account).
/// - to: The starknet contract address to send the funds to.
#[derive(Drop, starknet::Event)]
pub struct RagequitEvent {
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub amount: u128,
    pub to: ContractAddress,
}

/// Event emited when users declare their balances to the auditor.
///
/// - from: The Tongo account that is declaring its balance.
/// - nonce: The nonce of the Tongo accout.
/// - auditorPubKey: The current public key of the auditor.
/// - declaredCipherBalance: The balance of the user encrypted for the auditor pubkey.
/// - hint: AE encryption of the balance for the auditor fast decryption.
#[derive(Drop, starknet::Event)]
pub struct BalanceDeclared {
    #[key]
    pub from: PubKey,
    #[key]
    pub nonce: u64,
    pub auditorPubKey: PubKey,
    pub declaredCipherBalance: CipherBalance,
    pub hint: AEBalance,
}


/// Event emited when users declare a transfer to the auditor.
///
/// - from: The Tongo account that is executing the transfer.
/// - to: The Tongo account that is receiving the transfer.
/// - nonce: The nonce of the Tongo accout (from).
/// - auditorPubKey: The current public key of the auditor.
/// - declaredCipherBalance: The transfer amount encrypted for the auditor pubkey.
/// - hint: AE encryption of the balance for the auditor fast decryption.
#[derive(Drop, starknet::Event)]
pub struct TransferDeclared {
    #[key]
    pub from: PubKey,
    #[key]
    pub to: PubKey,
    #[key]
    pub nonce: u64,
    pub auditorPubKey: PubKey,
    pub declaredCipherBalance: CipherBalance,
    pub hint: AEBalance,
}

/// Event emited when the owner sets a public key for the auditor.
///
/// - keyNumber: An increasing number that identifies the public key
/// - AuditorPubKey: The newly set auditor public key.
#[derive(Drop, starknet::Event)]
pub struct AuditorPubKeySet {
    #[key]
    pub keyNumber: u128,
    pub AuditorPubKey: PubKey,
}
}

Operations

1. Fund Operation

Converts ERC20 tokens to encrypted balances:

#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Funds a tongo account. Callable only by the account owner
///
/// Emits FundEvent
fn fund(ref self: ContractState, fund: Fund) {
    verify_fund(/* public inputs */, proof);

    self._transfer_from_caller(amount);

    let cipher = CipherBalanceTrait::new(to, amount, 'fund');
    self._add_balance(to, cipher);
    self.emit(FundEvent);

    if self.auditor.is_some() {
        self._handle_audit(auditPart);
    }
}
}

2. Transfer Operation

Performs confidential transfers between accounts:

#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Transfer Tongos from the balance of the sender to the pending of the receiver
///
/// Emits TransferEvent
fn transfer(ref self: ContractState, transfer: Transfer) {
    verify_transfer(/* public inputs */, proof);

    self._subtract_balance(from, transferBalanceSelf);
    self._add_pending(to, transferBalance);
    self.emit( TransferEvent );

    if self.auditor.is_some() {
        self._handle_audit(auditPart);
    }
}
}

3. Rollover Operation

Merges pending transfers into main balance:

#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Moves to the balance the amount stored in the pending. Callable only by the account
/// owner.
///
/// Emits RolloverEvent
fn rollover(ref self: TContractState, rollover: Rollover) {
    verify_rollover(/* public inputs */, proof);

    self._pending_to_balance(to);

    self.emit( RolloverEvent );
}
}

4. Withdraw Operation

Convert back to standard ERC20 tokens:

#![allow(unused)]
fn main() {
This code is a simplification of the actual code

/// Withdraw Tongos and send the ERC20 to a starknet address.
///
/// Emits WithdrawEvent
fn withdraw(ref self: ContractState, withdraw: Withdraw) {
    verify_withdraw(/* public inputs */, proof);

    let cipher = CipherBalanceTrait::new(from, amount, 'withdraw');
    self._subtract_balance(from, cipher);
    self._transfer_to(to, amount);

    self.emit( WithdrawEvent );

    if self.auditor.is_some() {
        self._handle_audit(auditPart);
    }
}

}

Tongo TypeScript SDK

The Tongo TypeScript SDK provides a comprehensive interface for building confidential payment applications on Starknet. It handles key management, encryption, proof generation, and transaction serialization.

Features

  • Simple API: High-level methods for all Tongo operations
  • Type Safety: Full TypeScript support with complete type definitions
  • Proof Generation: Automatic ZK proof creation for all operations
  • Encryption Handling: Transparent management of encrypted balances
  • Starknet Integration: Seamless integration with Starknet wallets and providers
  • Relaying: Transact without a funded StarkNet account through the RelayerAccount

Package Information

Supported Networks

The SDK works on:

  • Starknet Mainnet - Production deployments
  • Starknet Sepolia - Testnet for development

Check the deployed Tongo Instances for information about Tongo contracts wrapping different tokens.

  • Quick Start - Install the SDK and send your first Tongo transaction
  • Account Class - The main interface for a Tongo account
  • Operations - Fund, transfer, rollover, withdraw, ragequit
  • Relaying - Transact without a StarkNet account

Quick Start

ho# Installation

Using npm

To use the SDK you need to install it together with a starknet.js version superior to v9.

npm install @fatsolutions/tongo-sdk
npm install starknet@9.x.x

Basic Concepts

Starknet Account Class

This is the starknet account that will pay the transanction costs of the tx you send to starknet. To set it up the first step is to set a provider and then initializathe an Account class from the starknet library

import { Account, RpcProvider } from "starknet";

// Setup Starknet provider 
const provider = new RpcProvider({
    nodeUrl: "YOUR_RPC_PROVIDER",
    specVersion: "0.10.0",
});

// Your Starknet account (for paying gas fees)
const signer = new Account({
    provider,
    address: "YOUR_STARKNET_ADDRESS",
    signer: "YOUR_STARKNET_PRIVATE_KEY"
});

At this step, the signer can execute a well constructed call with signer.execute(call). You can read more about how to interact with a starknet contract in the starknet.js documentation

Tongo Account Class

This class represents a user's Tongo Account. The main feature is that it can construct the payloads for different Tongo operations (Fund/Transfer/Rollover/Withdraw/Ragequit). To create an instance to a Tongo account class you need the private key of the account, the Tongo address to interact with and a rpc provider.

import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";

const tongoAddress = "TONGO_CONTRACT_ADDRESS";

const privateKey = "USER_TONGO_PRIVATE_KEY";

const tongoAccount = new TongoAccount(
    privateKey,
    tongoAddress,
    provider
);

console.log("Your Tongo public key is:", tongoAcount.publicKey);

You can read more about the Tongo Account Class here

Basic Interactions: Operations

Whit a Tongo Account and a Starkenet Account set up, you can start to create and execute Tongo Operations (Fund/Transfer/Rollover/Withdraw/Ragequit). You can read more about Operations and the way of creating them here. To execute an operation need to create the call with the Tongo Account class and the execute it with the Starknet Account class.

const operation = tongoAccount.someOperation({...params});

const call = operation.toCalldata();

signer.execute(call)

Account Class

The Account class is the main interface for interacting with Tongo. An instance represents a user's account for a specific Tongo contract. Some of its functionalities are:

  • Decrypting the balance of the user.
  • Creating the Operations with the ZK proofs needed.
  • Decrypting and showing the transaction history for the user.

Creating an Account

The provider can be an RpcProvider or directly the RPC url.

import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";
import { RpcProvider } from "starknet";

const provider = new RpcProvider({
    nodeUrl: "YOUR_RPC_URL",
    specVersion: "0.10.0",
});

const tongoAddress = "TONGO_CONTRACT_ADDRESS";
const privateKey = "USER_TONGO_PRIVATE_KEY";

const tongoAccount = new TongoAccount(privateKey, tongoAddress, provider);
import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";
import { RpcProvider } from "starknet";

const provider = new RpcProvider({
    nodeUrl: "YOUR_RPC_URL",
    specVersion: "0.8.1",
});

const tongoAddress = "TONGO_CONTRACT_ADDRESS";
const privateKey = "USER_TONGO_PRIVATE_KEY";

const tongoAccount = new TongoAccount(privateKey, tongoAddress, provider);

Public Key

Each instance of a Tongo Account is identified by its public key. At low level the public key is the elliptic curve point $$ pk = g^{sk} $$ where \(pk\) is the public key, \(sk\) is the secret key and \(g\) is the stark curve generator. This form is used at low level to create the Zero-Knowledge proofs. To read it use the publicKey property:

console.log(account.publicKey);
// { x: bigint, y: bigint }

For a cleaner representation we offer a base58-encoded one. We call it the Tongo address of the account:

const address = account.tongoAddress();
console.log(address);
// "Um6QEVHZaXkii8hWzayJf6PBWrJCTuJomAst75Zmy12"

Note We offer the utility functions pubKeyBase58ToAffine() and pubKeyAffineToBase58() in types.ts to convert between the two representations of the public key.

Account State

The high level state of a Tongo account is its balance, pending and nonce. Use the state() method:

const state = await account.state();
console.log(state);
/*
{
    balance: bigint,   // Decrypted balance
    pending: bigint,   // Decrypted pending
    nonce: bigint      // Account nonce
}
*/

state() queries the Tongo contract for the raw state and decrypts the balances using the encrypted hints stored in the contract. The raw (encrypted) state that lives on-chain can be read with rawState():

const rawState = await account.rawState();
console.log(rawState);
/*
{
    balanceCipher: CipherBalance,        // Encrypted balance
    pendingCipher: CipherBalance,        // Encrypted pending balance
    auditCipher: CipherBalance | undefined, // Encrypted balance for auditor
    aeBalance?: AEBalance,               // Hint to decrypt `balanceCipher`
    aeAuditBalance?: AEBalance,          // Hint for the auditor to decrypt `auditCipher`
    nonce: bigint
}
*/

Account Operations

Accounts create operations, the only way to transact within a Tongo contract. You can read more about them here. To transact without a StarkNet account of your own, see Relaying.

Transaction History

Each operation made in Tongo emits an event with the relevant (generally encrypted) information. getTxHistory() fetches those events, parses and decrypts them when necessary, and returns a block-ordered array of all Tongo transactions involving the account.

// getTxHistory(fromBlock, toBlock?, numEvents?)
const tx_history = await account.getTxHistory(0);
console.log(tx_history);
/*
[
  {
    type: 'withdraw',
    tx_hash: '0x3ee8a6a351b05b4684e3e329399f6df02c446ce986c1e0be925ca71b757c6e0',
    block_number: 6,
    nonce: 2n,
    amount: 1n,
    to: '0x075662cc8b986d55d709d58f698bbb47090e2474918343b010192f487e30c23f'
  },
  {
    type: 'transferOut',
    tx_hash: '0x3dc4e84d5212c125bb92e43c0c097d4630ec7899d60bdca408f7bcdb563b0c1',
    block_number: 4,
    nonce: 1n,
    amount: 23n,
    to: 'tpBg43FFq7SQhmimTMxubT7cJ4dDpjsp5r2TtYYToKV9'
  },
  {
    type: 'fund',
    tx_hash: '0x4e134a86b86db0fe494e030d9b3baa664f5ca51750051cda759d06e27931e1',
    block_number: 3,
    nonce: 0n,
    amount: 100n
  }
]
*/

toBlock (default "latest") and numEvents (default "all") let you page the history. If you only want one kind of event, per-type getters are available: getEventsFund, getEventsRollover, getEventsWithdraw, getEventsRagequit, getEventsTransferIn, getEventsTransferOut, and getEventsReceivedExternalTransfer.

Other methods

The account exposes a few more helpers used across operations and relaying:

  • nonceHash() — the SNIP-9 nonce for the account's current state, used when relaying.
  • signMessage(typedData, senderAddress) — signs a relayed operation with the Tongo key.
  • erc20ToTongo(amount) / tongoToErc20(amount) — convert between ERC20 and Tongo units using the contract rate.
  • decryptCipherBalance(cipher) / decryptAEBalance(cipher, nonce) — low-level balance decryption.
  • createAuditPart(...), generateExPost(...) / verifyExPost(...) — auditor and ex-post disclosure helpers.

Operations

Operations are objects that represent Tongo transactions. Each operation encapsulates the cryptographic proofs, encrypted data, and calldata needed for a specific action.

Creation and Execution

Operations are created by an instance of a Tongo Account Class and executed by a starknet signer. The general lifecycle of an Operation is

// 1. Create the operation
const operation = await tongoAccount.someOperation({...params});

// 2. Convert to calldata
const calldata = operation.toCalldata();

// 3. Execute with Starknet signer
const tx = await signer.execute([calldata]);

// 4. Wait for confirmation
await provider.waitForTransaction(tx.transaction_hash);

All operations have a toCalldata() method that serialized all the needed data and contructs the starknet call. This call is to be executed by the transaction sender or signer.

Each operation has its own set of params that are needed to create it. Some generalities are shared between the parameters of Operations and requires further explanations

The sender parameter

This parameter is present in all operations. It is the starknet account address that will execute the transaction (the signer address in the example). This address is binded in the Zero-Knowledge proofs. ZK proofs will only be valid if the sender of the transaction is matches this parameter.

The amount parameter

This parameter, when present, represents a amount of Tongos to transact with. This is NOT the amount of ERC20 you pretend to transact with. When you wrap some amount of ERC20 you recive some other amount of wraped ERC20 (we call them Tongos). The rate of conversion is fixed and defined when that particuar instance of Tongo was deployed.

Note that 1 unit of Tongo is the minimum amount that a operation can handle. The Tongo Account class has the rate() method to query the rate of the Tongo contract and the tongoToERC20() get te equivalent amount of ERC20 to some amount of Tongos.

Operation Types

Tongo supports five core operations, we describe them in the following sections:

  1. Fund - Convert ERC20 tokens to encrypted balance
  2. Transfer - Send encrypted amounts to another account
  3. Rollover - Claim pending incoming transfers
  4. Withdraw - Convert encrypted balance back to ERC20
  5. Ragequit - Emergency withdrawal of entire balance

Multi-Operations (v2)

A MultiOperation bundles several operations into a single StarkNet transaction that settles atomically. Because each operation's ZK proof commits to the state it acts on, they can't just be concatenated: the bundle chains them, proving each one against the state left by the previous.

You start a bundle with a sender and push operations into it. The sender is fixed by the bundle, so it is omitted from each descriptor:

import { OperationType } from "@fatsolutions/tongo-sdk";

const multi = await tongoAccount.startMultiOperation("SENDER_ADDRESS");
await tongoAccount.pushOperation(multi, { OperationType, ...params });

const tx = await signer.execute(multi.toCalldata());

Each descriptor takes a type and that operation's parameters. All operations in a bundle must share the same sender, contract and chain. Multioperations can also be seeded with a already-built basic operation.

Fund Operation

This operation converts ERC20 tokens into encrypted Tongo balance. The parameters are:

  • amount: The amount of encrypted Tongo balance you want to fund to your Tongo account.
  • sender: The sender of the transaction. In this case it is also the starknet account that will send the ERC20 to the Tongo contract to convert into encrypted Tongo balance.

On Fund operations, the Tongo contract calls the function transfer_from() form the ERC20 to pull assets and assign encrypted balance the to Tongo account. For this to work the sender has to sign an Approval in the ERC20 to allow the Tongo contract to move funds.

The Approval call is also created by the Fund operation, so the sender does not have to create it.

const fundOp = await account.fund({
    amount: "AMOUNT_TO_FUND"
    sender: "SENDER_ADDRESS"
});

// Execute both approval and fund
await signer.execute([
    fundOp.approve!,    // ERC20 approval
    fundOp.toCalldata() // Fund operation
]);

Balance Handling

When receiving a Fund operation for some amount, the cairo contract creates an ElGamal encryption of that amount for the account public key and fixed randomness. This encryption is added to the balance of the account. The pending balance of the account is not manipulated in this operation.

Zero-Knowledge Proof

In this opretaion, the only thing that has to be proven is the ownership of the Tongo account. This is done by proving knowledge of the account's private key.

Outside Fund (v2)

fund requires the account owner (it needs a proof of ownership). Outside Fund lets anyone fund a Tongo account without knowing its private key — useful to fund an account you don't control. The parameters are:

  • amount: The amount of Tongo balance to fund.
  • to: The public key of the Tongo account to fund.

Since it moves ERC20 from the caller, it also exposes an approve call, just like fund.

const outsideFundOp = await account.outsideFund({
    amount: "AMOUNT_TO_FUND",
    to: "RECEIVER_PUBLIC_KEY",
});

await signer.execute([
    outsideFundOp.approve!,     // ERC20 approval
    ...outsideFundOp.toCalldata()
]);

The funded amount is added to the receiver's balance. It emits an OutsideFundEvent.

Transfer Operation

This operation sends encrypted amounts between two Tongo accounts of the same Tongo instance without revealing the transfer amount. The parameters are:

  • amount: The amount of encrypted Tongo you want to transfer.
  • to: The public key of the Tongo account thaw will receive the transfer.
  • sender: The sender of the transaction.
  • feeToSender (optional, v2): An amount of Tongos paid to the transaction sender. Used to reimburse a relayer — see Relaying.
  • toTongo (optional, v2): The address of a different Tongo instance to send to. Turns the transfer into an External Transfer (see below).
const transferOp = await tongoAccount.transfer({
    to: "RECEIVER_PUBLIC_KEY",
    amount: "AMOUNT_TO_TRANSFER"
    sender: "SENDER_ADDRESS"
});

const tx = await signer.execute([transferOp.toCalldata()]);
await provider.waitForTransaction(tx.transaction_hash);

Balance Handling

As part of the Transfer operation, the user gives two ElGamal encryption of the same amount, one is encrypted for the sender's public key and it is subtracted from the sender's balance. The other one is encrypted for the receiver's public key and it is added to the receiver's pending balance.

Zero-Knowledge Proof

In this operation, the ZK proof given by the sender shows:

  • Ownership of the sender account.
  • The two given encrpytion are valid encryptions for the same amount under the correct public keys.
  • The amount encrypted in positive.
  • After the subtraction, the sender's balance is positive.

External Transfer (v2)

A regular transfer moves Tongos between two accounts of the same Tongo instance. An External Transfer moves them to an account living in a different Tongo instance deployed by the same Vault. You trigger it by passing the receiver instance address as toTongo:

const externalOp = await tongoAccount.transfer({
    to: "RECEIVER_PUBLIC_KEY",
    amount: "AMOUNT_TO_TRANSFER",
    sender: "SENDER_ADDRESS",
    toTongo: "RECEIVER_TONGO_INSTANCE_ADDRESS",
});

const tx = await signer.execute(externalOp.toCalldata());
await provider.waitForTransaction(tx.transaction_hash);

For this to succeed the receiver's Tongo instance must have approved the sender's instance on-chain. The transaction is declared for the auditor of both Tongo incantes (if they are set). The receiving instance emits a ReceivedExternalTransfer event.

Rollover Operation

This operation takes the pending balance of the account and converts it to actual usable balance for the account. The parameter is:

  • sender: The sender of the transaction.
const rolloverOp = await account.rollover({
    sender: "SENDER_ADDRESS"
});

await signer.execute([rolloverOp.toCalldata()]);

Balance Handling

When receiving a Rollover operation, the cairo contract adds the user's pending balance to the user's balance. After that the user's pending balance is reseted to zero.

Zero-Knowledge Proof

In this opretaion, the only thing that has to be proven is the ownership of the Tongo account. This is done by proving knowledge of the account's private key.

Withdraw Operation

This operation converts encrypted Tongo balance to ERC20 tokens and sends them to the given starknet account address. The parameters are:

  • amount: The amount of Tongo balance to withdraw.
  • to: The starknet account address to send the ERC20 to.
  • sender: The sender of the transaction.
  • feeToSender (optional, v2): An amount of Tongos paid to the transaction sender, used to reimburse a relayer. See Relaying.

Convert encrypted Tongo balance back to ERC20 tokens.

const withdrawOp = await tongoAccount.withdraw({
    to: "RECEIVER_STARKNET_ACCOUNT_ADDRESS",
    amount: "AMOUNT_TO_WITHDRAW"
    sender: "SENDER_ADDRESS"
});

const tx = await signer.execute([withdrawOp.toCalldata()]);
await provider.waitForTransaction(tx.transaction_hash);

Balance Handling

When receiving a Withdraw operation for some amount, the cairo contract creates an ElGamal encryption of that amount for the account public key and fixed randomness. This encryption is subtracted from balance of the account. The pending balance of the account is not manipulated in this operation.

Zero-Knowledge Proof

In this operation, the ZK proof given by the sender shows:

  • Ownership of the user account.
  • After the subtraction, the user's balance is positive.

Ragequit Operation

This operation converts all the encrypted Tongo balance to ERC20 tokens and sends them to the given starknet account address. The parameters are:

  • to: The starknet account address to send the ERC20 to.
  • sender: The sender of the transaction.
  • feeToSender (optional, v2): An amount of Tongos paid to the transaction sender, used to reimburse a relayer. See Relaying.
const ragequitOp = await tongoAccount.ragequit({
    to: "RECEIVER_STARKNET_ACCOUNT_ADDRESS",
    sender: "SENDER_ADDRESS"
});

const tx = await signer.execute([ragequit.toCalldata()]);
await provider.waitForTransaction(tx.transaction_hash);

Balance Handling

When receiving a Ragequit operation, the user discloses the total encrypted Tongo balance, after sending the unwrapped ERC20 to the starknet account address, the cairo contract resets the user's balance to zero. The pending balance of the account is not manipulated in this operation.

Zero-Knowledge Proof

In this operation, the ZK proof given by the sender shows:

  • Ownership of the user account.
  • The disclosed amount is the total balance of the user's account.

Relaying (v2)

Relaying lets a user operate a Tongo account without owning a funded StarkNet account. A relayer sends the transaction on the user's behalf, pays the gas, and is reimbursed in the token being transacted through the AVNU paymaster. The user only signs the operation with their Tongo key. See Relayer Architecture for how it works on-chain.

The SDK exposes a RelayerAccount for this.

Creating a RelayerAccount

import { Account as TongoAccount, RelayerAccount } from "@fatsolutions/tongo-sdk";

const providerUrl  = "YOUR_RPC_URL";
const paymasterUrl = "https://sepolia.paymaster.avnu.fi";
const relayerAddress = "RELAYER_CONTRACT_ADDRESS";
const tongoAddress   = "TONGO_CONTRACT_ADDRESS";

const relayer = new RelayerAccount(tongoAddress, relayerAddress, paymasterUrl, providerUrl);

const account = new TongoAccount("USER_TONGO_PRIVATE_KEY", tongoAddress, providerUrl);

The relay flow

Relaying an operation is a two-pass flow: you first build the operation to estimate the fee, then rebuild it with the real fee and hand it to the relayer to build, sign and execute.

  1. Build the operation with the relayer as sender and a placeholder fee, and estimate the fee.
  2. Rebuild the operation with the suggested fee.
  3. Build the transaction to sign, sign it with the Tongo account, and execute it through the relayer.
const sender = relayer.address;
const amount = 10n;
const to = receiver.publicKey;

// 1. estimate the fee (placeholder feeToSender)
const opToEstimate = await account.transfer({ to, amount, feeToSender: 1n, sender });
const fee = await relayer.estimateFee(opToEstimate);

// 2. rebuild with the suggested fee
const snip9_nonce = await account.nonceHash();
const opToExecute = await account.transfer({
    to,
    amount,
    feeToSender: fee.relayerSuggestedTongo,
    sender,
});

// 3. build, sign with the Tongo key, and execute through the relayer
const prepared  = await relayer.buildTransactionToSign(opToExecute, snip9_nonce);
const signature = await account.signMessage(prepared.typedData, sender);
const txHash    = await relayer.execute(prepared, signature);

await provider.waitForTransaction(txHash);

estimateFee returns a RelayFeeEstimate with the AVNU estimated/suggested fee in both ERC20 and Tongo units, plus relayerSuggestedTongo — the value to use as feeToSender.

Relaying a rollover

A plain rollover carries no fee, so it can't reimburse a relayer on its own. Use relayerRollover, which bundles the rollover with a fee-paying operation, then run the same flow:

const opToEstimate = await account.relayerRollover({ sender, feeToSender: 1n });
const fee = await relayer.estimateFee(opToEstimate);

const snip9_nonce = await account.nonceHash();
const opToExecute = await account.relayerRollover({ sender, feeToSender: fee.relayerSuggestedTongo });

const prepared  = await relayer.buildTransactionToSign(opToExecute, snip9_nonce);
const signature = await account.signMessage(prepared.typedData, sender);
const txHash    = await relayer.execute(prepared, signature);

The relayable operations are transfer, withdraw, ragequit and (bundled) rollover.

SHE Cryptography Library

The Starknet Homomorphic Encryption (SHE) library provides low-level cryptographic primitives for proving and verification of Sigma protocols over the Stark elliptic curve. This includes Zero-Knowledge proof of ElGamal encryption.

SHE is the cryptographic foundation of Tongo but its building blocks can be used in other products that rellies on basic Sigma Protocols.

Package Information

Protocols

Implemented zero-knowledge proofs:

  • POE: Proof of Exponent (knowledge of discrete log)
  • POE2: Proof of double exponent
  • POEN: Proof of N exponents
  • Bit: Proof that committed value is 0 or 1
  • Range: Proof that value is in [0, 2^n)
  • ElGamal Proof that a encryption is a correct ElGamal encryption
  • SameEncryption: Proof that two ElGamal encryptions encrypt the same value

Sigma Protocols

All protocols implemented in SHE are Sigma protocols. A Sigma protocol is a protocol in which a prover \(P\) and a verifier \(V\) interact, after the interaction the verrifier can be convinced that the prover has knowledge of some witness \(x\) that satisfies a statement \(y\), the general strutcute of the interaction between the prover and the verifier is

  1. \(P\) computes a message \(A\) called commitment and sends it to \(V\)
  2. Upon receiving \(P\)'s commitment \(A\), \(V\) chooses a challenge \(c\) at random and sends it to \(P\)
  3. Upon receiving \(V\)'s challenge \(c\), \(P\) computes a response \(s\) and sends it to \(V\)
  4. Upon receiving \(P\)'s response \(c\), \(V\) outputs either accept or reject based only on the statement \(y\) and the interaction \((A, c, s)\).

caption

Fiat-Shamir Euristics

The protocol descrived above is interactive because the prover and the verifier are forced to respond one to another. The standar way of converting an interactive protocol into a non-interactive one is by using the Fiat-Shamir heuristic. Broadly speaking this means to use as a challenge \(c\) some hash of the commitment \(A\). This means the prover can compute \(c\) and there is not need to wait for the verifier to create a full proof.

Warning Appliying the Fiat-Shamir transformation is very critical. There is a fundamental reason in the original protocol for the verifier to wait upon reception of the commitment \(A\) to create a challenge \(c\). Usually a prover that has seen the challenge before commiting anything can forge a proof without knowledge of the witness \(x\).

Implementation in SHE

In the cairo implementation of the SHE protocols, for each protocol we expose a verify() function. This function mimics the last step for the verifier. It accepts or rejects the proof and take as imputs the statement \(y\), the commitment \(A\), the challenge \(c\) and the response \(s\).

We have also exposed functions called verify_with_prefix(). These functions dont take the challenge \(c\) as an input, in its place the take a prefix. Internally the function computes the challenge \(c\) by hashing the comitment \(A\) with the given prefix $$ c = \text{Hash}(\text{prefix}, A) $$ The prefix is useful to bind some external data to the proof (the proof at this stage can also be seen as a signature of the prefix). For example in Tongo part of the prefix is the chain_id so any proof intended to be validated in mainet will not be valid in sepolia.

Here there is a example of the implementation of this function for the POE protocol

#![allow(unused)]
fn main() {
pub fn verify_with_prefix(inputs: PoeInputs, proof: PoeProofWithPrefix) -> Result<(), Errors> {
    let PoeInputs { y, g } = inputs;
    let PoeProofWithPrefix { A, prefix, s } = proof;
    let commitments = array![A];
    let c = compute_challenge(prefix, commitments);
    verify(y, g, A, c, s)
}
}

Proof of Exponent (POE)

The POE protocol is a building block of other protocols. Given the relation

$$ y = g^x $$

where \(g\) is a known generator point, \(y\) is a known public point and \(x\) is the secret witness. A Zero-Knowledge proof of exponent is used to show knowledge of \(x\) such that the previous relation holds.

Protocol (Interactive)

caption

Cost Analysis (EC Operations)

Prover Complexity

  • 1 EC multiplications

Verifier Complexity

  • 2 EC multiplications
  • 1 EC addition

Usage in Tongo

POE is directly used in Tongo to prove account ownership in all operations. It is also used indirectly as a building block of other SHE protocols.

Proof of 2 Exponent (POE2)

The POE2 protocol is the fisrt generalization of the POE protocol. Given the relation

$$ y = g_1^{x_1} g_2^{x_2} $$

where \(g_1, g_2\) are two generator points with discrete log relation unknown, \(y\) is a known public point and \(x_1, x_2\) are the secret witnesses. The POE2 procolo is used to show knowledge of \(x_1, x_2\) such that the previous relation holds. The POE2 protocol is used by SHE in the ElGamal protocol, which is a ZK protocol that shows the correctness of an ElGamal encryption.

Protocol (Interactive)

caption

Cost Analysis (EC Operations)

Prover Complexity

  • 2 EC multiplications
  • 1 EC addition

Verifier Complexity

  • 3 EC multiplications
  • 2 EC addition

Usage in Tongo

POE2 is not used directly by Tongo, it is used in an indirect way as a part of ElGamal protocol to verify encryption related Zero-Knowledge proofs.

Proof of N Exponent (POEN)

The POEN protocol is the generalization of POE to N exponents. Given the relation

$$ y = \prod_{i=1}^{N} g_i^{x_i}$$

where \(g_i\) are \(N\) different generatos with discrete log relation unknown, \(y\) is a known public point and the set of \(x_i\) are the secret witnesses. The POEN procolo is used to show knowledge of \(x_i\) such that the previous relation holds.

Design choice: Althoug POE and POE2 are particular cases of this protocol and having only the protocol POEN should be enough, we have decided to have the three protocols to reduce some overhead.

Protocol (Interactive)

caption

Cost Analysis (EC Operations)

Prover Complexity

  • N EC multiplications
  • N-1 EC addition

Verifier Complexity

  • N+1 EC multiplications
  • N EC addition

Usage in Tongo

POEN is not directly used by Tongo and it is not used by another SHE protocol at the moment.

ElGamal

Given a public key \(y\), an ElGamal encryption of an amount \(b\) with randomness \(r\) is a pair of elliptic curve points of the for

$$ (L, R) = \left( g^b y^r,\ g^r\right) $$ where $g$ is the chosen generator. This protocol is used to prove to a verifier that a given ElGamal encryption is exactly of this form. To show this, the prover must prove knowledge of:

  1. \(r\) such that \(R = g^r\)
  2. \(b\) such that \(L = g^b y^r\) for the given public key and with the same \(r\) as before.

Note that the first assertion can be proven with a POE protocol and the second one with a POE2 protocol. We just need to combine the both of them in a single protocol.

Aclaration: The two generators used in the POE2 procol must satisfy that there is not know discrete log relation between them. Here, the two generators would be \(g\) and \(y\) whose discrete log relation is the secret key \(x\) known by the owner of the public key. A simple POE2 protocol in this setup would be insecure. The extra restriction that the blinding factor \(r\) is encoded in the $R$ part of the encryption is enoguh to avoid any posible attack to the POE2 protocol.

Protocol (Interactive)

caption The checks the verifier performs are actually a POE check and a POE2 check. This protocol delegates the assertions to the fundamental building blocks POE and POE2.

Cost Analysis (EC Operations)

Prover Complexity

  • 3 EC multiplications
  • 1 EC addition

Verifier Complexity

  • 5 EC multiplications
  • 3 EC addition

Usage in Tongo

All encryptions given by the user in Tongo pass through ElGamal protocol. Most of them are invoked by another SHE protocol that shows that two given encryptions are valid and they are indeed encrypting the same amount.

Same Encryption Proof

Given two ElGamal encryptions of an amount \(b\) for two (possibly different) public keys \(y_1\) and \(y_2\), this protocol is used to convince a verifier that both encryptions are valid ElGamal encryptions under their respective public key and that both of them encrypt the same amount. In this proof, the prover is assumed to know the randomness of both encryptions and, of course, the amount \(b\).

The encryptios are $$ (L1, R1) = \left( g^b y_1^{r_1},\ g^{r_1}\right)\\ (L2, R2) = \left( g^b y_2^{r_2},\ g^{r_2}\right) $$ The prover must show that:

  1. \((L1, R1)\) is a valid encryption under \(y_2\) for and amount \(b\)
  2. \((L2, R2)\) is a valid encryption under \(y_2\) for the same amount \(b\)

Note that the both assertion can be proven with ElGamal protocol. We just have to combine them in a way that the share the same secret value \(b\)

Protocol (Interactive)

caption The checks the verifier performs are actually two ElGamal protocol checks. This protocol just delegates these assertions.

Cost Analysis (EC Operations)

Prover Complexity

  • 6 EC multiplications
  • 2 EC addition

Verifier Complexity

  • 10 EC multiplications
  • 6 EC addition

Usage in Tongo

In a trnasfer operation in Tongo, the sender must provide two encryption of the same amount, one is to be added to the pending balance of the receiver and the other one to be subracted from the sender's balance. The sender uses this protocol to show the validity of those encryptions.

Variation: Unknown Random

This variation of the previous protocol allows the prover to ignore the randomness of one of the encryptions. The price to pay is that the prover must know the secret \(x\) of the public key \(y= g^x\) the encryptions is made for. This works because the \(L\) point in a valid ElGamal encryption can be seen as a commitment to \(b\) and \(x\) with generators \(g\) and \(R\), that is

$$ (L, R) = \left( g^b y^{r},\ g^{r}\right) = \left( g^b R^{x},\ R\right) $$ In the RHS of the previous equation, the randomness is unknown. We can prove that this is a correct encryption with a POE2 protocol for the \(L\) part of the encryption

Aclaration: The two generators used in the POE2 procol must satisfy that there is not know discrete log relation between them. Here, the two generators would be \(g\) and \(R\) whose discrete log relation is the randomness \(r\) that might be known by the constructor of the original encryption. A simple POE2 protocol in this setup would be insecure. The extra restriction that the secret \(x\) is exactly the secret of the public key \(y\) is enoguh to avoid any posible attack to the POE2 protocol.

We have in this setup two encryptions $$ (L1, R1) = \left( g^b R1^{x},\ R1\right) \\ (L2, R2) = \left( g^b y_2^{r_2},\ g^{r_2}\right) $$

The prover must show that:

  1. Knowledge of \(x\) such that \(y = g^x\)
  2. Knowledge \(b\) such that \(L1 = g^b R1^x\) for the given \(R1\) key and with the same \(x\) as before.
  3. \((L2, R2)\) is a valid encryption under \(y_2\) for the same amount \(b\)

Design choice There is a way to reuse the original SameEncrypt protocol to prove this. The way of do it requires swapping \(r_1 \leftrightarrow x\) and \(R1 \leftrightarrow y_1\). We think that making these changes to call provers/verifiers would be very confusing and error prone. So we have decided to write a separate protocol to handle this case.

Protocol (Interactive)

caption

Verifier Complexity

  • 10 EC multiplications
  • 6 EC addition

Usage in Tongo

In transfers/withdraw operations in Tongo, user must show that the remaining balance, after the operation, is positive. Zero-Knowledge proof in these cases are to be checked against the current balance of the account. Users generaly dont know the randomness of the encryption of the current balance: is the sum of all randomness generated by the senders, of previous incoming transfers that had them as receiver.

To prove that the remaining balance is positive, the Zero-Knowledge proof consists in the creation of an auxiliar encryption, for this encryption a Range protocol shows that is encrypting a positive balance, and then the prover shows that this auxiliar encryption and the current balance of the account are the same. For this last step, the UnknownRandom version of the SameEncrypt protocol is used

Bit Proofs

This protocol is usded to prove that a committed value is either zeor or one without revealing which one is correct. It is a fundamental piece of the Range protocol. Given a commitment of the form $$ V = g^b h^r $$ where \(g\) and \(h\) are two generator points with discrete log relation unknown, \(b\) is either \(0\) or \(1\) and \(r\) is a blinding factor. We have two path here

  1. \(b\) is \(0\), then \(V = h^r\)
  2. \(b\) is \(1\), then \(\frac{V}{g} = h^r\)

Note that in each cases it is enough to use a POE to show knowledge of \(r\). Two show that one of the paths is the correct one without revealing which one is the case, we have to combine two POE protocols with an OR statement.

Note: Given two Sigma protocols, the way of combine them in an OR statement is well known. The idea is that the prover will use the standard protocol for the statement that is true (i.e. that can be proven) and simulate a valid transcript for the statment that is false. On validation, the verifier will know that only one of the statement is simulated but it will be imposible to known which one it is.

Bit proofs demonstrate that a committed value is either 0 or 1 using OR proof construction.

Simulator for POE

A valid transcript for the POE protocol is a triad \((A, c, s)\) that passes the validation check of the verifier. The POE protocol is used to show, given a \(y\), knowledge of \(x\) shuch that \(y = g^x\). A simulator for this protocols is an algorithm that, given \(y\), produces a valid transcript, without knowledge of \(x\). In this case the simulator is $$\begin{array}{ll} Sim\left(y,g\right) &\rightarrow (A, c,s ): \lbrace \\ & c \leftarrow \mathbb{F}_p^{*}\\ & s \leftarrow \mathbb{F}_p^{*} \\ & A = g^s/y^c \\ & \text{returns} (A,c,s) \\ \rbrace & \end{array} $$ this transcript will pass the verifier because $$ g^s = A y^c = \dfrac{g^s}{y^c} y^c = g^s $$

Aclaration: Even though the simulaton can produce a valid proof for the POE protocol, it can never be used to convince to a verifier by engaging the a real interaction. In Sigma protocols the challenge \(c\) is chosen by the verifier after all the messages \(A\) were committed by the prover. The flow of the simulator implies that the message \(A\) is to be selected after the prover sees the challenge.

Protocol (Interactive)

Let say we are in the fisrt case, that is, we have \(b=0\), the commitment \(V\) is $$ V = h^r $$

caption

If we are in the second case, that is, we have \(b=1\), the commitment \(V\) is $$ V = g\ h^r $$

caption

Note that in both cases the verifier performs the same steps. The prove must perform different steps according what POE is going to be simulated. In both cases the prover sends $c_0$, even in the case that $c_0$ is given by the simulator. This is part of the protocol and it is to avoid revealing which $c_i$ was simulated.

Cost Analysis (EC Operations)

Prover Complexity

  • 3 EC multiplications
  • 1 EC addition

Verifier Complexity

  • 4 EC multiplications
  • 3 EC addition

Usage in Tongo

This protocol is not directly used in Tongo. It is used in an indirect way as part of Range protocol.

Range Proofs

This protocol allows a prover to convince a verifier that a commited value \(b\) belongs to a range \([0, 2^n)\). The protocol does this by ussing the binary decomposition of \(b\). We call bit_size to the integer \(n\). The commitment is of the form $$ V = g^b h^r $$ where \(g\) and \(h\) are two generator points with discrete log relation unknown, \(b\) is the number we want to show belongs to the range \([0, 2^n)\) and \(r\) is a blinding factor.

Protocol

Any value \(b \in [0, 2^n)\) can be written as:

$$b = \sum_{i=0}^{n-1} b_i \cdot 2^i$$

Where each \(b_i \in {0, 1}\). For each bit \(b_i\), the prover creates a commitment with independent randomness \(r_i\) of the form

$$V_i = g^{b_i} \cdot h^{r_i}$$

for each one of this commitments, the prover uses a Bit protocol to show that \(V_i\) is encoding either zero or one. Note that we can construct a commitment \(V\) with the commitments \(V_i\) by computing $$ V = \prod_{i=0}^{n-1} V_i^{2^i} = g^b h^{r_{total}} $$ where \(r_{total} = \sum_{i=0}^{n-1} r_i 2^i\). A verifier that constructs this \(V\) after all commitments \(V_i\) verify the Bit protocol will be convinced that $V$ is a comitment to a value \(b \in [0, 2^n)\). This commitment $V$ can now be used as part of other protocols depending on what the prover wants to show.

Cost Analysis (EC Operations)

Prover Complexity

  • 3n EC multiplications
  • n EC addition

Verifier Complexity

  • 5n EC multiplications
  • 4n EC addition

Usage in Tongo

In a transfer operation in Tongo, the sender must show that the sended amount is positive and the remaining balance is positive. To prove anyone of this, the prover submits a Range proof, then the reconstructed commitment \(V\) is used as the \(L\) part of a ElGamal encryption $$ (L, R) = (V, g^{r_{total}}) $$ this is a valid encryption for the publick key \(h\). The sender then uses the SameEncrypt protocol to shows that it encrypts the same amount as the encryption created for a transfer (showing then that the transfered balance is positive), or the UnknownRandom version to show that it encrypts the same amount of the remaining balance (showing then that the remainign balance is positive)