Token Transfer Circuit Description

The goal of this post is to describe the latest design of the token transfer circuit.

High-level overview

Token Transfer RM application allows to:

  • wrap Ethereum ERC20 tokens in resources
  • transfer resources between Anoma identities
  • unwrap the tokens and withdraw the corresponding ERC20 tokens

A cross-chain application can be explored in the framework of communication between three main entities:

  • Protocol adapter. PA is an Ethereum contract that connects Ethereum state with Anoma state. It processes and verifies the Anoma transactions, performs required calls to forwarder contracts, and maintains global RM structures such as the resource commitment tree and nullifier set. Protocol adapter is application-independent.
  • Forwarder contract (FC). A forwarder contract is an Ethereum contract associated with an ARM application. It performs calls required by that application (and requested by the PA).
    • In the context of this application, the forwarder contract is responsible for receiving (when the token is being wrapped in a resource) and sending (when the token is being withdrawn) ERC20 tokens to Ethereum user addresses.
  • Application Circuit. An application circuit is an Anoma-native low-level description of the application logic. It is encoded in a resource object logicRef field and contains constraints that define when resources corresponding to that application can be created or consumed. The ARM ensures that the application constraints are satisfied.
    • Token Transfer Circuit defines when persistent resources, that represent wrapped ERC-20 tokens, can be created and consumed. Ephemeral resources are used to enforce external state changes, i.e., binding forwarder contract calls to the Anoma state.

The rest of the post describes the functions enabled by the design, the resource object structure, and the token transfer circuit constraints and inputs.

Functions enabled by TTC

Each enabled function corresponds to an ARM action.

Function Description Authorised by Authorization method [1] Resources in the action Requires communication with FC
Wrap Deposit a token on the Ethereum side, mint a corresponding token on the Anoma side The owner of the deposited ERC20 token, represented by user_address Permit2 signature over the corresponding action One ephemeral resource consumed, one or more persistent resources created :white_check_mark:
Unwrap Burns the corresponding resource on the Anoma side, withdraw the token on the Ethereum side The owner of the burned resource, represented by authority public key apk ECDSA signature over the corresponding action One ephemeral resource created, one or more persistent resources consumed :white_check_mark:
Transfer Transfers a persistent resource from one owner to another by consuming the old persistent resource and creating a new one with the new owner The owner of the transferred resource, represented by authority public key apk ECDSA signature over the corresponding action One or more persistent resources consumed, one or more persistent resources created :cross_mark:

Resource Object Structure

Resource Object Field Is application-specific Description
logicRef :white_check_mark: Contains the verifying key of the token transfer circuit
labelRef :white_check_mark: Contains a hash of the label. Label includes forwarder address and token address: label = (forwarder_address, erc20_address)
valueRef :white_check_mark: Contains a hash of the value. For persistent resources, the value contains the owner’s authority public key apk and static encryption public key sepk . For ephemeral resources, it is an associated user address on Ethereum
quantity :white_check_mark: For ephemeral resources, contains the amount of ERC20 token being wrapped or unwrapped. For persistent resources it reflects the carried amount.
isEphemeral :white_check_mark: Ephemeral resources are used for wrapping and unwrapping only. Creation and consumption of ephemeral resources triggers forwarder contract calls. Persistent resources represent the wrapped token in the Anoma state.
nonce :cross_mark:
nullifierKeyCommitment :cross_mark:
randSeed :cross_mark:

Circuit design overview

TTC constrains resource fields and transaction payloads to ensure correct realization of the desired functions: wrap, unwrap, and transfer. Recall that each resource in a transaction has 4 associated payload types:

  • Resource payload. This payload is used for in-band distribution. It contains the encrypted resource object and (also encrypted) pre-images associated with referenced resource fields (labelRef and valueRef). For TTC, only labelRef pre-image has to be included. The correctness of encryption is verified in TTC.
  • Discovery payload. This contains data needed for easier discovery of transactions. Not application-specific and not verified by TTC. [2]
  • External payload. This contains data associated with calls to FC. Only Wrap and Unwrap require external calls.
  • Application payload. It contains other application-specific data. For TTC, this is always empty.

Inputs

The lists below contain all public and private inputs that TTC takes. In risc0, all circuit inputs (public or private) are called witness, and instance is a return value of the circuit. Therefore the public values below correspond to the witness elements that are included in the instance (or computed from the witness elements and included in the instance) and private values are the witness elements that are not included in the instance.

Public inputs

  • resource tag
  • is_consumed
  • action tree root
  • resource payload:
    • encrypted resource object
    • nonce
    • sender’s public key
  • discovery payload:
    • discovery ciphertext
    • discovery nonce
    • discovery sender’s public key
  • external payload:
    • forwarder address
    • input [3]:
      • call type [4]
      • user address
      • token address
      • deposited/withdrawn amount
      • permit data
        • permit nonce
        • permit deadline
        • permit signature
        • action tree root [5]
    • output: empty
  • application payload: always empty

Some of the inputs are only expected in some cases. The table below describes when inputs are expected to be non-empty

Public input Persistent created Persistent consumed Ephemeral created Ephemeral consumed
Resource tag :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
is_Consumed :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
Action tree root :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
Resource payload :white_check_mark: Empty Empty Empty
Discovery payload :white_check_mark: Empty Empty Empty
External payload Empty Empty :white_check_mark: :white_check_mark:
Permit data (external payload) Empty Empty Empty :white_check_mark:
Application payload Empty Empty Empty Empty

Private inputs

Some of the private inputs are also only expected to be non-empty in some cases. The table below describes when private inputs are expected to be non-empty

Public input Persistent created Persistent consumed Ephemeral created Ephemeral consumed
Resource object :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
nk Empty :white_check_mark: Empty :white_check_mark:
apk_{send} Empty :white_check_mark: Empty Empty
Signature Empty :white_check_mark: Empty Empty
sepk_{recv} :white_check_mark: Empty Empty Empty
sesk_{send} :white_check_mark: Empty Empty Empty

Constraints

  1. Resource commitment and the resource tag are correctly computed [6]
  2. If ephemeral [7]:
    1. self.label = hash(forwarder_address, token_address)
    2. self.quantity = amount
    3. If consumed:
      1. Forwarder call type is Wrap
      2. input = [user_address, token_address, amount, permit nonce, permit deadline, permit signature, action tree root][8]
    4. If created:
      1. Forwarder call type is Unwrap
      2. input = [user_address, token_address, amount]
      3. self.value = hash(user_address) [9]
    5. resource_payload = []
    6. discovery_payload = []
    7. external_payload = [forwarder_address, input, []]
  3. If persistent:
    1. If consumed:
      1. self.value = hash(auth_pk, receiver_pk)
      2. verify(auth_singature, auth_pk, action_tree_root) = true
      3. resource_payload = []
      4. discovery_payload = []
    2. If created:
      1. Verify encryption: ciphertext = encrypt([self.resource, forwarder_address, token_address], nonce, DH(receiver_pk, sender_sk))
      2. resource_payload = [ciphertext, nonce, sender_pk]
      3. discovery_payload = input_discovery_payload (not recomputed)
    3. external_payload = []
  4. application_payload = []

Links


  1. the action is authorised by signing it. It is either done on Ethereum side (permit2) or Anoma side (ECDSA with authority key pair) ↩︎

  2. It still has to be passed as a public input ↩︎

  3. input and output correspond to the expected inputs and outputs of the forwarder call ↩︎

  4. Wrap or Unwrap ↩︎

  5. the signed message ↩︎

  6. For consumed resources, we implicitly verify the correctness of the commitment computation. For created resources, this constraint merges into a single one since commitment = tag ↩︎

  7. Wrap or Unwrap constraints ↩︎

  8. I simplified structure and removed some brackets here ↩︎

  9. This is done to implicitly authorize user_address when withdrawing assets. Since user_address is included in the resource, it was also included when computing the tag of the corresponding resource, which was used to compute the action tree root the user signs. ↩︎

3 Likes