Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3cc85fe
add test
critesjosh Jan 28, 2025
310b3eb
update
critesjosh Jan 28, 2025
5d066b1
rm old
critesjosh Jan 28, 2025
546d083
fix links
critesjosh Jan 29, 2025
48536f1
update comments
critesjosh Jan 29, 2025
8df63f6
write tutorial
critesjosh Jan 29, 2025
903887a
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Jan 29, 2025
7220398
clarify
critesjosh Jan 29, 2025
a276228
Merge branch 'jc/docs-bridge-tutorial' of https://github.com/AztecPro…
critesjosh Jan 29, 2025
cb2e4c4
use compose for test
critesjosh Jan 30, 2025
84528d3
fix link to test file
critesjosh Jan 30, 2025
2353b49
fmt
critesjosh Jan 30, 2025
402a54e
run test w/ compose
critesjosh Jan 30, 2025
37a88e4
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Jan 30, 2025
cf5cc72
fmt
critesjosh Jan 30, 2025
c2f8201
fix filename
critesjosh Jan 30, 2025
61f46b1
add token bridge test to ci
critesjosh Jan 31, 2025
a4a7cf0
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Jan 31, 2025
e884962
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Jan 31, 2025
d7c93fa
passing
critesjosh Jan 31, 2025
9732e5f
try bump test timeout
critesjosh Jan 31, 2025
4890cbf
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Jan 31, 2025
d742f5a
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 2, 2025
74f107f
cats comments
critesjosh Feb 5, 2025
f8dba2a
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 5, 2025
a3855c7
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 5, 2025
4ffb66d
fix links
critesjosh Feb 5, 2025
450ac31
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 5, 2025
80acf5c
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 6, 2025
47036ce
add cat's suggestions
critesjosh Feb 7, 2025
da23343
update rootDir
critesjosh Feb 7, 2025
685c44f
simp tsconfig, specify node version
critesjosh Feb 7, 2025
531540b
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 7, 2025
41c5188
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 10, 2025
eedceea
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 10, 2025
8037bae
james comments
critesjosh Feb 11, 2025
9ce59b4
Merge branch 'master' into jc/docs-bridge-tutorial
critesjosh Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Hence, it's necessary to add a "randomness" field to your note to prevent such a

### L1 -- L2 interactions

Refer to [Token Portal codealong tutorial on bridging tokens between L1 and L2](../../../../../tutorials/codealong/contract_tutorials/token_bridge/index.md) and/or [Uniswap smart contract example that shows how to swap on L1 using funds on L2](../../../../../tutorials/examples/uniswap/index.md). Both examples show how to:
Refer to [Token Portal codealong tutorial on bridging tokens between L1 and L2](../../../../../tutorials/codealong/contract_tutorials/token_bridge.md) and/or [Uniswap smart contract example that shows how to swap on L1 using funds on L2](../../../../../tutorials/examples/uniswap/index.md). Both examples show how to:

1. L1 -> L2 message flow
2. L2 -> L1 message flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Communicating with L1
tags: [contracts, portals]
---

Follow the [token bridge tutorial](../../../../../tutorials/codealong/contract_tutorials/token_bridge/index.md) for hands-on experience writing and deploying a Portal contract.
Follow the [token bridge tutorial](../../../../../tutorials/codealong/contract_tutorials/token_bridge.md) for hands-on experience writing and deploying a Portal contract.

## Passing data to the rollup

Expand All @@ -13,10 +13,10 @@ The `Inbox` can be seen as a mailbox to the rollup, portals put messages into th

When sending messages, we need to specify quite a bit of information beyond just the content that we are sharing. Namely we need to specify:

| Name | Type | Description |
| ----------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Recipient | `L2Actor` | The message recipient. This **MUST** match the rollup version and an Aztec contract that is **attached** to the contract making this call. If the recipient is not attached to the caller, the message cannot be consumed by it. |
| Secret Hash | `field` (~254 bits) | A hash of a secret that is used when consuming the message on L2. Keep this preimage a secret to make the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so make sure your app keeps track of the pre-images! Use `computeSecretHash` to compute it from a secret. |
| Name | Type | Description |
| ----------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Recipient | `L2Actor` | The message recipient. This **MUST** match the rollup version and an Aztec contract that is **attached** to the contract making this call. If the recipient is not attached to the caller, the message cannot be consumed by it. |
| Secret Hash | `field` (~254 bits) | A hash of a secret that is used when consuming the message on L2. Keep this preimage a secret to make the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so make sure your app keeps track of the pre-images! Use `computeSecretHash` to compute it from a secret. |
| Content | `field` (~254 bits) | The content of the message. This is the data that will be passed to the recipient. The content is limited to be a single field. If the content is small enough it can just be passed along, otherwise it should be hashed and the hash passed along (you can use our [`Hash` (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/libraries/Hash.sol) utilities with `sha256ToField` functions) |

With all that information at hand, we can call the `sendL2Message` function on the Inbox. The function will return a `field` (inside `bytes32`) that is the hash of the message. This hash can be used as an identifier to spot when your message has been included in a rollup block.
Expand All @@ -41,7 +41,7 @@ Note that while the `secret` and the `content` are both hashed, they are actuall

### Token bridge example

Computing the `content` must currently be done manually, as we are still adding a number of bytes utilities. A good example exists within the [Token bridge example (codealong tutorial)](../../../../../tutorials/codealong/contract_tutorials/token_bridge/index.md).
Computing the `content` must currently be done manually, as we are still adding a number of bytes utilities. A good example exists within the [Token bridge example (codealong tutorial)](../../../../../tutorials/codealong/contract_tutorials/token_bridge.md).

#include_code claim_public /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust

Expand Down
256 changes: 256 additions & 0 deletions docs/docs/tutorials/codealong/contract_tutorials/token_bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
---
title: "Token Bridge Tutorial"
---

This tutorial goes over how to create the contracts necessary to create a portal (aka token bridge) and how a developer can use it.

In this tutorial, we will go over the components of a token bridge and how to deploy them, as well as show how to bridge tokens publicly from L1 to L2 and back, using aztec.js.

## Components
Comment thread
critesjosh marked this conversation as resolved.

Bridges in Aztec involve several components across L1 and L2:

- L1 contracts:
- `ERC20.sol`: An ERC20 contract that represents assets on L1
- `TokenPortal.sol`: Manages the passing of messages from L1 to L2
- L2 contracts:
- `Token`: Manages the tokens on L2
- `TokenBridge`: Manages the bridging of tokens between L2 and L1

### `TokenPortal.sol`

`TokenPortal.sol` is the contract that manages the passing of messages from L1 to L2. It is deployed on L1, is linked to a specific token on L1 and a corresponding contract on L2. The `registry` is used to find the rollup and the corresponding `inbox` and `outbox` contracts.

### Deposit to Aztec

Messages content that is passed to Aztec is limited to a single field element (~254 bits), so if the message content is larger than that, it is hashed, and the message hash is passed an verified on the receiving contract. There is a utility function in the `Hash` library to hash messages (using `sha256`) to field elements.

It is a good practice to include all of the parameters used by the L2 contract in the message content, so that the receiving contract can verify the message and malicious actors cannot modify the message.

The Aztec message Inbox expects a recipient Aztec address that can consume the message (the corresponding L2 bridge contract), the Aztec version (similar to Ethereum's `chainId`), the message content hash (which includes the token recipient and amount in this case), and a `secretHash`, where the corresponding `secret` is used to consume the message on the receiving contract.

So in summary, it deposits tokens to the portal, encodes a mint message, hashes it, and sends it to the Aztec rollup via the Inbox. The L2 token contract can then mint the tokens when it processes the message.

Note that because L1 is public, everyone can inspect and figure out the contentHash and the recipient contract address.

#### `depositToAztecPublic`

#include_code deposit_public l1-contracts/test/portals/TokenPortal.sol solidity

#### `depositToAztecPrivate`

#include_code deposit_private l1-contracts/test/portals/TokenPortal.sol solidity

**So how do we privately consume the message on Aztec?**

On Aztec, anytime something is consumed (i.e. deleted), we emit a nullifier hash and add it to the nullifier tree. This prevents double-spends. The nullifier hash is a hash of the message that is consumed. So without the secret, one could reverse engineer the expected nullifier hash that might be emitted on L2 upon message consumption. To consume the message on L2, the user provides a secret to the private function, which computes the hash and asserts that it matches to what was provided in the L1->L2 message. This secret is included in the nullifier hash computation and the nullifier is added to the nullifier tree. Anyone inspecting the blockchain won’t know which nullifier hash corresponds to the L1->L2 message consumption.

### Minting on Aztec

In the previous step, we moved our funds to the bridge and created a L1->L2 message. Upon building the next rollup block, the sequencer asks the L1 inbox contract for any incoming messages and adds them to the Aztec block's L1->L2 message tree, so an application on L2 can prove that the message exists and can consume it.

#include_code claim_public /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust

What's happening here?

1. compute the content hash of the message
2. consume the message
3. mint the tokens

:::note

The Aztec `TokenBridge` contract should be an authorized minter in the corresponding Aztec `Token` contract so that it is able to complete mints to the intended recipient.

:::

### Withdraw to L1

Now we have tokens on L2, we can withdraw them back to L1. You can withdraw part of a public or private balance to L1, but the amount and the recipient on L1 will be public.

Sending tokens to L1 involves burning the tokens on L2 and creating a L2->L1 message. The message content is the `amount` to burn, the recipient address, and who can execute the withdraw on the L1 portal on behalf of the user. It can be `0x0` for anyone, or a specified address.

For both the public and private flow, we use the same mechanism to determine the content hash. This is because on L1, things are public anyway. The only different between the two functions is that in the private domain we have to nullify user’s notes where as in the public domain we subtract the balance from the user.

### Aztec `TokenBridge`

#### `exit_to_L1_public`

#include_code exit_to_l1_public /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust

#### `exit_to_L1_private`

This function works very similarly to the public version, except here we burn user’s private notes.

#include_code exit_to_l1_private /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust

Since this is a private method, it can't read what token is publicly stored. So instead the user passes a token address, and `_assert_token_is_same()` checks that this user provided address is same as the one in storage.

Because public functions are executed by the sequencer while private methods are executed locally, all public calls are always done _after_ all private calls are completed. So first the burn would happen and only later the sequencer asserts that the token is same. The sequencer just sees a request to `execute_assert_token_is_same` and therefore has no context on what the appropriate private method was. If the assertion fails, then the kernel circuit will fail to create a proof and hence the transaction will be dropped.

A user must sign an approval message to let the contract burn tokens on their behalf. The nonce refers to this approval message.

### L1 `TokenPortal.sol`

After the transaction is completed on L2, the portal must call the outbox to successfully transfer funds to the user on L1. Like with deposits, things can be complex here. For example, what happens if the transaction was done on L2 to burn tokens but can’t be withdrawn to L1? Then the funds are lost forever! How do we prevent this?

#include_code token_portal_withdraw /l1-contracts/test/portals/TokenPortal.sol solidity

Here we reconstruct the L2 to L1 message and check that this message exists on the outbox. If so, we consume it and transfer the funds to the recipient. As part of the reconstruction, the content hash looks similar to what we did in our bridge contract on aztec where we pass the amount and recipient to the hash. This way a malicious actor can’t change the recipient parameter to the address and withdraw funds to themselves.

We also use a `_withCaller` parameter to determine the appropriate party that can execute this function on behalf of the recipient. If `withCaller` is false, then anyone can call the method and hence we use address(0), otherwise only msg.sender should be able to execute. This address should match the `callerOnL1` address we passed in aztec when withdrawing from L2.

We call this pattern _designed caller_ which enables a new paradigm **where we can construct other such portals that talk to the token portal and therefore create more seamless crosschain legos** between L1 and L2.

## Running with Aztec.js

Let's run through the entire process of depositing, minting and withdrawing tokens in Typescript, so you can see how it works in practice.

### Prerequisites

Same prerequisites as the [getting started guide](../../../guides/getting_started.md#prerequisites) and the sandbox.

### ProjectSetup

Create a new directory for the tutorial and install the dependencies:

```bash
mkdir token-bridge-tutorial
cd token-bridge-tutorial
yarn init -y
yarn add @aztec/aztec.js @aztec/noir-contracts.js @aztec/l1-artifacts @aztec/accounts @aztec/ethereum @aztec/types @types/node typescript@^5.0.4 viem@^2.22.8 tsx
touch tsconfig.json
touch index.ts
```

Add this to your `tsconfig.json`:

```json
{
"compilerOptions": {
"rootDir": "../",
"outDir": "./dest",
"target": "es2020",
"lib": ["dom", "esnext", "es2017.object"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"downlevelIteration": true,
"inlineSourceMap": true,
"declarationMap": true,
"importHelpers": true,
"resolveJsonModule": true,
"composite": true,
"skipLibCheck": true
}
}
```

and add this to your `package.json`:

```json
// ...
"type": "module",
"scripts": {
"start": "node --import tsx index.ts"
},
// ...
```

You can run the script we will build in `index.ts` at any point with `yarn start`.

### Imports

Add the following imports to your `index.ts`:

#include_code imports /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

### Utility functions

Add the following utility functions to your `index.ts` below the imports:

#include_code utils /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

### Sandbox Setup

Start the sandbox with:

```bash
aztec start --sandbox
```

And add the following code to your `index.ts`:

```ts
async function main() {
#include_code setup /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts raw
}

main();
```

The rest of the code in the tutorial will go inside the `main()` function.

Run the script with `yarn start` and you should see the L1 contract addresses printed out.

### Deploying the contracts

Add the following code to `index.ts` to deploy the L2 token contract:

#include_code deploy-l2-token /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

Add the following code to `index.ts` to deploy the L1 token contract and set up the `L1TokenManager` (a utility class to interact with the L1 token contract):

#include_code deploy-l1-token /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

Add the following code to `index.ts` to deploy the L1 portal contract:

#include_code deploy-portal /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

Add the following code to `index.ts` to deploy the L2 bridge contract:

#include_code deploy-l2-bridge /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

### Setup contracts

Add the following code to `index.ts` to authorize the L2 bridge contract to mint tokens on the L2 token contract:

#include_code authorize-l2-bridge /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

Add the following code to `index.ts` to set up the L1 portal contract and `L1TokenPortalManager` (a utility class to interact with the L1 portal contract):

#include_code setup-portal /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

### Bridge tokens

Add the following code to `index.ts` to bridge tokens from L1 to L2:

#include_code l1-bridge-public /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

We have to send two additional transactions because the network must process 2 blocks for the message to be processed by the archiver. We need to progress by 2 because there is a 1 block lag between when the message is sent to Inbox and when the subtree containing the message is included in the block. Then when it's included it becomes available for consumption in the next block.

### Claim on Aztec

Add the following code to `index.ts` to claim the tokens publicly on Aztec:

#include_code claim /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

### Withdraw

Add the following code to `index.ts` to start the withdraw the tokens to L1:

#include_code setup-withdrawal /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

We have to send a public authwit to allow the bridge contract to burn tokens on behalf of the user.

Add the following code to `index.ts` to start the withdraw process on Aztec:

#include_code l2-withdraw /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

Add the following code to `index.ts` to complete the withdraw process on L1:

#include_code l1-withdraw /yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts typescript

Run `yarn start` to run the script and see the entire process in action.
Loading