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
logicReffield 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 | |
| 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 | |
| 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 |
Resource Object Structure
| Resource Object Field | Is application-specific | Description |
|---|---|---|
logicRef |
Contains the verifying key of the token transfer circuit | |
labelRef |
Contains a hash of the label. Label includes forwarder address and token address: label = (forwarder_address, erc20_address) |
|
valueRef |
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 |
For ephemeral resources, contains the amount of ERC20 token being wrapped or unwrapped. For persistent resources it reflects the carried amount. | |
isEphemeral |
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 |
||
nullifierKeyCommitment |
||
randSeed |
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 (
labelRefandvalueRef). For TTC, onlylabelRefpre-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
WrapandUnwraprequire 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:
- 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 | ||||
is_Consumed |
||||
| Action tree root | ||||
| Resource payload | Empty | Empty | Empty | |
| Discovery payload | Empty | Empty | Empty | |
| External payload | Empty | Empty | ||
| Permit data (external payload) | Empty | Empty | Empty | |
| Application payload | Empty | Empty | Empty | Empty |
Private inputs
- Resource object
- Nullifier key nk
- Authorization
- Sender’s authority public key apk_{send}
- Signature
- Resource encryption
- Receiver’s static encryption public key sepk_{recv}
- Sender’s ephemeral encryption secret key sesk_{send}
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 | ||||
| nk | Empty | Empty | ||
| apk_{send} | Empty | Empty | Empty | |
| Signature | Empty | Empty | Empty | |
| sepk_{recv} | Empty | Empty | Empty | |
| sesk_{send} | Empty | Empty | Empty |
Constraints
- Resource commitment and the resource tag are correctly computed [6]
- If ephemeral [7]:
self.label = hash(forwarder_address, token_address)self.quantity = amount- If consumed:
- Forwarder call type is
Wrap input = [user_address, token_address, amount, permit nonce, permit deadline, permit signature, action tree root][8]
- Forwarder call type is
- If created:
- Forwarder call type is
Unwrap input = [user_address, token_address, amount]self.value = hash(user_address)[9]
- Forwarder call type is
resource_payload = []discovery_payload = []external_payload = [forwarder_address, input, []]
- If persistent:
- If consumed:
self.value = hash(auth_pk, receiver_pk)verify(auth_singature, auth_pk, action_tree_root) = trueresource_payload = []discovery_payload = []
- If created:
- Verify encryption:
ciphertext = encrypt([self.resource, forwarder_address, token_address], nonce, DH(receiver_pk, sender_sk)) resource_payload = [ciphertext, nonce, sender_pk]discovery_payload = input_discovery_payload(not recomputed)
- Verify encryption:
external_payload = []
- If consumed:
application_payload = []
Links
- TTC implementation
- Prior design discussion threads [1][2]
the action is authorised by signing it. It is either done on Ethereum side (permit2) or Anoma side (ECDSA with authority key pair) ↩︎
It still has to be passed as a public input ↩︎
inputandoutputcorrespond to the expected inputs and outputs of the forwarder call ↩︎WraporUnwrap↩︎the signed message ↩︎
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 ↩︎
WraporUnwrapconstraints ↩︎I simplified structure and removed some brackets here ↩︎
This is done to implicitly authorize
user_addresswhen withdrawing assets. Sinceuser_addressis 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. ↩︎
