From aaf42a7040d1cabc16083ce2acfa6955aa6dd754 Mon Sep 17 00:00:00 2001 From: josh crites Date: Wed, 12 Feb 2025 12:32:00 -0500 Subject: [PATCH] fix(docs): Update the token bridge tutorial (#11578) This PR updates the token bridge tutorial pages in the docs, and adds an e2e test to ensure that referenced code is always working. closes https://github.com/AztecProtocol/aztec-packages/issues/10638 --- .../common_patterns/index.md | 2 +- .../portals/communicate_with_portal.md | 12 +- .../contract_tutorials/token_bridge.md | 294 ++++++++++++++++++ .../token_bridge/0_setup.md | 245 --------------- .../token_bridge/1_depositing_to_aztec.md | 95 ------ .../token_bridge/2_minting_on_aztec.md | 53 ---- .../token_bridge/3_withdrawing_to_l1.md | 92 ------ .../token_bridge/4_typescript_glue_code.md | 174 ----------- .../contract_tutorials/token_bridge/index.md | 63 ---- .../contract_tutorials/uniswap/index.md | 6 +- .../contract_tutorials/uniswap/l1_contract.md | 2 +- .../contract_tutorials/uniswap/l2_contract.md | 4 +- .../contract_tutorials/token_bridge.md | 261 ++++++++++++++++ docs/docs/tutorials/examples/uniswap/index.md | 31 ++ docs/netlify.toml | 60 ++-- l1-contracts/test/portals/TokenPortal.sol | 12 +- scripts/ci/get_e2e_jobs.sh | 1 + yarn-project/end-to-end/README.md | 2 + yarn-project/end-to-end/bootstrap.sh | 1 + .../end-to-end/scripts/e2e_test_config.yml | 4 +- .../e2e_token_bridge_tutorial_test.test.ts | 199 ++++++++++++ 21 files changed, 834 insertions(+), 779 deletions(-) create mode 100644 docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge.md delete mode 100644 docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/0_setup.md delete mode 100644 docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/1_depositing_to_aztec.md delete mode 100644 docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/2_minting_on_aztec.md delete mode 100644 docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/3_withdrawing_to_l1.md delete mode 100644 docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/4_typescript_glue_code.md delete mode 100644 docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/index.md create mode 100644 docs/docs/tutorials/codealong/contract_tutorials/token_bridge.md create mode 100644 docs/docs/tutorials/examples/uniswap/index.md create mode 100644 yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts diff --git a/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md b/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md index fd82ae2210d..5c35a86c5a4 100644 --- a/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md +++ b/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md @@ -106,7 +106,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/0_setup.md) and/or [Uniswap smart contract example that shows how to swap on L1 using funds on L2](../../../../tutorials/codealong/contract_tutorials/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/codealong/contract_tutorials/uniswap/index.md). Both examples show how to: 1. L1 -> L2 message flow 2. L2 -> L1 message flow diff --git a/docs/docs/developers/guides/smart_contracts/writing_contracts/portals/communicate_with_portal.md b/docs/docs/developers/guides/smart_contracts/writing_contracts/portals/communicate_with_portal.md index 7ea4d5fb30d..110393f05a2 100644 --- a/docs/docs/developers/guides/smart_contracts/writing_contracts/portals/communicate_with_portal.md +++ b/docs/docs/developers/guides/smart_contracts/writing_contracts/portals/communicate_with_portal.md @@ -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 @@ -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. @@ -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 diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge.md new file mode 100644 index 00000000000..a72978ecd52 --- /dev/null +++ b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge.md @@ -0,0 +1,294 @@ +--- +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. + +The first half of this page reviews the process and contracts for bridging token from Ethereum (L1) to Aztec (L2). The second half the page (starting with [Running with Aztec.js](#running-with-aztecjs)) goes over writing your own Typescript script for: + +- deploying and initializing contracts to L1 and L2 +- minting tokens on L1 +- sending tokens into the portal on L1 +- minting tokens on L2 +- sending tokens from L2 back to L1 +- withdrawing tokens from the L1 portal + +## Components + +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. 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. +- L2 contracts: + - `Token`: Manages the tokens on L2 + - `TokenBridge`: Manages the bridging of tokens between L2 and L1 + +`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. + +## How it works + +### Deposit to Aztec + +`TokenPortal.sol` passes messages to Aztec both publicly and privately. + +This diagram shows the logical flow of information among components involved in depositing to Aztec. + +```mermaid +sequenceDiagram + participant L1 User + participant L1 TokenPortal + participant L1 Aztec Inbox + participant L2 Bridge Contract + participant L2 Token Contract + + User->>TokenPortal: Deposit Tokens + + Note over TokenPortal: 1. Encode mint message
(recipient + amount)
2. Hash message to field
element (~254 bits) + + TokenPortal->>Aztec Inbox: Send message + Note over Aztec Inbox: Validates:
1. Recipient Aztec address
2. Aztec version
3. Message content hash
4. Secret hash + + Aztec Inbox-->>L2 Bridge Contract: Forward message + Note over L2 Bridge Contract: 1. Verify message
2. Process secret
3. Decode mint parameters + + L2 Bridge Contract->>L2 Token Contract: Call mint function + Note over L2 Token Contract: Mints tokens to
specified recipient +``` + +Message 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 and verified on the receiving contract. There is a utility function in the `Hash` library to hash messages (using `sha256`) to field elements. + +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 the corresponding L2 bridge contract processes the message. + +Note that because L1 is public, everyone can inspect and figure out the contentHash and the recipient contract address. + +#### `depositToAztecPublic` (TokenPortal.sol) + +#include_code deposit_public l1-contracts/test/portals/TokenPortal.sol solidity + +#### `depositToAztecPrivate` (TokenPortal.sol) + +#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 + +The previous code snippets moved 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. + +This happens inside the `TokenBridge` contract on Aztec. + +#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. + +::: + +The token bridge also allows tokens to be withdrawn back to L1 from L2. 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 difference between the two functions is that in the private domain we have to nullify user’s notes whereas in the public domain we subtract the balance from the user. + +#### `exit_to_L1_public` (TokenBridge.nr) + +#include_code exit_to_l1_public /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust + +#### `exit_to_L1_private` (TokenBridge.nr) + +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. + +### Claiming on L1 + +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 + +#### `token_portal_withdraw` (TokenPortal.sol) + +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](../../../../developers/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 +echo "nodeLinker: node-modules" > .yarnrc.yml +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 + +Run `yarn start` to confirm that all of the contracts are deployed. + +### 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 + +Run `yarn start` to confirm that tokens are claimed on Aztec. + +### 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. diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/0_setup.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/0_setup.md deleted file mode 100644 index 0a4d73b13e0..00000000000 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/0_setup.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -title: Setup and Installation ---- - -In this step, we’re going to - -1. Install prerequisites -2. Create a yarn project to house everything -3. Create a noir project for our Aztec contract -4. Create a hardhat project for our Ethereum contract(s) -5. Import all the Ethereum contracts we need -6. Create a yarn project that will interact with our contracts on L1 and the sandbox - -We recommend going through this setup to fully understand where things live. - -## Prerequisites - -- [node v18+ (GitHub link)](https://github.com/tj/n) -- [docker](https://docs.docker.com/) -- [Aztec sandbox](../../../../../developers/getting_started) - you should have this running before starting the tutorial - -Start the sandbox - -```bash -aztec start --sandbox -``` - -## Create the root project and packages - -Our root project will house everything ✨ - -```bash -mkdir aztec-token-bridge -cd aztec-token-bridge && mkdir packages -``` - -We will hold our projects inside of `packages` to follow the design of other projects. - -## Create a noir project - -Inside `packages` create a new directory `aztec-contracts`: - -```bash -cd packages && mkdir aztec-contracts -``` - -Inside `aztec-contracts` create a new contract project like this: - -```bash -cd aztec-contracts && aztec-nargo new --contract token_bridge -``` - -Your file structure should look something like this: - -```tree -aztec-contracts -└── token_bridge - ├── Nargo.toml - ├── src - ├── main.nr -``` - -Inside `Nargo.toml` add the following dependencies: - -```toml -[dependencies] -aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/aztec" } -token_portal_content_hash_lib = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/noir-contracts/contracts/token_portal_content_hash_lib" } -token = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/noir-contracts/contracts/token_contract" } -``` - -We will also be writing some helper functions that should exist elsewhere so we don't overcomplicated our contract. In `src` create one more file called `util.nr` - so your dir structure should now look like this: - -```tree -aztec-contracts -└── token_bridge - ├── Nargo.toml - ├── src - ├── main.nr - ├── util.nr -``` - -## Create a JS hardhat project - -In the `packages` dir, create a new directory called `l1-contracts` and run `yarn init -yp && -npx hardhat init` inside of it. Keep hitting enter so you get the default setup (Javascript project) - -```bash -mkdir l1-contracts -cd l1-contracts -yarn init -yp -npx hardhat init -``` - -Once you have a hardhat project set up, delete the existing contracts, tests, and scripts, and create a `TokenPortal.sol`: - -```bash -rm -rf contracts test scripts ignition -mkdir contracts && cd contracts -touch TokenPortal.sol -``` - -Now add dependencies that are required. These include interfaces to Aztec Inbox, Outbox and Registry smart contracts, OpenZeppelin contracts, and NomicFoundation. - -```bash -yarn add @aztec/foundation @aztec/l1-contracts @openzeppelin/contracts ethers && yarn add --dev @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan @types/chai @types/mocha @typechain/ethers-v5 @typechain/hardhat chai@4.0.0 hardhat-gas-reporter solidity-coverage ts-node typechain typescript @nomicfoundation/hardhat-ignition @nomicfoundation/hardhat-ignition-ethers @nomicfoundation/hardhat-verify -``` - -This is what your `l1-contracts` should look like: - -```tree -├── README.md -├── contracts -├── hardhat.config.js -├── node_modules -└── package.json -``` - -We will need to ensure we are using the correct Solidity version. Inside your `hardhat.config.js` update `solidity` version to this: - -```json - solidity: "0.8.20", -``` - -# Create src yarn project - -In this directory, we will write TS code that will interact with our L1 and L2 contracts and run them against the sandbox. - -We will use `viem` in this tutorial and `jest` for testing. - -Inside the `packages` directory, run - -```bash -mkdir src && cd src && yarn init -yp -yarn add typescript @aztec/aztec.js @aztec/accounts @aztec/noir-contracts.js @aztec/types @aztec/foundation @aztec/l1-artifacts viem@^2.7.15 "@types/node@^20.8.2" -yarn add -D jest @jest/globals ts-jest -``` - -If you are going to track this repo using git, consider adding a `.gitignore` file to your `src` directory and adding `node_modules` to it. - -In `package.json`, add: - -```json -"type": "module", -"scripts": { - "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest)" -}, -``` - -Your `package.json` should look something like this (do not copy and paste): - -```json -{ - "name": "src", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "private": true, - "type": "module", - "dependencies": { - "dep": "version" - }, - "devDependencies": { - "dep": "version" - }, - "scripts": { - "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest)" - } -} -``` - -Create a `tsconfig.json` and paste this: - -```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 - }, - "include": [ - "packages/src/**/*", - "contracts/**/*.json", - "packages/src/**/*", - "packages/aztec-contracts/token_bridge/target/*.json" - ], - "exclude": ["node_modules", "**/*.spec.ts", "contracts/**/*.ts"], - "references": [] -} -``` - -The main thing this will allow us to do is to access TS artifacts that we generate later from our test. - -Then create a jest config file: `jest.config.json` - -```json -{ - "preset": "ts-jest/presets/default-esm", - "globals": { - "ts-jest": { - "useESM": true - } - }, - "moduleNameMapper": { - "^(\\.{1,2}/.*)\\.js$": "$1" - }, - "testRegex": "./test/.*\\.test\\.ts$", - "rootDir": "./test" -} -``` - -Finally, we will create a test file. Run this in the `src` directory.: - -```bash -mkdir test && cd test -touch cross_chain_messaging.test.ts -``` - -Your `src` dir should look like: - -```tree -src -├── node_modules -└── test - └── cross_chain_messaging.test.ts -├── jest.config.json -├── package.json -├── tsconfig.json -``` - -In the next step, we’ll start writing our L1 smart contract with some logic to deposit tokens to Aztec from L1. diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/1_depositing_to_aztec.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/1_depositing_to_aztec.md deleted file mode 100644 index cb426b2ef98..00000000000 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/1_depositing_to_aztec.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Depositing Tokens to Aztec ---- - -In this step, we will write our token portal contract on L1. - -## Initialize Solidity contract - -In `l1-contracts/contracts` in your file called `TokenPortal.sol` paste this: - -```solidity -pragma solidity >=0.8.18; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -// Messaging -import {IRegistry} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol"; -import {IInbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol"; -import {IOutbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol"; -import {DataStructures} from "@aztec/l1-contracts/src/core/libraries/DataStructures.sol"; -import {Hash} from "@aztec/l1-contracts/src/core/libraries/Hash.sol"; - -#include_code init /l1-contracts/test/portals/TokenPortal.sol raw -``` - -This imports relevant files including the interfaces used by the Aztec rollup. And initializes the contract with the following parameters: - -- rollup registry address (that stores the current rollup, inbox and outbox contract addresses) -- The ERC20 token the portal corresponds to -- The address of the sister contract on Aztec to where the token will send messages to (for depositing tokens or from where to withdraw the tokens) - -Create a basic ERC20 contract that can mint tokens to anyone. We will use this to test. - -Create a file `TestERC20.sol` in the same folder and add: - -#include_code contract /l1-contracts/src/mock/TestERC20.sol solidity - -Replace the openzeppelin import with this: - -```solidity -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -``` - -## Depositing tokens to Aztec publicly - -Next, we will write a function that is used to deposit funds on L1 that a user may have into an Aztec portal and send a message to the Aztec rollup to mint tokens _publicly_ on Aztec. - -Paste this in `TokenPortal.sol` - -#include_code deposit_public /l1-contracts/test/portals/TokenPortal.sol solidity - -Here is an explanation of what it is doing: - -1. We first ask the registry for the inbox contract address (to which we send messages to) -2. We construct the “content” of the message we need to send to the recipient on Aztec. - - The content is limited to a single field (~254 bits). So if the content is larger, we have to hash it and the hash can be passed along. - - We use our utility method that creates a sha256 hash but truncates it to fit into a field - - Since we want to mint tokens on Aztec publicly, the content here is the amount to mint and the address on Aztec who will receive the tokens. - - We encode this message as a mint_to_public function call, to specify the exact intentions and parameters we want to execute on L2. - - In reality the content can be constructed in any manner as long as the sister contract on L2 can also create it. But for clarity, we are constructing the content like an ABI encoded function call. - - It is good practice to include all parameters used by L2 into this content (like the amount and to) so that a malicious actor can’t change the to to themselves when consuming the message. -3. The tokens are transferred from the user to the portal using `underlying.safeTransferFrom()`. This puts the funds under the portal's control. -4. Next we send the message to the inbox contract. The inbox expects the following parameters: - - recipient (called `actor` here), a struct: - - the sister contract address on L2 that can consume the message. - - The version - akin to THE chainID of Ethereum. By including a version, an ID, we can prevent replay attacks of the message (without this the same message might be replayable on other aztec networks that might exist). - - A secret hash (fit to a field element). This is mainly used in the private domain and the preimage of the hash doesn’t need to be secret for the public flow. When consuming the message, one must provide the preimage. More on this when we create the private flow for depositing tokens. -5. It returns a `bytes32 key` which is the id for this message in the Inbox. - -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. - -## Depositing tokens to Aztec privately - -Let’s do the similar for the private flow: - -#include_code deposit_private /l1-contracts/test/portals/TokenPortal.sol solidity - -Here we want to send a message to mint tokens privately on Aztec! Some key differences from the previous method are: - -- The content hash uses a different function name - `mint_to_private`. This is done to make it easy to separate concerns. If the contentHash between the public and private message was the same, then an attacker could consume a private message publicly! -- Like with the public flow, we move the user’s funds to the portal -- We now send the message to the inbox with the `recipient` (the sister contract on L2 along with the version of aztec the message is intended for) and the `secretHashForL2MessageConsumption` (such that on L2, the consumption of the message can be private). - -Note that because L1 is public, everyone can inspect and figure out the contentHash and the recipient contract address. - -**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. - -:::note -Secret hashes are Pedersen hashes since the hash has to be computed on L2 and sha256 hash is very expensive for zk circuits. The content hash however is a sha256 hash truncated to a field as shown before. -::: - -In the next step we will start writing our L2 smart contract to mint these tokens on L2. diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/2_minting_on_aztec.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/2_minting_on_aztec.md deleted file mode 100644 index 194628c5877..00000000000 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/2_minting_on_aztec.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Minting tokens on Aztec ---- - -In this step we will start writing our Aztec.nr bridge smart contract and write a function to consume the message from the token portal to mint funds on Aztec - -## Initial contract setup - -In our `token_bridge` Aztec project in `aztec-contracts`, under `src` there is an example `main.nr` file. Paste this to define imports: - -```rust -#include_code token_bridge_imports /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr raw -} -``` - -Inside this block (before the last `}`), paste this to initialize the constructor: - -#include_code token_bridge_storage_and_constructor /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust - -## Consume the L1 message - -In the previous step, we have moved our funds to the portal and created a L1->L2 message. Upon building the next rollup, the sequencer asks the inbox for any incoming messages and adds them to Aztec’s L1->L2 message tree, so an application on L2 can prove that the message exists and consumes it. - -In `main.nr`, now paste this `claim_public` function: -#include_code claim_public /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust - -The `claim_public` function enables anyone to consume the message on the user's behalf and mint tokens for them on L2. This is fine as the minting of tokens is done publicly anyway. - -**What’s happening here?** - -1. We first recompute the L1->L2 message content by calling `get_mint_to_public_content_hash()`. Note that the method does exactly the same as what the TokenPortal contract does in `depositToAztecPublic()` to create the content hash. -2. We then attempt to consume the L1->L2 message. Since we are depositing to Aztec publicly, all of the inputs are public. - - `context.consume_l1_to_l2_message()` takes in the few parameters: - - `content_hash`: The content - which is reconstructed in the `get_mint_to_public_content_hash()` - - `secret`: The secret used for consumption, often 0 for public messages - - `sender`: Who on L1 sent the message. Which should match the stored `portal_address` in our case as we only want to allow messages from a specific sender. - - `message_leaf_index`: The index in the message tree of the message. - - Note that the `content_hash` requires `to` and `amount`. If a malicious user tries to mint tokens to their address by changing the to address, the content hash will be different to what the token portal had calculated on L1 and thus not be in the tree, failing the consumption. This is why we add these parameters into the content. -3. Then we call `Token::at(storage.token.read()).mint_to_public()` to mint the tokens to the to address. - -## Private flow - -Now we will create a function to mint the amount privately. Paste this into your `main.nr` - -#include_code claim_private /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust - -The `get_mint_to_private_content_hash` function is imported from the `token_portal_content_hash_lib`. - -If the content hashes were constructed similarly for `mint_to_private` and `mint_to_public`, then content intended for private execution could have been consumed by calling the `claim_public` method. By making these two content hashes distinct, we prevent this scenario. - -Note that the `TokenBridge` contract should be an authorized minter in the corresponding `Token` contract so that it is able to complete the private mint to the intended recipient. - -In the next step we will see how we can cancel a message. diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/3_withdrawing_to_l1.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/3_withdrawing_to_l1.md deleted file mode 100644 index cac5c058289..00000000000 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/3_withdrawing_to_l1.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Withdrawing to L1 ---- - -This is where we have tokens on Aztec and want to withdraw them back to L1 (i.e. burn them on L2 and mint on L1). Withdrawing from L1 will be public. - -## Withdrawing publicly - -In your `main.nr` paste this: - -#include_code exit_to_l1_public /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust - -For this to work we import the `get_withdraw_content_hash` helper function from the `token_portal_content_hash_lib`. - -**What’s happening here?** - -The `exit_to_l1_public` function enables anyone to withdraw their L2 tokens back to L1 publicly. This is done by burning tokens on L2 and then creating an L2->L1 message. - -1. Like with our deposit function, we need to create the L2 to L1 message. The 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. -2. `context.message_portal()` passes this content to the kernel circuit which creates the proof for the transaction. The kernel circuit then adds the sender (the L2 address of the bridge + version of aztec) and the recipient (the portal to the L2 address + the chain ID of L1) under the hood, to create the message which gets added as part of the transaction data published by the sequencer and is stored in the outbox for consumption. -3. The `context.message_portal()` takes the recipient and content as input, and will insert a message into the outbox. We set the recipient to be the portal address read from storage of the contract. -4. Finally, you also burn the tokens on L2! Note that it burning is done at the end to follow the check effects interaction pattern. Note that the caller has to first approve the bridge contract to burn tokens on its behalf. Refer to `burn_public` function on the [token contract](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/token_contract/src/main.nr). - - We burn the tokens from the `msg_sender()`. Otherwise, a malicious user could burn someone else’s tokens and mint tokens on L1 to themselves. One could add another approval flow on the bridge but that might make it complex for other applications to call the bridge. - -## Withdrawing Privately - -This function works very similarly to the public version, except here we burn user’s private notes. Under the public function in your `main.nr`, paste this: - -#include_code exit_to_l1_private /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust - -#include_code assert_token_is_same /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. - -Once again, a user must sign an approval message to let the contract burn tokens on their behalf. The nonce refers to this approval message. - -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. - -## Withdrawing on L1 - -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? - -Paste this in your `TokenPortal.sol`: - -```solidity -#include_code token_portal_withdraw /l1-contracts/test/portals/TokenPortal.sol raw -} -``` - -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. - -Before we can compile and use the contract, we need to add 1 additional function. - -We need a function that lets us read the token value. Paste this into `main.nr`: - -#include_code get_config /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust - -## Compile code - -Congratulations, you have written all the contracts we need for this tutorial! Now let's compile them. - -Compile your Solidity contracts using hardhat. Run this in the `packages` directory: - -```bash -cd l1-contracts -yarn hardhat compile -``` - -And compile your Aztec.nr contracts like this: - -```bash -cd aztec-contracts/token_bridge -aztec-nargo compile -``` - -You may get some unused variable warnings - you can ignore these. - -And generate the TypeScript interface for the contract and add it to the test dir. Run this inside `aztec-contracts/token_bridge`: - -```bash -aztec codegen target -o ../../src/test/fixtures -``` - -This will create a TS interface inside `fixtures` dir in our `src/test` folder! - -In the next step we will write the TypeScript code to deploy our contracts and call on both L1 and L2 so we can see how everything works together. diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/4_typescript_glue_code.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/4_typescript_glue_code.md deleted file mode 100644 index 7af4ab182d6..00000000000 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/4_typescript_glue_code.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: Deploy & Call Contracts with Typescript ---- - -In this step we will write a Typescript test to interact with the sandbox and call our contracts! - -## Test imports and setup - -We need some helper files that can keep our code clean. Inside your `src/test` directory: - -```bash -mkdir shared && cd shared -touch cross_chain_test_harness.ts -``` - -In `cross_chain_test_harness.ts`, add: - -```ts -import { expect } from '@jest/globals' -#include_code cross_chain_test_harness /yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts raw -``` - -This - -- gets your Solidity contract ABIs -- uses Aztec.js to deploy them to Ethereum -- uses Aztec.js to deploy the token and token bridge contract on L2, sets the bridge's portal address to `tokenPortalAddress` and initializes all the contracts -- exposes easy to use helper methods to interact with our contracts. - -Now let's write our tests. - -We will write two tests: - -1. Test the deposit and withdraw in the private flow -2. Do the same in the public flow - -Open `cross_chain_messaging.test.ts` and paste the initial description of the test: - -```typescript -import { beforeAll, describe, beforeEach, expect, jest, it} from '@jest/globals' -import { AccountWallet, AztecAddress, BatchCall, type Logger, EthAddress, Fr, computeAuthWitMessageHash, createLogger, createPXEClient, waitForPXE, L1ToL2Message, L1Actor, L2Actor, type PXE, type Wallet } from '@aztec/aztec.js'; -import { getInitialTestAccountsWallets } from '@aztec/accounts/testing'; -import { TokenContract } from '@aztec/noir-contracts.js/Token'; -import { sha256ToField } from '@aztec/foundation/crypto'; -import { TokenBridgeContract } from './fixtures/TokenBridge.js'; -import { createAztecNodeClient } from '@aztec/circuit-types'; -import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; -import { SchnorrAccountContractArtifact } from '@aztec/accounts/schnorr'; - -import { CrossChainTestHarness } from './shared/cross_chain_test_harness.js'; -import { mnemonicToAccount } from 'viem/accounts'; -import { createPublicClient, createWalletClient, http, toFunctionSelector } from 'viem'; -import { foundry } from 'viem/chains'; - -const { PXE_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; -const MNEMONIC = 'test test test test test test test test test test test junk'; -const hdAccount = mnemonicToAccount(MNEMONIC); -const aztecNode = createAztecNodeClient(PXE_URL); -export const NO_L1_TO_L2_MSG_ERROR = - /No non-nullified L1 to L2 message found for message hash|Tried to consume nonexistent L1-to-L2 message/; - -async function publicDeployAccounts(sender: Wallet, accountsToDeploy: Wallet[], pxe: PXE) { - const accountAddressesToDeploy = await Promise.all( - accountsToDeploy.map(async a => { - const address = await a.getAddress(); - const isDeployed = await pxe.isContractPubliclyDeployed(address); - return { address, isDeployed }; - }) - ).then(results => results.filter(result => !result.isDeployed).map(result => result.address)); - if (accountAddressesToDeploy.length === 0) return - const instances = await Promise.all(accountAddressesToDeploy.map(account => sender.getContractInstance(account))); - const batch = new BatchCall(sender, [ - (await registerContractClass(sender, SchnorrAccountContractArtifact)).request(), - ...instances.map(instance => deployInstance(sender, instance!).request()), - ]); - await batch.send().wait(); -} - -describe('e2e_cross_chain_messaging', () => { - jest.setTimeout(90_000); - - let logger: Logger; - let wallets: AccountWallet[]; - let user1Wallet: AccountWallet; - let user2Wallet: AccountWallet; - let ethAccount: EthAddress; - let ownerAddress: AztecAddress; - - let crossChainTestHarness: CrossChainTestHarness; - let l2Token: TokenContract; - let l2Bridge: TokenBridgeContract; - let outbox: any; - - beforeAll(async () => { - logger = createLogger('aztec:e2e_uniswap'); - const pxe = createPXEClient(PXE_URL); - await waitForPXE(pxe); - wallets = await getInitialTestAccountsWallets(pxe); - - // deploy the accounts publicly to use public authwits - await publicDeployAccounts(wallets[0], wallets, pxe); - }) - - beforeEach(async () => { - logger = createLogger('aztec:e2e_uniswap'); - const pxe = createPXEClient(PXE_URL); - await waitForPXE(pxe); - - const walletClient = createWalletClient({ - account: hdAccount, - chain: foundry, - transport: http(ETHEREUM_HOST), - }); - const publicClient = createPublicClient({ - chain: foundry, - transport: http(ETHEREUM_HOST), - }); - - crossChainTestHarness = await CrossChainTestHarness.new( - aztecNode, - pxe, - publicClient, - walletClient, - wallets[0], - logger, - ); - - l2Token = crossChainTestHarness.l2Token; - l2Bridge = crossChainTestHarness.l2Bridge; - ethAccount = crossChainTestHarness.ethAccount; - ownerAddress = crossChainTestHarness.ownerAddress; - outbox = crossChainTestHarness.outbox; - user1Wallet = wallets[0]; - user2Wallet = wallets[1]; - }); -``` - -This fetches the wallets from the sandbox and deploys our cross chain harness on the sandbox! - -## Private flow test - -Paste the private flow test below the setup: - -#include_code e2e_private_cross_chain /yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts typescript - -## Public flow test - -Paste the public flow below the private flow: - -```ts -#include_code e2e_public_cross_chain /yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts raw -}) -``` - -## Running the test - -```bash -cd packages/src -DEBUG='aztec:e2e_uniswap' yarn test -``` - -Note - you might have a jest error at the end of each test saying "expected 1-2 arguments but got 3". In case case simply remove the "120_000" at the end of each test. We have already set the timeout at the top so this shouldn't be a problem. - -**Congratulations!** You have just written a set of contracts for Ethereum and Aztec that have private and public interactions with each other, and tested them with TypeScript. - -## Next Steps - -### Follow a more detailed Aztec.js tutorial - -Follow the tutorial [here](../../js_tutorials/aztecjs-getting-started.md). - -### Optional: Learn more about concepts mentioned here - -- [Functions under the hood (concepts)](../../../../../aztec/smart_contracts/functions/function_transforms.md) diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/index.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/index.md deleted file mode 100644 index b853430a2a1..00000000000 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/token_bridge/index.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Token Bridge -sidebar_position: 6 ---- - -import Image from "@theme/IdealImage"; - -In this tutorial, we will learn how to build the entire flow of a cross-chain token using portals. - -## A refresher on Portals - -A portal is the point of contact between L1 and a specific contract on Aztec. It allows for arbitrary message passing between L1 and Aztec, siloed just for the portal contract and its sister contract on Aztec. For applications such as token bridges, this is the point where the tokens are held on L1 while used in L2. - -### But why? - -Before portals, you had legos either just on L1 or just on L2. But for cross-chain, there was no arbitrary message passing bridge that didn’t introduce their own trust assumptions. - -Portals change this. With portals you can now have arbitrary execution across L1 and L2, paving the ground for seamless trustless composability with L1 and L2 legos, without introducing any additional trust assumptions across the L1 or Aztec network. - -That means your L1 application can have a sister application residing on L2 and both of them can work together across the two networks. - - - -### Cheap and private access to Ethereum - -Using portals, you could implement Aztec Connect-like functionality where you could deposit funds into a variety of DeFi protocols that reside on Ethereum even though your funds are on Aztec. This enables cheaper and private usage of existing dapps on Ethereum and prevents liquidity fragmentation. - -You could swap your L2 WETH into DAI on Uniswap on Ethereum and get the DAI on Aztec. Similarly, you could stake your L2 ETH into Lido on Ethereum and get stETH on Aztec! - -### L1\<\>L2 communication on Aztec - -Aztec has the following core smart contracts on L1 that we need to know about: - -- `Rollup.sol` - stores the current state of the rollup and includes logic to progress the rollup (i.e. the state transition function) -- `Inbox.sol` - a mailbox to the rollup for L1 to L2 messages (e.g. depositing tokens). Portals put messages into the box, and the sequencers then decide which of these messages they want to include in their blocks, based on the inclusion fees they receive. -- `Outbox.sol` - a mailbox to the rollup for L2 to L1 messages (e.g. withdrawing tokens). Aztec contracts emit these messages and the sequencer adds these to the outbox. Portals then consume these messages. -- `Registry.sol` - just like L1, we assume there will be various versions of Aztec (due to upgrades, forks etc). In such a case messages must not be replayable in other Aztec “domains”. A portal must decide which version/ID of Aztec the message is for. The registry stores the rollup, inbox and outbox address for each version of Aztec deployments, so the portal can find out the address of the mailbox it wants to talk to - -## Building a Token Bridge with Portals - -The goal for this tutorial is to create functionality such that a token can be bridged to and from Aztec. We’ll be using L1 to refer to Ethereum and L2 to refer to Aztec. - -This is just a reference implementation for educational purposes only. It has not been through an in-depth security audit. - -Let’s assume a token exists on Ethereum and Aztec (see [the example token contract](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/token_contract/src/main.nr)). - -We will build: - -- a `Token Portal` Solidity contract on L1 that will be responsible for sending messages to the Inbox and consuming from the Outbox. -- a `Token Bridge` Aztec.nr contract on L2 that can consume L1 to L2 messages to mint tokens on L2 and create L2 to L1 messages to withdraw tokens back to L1. -- Some TypeScript code that can call the methods on the contracts and communicate with the sandbox. - -Our contracts will be able to work with _both_ private and public state i.e. how to deposit tokens into Aztec privately and publicly and withdraw tokens privately and publicly. - - - -This just shows the private flow. The green is the deposit to L2 flow, while the red is the withdrawal from L2 flow. The blue user represents an operator - a 3rd person who can act on behalf of the user! - -The token portal resides on L1 and must be able to deposit tokens to Aztec (both privately and publicly). It must also be able to withdraw funds from Aztec and cancel any deposit messages (L1->L2 messages) should the user change their mind or if the message wasn’t picked up on time. - -The token bridge resides on L2 and is the “sister” contract that can claim the deposit message to mint tokens on L2 (publicly or privately). Similarly, it should be able to burn tokens on L2 and withdraw them on L1. - -More about the flow will be clear as we code along! In the next section, we’ll set up our Ethereum and Aztec environments. diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/index.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/index.md index 1e3e804f06c..a8e97a18ef0 100644 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/index.md +++ b/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/index.md @@ -15,11 +15,12 @@ The flow will be: 2. We create an L2 → L1 message to swap on L1 3. On L1, the user gets their input tokens, consumes the swap message, and executes the swap 4. The user deposits the “output” tokens to the output token portal so it can be deposited into L2 -5. We will assume that token portals and token bridges for the input and output tokens must exist. These are what we built in the [token bridge tutorial](../token_bridge/index.md). +5. We will assume that token portals and token bridges for the input and output tokens must exist. These are what we built in the [token bridge tutorial](../token_bridge.md). The execution of swap on L1 should be designed such that any 3rd party can execute the swap on behalf of the user. This helps maintain user privacy by not requiring links between L1 and L2 activity. This reference will cover: + 1. Uniswap Portal - a contract on L1 that talks to the input token portal to withdraw the assets, executes the swap, and deposits the swapped tokens back to L2 2. Uniswap L2 contract - a contract on L2 that creates the needed messages to perform the swap on L1 @@ -27,5 +28,4 @@ This reference will cover: This diagram describes the private flow. -This code works alongside a token portal that you can learn to build [in this codealong tutorial](../token_bridge/index.md). - +This code works alongside a token portal that you can learn to build [in this codealong tutorial](../token_bridge.md). diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l1_contract.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l1_contract.md index eec465e9a20..88d229e58b9 100644 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l1_contract.md +++ b/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l1_contract.md @@ -3,7 +3,7 @@ title: L1 contracts (EVM) sidebar_position: 2 --- -This page goes over the code in the L1 contract for Uniswap, which works alongside a [token portal (codealong tutorial)](../token_bridge/index.md). +This page goes over the code in the L1 contract for Uniswap, which works alongside a [token portal (codealong tutorial)](../token_bridge.md). ## Setup diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l2_contract.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l2_contract.md index e448ead8861..e8c8315d8de 100644 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l2_contract.md +++ b/docs/docs/developers/tutorials/codealong/contract_tutorials/uniswap/l2_contract.md @@ -3,7 +3,7 @@ title: L2 Contracts (Aztec) sidebar_position: 1 --- -This page goes over the code in the L2 contract for Uniswap, which works alongside a [token bridge (codealong tutorial)](../token_bridge/index.md). +This page goes over the code in the L2 contract for Uniswap, which works alongside a [token bridge (codealong tutorial)](../token_bridge.md). ## Main.nr @@ -20,7 +20,7 @@ We just need to store the portal address for the token that we want to swap. 2. We fetch the underlying aztec token that needs to be swapped. 3. We transfer the user’s funds to the Uniswap contract. Like with Ethereum, the user must have provided approval to the Uniswap contract to do so. The user must provide the nonce they used in the approval for transfer, so that Uniswap can send it to the token contract, to prove it has appropriate approval. 4. Funds are added to the Uniswap contract. -5. Uniswap must exit the input tokens to L1. For this it has to approve the bridge to burn its tokens on its behalf and then actually exit the funds. We call the [`exit_to_l1_public()` method on the token bridge](../token_bridge/3_withdrawing_to_l1.md). We use the public flow for exiting since we are operating on public state. +5. Uniswap must exit the input tokens to L1. For this it has to approve the bridge to burn its tokens on its behalf and then actually exit the funds. We call the [`exit_to_l1_public()` method on the token bridge](../token_bridge.md). We use the public flow for exiting since we are operating on public state. 6. It is not enough for us to simply emit a message to withdraw the funds. We also need to emit a message to display our swap intention. If we do not do this, there is nothing stopping a third party from calling the Uniswap portal with their own parameters and consuming our message. So the Uniswap portal (on L1) needs to know: diff --git a/docs/docs/tutorials/codealong/contract_tutorials/token_bridge.md b/docs/docs/tutorials/codealong/contract_tutorials/token_bridge.md new file mode 100644 index 00000000000..1defa5cd08b --- /dev/null +++ b/docs/docs/tutorials/codealong/contract_tutorials/token_bridge.md @@ -0,0 +1,261 @@ +--- +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. + +The first half of this page reviews the process and contracts for bridging token from Ethereum (L1) to Aztec (L2). The second half the page (starting with [Running with Aztec.js](#running-with-aztecjs)) goes over writing your own Typescript script for: + +- deploying and initializing contracts to L1 and L2 +- minting tokens on L1 +- sending tokens into the portal on L1 +- minting tokens on L2 +- sending tokens from L2 back to L1 +- withdrawing tokens from the L1 portal + +## Components + +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](../../../developers/getting_started.md#prerequisites) and the sandbox. + +:::warning + +Make sure you are using Node.js version 18.x. + +::: + +### 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": { + "target": "es2020", + "lib": ["dom", "esnext", "es2017.object"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": 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. diff --git a/docs/docs/tutorials/examples/uniswap/index.md b/docs/docs/tutorials/examples/uniswap/index.md new file mode 100644 index 00000000000..30707fe3fe0 --- /dev/null +++ b/docs/docs/tutorials/examples/uniswap/index.md @@ -0,0 +1,31 @@ +--- +title: Overview +sidebar_position: 0 +--- + +import Image from "@theme/IdealImage"; + +# Swap on L1 Uniswap from L2 + +This smart contract example allows someone with funds on L2 to be able to swap using L1 Uniswap and then get the swapped assets back to L2. In this example, L1 will refer to Ethereum and L2 will refer to Aztec. + +The flow will be: + +1. The user withdraws their “input” assets to L1 (i.e. burn them on L2 and create a L2 to L1 message to withdraw) +2. We create an L2 → L1 message to swap on L1 +3. On L1, the user gets their input tokens, consumes the swap message, and executes the swap +4. The user deposits the “output” tokens to the output token portal so it can be deposited into L2 +5. We will assume that token portals and token bridges for the input and output tokens must exist. These are what we built in the [token bridge tutorial](../../codealong/contract_tutorials/token_bridge.md). + +The execution of swap on L1 should be designed such that any 3rd party can execute the swap on behalf of the user. This helps maintain user privacy by not requiring links between L1 and L2 activity. + +This reference will cover: + +1. Uniswap Portal - a contract on L1 that talks to the input token portal to withdraw the assets, executes the swap, and deposits the swapped tokens back to L2 +2. Uniswap L2 contract - a contract on L2 that creates the needed messages to perform the swap on L1 + + + +This diagram describes the private flow. + +This code works alongside a token portal that you can learn to build [in this codealong tutorial](../../codealong/contract_tutorials/token_bridge.md). diff --git a/docs/netlify.toml b/docs/netlify.toml index 5dba9ffe4bb..a3063898798 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -22,30 +22,6 @@ from = "/tutorials/simple_dapp/*" to = "developers/tutorials/codealong/js_tutorials/simple_dapp" -[[redirects]] - from = "/tutorials/contract_tutorials//token_bridge/typescript_glue_code" - to = "/tutorials/codealong/contract_tutorials/token_bridge/typescript_glue_code" - -[[redirects]] - from = "/tutorials/contract_tutorials//token_bridge/withdrawing_to_l1" - to = "/tutorials/codealong/contract_tutorials/token_bridge/withdrawing_to_l1" - -[[redirects]] - from = "/tutorials/contract_tutorials//token_bridge/minting_on_aztec" - to = "/tutorials/codealong/contract_tutorials/token_bridge/minting_on_aztec" - -[[redirects]] - from = "/tutorials/contract_tutorials//token_bridge/depositing_to_aztec" - to = "/tutorials/codealong/contract_tutorials/token_bridge/depositing_to_aztec" - -[[redirects]] - from = "/tutorials/contract_tutorials//token_bridge" - to = "/tutorials/codealong/contract_tutorials/token_bridge" - -[[redirects]] - from = "/tutorials/contract_tutorials//token_bridge/setup" - to = "/tutorials/codealong/contract_tutorials/token_bridge/setup" - [[redirects]] from = "/tutorials/contract_tutorials/crowdfunding_contract" to = "/tutorials/codealong/contract_tutorials/crowdfunding_contract" @@ -140,60 +116,64 @@ [[redirects]] from = "/reference/sandbox_reference/sandbox-reference" - to = "/developers/getting_started" + to = "/developers/getting_started" [[redirects]] from = "/reference/sandbox_reference" - to = "/developers/getting_started" + to = "/developers/getting_started" [[redirects]] from = "/guides/getting_started/*" - to = "/developers/getting_started/*" + to = "/developers/getting_started/*" [[redirects]] from = "/guides/developer_guides/getting_started/quickstart" - to = "/developers/getting_started" - + to = "/developers/getting_started" + [[redirects]] from = "/aztec/concepts_overview" - to = "/aztec" + to = "/aztec" [[redirects]] from = "/aztec/concepts/accounts/authwit" - to = "/aztec/concepts/advanced/authwit" + to = "/aztec/concepts/advanced/authwit" [[redirects]] from = "/aztec/concepts/storage/storage_slots" - to = "/aztec/concepts/advanced/storage/storage_slots" + to = "/aztec/concepts/advanced/storage/storage_slots" [[redirects]] from = "/aztec/concepts/storage/trees/*" - to = "/aztec/concepts/advanced/storage/indexed_merkle_tree" + to = "/aztec/concepts/advanced/storage/indexed_merkle_tree" [[redirects]] from = "/aztec/concepts/storage/partial_notes" - to = "/aztec/concepts/advanced/storage/partial_notes" + to = "/aztec/concepts/advanced/storage/partial_notes" [[redirects]] from = "/aztec/concepts/circuits/*" - to = "/aztec/concepts/advanced/circuits" + to = "/aztec/concepts/advanced/circuits" [[redirects]] from = "/tutorials/*" - to = "/developers/tutorials/*" + to = "/developers/tutorials/*" [[redirects]] from = "/guides/*" - to = "/developers/guides/*" + to = "/developers/guides/*" [[redirects]] from = "/reference/*" - to = "/developers/reference/*" + to = "/developers/reference/*" [[redirects]] from = "/guides/privacy_considerations" - to = "/developers/reference/considerations/privacy_considerations" + to = "/developers/reference/considerations/privacy_considerations" [[redirects]] from = "/reference/developer_references/limitations" - to = "/developers/reference/considerations/limitations" \ No newline at end of file + to = "/developers/reference/considerations/limitations" + +[[redirects]] + from = "/tutorials/codealong/contract_tutorials/token_bridge/*" + to = "/tutorials/codealong/contract_tutorials/token_bridge" \ No newline at end of file diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index 115132e5234..0b8e82924bb 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -13,7 +13,6 @@ import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; // docs:end:content_hash_sol_import -// docs:start:init contract TokenPortal { using SafeERC20 for IERC20; @@ -29,6 +28,13 @@ contract TokenPortal { IERC20 public underlying; bytes32 public l2Bridge; + /** + * @notice Initialize the portal + * @param _registry - The registry address + * @param _underlying - The underlying token address + * @param _l2Bridge - The L2 bridge address + */ + // docs:start:init function initialize(address _registry, address _underlying, bytes32 _l2Bridge) external { registry = IRegistry(_registry); underlying = IERC20(_underlying); @@ -47,6 +53,7 @@ contract TokenPortal { function depositToAztecPublic(bytes32 _to, uint256 _amount, bytes32 _secretHash) external returns (bytes32, uint256) + // docs:end:deposit_public { // Preamble IInbox inbox = IRollup(registry.getRollup()).INBOX(); @@ -69,7 +76,6 @@ contract TokenPortal { return (key, index); } - // docs:end:deposit_public // docs:start:deposit_private /** @@ -81,6 +87,7 @@ contract TokenPortal { function depositToAztecPrivate(uint256 _amount, bytes32 _secretHashForL2MessageConsumption) external returns (bytes32, uint256) + // docs:end:deposit_private { // Preamble IInbox inbox = IRollup(registry.getRollup()).INBOX(); @@ -103,7 +110,6 @@ contract TokenPortal { return (key, index); } - // docs:end:deposit_private // docs:start:token_portal_withdraw /** diff --git a/scripts/ci/get_e2e_jobs.sh b/scripts/ci/get_e2e_jobs.sh index 3551478a466..f79869b3eef 100755 --- a/scripts/ci/get_e2e_jobs.sh +++ b/scripts/ci/get_e2e_jobs.sh @@ -37,6 +37,7 @@ allow_list=( "e2e_ordering" "e2e_pruned_blocks" "e2e_static_calls" + "e2e_token_bridge_tutorial_test" "integration_l1_publisher" "e2e_cheat_codes" "e2e_prover_fake_proofs" diff --git a/yarn-project/end-to-end/README.md b/yarn-project/end-to-end/README.md index 5af11b9a5bc..ee91fb01da2 100644 --- a/yarn-project/end-to-end/README.md +++ b/yarn-project/end-to-end/README.md @@ -22,3 +22,5 @@ yarn test:integration which will spawn the two processes. You can also run this by `docker-compose up` which will spawn 2 different containers for Anvil and the test runner. + +You can run a single test by running `yarn test:compose `. diff --git a/yarn-project/end-to-end/bootstrap.sh b/yarn-project/end-to-end/bootstrap.sh index a65ec8c0550..14aeafc66e2 100755 --- a/yarn-project/end-to-end/bootstrap.sh +++ b/yarn-project/end-to-end/bootstrap.sh @@ -82,6 +82,7 @@ function test_cmds { echo "$run_test compose composed/e2e_pxe" echo "$run_test compose composed/e2e_sandbox_example" echo "$run_test compose composed/integration_l1_publisher" + echo "$run_test compose e2e_token_bridge_tutorial_test" echo "$run_test compose sample-dapp/index" echo "$run_test compose sample-dapp/ci/index" echo "$run_test compose guides/dapp_testing" diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml index 13bc4781cde..7ee6e3bc884 100644 --- a/yarn-project/end-to-end/scripts/e2e_test_config.yml +++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml @@ -27,7 +27,7 @@ tests: test_path: 'e2e_fees/account_init.test.ts' # TODO(https://github.com/AztecProtocol/aztec-packages/issues/9488): reenable # e2e_fees_dapp_subscription: - # test_path: "e2e_fees/dapp_subscription.test.ts" + # test_path: 'e2e_fees/dapp_subscription.test.ts' e2e_fees_failures: test_path: 'e2e_fees/failures.test.ts' e2e_fees_fee_juice_payments: @@ -71,6 +71,8 @@ tests: e2e_state_vars: {} e2e_static_calls: {} e2e_synching: {} + e2e_token_bridge_tutorial_test: + use_compose: true e2e_token_contract: with_alerts: true e2e_p2p_gossip: diff --git a/yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts b/yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts new file mode 100644 index 00000000000..cf187c0621a --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts @@ -0,0 +1,199 @@ +// This test should only use packages that are published to npm +// docs:start:imports +import { getInitialTestAccountsWallets } from '@aztec/accounts/testing'; +import { + EthAddress, + Fr, + L1TokenManager, + L1TokenPortalManager, + createLogger, + createPXEClient, + waitForPXE, +} from '@aztec/aztec.js'; +import { createL1Clients, deployL1Contract } from '@aztec/ethereum'; +import { TestERC20Abi, TestERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } from '@aztec/l1-artifacts'; +import { TokenContract } from '@aztec/noir-contracts.js/Token'; +import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge'; + +import { getContract } from 'viem'; + +// docs:end:imports +// docs:start:utils +const MNEMONIC = 'test test test test test test test test test test test junk'; +const { ETHEREUM_HOST = 'http://localhost:8545' } = process.env; + +const { walletClient, publicClient } = createL1Clients(ETHEREUM_HOST, MNEMONIC); +const ownerEthAddress = walletClient.account.address; + +const setupSandbox = async () => { + const { PXE_URL = 'http://localhost:8080' } = process.env; + // eslint-disable-next-line @typescript-eslint/await-thenable + const pxe = await createPXEClient(PXE_URL); + await waitForPXE(pxe); + return pxe; +}; + +async function deployTestERC20(): Promise { + const constructorArgs = ['Test Token', 'TEST', walletClient.account.address]; + + return await deployL1Contract(walletClient, publicClient, TestERC20Abi, TestERC20Bytecode, constructorArgs).then( + ({ address }) => address, + ); +} + +async function deployTokenPortal(): Promise { + return await deployL1Contract(walletClient, publicClient, TokenPortalAbi, TokenPortalBytecode, []).then( + ({ address }) => address, + ); +} +// docs:end:utils + +describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { + it('Deploys tokens & bridges to L1 & L2, mints & publicly bridges tokens', async () => { + // docs:start:setup + const logger = createLogger('aztec:token-bridge-tutorial'); + const amount = BigInt(100); + const pxe = await setupSandbox(); + const wallets = await getInitialTestAccountsWallets(pxe); + const ownerWallet = wallets[0]; + const ownerAztecAddress = wallets[0].getAddress(); + const l1ContractAddresses = (await pxe.getNodeInfo()).l1ContractAddresses; + logger.info('L1 Contract Addresses:'); + logger.info(`Registry Address: ${l1ContractAddresses.registryAddress}`); + logger.info(`Inbox Address: ${l1ContractAddresses.inboxAddress}`); + logger.info(`Outbox Address: ${l1ContractAddresses.outboxAddress}`); + logger.info(`Rollup Address: ${l1ContractAddresses.rollupAddress}`); + // docs:end:setup + + // Deploy L2 token contract + // docs:start:deploy-l2-token + const l2TokenContract = await TokenContract.deploy(ownerWallet, ownerAztecAddress, 'L2 Token', 'L2', 18) + .send() + .deployed(); + logger.info(`L2 token contract deployed at ${l2TokenContract.address}`); + // docs:end:deploy-l2-token + + // Deploy L1 token contract & mint tokens + // docs:start:deploy-l1-token + const l1TokenContract = await deployTestERC20(); + logger.info('erc20 contract deployed'); + + const l1TokenManager = new L1TokenManager(l1TokenContract, publicClient, walletClient, logger); + // docs:end:deploy-l1-token + + // Deploy L1 portal contract + // docs:start:deploy-portal + const l1PortalContractAddress = await deployTokenPortal(); + logger.info('L1 portal contract deployed'); + + const l1Portal = getContract({ + address: l1PortalContractAddress.toString(), + abi: TokenPortalAbi, + client: walletClient, + }); + // docs:end:deploy-portal + // Deploy L2 bridge contract + // docs:start:deploy-l2-bridge + const l2BridgeContract = await TokenBridgeContract.deploy( + ownerWallet, + l2TokenContract.address, + l1PortalContractAddress, + ) + .send() + .deployed(); + logger.info(`L2 token bridge contract deployed at ${l2BridgeContract.address}`); + // docs:end:deploy-l2-bridge + + // Set Bridge as a minter + // docs:start:authorize-l2-bridge + await l2TokenContract.methods.set_minter(l2BridgeContract.address, true).send().wait(); + // docs:end:authorize-l2-bridge + + // Initialize L1 portal contract + // docs:start:setup-portal + await l1Portal.write.initialize( + [l1ContractAddresses.registryAddress.toString(), l1TokenContract.toString(), l2BridgeContract.address.toString()], + {}, + ); + logger.info('L1 portal contract initialized'); + + const l1PortalManager = new L1TokenPortalManager( + l1PortalContractAddress, + l1TokenContract, + l1ContractAddresses.outboxAddress, + publicClient, + walletClient, + logger, + ); + // docs:end:setup-portal + + // docs:start:l1-bridge-public + const claim = await l1PortalManager.bridgeTokensPublic(ownerAztecAddress, amount, true); + + // Do 2 unrleated actions because + // https://github.com/AztecProtocol/aztec-packages/blob/7e9e2681e314145237f95f79ffdc95ad25a0e319/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts#L354-L355 + await l2TokenContract.methods.mint_to_public(ownerAztecAddress, 0n).send().wait(); + await l2TokenContract.methods.mint_to_public(ownerAztecAddress, 0n).send().wait(); + // docs:end:l1-bridge-public + + // Claim tokens publicly on L2 + // docs:start:claim + await l2BridgeContract.methods + .claim_public(ownerAztecAddress, amount, claim.claimSecret, claim.messageLeafIndex) + .send() + .wait(); + const balance = await l2TokenContract.methods.balance_of_public(ownerAztecAddress).simulate(); + logger.info(`Public L2 balance of ${ownerAztecAddress} is ${balance}`); + // docs:end:claim + + logger.info('Withdrawing funds from L2'); + + // docs:start:setup-withdrawal + const withdrawAmount = 9n; + const nonce = Fr.random(); + + // Give approval to bridge to burn owner's funds: + const authwit = await ownerWallet.setPublicAuthWit( + { + caller: l2BridgeContract.address, + action: l2TokenContract.methods.burn_public(ownerAztecAddress, withdrawAmount, nonce), + }, + true, + ); + await authwit.send().wait(); + // docs:end:setup-withdrawal + + // docs:start:l2-withdraw + const l2ToL1Message = l1PortalManager.getL2ToL1MessageLeaf( + withdrawAmount, + EthAddress.fromString(ownerEthAddress), + l2BridgeContract.address, + EthAddress.ZERO, + ); + const l2TxReceipt = await l2BridgeContract.methods + .exit_to_l1_public(EthAddress.fromString(ownerEthAddress), withdrawAmount, EthAddress.ZERO, nonce) + .send() + .wait(); + + const newL2Balance = await l2TokenContract.methods.balance_of_public(ownerAztecAddress).simulate(); + logger.info(`New L2 balance of ${ownerAztecAddress} is ${newL2Balance}`); + // docs:end:l2-withdraw + + // docs:start:l1-withdraw + const [l2ToL1MessageIndex, siblingPath] = await pxe.getL2ToL1MembershipWitness( + await pxe.getBlockNumber(), + l2ToL1Message, + ); + await l1PortalManager.withdrawFunds( + withdrawAmount, + EthAddress.fromString(ownerEthAddress), + BigInt(l2TxReceipt.blockNumber!), + l2ToL1MessageIndex, + siblingPath, + ); + const newL1Balance = await l1TokenManager.getL1TokenBalance(ownerEthAddress); + logger.info(`New L1 balance of ${ownerEthAddress} is ${newL1Balance}`); + // docs:end:l1-withdraw + expect(newL1Balance).toBe(withdrawAmount); + }, 60000); +});