diff --git a/docs/docs/dev_docs/tutorials/token_portal/cancelling_deposits.md b/docs/docs/dev_docs/tutorials/token_portal/cancelling_deposits.md new file mode 100644 index 00000000000..a1f2e490c5a --- /dev/null +++ b/docs/docs/dev_docs/tutorials/token_portal/cancelling_deposits.md @@ -0,0 +1,21 @@ +--- +title: Cancelling Deposits +--- + +A special type of error is an _underpriced transaction_ - it means that a message is inserted on L1, but the attached fee is too low to be included in a rollup block. In such a case your funds could be stuck in the portal and not minted on L2 (lost forever!) + +To address this, the Inbox supports cancelling messages after a deadline. However, this must be called by the portal itself, as it will need to "undo" the state changes is made (for example by sending the tokens back to the user). + +In your `TokenPortal.sol` smart contract, paste this: + +#include_code token_portal_cancel /l1-contracts/test/portals/TokenPortal.sol solidity + +To cancel a message, the portal must reconstruct it - this way we avoid storing messages in the portal itself. Note that just as with deposits we need to support cancelling messages for minting privately and publicly. + +Note that the portal uses `msg.sender` as the canceller when computing the secret hash. This is an access control mechanism to restrict only the intended address to cancel a message. + +Once the message is cancelled on the inbox, we return the funds back to the user. + +The inbox requires each message to provide a deadline by which a message must be consumed. After this time, if the message is still not consumed, the message can be cancelled. + +In the next step we will write L1 and L2 logic to withdraw funds from L2 to L1. diff --git a/docs/docs/dev_docs/tutorials/token_portal/depositing_to_aztec.md b/docs/docs/dev_docs/tutorials/token_portal/depositing_to_aztec.md new file mode 100644 index 00000000000..4592cc2e0f6 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/token_portal/depositing_to_aztec.md @@ -0,0 +1,115 @@ +--- +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.20; + +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 {DataStructures} from "@aztec/l1-contracts/src/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec/l1-contracts/src/core/libraries/Hash.sol"; + +contract TokenPortal { + using SafeERC20 for IERC20; + + IRegistry public registry; + IERC20 public underlying; + bytes32 public l2TokenAddress; + + function initialize(address _registry, address _underlying, bytes32 _l2TokenAddress) external { + registry = IRegistry(_registry); + underlying = IERC20(_underlying); + l2TokenAddress = _l2TokenAddress; + } +} +``` + +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 `PortalERC20.sol` in the same folder and add: + +```solidity +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract PortalERC20 is ERC20 { + constructor() ERC20("Portal", "PORTAL") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} +``` + +## 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 also include the L1 address that can cancel the L1->L2 message. Adding this into the content hash makes it so that only the appropriate person can cancel the message and not just any malicious 3rd party. + - More on cancellers can be found in [this upcoming section](./cancelling_deposits.md) + - We encode this message as a mint_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 a 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). + - Deadline by which the sequencer on L2 must consume the method. After this time, the message can be canceled by the “canceller”. We will implement this functionality later in the doc. + - 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. + - We also pass a fee to the sequencer for including the message. It is a uint64. +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_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! +- Since we want to mint tokens privately, we shouldn’t specify a `to` Aztec address (remember that Ethereum is completely public). Instead, we will use a secret hash - `secretHashForRedeemingMintedNotes`. Only he who knows the preimage to the secret hash can actually mint the notes. This is similar to the mechanism we use for message consumption on L2 +- Like with the public flow, we move the user’s funds to the portal +- We now send the message to the inbox with the `fee`, `deadline`, 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 fee, contentHash, deadline, recipient contract address. + +**So how do we privately consume the message on Aztec?** + +On Aztec, anytime something is consumed, 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. Hence, to consume the message on L2, the user provides a secret to the private noir function, which computes the hash and asserts that it matches to what was provided in the L1->L2 message. This secret is then included in the nullifier hash computation and emits this nullifier. This way, anyone inspecting the blockchain, won’t know which nullifier hash corresponds to the L1->L2 message consumption. + +Note: the 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 clearly 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/dev_docs/tutorials/token_portal/main.md b/docs/docs/dev_docs/tutorials/token_portal/main.md new file mode 100644 index 00000000000..5e9caf8ce54 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/token_portal/main.md @@ -0,0 +1,64 @@ +--- +title: Build a Token Bridge +--- + +import Image from "@theme/IdealImage"; + +In this tutorial, we will learn how to build the entire flow of a cross-chain token using portals. If this is your first time hearing the word portal, you’ll want to read [this](../../contracts/portals/main). + +## 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 + +For more information, read [cross-chain calls](../../../concepts/foundation/communication/cross_chain_calls). + +## 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 a [guide on writing a token contract on Aztec here](../writing_token_contract)). + +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/dev_docs/tutorials/token_portal/minting_on_aztec.md b/docs/docs/dev_docs/tutorials/token_portal/minting_on_aztec.md new file mode 100644 index 00000000000..e09a68929c8 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/token_portal/minting_on_aztec.md @@ -0,0 +1,67 @@ +--- +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` nargo project in `aztec-contracts`, under `src` there is an example `main.nr` file. Delete all the code in here and paste this to define imports and initialize the constructor: + +```rust +mod util; +#include_code token_bridge_imports /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust raw + use crate::token_interface::Token; + use crate::util::{get_mint_public_content_hash, get_mint_private_content_hash, get_withdraw_content_hash}; +#include_code token_bridge_storage_and_constructor /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust raw +``` +This imports Aztec-related dependencies and our two helper files `token_interface.nr` and `util.nr`. +(The code above will give errors right now - this is because we haven't implemented util and token_interface yet.) + +In `token_interface.nr`, add this: +#include_code token_brodge_token_interface /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr rust + +We will write `util.nr` as needed. + +## 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 /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust + +In your `util.nr` paste this `mint_public_content_hash` function: +#include_code mint_public_content_hash_nr /yarn-project/noir-contracts/src/contracts/token_portal_content_hash_lib/src/lib.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_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 by passing the `msg_key`, the the content hash, and the "secret". Since we are depositing to Aztec publicly, this secret is public, anyone can know this and is usually 0. + - `context.consume_l1_to_l2_message()` takes in the content_hash and secret to recreate the original message. The L1 to L2 message consists of: + - Sender - who on L1 sent the message + chain ID of L1. The context variable knows the portal address on L1 and adds that + - Recipient - i.e. this aztec contract address which is consuming the message + the current version of the aztec rollup. + - The content - which is reconstructed in the `get_mint_public_content_hash()` + - Note that the `content_hash` requires `to`, `amount` and `canceller`. 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 the `msg_Key` will also be different, thus preventing the L1->L2 message from being consumed. This is why we add these parameters into the content. +3. Then we call `Token::at(storage.token.read()).mint_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 /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust + +#include_code call_mint_on_token /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust + +Then inside your `util.nr`, paste this: + +#include_code get_mint_private_content_hash /yarn-project/noir-contracts/src/contracts/token_portal_content_hash_lib/src/lib.nr rust + +If the content hashes were constructed similarly for `mint_private` and `mint_publicly`, 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. + +While we mint the tokens on L2, we _still don’t actually mint them to a certain address_. Instead we continue to pass the `secret_hash_for_redeeming_minted_notes` like we did on L1. This means that a user could reveal their secret for L2 message consumption for anyone to mint tokens on L2 but they can redeem these notes at a later time. **This enables a paradigm where an app can manage user’s secrets for L2 message consumption on their behalf**. **The app or any external party can also mint tokens on the user’s behalf should they be comfortable with leaking the secret for L2 Message consumption.** This doesn’t leak any new information to the app because their smart contract on L1 knew that a user wanted to move some amount of tokens to L2. The app still doesn’t know which address on L2 the user wants these notes to be in, but they can mint tokens nevertheless on their behalf. + +To mint tokens privately, `claim_private` calls an internal function `_call_mint_on_token()` which then calls [token.mint_private()](../writing_token_contract.md#mint_private) which is a public method since it operates on public storage. Note that mint_private (on the token contract) is public because it too reads from public storage. Since the `secret_hash_for_redeeming_minted_notes` is passed publicly (and not the secret), nothing that should be leaked is, and the only the person that knows the secret can actually redeem their notes at a later time by calling [`Token.redeem_shield(secret, amount)`](../writing_token_contract.md#redeem_shield). + +In the next step we will see how we can cancel a message. diff --git a/docs/docs/dev_docs/tutorials/token_portal/setup.md b/docs/docs/dev_docs/tutorials/token_portal/setup.md new file mode 100644 index 00000000000..4c72b1e6545 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/token_portal/setup.md @@ -0,0 +1,261 @@ +--- +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 nargo 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. + +However if you’d rather skip this part, our dev-rels repo contains the starter code here. + +# Prerequisites + +- [node v18+](https://github.com/tj/n) +- [docker](https://docs.docker.com/) +- [Aztec sandbox](https://docs.aztec.network/dev_docs/getting_started/sandbox) - you should have this running before starting the tutorial + +```sh +/bin/sh -c "$(curl -fsSL 'https://sandbox.aztec.network')" +``` + +- Nargo + +```sh +curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | sh +noirup -v #include_noir_version +``` + +# Create the root project + +Our root project will house everything ✨ + +```sh +mkdir aztec-token-bridge +``` + +# Create a nargo project + +Now inside `aztec-token-bridge` create a new directory called `aztec-contracts` + +Inside `aztec-contracts`, create a nargo contract project by running + +```sh +mkdir aztec-contracts +cd aztec-contracts +nargo new --contract token_bridge +``` + +Your structure will look something like this + +``` +aztec-contracts +└── token_bridge + ├── Nargo.toml + ├── src + ├── main +``` + +Inside `Nargo.toml` you will need to add some dependencies. Put this at the bottom: + +```toml +[dependencies] +aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/aztec" } +value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/value-note"} +safe_math = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/safe-math"} +``` + +Inside `src` you will see a `main.nr` file. This is where our main smart contract will go. + +We will also be writing some helper functions that should exist elsewhere so we don't overcomplicated our contract. In `src` create two more files - one called `util.nr` and one called `token_interface` - so your dir structure should now look like this: + +``` +aztec-contracts +└── token_bridge + ├── Nargo.toml + ├── src + ├── main.nr + ├── token_interface.nr + ├── util.nr +``` + +# Create a JS hardhat project + +In the root dir `aztec-token-bridge`, create a new directory called `l1-contracts` and run `npx hardhat init` inside of it. Keep hitting enter so you get the default setup (Javascript project) + +```sh +mkdir l1-contracts +cd l1-contracts +npx hardhat init +``` + +Once you have a hardhat project set up, delete the existing contracts and create a `TokenPortal.sol`: + +```sh +cd contracts +rm *.sol +touch TokenPortal.sol +``` + +Now add dependencies that are required. These include interfaces to Aztec Inbox, Outbox and Registry smart contracts, OpenZeppelin contracts, and NomicFoundation. + +```sh +yarn add @aztec/l1-contracts @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan @types/chai @types/mocha @typechain/ethers-v5 @typechain/hardhat chai hardhat-gas-reporter solidity-coverage ts-node typechain typescript @openzeppelin/contracts + +``` + +This is what your `l1-contracts` should look like: + +```tree +├── README.md +├── artifacts +├── cache +├── 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 root directory, run + +```sh +mkdir src && cd src && yarn init -yp +yarn add @aztec/aztec.js @aztec/noir-contracts @aztec/types @aztec/foundation @aztec/l1-artifacts viem "@types/node@^20.8.2" +yarn add -D jest @jest/globals ts-jest +``` + +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: + +```json +{ + "name": "src", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "private": true, + "type": "module", + "dependencies": { + "@aztec/aztec.js": "^0.8.7", + "@aztec/foundation": "^0.8.7", + "@aztec/noir-contracts": "^0.8.7", + "@aztec/types": "^0.8.7", + "@types/node": "^20.8.2", + "ethers": "^5.7" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + }, + "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" +} +``` + +You will also need to install some dependencies: + +```sh +yarn add --dev typescript @types/jest ts-jest +``` + +Finally, we will create a test file. Run this in the `src` directory.: + +```sh +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 +``` + +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/dev_docs/tutorials/token_portal/typescript_glue_code.md b/docs/docs/dev_docs/tutorials/token_portal/typescript_glue_code.md new file mode 100644 index 00000000000..ae99c000411 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/token_portal/typescript_glue_code.md @@ -0,0 +1,173 @@ +--- +title: Deploy & Call Contracts with Typescript +--- + +In this step we will write a Typescript test to interact with the sandbox and call our contracts! + +Go to the `src/test` directory in your root dir and create a new file called `cross_chain_messaging.test.ts`: + +```sh +cd src/test +touch cross_chain_messaging.test.ts +``` + +Open `cross_chain_messaging.test.ts`. + +We will write two tests: + +1. Test the deposit and withdraw in the private flow +2. Do the same in the public flow + +## Test imports and setup + +We need some helper files that can keep our code clean. Inside your `src/test` directory: + +```bash +cd fixtures +touch utils.ts +touch cross_chain_test_harness.ts +``` + +In `utils.ts`, put: + +```typescript +import * as fs from "fs"; +import { AztecAddress, EthAddress, TxStatus, Wallet } from "@aztec/aztec.js"; +import { TokenContract } from "@aztec/noir-contracts/types"; +import { + Account, + Chain, + Hex, + HttpTransport, + PublicClient, + WalletClient, + getContract, +} from "viem"; +import type { Abi, Narrow } from "abitype"; + +import { TokenBridgeContract } from "./TokenBridge.js"; + +const PATH = "../../packages/l1-contracts/artifacts/contracts"; +const EXT = ".sol"; +function getL1ContractABIAndBytecode(contractName: string) { + const pathToArtifact = `${PATH}/${contractName}${EXT}/${contractName}.json`; + const artifacts = JSON.parse(fs.readFileSync(pathToArtifact, "utf-8")); + return [artifacts.abi, artifacts.bytecode]; +} + +const [PortalERC20Abi, PortalERC20Bytecode] = + getL1ContractABIAndBytecode("PortalERC20"); +const [TokenPortalAbi, TokenPortalBytecode] = + getL1ContractABIAndBytecode("TokenPortal"); + +#include_code deployL1Contract /yarn-project/ethereum/src/deploy_l1_contracts.ts typescript raw + +#include_code deployAndInitializeTokenAndBridgeContracts /yarn-project/end-to-end/src/fixtures/utils.ts typescript raw + +#include_code delay /yarn-project/end-to-end/src/fixtures/utils.ts typescript raw +``` + +This code + +- gets your Solidity contract ABIs +- uses viem 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 + +Now let's create another util file to can handle interaction with these contracts to mint/deposit the functions: + +In `cross_chain_test_harness.ts`, add: + +#include_code cross_chain_test_harness /yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts typescript + +This is a class that holds all contracts as objects and exposes easy to use helper methods to interact with our contracts. + +Now let's write our tests. + +Open `cross_chain_messaging.test.ts` and paste the initial description of the test: + +```typescript +import { expect, jest} from '@jest/globals' +import { AccountWallet, AztecAddress, DebugLogger, EthAddress, Fr, computeAuthWitMessageHash, createDebugLogger, createPXEClient, getSandboxAccountsWallets, waitForSandbox } from '@aztec/aztec.js'; +import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; + +import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; +import { delay } from './fixtures/utils.js'; +import { mnemonicToAccount } from 'viem/accounts'; +import { createPublicClient, createWalletClient, http } 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); + +describe('e2e_cross_chain_messaging', () => { + jest.setTimeout(90_000); + + let logger: DebugLogger; + // include code: + let user1Wallet: AccountWallet; + let user2Wallet: AccountWallet; + let ethAccount: EthAddress; + let ownerAddress: AztecAddress; + + let crossChainTestHarness: CrossChainTestHarness; + let l2Token: TokenContract; + let l2Bridge: TokenBridgeContract; + let outbox: any; + + beforeEach(async () => { + logger = createDebugLogger('aztec:canary_uniswap'); + const pxe = createPXEClient(PXE_URL); + await waitForSandbox(pxe); + const wallets = await getSandboxAccountsWallets(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( + 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]; + logger = logger; + logger('Successfully deployed contracts and initialized portal'); + }); +``` + +This fetches the wallets from the sandbox and deploys our cross chain harness on the sandbox! + +## Private flow test + +#include_code e2e_private_cross_chain /yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts typescript + +## Public flow test + +#include_code e2e_public_cross_chain /yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts typescript + +## Running the test + +```bash +cd packages/src +yarn test +``` + +### Error handling + +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. diff --git a/docs/docs/dev_docs/tutorials/token_portal/withdrawing_to_l1.md b/docs/docs/dev_docs/tutorials/token_portal/withdrawing_to_l1.md new file mode 100644 index 00000000000..9e0a4aad9e5 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/token_portal/withdrawing_to_l1.md @@ -0,0 +1,76 @@ +--- +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 + +Go back to your `main.nr` and paste this: + +#include_code exit_to_l1_public /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust + +For this to work we will need this helper function, in `util.nr`: + +#include_code get_withdraw_content_hash /yarn-project/noir-contracts/src/contracts/token_portal_content_hash_lib/src/lib.nr rust + +**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](../../../concepts/advanced/circuits/kernels/public_kernel.md) 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 rollup calldata by the sequencer and is stored in the outbox for consumption. +3. 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](../writing_token_contract.md#burn_public). The nonce parameter refers to the approval message that the user creates - also refer to [authorizing token spends here](../writing_token_contract.md#authorizing-token-spends). + - 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 /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust + +#include_code assert_token_is_same /yarn-project/noir-contracts/src/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`: + +#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 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. + +## 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 root of your project: + +```bash +cd l1-contracts +npx hardhat compile +``` + +And compile your Aztec.nr contracts like this: + +```bash +cd aztec-contracts +aztec-cli compile --typescript ../../src/test/fixtures token_bridge +``` + +This will create a TS interface 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/dev_docs/tutorials/uniswap/execute_private_swap_on_l1.md b/docs/docs/dev_docs/tutorials/uniswap/execute_private_swap_on_l1.md new file mode 100644 index 00000000000..834e1c18039 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/execute_private_swap_on_l1.md @@ -0,0 +1,9 @@ +--- +title: Executing Private Swap on L1 +--- + +To execute the swaps on L1, go back to the `UniswapPortal.sol` we [created earlier](./l1_portal.md) in `l1-contracts`. + +#include_code solidity_uniswap_swap_private l1-contracts/test/portals/UniswapPortal.sol solidity + +This works very similarly to the public flow. diff --git a/docs/docs/dev_docs/tutorials/uniswap/execute_public_swap_on_l1.md b/docs/docs/dev_docs/tutorials/uniswap/execute_public_swap_on_l1.md new file mode 100644 index 00000000000..9b92eb92c65 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/execute_public_swap_on_l1.md @@ -0,0 +1,28 @@ +--- +title: Solidity Code to Execute Swap on L1 +--- + +To execute the swaps on L1, go back to the `UniswapPortal.sol` we [created earlier](./l1_portal.md) in `l1-contracts`. + +Under the struct, paste this code that will manage the public flow: + +#include_code solidity_uniswap_swap_public l1-contracts/test/portals/UniswapPortal.sol solidity + +**What’s happening here?** + +1. It fetches the input and output tokens we are swapping. The Uniswap portal only needs to know the portal addresses of the input and output as they store the underlying ERC20 token address. +2. Consumes the `withdraw` message to get input tokens on L1 to itself. This is needed to execute the swap. + + Before it actually can swap, it checks if the provided swap parameters were what the user actually wanted by creating a message content hash (similar to what we did in the L2 contract) to ensure the right parameters are used. + +3. Executes the swap and receives the output funds to itself. + + The deadline by which the funds should be swapped is `block.timestamp` i.e. this block itself. This makes things atomic on the L1 side. + +4. The portal must deposit the output funds back to L2 using the output token’s portal. For this we first approve the token portal to move Uniswap funds, and then call the portal’s `depositToAztecPublic()` method to transfer funds to the portal and create a L1 → l2 message to mint the right amount of output tokens on L2. + + To incentivize the sequencer to pick up this message, we pass a fee to the deposit message. + +This concludes the public flow. + +In the next step, we will code a private flow in the Aztec.nr contract. diff --git a/docs/docs/dev_docs/tutorials/uniswap/l1_portal.md b/docs/docs/dev_docs/tutorials/uniswap/l1_portal.md new file mode 100644 index 00000000000..48623f896ed --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/l1_portal.md @@ -0,0 +1,31 @@ +--- +title: Uniswap Portal on L1 +--- + +In this step we will set up our Solidity portal contract. + +In `l1-tokens` create a new file called `UniswapPortal.sol` + +```sh +cd l1-contracts/contracts && touch UniswapPortal.sol +``` + +and paste this inside: + +```solidity +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IRegistry} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IRegistry.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 setup l1-contracts/test/portals/UniswapPortal.sol solidity raw + +In this set up we defined the `initialize()` function and a struct (`LocalSwapVars`) to manage assets being swapped. + +Like we saw in the [TokenPortal](../token_portal/depositing_to_aztec.md), we initialize this portal with the registry contract address (to fetch the appropriate inbox and outbox) and the portal’s sister contract address on L2. + +In the next step we will set up the appropriate L2 Uniswap contract! diff --git a/docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md b/docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md new file mode 100644 index 00000000000..0e451114328 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md @@ -0,0 +1,34 @@ +--- +title: L2 Contract Setup +--- + +As we mentioned in [the overview](./main.md), the Uniswap L2 contract will receive funds from the user and then burn funds on L2 to withdraw. To do this it calls `TokenBridge.exit_to_l1_public()` which burns funds on the Uniswap contract. The bridge needs approval from the Uniswap contract to burn its funds. + +In this step, we will set up the storage struct for our Uniswap contract and define the functions to approve and validate burn actions. + +Our main contract will live inside `uniswap/src/main.nr`. In `main.nr`, paste this initial setup code: + +#include_code uniswap_setup yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr rust + +**What’s happening here?** + +Because Uniswap has to approve the bridge to withdraw funds, it has to handle the approvals. So it stores a map of all the actions that are approved. The approval message is hashed to a field and stored in the contract’s storage in the `approved_action` map. + +To ensure there are no collisions (i.e. when the contract wants to approve the bridge of the exact same amount, the message hash would be the same), we also keep a nonce that gets incremented each time after use in a message. + +## Building the approval flow +Next, paste this function: + +#include_code authwit_uniswap_get yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr rust + +In this function, the token contract calls the Uniswap contract to check if Uniswap has indeed done the approval. The token contract expects a `is_valid()` function to exit for private approvals and `is_valid_public()` for public approvals. If the action is indeed approved, it expects that the contract would return the function selector for `is_valid()`  in both cases. The Aztec.nr library exposes this constant for ease of use. The token contract also emits a nullifier for this message so that this approval (with the nonce) can’t be used again. + +This is similar to the [Authwit flow](../../contracts/resources/common_patterns/authwit.md). + +However we don't have a function that actually creates the approved message and stores the action. This method should be responsible for creating the approval and then calling the token bridge to withdraw the funds to L1: + +#include_code authwit_uniswap_set yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr rust + +Notice how the nonce also gets incremented. + +In the next step we’ll go through a public swapping flow. diff --git a/docs/docs/dev_docs/tutorials/uniswap/main.md b/docs/docs/dev_docs/tutorials/uniswap/main.md new file mode 100644 index 00000000000..d265f44c316 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/main.md @@ -0,0 +1,35 @@ +--- +title: Build an Aztec Connect-style Uniswap +--- + +import Image from "@theme/IdealImage"; + +:::note +💡 Before going through this tutorial, you will need to have completed the [Token Bridge tutorial](../token_portal/main.md) +::: + +Our goal here is for someone with funds on L2 to be able to swap using L1 Uniswap and then get the swapped assets back to L2. In this tutorial, 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 + +We will assume that token portals and token bridges for the input and output tokens must exist. These are what we built in the previous tutorial. + +The execution of swap on L1 should be designed such that any 3rd party can execute the swap on behalf of the user. + +In this tutorial, we will code both the private and public flow! + +We will create: + +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. + +Let’s get to the setup! diff --git a/docs/docs/dev_docs/tutorials/uniswap/redeeming_swapped_assets_on_l2.md b/docs/docs/dev_docs/tutorials/uniswap/redeeming_swapped_assets_on_l2.md new file mode 100644 index 00000000000..befecf8b226 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/redeeming_swapped_assets_on_l2.md @@ -0,0 +1,12 @@ +--- +title: Redeeming Swapped Assets on L2 +--- +So you emitted a message to withdraw input tokens to L1 and a message to swap. Then you or someone on your behalf can swap on L1 and emit a message to deposit swapped assets to L2, + +You still need to "claim" these swapped funds on L2. + +In the public flow, you can call [`claim_public()`](../token_portal/minting_on_aztec.md) on the output token bridge which consumes the deposit message and mints your assets. + +In the private flow, you can choose to leak your secret for L1 → L2 message consumption to let someone mint the notes on L2 (by calling [`claim_private()`](../token_portal/minting_on_aztec.md) on the output token bridge) and then you can later redeem these notes to yourself by presenting the preimage to `secret_hash_for_redeeming_minted_notes` and calling the `redeem_shield()` method on the token contract. + +In the next step we will write the typescript code that interacts with all these contracts on the sandbox to actually execute the swaps! \ No newline at end of file diff --git a/docs/docs/dev_docs/tutorials/uniswap/setup.md b/docs/docs/dev_docs/tutorials/uniswap/setup.md new file mode 100644 index 00000000000..5fce5ff89bd --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/setup.md @@ -0,0 +1,82 @@ +--- +title: Setup & Installation +--- + +This tutorial builds on top of the project created in the previous tutorial. It can exist on its own, but for our sake it is much easier to utilize the existing L1 contracts that we already have in place. + +If you don’t have this, you can find the code for it [in our dev-rels repo](https://github.com/AztecProtocol/dev-rel/tree/main/tutorials/token-bridge-e2e). + +# Uniswap contract + +To interact with Uniswap we need to add its interface. In the root repo we created in the [token bridge tutorial](../token_portal/main.md), run this: + +```bash +cd l1-contracts +mkdir external && cd external +touch ISwapRouter.sol +``` + +Inside `ISwapRouter.sol` paste this: + +#include_code iswaprouter /l1-contracts/test/external/ISwapRouter.sol solidity + +This is an interface for the Uniswap V3 Router, providing token swapping functionality. The contract defines methods for token swaps, both between two tokens or via a multi-hop path. Our portal will interact with the Uniswap V3 Router via this interface to perform token swaps on L1. We’ll see more about this in the next step. + +# Create another nargo project + +In `aztec-contracts` create a new nargo project. + +```bash +cd aztec-contracts && nargo new --contract uniswap +``` + +Now your `aztec-contracts` will look like this: + +```bash +aztec-contracts +└── token_bridge + ├── Nargo.toml + ├── src + ├── main.nr +└── uniswap + ├── Nargo.toml + ├── src + ├── main.nr +``` + +Inside `uniswap/Nargo.toml` paste this in `[dependencies]`: + +```json +[dependencies] +aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/aztec" } +value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/value-note"} +safe_math = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/safe-math"} +authwit = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/authwit"} +``` + +# L2 contracts + +The `main.nr` will utilize a few helper functions that are outside the scope of this tutorial. Inside `uniswap/src` create two new files: + +```bash +cd uniswap/src && touch util.nr && touch interfaces.nr +``` + +Inside `interfaces.nr` paste this: + +#include_code interfaces yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr rust + +This creates interfaces for the `Token` contract and `TokenBridge` contract + +- `Token` is a reference implementation for a token on Aztec. Here we just need two methods - [`transfer_public`](../writing_token_contract.md#transfer_public) and [`unshield()`](../writing_token_contract.md#unshield). +- The `TokenBridge` facilitates interactions with our [bridge contract](../token_portal/main.md). Here we just need its [`exit_to_l1_public`](../token_portal/withdrawing_to_l1.md) + +# Run Aztec sandbox + +You will need a running sandbox. + +```bash +/bin/bash -c "$(curl -fsSL 'https://sandbox.aztec.network')" +``` + +In the next step, we will write the L1 Uniswap Portal. diff --git a/docs/docs/dev_docs/tutorials/uniswap/swap_privately.md b/docs/docs/dev_docs/tutorials/uniswap/swap_privately.md new file mode 100644 index 00000000000..a0fa6d52b43 --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/swap_privately.md @@ -0,0 +1,23 @@ +--- +title: Swapping Privately +--- + +In the `uniswap/src/main.nr` contract we created [previously](./l2_contract_setup.md) in `aztec-contracts/uniswap`, paste these functions: + +#include_code swap_private yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr rust +#include_code assert_token_is_same yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr rust + +This uses a util function `compute_swap_private_content_hash()` - let's add that. + +In `util.nr`, add: +#include_code compute_swap_private_content_hash yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr rust + +This flow works similarly to the public flow with a few notable changes: + +- Notice how in the `swap_private()`, user has to pass in `token` address which they didn't in the public flow? Since `swap_private()` is a private method, it can't read what token is publicly stored on the token bridge, 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. Note that 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 done. So first the burn would happen and only later the sequencer asserts that the token is same. Note that 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. +- In the public flow, the user calls `transfer_public()`. Here instead, the user calls `unshield()`. Why? The user can't directly transfer their private tokens, their notes to the uniswap contract, because later the Uniswap contract has to approve the bridge to burn these notes and withdraw to L1. The authwit flow for the private domain requires a signature from the `sender`, which in this case would be the Uniswap contract. For the contract to sign, it would need a private key associated to it. But who would operate this key? +- To work around this, the user can unshield their private tokens into Uniswap L2 contract. Unshielding would convert user's private notes to public balance. It is a private method on the token contract that reduces a user’s private balance and then calls a public method to increase the recipient’s (ie Uniswap) public balance. **Remember that first all private methods are executed and then later all public methods will be - so the Uniswap contract won’t have the funds until public execution begins.** +- Now uniswap has public balance (like with the public flow). Hence, `swap_private()` calls the internal public method which approves the input token bridge to burn Uniswap’s tokens and calls `exit_to_l1_public` to create an L2 → L1 message to exit to L1. +- Constructing the message content for swapping works exactly as the public flow except instead of specifying who would be the Aztec address that receives the swapped funds, we specify a secret hash (`secret_hash_for_redeeming_minted_notes`). Only those who know the preimage to the secret can later redeem the minted notes to themselves. + +In the next step we will write the code to execute this swap on L1. diff --git a/docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md b/docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md new file mode 100644 index 00000000000..f5f1c984d7c --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md @@ -0,0 +1,41 @@ +--- +title: Swapping Publicly +--- + +In this step we will create the flow for allowing a user to swap their tokens publicly on L1. It will have the functionality of letting anyone call this method on behalf of the user, assuming they have appropriate approvals. This means that an operator can pay gas fees on behalf of the user! + +In `main.nr` paste this: + +#include_code swap_public yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr rust + +This uses a util function `compute_swap_public_content_hash()` - let's add that. + +In `util.nr`, add: +#include_code uniswap_public_content_hash yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr rust + +**What’s happening here?** + +1. We check that `msg.sender()` has appropriate approval to call this on behalf of the sender by constructing an authwit message and checking if `from` has given the approval (read more about authwit [here](../../contracts/resources/common_patterns/authwit.md)). +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_portal/withdrawing_to_l1.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: + +- The token portals for the input and output token (to withdraw the input token to L1 and later deposit the output token to L2) +- The amount of input tokens they want to swap +- The Uniswap fee tier they want to use +- The minimum output amount they can accept (for slippage protection) + +The Uniswap portal must first withdraw the input tokens, then check that the swap message exists in the outbox, execute the swap, and then call the output token to deposit the swapped tokens to L2. So the Uniswap portal must also be pass any parameters needed to complete the deposit of swapped tokens to L2. From the tutorial on building token bridges we know these are: + +- The address on L2 which must receive the output tokens (remember this is public flow) +- The secret hash for consume the L1 to L2 message. Since this is the public flow the preimage doesn’t need to be a secret +- The deadline to consume the l1 to l2 message (this is so funds aren’t stuck in the processing state forever and the message can be cancelled. Else the swapped tokens would be stuck forever) +- The address that can cancel the message (and receive the swapped tokens) + +6. We include these params in the L2 → L1 `swap_public message content` too. Under the hood, the protocol adds the sender (the Uniswap l2 contract) and the recipient (the Uniswap portal contract on L1). + +In the next step we will write the code to execute this swap on L1. diff --git a/docs/docs/dev_docs/tutorials/uniswap/typescript_glue_code.md b/docs/docs/dev_docs/tutorials/uniswap/typescript_glue_code.md new file mode 100644 index 00000000000..887786a5a9d --- /dev/null +++ b/docs/docs/dev_docs/tutorials/uniswap/typescript_glue_code.md @@ -0,0 +1,142 @@ +--- +title: Deploy & Call Contracts with Typescript +--- + +In this step, we We will now write a Typescript to interact with the sandbox and see our Solidity and Aztec.nr contracts in action. + +In the root folder, go to `src` dir we created in [the token bridge tutorial](../token_portal/setup.md). + +```sh +cd src/test +touch uniswap.test.ts +``` + +Open `uniswap.test.ts` in your editor. + +We will write two tests: + +1. Test the private flow (i.e. mint tokens on L1, deposit them to L2, give your intention to swap L2 asset on L1, swap on L1, bridge swapped assets back to L2) +2. Do the same in the public flow + +## Compile our contracts + +To compile the Solidity contracts, run this: + +```sh +cd l1-contracts +npx hardhat compile +``` + +and the Aztec.nr contracts: + +```sh +cd aztec-contracts +aztec-cli compile --typescript ../../src/test/fixtures uniswap +``` + +This will create a TS interface in our `src/test` folder that will help us write our test. + +## Test imports and setup + +We will use the same `utils.ts` and `cross_chain_test_harness.ts` we created in the tutorial [here](../token_portal/typescript_glue_code.md#test-imports-and-setup). + +In `utils.ts`, add: + +```typescript +export const [UniswapPortalAbi, UniswapPortalBytecode] = + getL1ContractABIAndBytecode("UniswapPortal"); +``` + +### Setup the fork + +Since we want to use L1 Uniswap, we need the sandbox to execute against a fork of L1. This has be easily done: +in your terminal add the following variables: + +``` +export FORK_BLOCK_NUMBER=17514288 +export FORK_URL= +``` + +Now rerun the sandbox: +```sh +/bin/sh -c "$(curl -fsSL 'https://sandbox.aztec.network')" +``` + +### Back to test setup + +Okay now we are ready to write our tests: + +open `uniswap.test.ts` and lets do the initial description of the test: + +```typescript +import { + AccountWallet, + AztecAddress, + DebugLogger, + EthAddress, + Fr, + PXE, + TxStatus, + computeAuthWitMessageHash, + createDebugLogger, + createPXEClient, + getSandboxAccountsWallets, + waitForSandbox, +} from "@aztec/aztec.js"; +import { + Chain, + HttpTransport, + PublicClient, + createPublicClient, + createWalletClient, + getContract, + http, + parseEther, +} from "viem"; +import { foundry } from "viem/chains"; +import { CrossChainTestHarness } from "./fixtures/cross_chain_test_harness.js"; +import { UniswapContract } from "./fixtures/Uniswap.js"; +import { beforeAll, expect, jest } from "@jest/globals"; +import { + UniswapPortalAbi, + UniswapPortalBytecode, + delay, + deployL1Contract, +} from "./fixtures/utils.js"; +import { mnemonicToAccount } from "viem/accounts"; + +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 expectedForkBlockNumber = 17514288; + +#include_code uniswap_l1_l2_test_setup_const yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts typescript raw +#include_code uniswap_setup yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts typescript raw +#include_code uniswap_l1_l2_test_beforeAll yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts typescript raw +``` + +## Private flow test + +#include_code uniswap_private yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts typescript + +## Public flow test + +#include_code uniswap_public yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts typescript + +## Running the test + +Make sure your sandbox is running. + +```sh +cd ~/.aztec && docker-compose up +``` + +Then run this in the root directory. + +```bash +cd src +yarn test uniswap +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 3327fbf99e8..f8934b0138c 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -228,6 +228,41 @@ const sidebars = { "dev_docs/tutorials/writing_dapp/testing", ], }, + { + label: "Build a Token Bridge", + type: "category", + link: { + type: "doc", + id: "dev_docs/tutorials/token_portal/main", + }, + items: [ + "dev_docs/tutorials/token_portal/setup", + "dev_docs/tutorials/token_portal/depositing_to_aztec", + "dev_docs/tutorials/token_portal/minting_on_aztec", + "dev_docs/tutorials/token_portal/cancelling_deposits", + "dev_docs/tutorials/token_portal/withdrawing_to_l1", + "dev_docs/tutorials/token_portal/typescript_glue_code", + ], + }, + { + label: "Build Uniswap with Portals", + type: "category", + link: { + type: "doc", + id: "dev_docs/tutorials/uniswap/main", + }, + items: [ + "dev_docs/tutorials/uniswap/setup", + "dev_docs/tutorials/uniswap/l1_portal", + "dev_docs/tutorials/uniswap/l2_contract_setup", + "dev_docs/tutorials/uniswap/swap_publicly", + "dev_docs/tutorials/uniswap/execute_public_swap_on_l1", + "dev_docs/tutorials/uniswap/swap_privately", + "dev_docs/tutorials/uniswap/execute_private_swap_on_l1", + "dev_docs/tutorials/uniswap/redeeming_swapped_assets_on_l2", + "dev_docs/tutorials/uniswap/typescript_glue_code", + ], + }, "dev_docs/tutorials/testing", ], }, diff --git a/docs/static/img/tutorials/portals_shilling.png b/docs/static/img/tutorials/portals_shilling.png new file mode 100644 index 00000000000..1bedd29b603 Binary files /dev/null and b/docs/static/img/tutorials/portals_shilling.png differ diff --git a/docs/static/img/tutorials/token_bridge_diagram.png b/docs/static/img/tutorials/token_bridge_diagram.png new file mode 100644 index 00000000000..f5ff115d61d Binary files /dev/null and b/docs/static/img/tutorials/token_bridge_diagram.png differ diff --git a/docs/static/img/tutorials/uniswap_flow.png b/docs/static/img/tutorials/uniswap_flow.png new file mode 100644 index 00000000000..381f814e468 Binary files /dev/null and b/docs/static/img/tutorials/uniswap_flow.png differ diff --git a/l1-contracts/test/external/ISwapRouter.sol b/l1-contracts/test/external/ISwapRouter.sol index 93c84a2d0aa..daf041a19f0 100644 --- a/l1-contracts/test/external/ISwapRouter.sol +++ b/l1-contracts/test/external/ISwapRouter.sol @@ -1,3 +1,4 @@ +// docs:start:iswaprouter // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.7.5; pragma abicoder v2; @@ -75,3 +76,4 @@ interface ISwapRouter { payable returns (uint256 amountIn); } +// docs:end:iswaprouter diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index 19ef29bb3a5..c3afb268351 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -58,6 +58,7 @@ contract TokenPortal { } // docs:end:deposit_public + // docs:start:deposit_private /** * @notice Deposit funds into the portal and adds an L2 message which can only be consumed privately on Aztec * @param _secretHashForRedeemingMintedNotes - The hash of the secret to redeem minted notes privately on Aztec. The hash should be 254 bits (so it can fit in a Field element) @@ -96,6 +97,7 @@ contract TokenPortal { actor, _deadline, contentHash, _secretHashForL2MessageConsumption ); } + // docs:end:deposit_private // docs:start:token_portal_cancel /** diff --git a/l1-contracts/test/portals/UniswapPortal.sol b/l1-contracts/test/portals/UniswapPortal.sol index 41a29c39469..75e32f22c43 100644 --- a/l1-contracts/test/portals/UniswapPortal.sol +++ b/l1-contracts/test/portals/UniswapPortal.sol @@ -1,12 +1,14 @@ pragma solidity >=0.8.18; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + import {IRegistry} from "../../src/core/interfaces/messagebridge/IRegistry.sol"; +import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; +import {Hash} from "../../src/core/libraries/Hash.sol"; +// docs:start:setup import {TokenPortal} from "./TokenPortal.sol"; import {ISwapRouter} from "../external/ISwapRouter.sol"; -import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; -import {Hash} from "../../src/core/libraries/Hash.sol"; /** * @title UniswapPortal @@ -33,8 +35,9 @@ contract UniswapPortal { IERC20 outputAsset; bytes32 contentHash; } + // docs:end:setup - // docs:start:solidity_uniswap_swap + // docs:start:solidity_uniswap_swap_public /** * @notice Exit with funds from L2, perform swap on L1 and deposit output asset to L2 again publicly * @dev `msg.value` indicates fee to submit message to inbox. Currently, anyone can call this method on your behalf. @@ -126,8 +129,9 @@ contract UniswapPortal { _aztecRecipient, amountOut, _canceller, _deadlineForL1ToL2Message, _secretHashForL1ToL2Message ); } - // docs:end:solidity_uniswap_swap + // docs:end:solidity_uniswap_swap_public + // docs:start:solidity_uniswap_swap_private /** * @notice Exit with funds from L2, perform swap on L1 and deposit output asset to L2 again privately * @dev `msg.value` indicates fee to submit message to inbox. Currently, anyone can call this method on your behalf. @@ -224,3 +228,4 @@ contract UniswapPortal { ); } } +// docs:end:solidity_uniswap_swap_private diff --git a/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts index 4b5d5256af6..6b5ceed4b86 100644 --- a/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts @@ -13,7 +13,8 @@ const EXPECTED_FORKED_BLOCK = 17514288; // We tell the archiver to only sync from this block. process.env.SEARCH_START_BLOCK = EXPECTED_FORKED_BLOCK.toString(); -const setupRPC = async (): Promise => { +// docs:start:uniswap_setup +const setup = async (): Promise => { const logger = createDebugLogger('aztec:canary_uniswap'); const pxe = createPXEClient(PXE_URL); await waitForSandbox(pxe); @@ -32,5 +33,6 @@ const setupRPC = async (): Promise => { return { pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet }; }; +// docs:end:uniswap_setup -uniswapL1L2TestSuite(setupRPC, () => Promise.resolve(), EXPECTED_FORKED_BLOCK); +uniswapL1L2TestSuite(setup, () => Promise.resolve(), EXPECTED_FORKED_BLOCK); diff --git a/yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts index 6ff3289d3c8..ab45ab2fac0 100644 --- a/yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts @@ -1,11 +1,16 @@ -import { AccountWallet, AztecAddress, computeAuthWitMessageHash } from '@aztec/aztec.js'; +import { + AccountWallet, + AztecAddress, + DebugLogger, + EthAddress, + Fr, + PXE, + TxStatus, + computeAuthWitMessageHash, +} from '@aztec/aztec.js'; import { deployL1Contract } from '@aztec/ethereum'; -import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr } from '@aztec/foundation/fields'; -import { DebugLogger } from '@aztec/foundation/log'; import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; import { UniswapContract } from '@aztec/noir-contracts/types'; -import { PXE, TxStatus } from '@aztec/types'; import { jest } from '@jest/globals'; import { Chain, HttpTransport, PublicClient, getContract, parseEther } from 'viem'; @@ -20,6 +25,7 @@ import { delay } from '../fixtures/utils.js'; // anvil --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c --fork-block-number 17514288 --chain-id 31337 // For CI, this is configured in `run_tests.sh` and `docker-compose.yml` +// docs:start:uniswap_l1_l2_test_setup_const const TIMEOUT = 90_000; /** Objects to be returned by the uniswap setup function */ @@ -37,12 +43,14 @@ export type UniswapSetupContext = { /** The sponsor wallet. */ sponsorWallet: AccountWallet; }; +// docs:end:uniswap_l1_l2_test_setup_const export const uniswapL1L2TestSuite = ( setup: () => Promise, cleanup: () => Promise, expectedForkBlockNumber = 17514288, ) => { + // docs:start:uniswap_l1_l2_test_beforeAll describe('uniswap_trade_on_l1_from_l2', () => { jest.setTimeout(TIMEOUT); @@ -138,11 +146,12 @@ export const uniswapL1L2TestSuite = ( logger('Getting some weth'); await walletClient.sendTransaction({ to: WETH9_ADDRESS.toString(), value: parseEther('1') }); }); + // docs:end:uniswap_l1_l2_test_beforeAll afterAll(async () => { await cleanup(); }); - + // docs:start:uniswap_private it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); @@ -288,7 +297,9 @@ export const uniswapL1L2TestSuite = ( logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); }); + // docs:end:uniswap_private + // docs:start:uniswap_public it('should uniswap trade on L1 from L2 funds publicly (swaps WETH -> DAI)', async () => { const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); @@ -431,6 +442,7 @@ export const uniswapL1L2TestSuite = ( logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); }); + // docs:end:uniswap_public // Edge cases for the private flow: // note - tests for uniswapPortal.sol and minting asset on L2 are covered in other tests. diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 35e152271e6..3f4fbd352f6 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -1,9 +1,13 @@ -import { AccountWallet, AztecAddress, computeAuthWitMessageHash } from '@aztec/aztec.js'; -import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr } from '@aztec/foundation/fields'; -import { DebugLogger } from '@aztec/foundation/log'; +import { + AccountWallet, + AztecAddress, + DebugLogger, + EthAddress, + Fr, + TxStatus, + computeAuthWitMessageHash, +} from '@aztec/aztec.js'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; -import { TxStatus } from '@aztec/types'; import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; import { delay, setup } from './fixtures/utils.js'; @@ -48,8 +52,8 @@ describe('e2e_cross_chain_messaging', () => { afterEach(async () => { await teardown(); }); - - it('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { + // docs:start:e2e_private_cross_chain + it('Privately deposit funds from L1 -> L2 and withdraw back to L1', async () => { // Generate a claim secret using pedersen const l1TokenBalance = 1000000n; const bridgeAmount = 100n; @@ -113,6 +117,7 @@ describe('e2e_cross_chain_messaging', () => { expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); + // docs:end:e2e_private_cross_chain // Unit tests for TokenBridge's private methods. it('Someone else can mint funds to me on my behalf (privately)', async () => { diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index 1135d44ba7c..082a6aad120 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -12,9 +12,9 @@ describe('e2e_public_cross_chain_messaging', () => { let logger: DebugLogger; let teardown: () => Promise; - let ownerWallet: AccountWallet; + let user1Wallet: AccountWallet; let user2Wallet: AccountWallet; - let ownerEthAddress: EthAddress; + let ethAccount: EthAddress; let ownerAddress: AztecAddress; let crossChainTestHarness: CrossChainTestHarness; @@ -33,11 +33,11 @@ describe('e2e_public_cross_chain_messaging', () => { ); l2Token = crossChainTestHarness.l2Token; l2Bridge = crossChainTestHarness.l2Bridge; - ownerEthAddress = crossChainTestHarness.ethAccount; + ethAccount = crossChainTestHarness.ethAccount; ownerAddress = crossChainTestHarness.ownerAddress; outbox = crossChainTestHarness.outbox; teardown = teardown_; - ownerWallet = wallets[0]; + user1Wallet = wallets[0]; user2Wallet = wallets[1]; logger = logger_; @@ -48,7 +48,8 @@ describe('e2e_public_cross_chain_messaging', () => { await teardown(); }); - it('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { + // docs:start:e2e_public_cross_chain + it('Publicly deposit funds from L1 -> L2 and withdraw back to L1', async () => { // Generate a claim secret using pedersen const l1TokenBalance = 1000000n; const bridgeAmount = 100n; @@ -60,7 +61,7 @@ describe('e2e_public_cross_chain_messaging', () => { // 2. Deposit tokens to the TokenPortal const messageKey = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); - expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. @@ -86,7 +87,7 @@ describe('e2e_public_cross_chain_messaging', () => { l2Bridge.address, l2Token.methods.burn_public(ownerAddress, withdrawAmount, nonce).request(), ); - await ownerWallet.setPublicAuth(burnMessageHash, true).send().wait(); + await user1Wallet.setPublicAuth(burnMessageHash, true).send().wait(); // 5. Withdraw owner's funds from L2 to L1 const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); @@ -94,14 +95,13 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, afterBalance - withdrawAmount); // Check balance before and after exit. - expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); await crossChainTestHarness.withdrawFundsFromBridgeOnL1(withdrawAmount, entryKey); - expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe( - l1TokenBalance - bridgeAmount + withdrawAmount, - ); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); + // docs:end:e2e_public_cross_chain // Unit tests for TokenBridge's public methods. @@ -114,7 +114,7 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); const messageKey = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); - expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. @@ -128,7 +128,7 @@ describe('e2e_public_cross_chain_messaging', () => { await expect( l2Bridge .withWallet(user2Wallet) - .methods.claim_public(user2Wallet.getAddress(), bridgeAmount, ownerEthAddress, messageKey, secret) + .methods.claim_public(user2Wallet.getAddress(), bridgeAmount, ethAccount, messageKey, secret) .simulate(), ).rejects.toThrow(); @@ -136,7 +136,7 @@ describe('e2e_public_cross_chain_messaging', () => { logger("user2 consumes owner's message on L2 Publicly"); const tx = l2Bridge .withWallet(user2Wallet) - .methods.claim_public(ownerAddress, bridgeAmount, ownerEthAddress, messageKey, secret) + .methods.claim_public(ownerAddress, bridgeAmount, ethAccount, messageKey, secret) .send(); const receipt = await tx.wait(); expect(receipt.status).toBe(TxStatus.MINED); @@ -154,8 +154,8 @@ describe('e2e_public_cross_chain_messaging', () => { // Should fail as owner has not given approval to bridge burn their funds. await expect( l2Bridge - .withWallet(ownerWallet) - .methods.exit_to_l1_public(ownerEthAddress, withdrawAmount, EthAddress.ZERO, nonce) + .withWallet(user1Wallet) + .methods.exit_to_l1_public(ethAccount, withdrawAmount, EthAddress.ZERO, nonce) .simulate(), ).rejects.toThrowError('Assertion failed: Message not authorized by account'); }); @@ -166,7 +166,7 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness.mintTokensOnL1(bridgeAmount); const messageKey = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); - expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe(0n); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(0n); // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. @@ -177,7 +177,7 @@ describe('e2e_public_cross_chain_messaging', () => { await expect( l2Bridge .withWallet(user2Wallet) - .methods.claim_private(secretHash, bridgeAmount, ownerEthAddress, messageKey, secret) + .methods.claim_private(secretHash, bridgeAmount, ethAccount, messageKey, secret) .simulate(), ).rejects.toThrowError("Cannot satisfy constraint 'l1_to_l2_message_data.message.content == content"); }); diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 66c61f6aa68..2a173c1e597 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -1,7 +1,6 @@ -import { TxHash, Wallet, computeMessageSecretHash } from '@aztec/aztec.js'; -import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js'; +// docs:start:cross_chain_test_harness +import { AztecAddress, DebugLogger, EthAddress, Fr, TxHash, Wallet, computeMessageSecretHash } from '@aztec/aztec.js'; import { sha256ToField } from '@aztec/foundation/crypto'; -import { DebugLogger } from '@aztec/foundation/log'; import { OutboxAbi } from '@aztec/l1-artifacts'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; import { NotePreimage, PXE, TxStatus } from '@aztec/types'; @@ -323,3 +322,4 @@ export class CrossChainTestHarness { expect(unshieldReceipt.status).toBe(TxStatus.MINED); } } +// docs:end:cross_chain_test_harness diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 48c5179190b..23eb4004f22 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -338,6 +338,7 @@ export function getLogger() { return createDebugLogger('aztec:' + describeBlockName); } +// docs:start:deployAndInitializeTokenAndBridgeContracts /** * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract, its L2 bridge contract and attach is to the portal. * @param wallet - the wallet instance @@ -429,7 +430,9 @@ export async function deployAndInitializeTokenAndBridgeContracts( return { token, bridge, tokenPortalAddress, tokenPortal, underlyingERC20 }; } +// docs:end:deployAndInitializeTokenAndBridgeContracts +// docs:start:delay /** * Sleep for a given number of milliseconds. * @param ms - the number of milliseconds to sleep for @@ -437,6 +440,7 @@ export async function deployAndInitializeTokenAndBridgeContracts( export function delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } +// docs:end:delay /** * Checks the number of encrypted logs in the last block is as expected. diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 49933e2477c..02f4b0c05ae 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -192,6 +192,7 @@ export const deployL1Contracts = async ( }; }; +// docs:start:deployL1Contract /** * Helper function to deploy ETH contracts. * @param walletClient - A viem WalletClient. @@ -222,3 +223,4 @@ export async function deployL1Contract( return EthAddress.fromString(receipt.contractAddress!); } +// docs:end:deployL1Contract diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr index a27049a7e4e..18c4cc849ef 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr @@ -1,3 +1,4 @@ +// docs:start:token_bridge_imports mod token_interface; // Minimal implementation of the token bridge that can move funds between L1 <> L2. @@ -16,10 +17,13 @@ contract TokenBridge { types::address::{AztecAddress, EthereumAddress}, selector::compute_selector, }; + // docs:end:token_bridge_imports + use dep::token_portal_content_hash_lib::{get_mint_public_content_hash, get_mint_private_content_hash, get_withdraw_content_hash}; use crate::token_interface::Token; + // docs:start:token_bridge_storage_and_constructor // Storage structure, containing all storage, and specifying what slots they use. struct Storage { token: PublicState, @@ -43,6 +47,8 @@ contract TokenBridge { let selector = compute_selector("_initialize((Field))"); context.call_public_function(context.this_address(), selector, [token.address]); } + // docs:end:token_bridge_storage_and_constructor + // docs:start:claim_public // Consumes a L1->L2 message and calls the token contract to mint the appropriate amount publicly @@ -85,7 +91,7 @@ contract TokenBridge { 1 } // docs:end:exit_to_l1_public - + // docs:start:claim_private // Consumes a L1->L2 message and calls the token contract to mint the appropriate amount in private assets // User needs to call token.redeem_shield() to get the private assets #[aztec(private)] @@ -112,6 +118,7 @@ contract TokenBridge { 1 } + // docs:end:claim_private // docs:start:exit_to_l1_private // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately @@ -156,6 +163,7 @@ contract TokenBridge { storage.token.write(token.address); } + // docs:start:call_mint_on_token // This is a public call as we need to read from public storage. // Also, note that user hashes their secret in private and only sends the hash in public // meaning only user can `redeem_shield` at a later time with their secret. @@ -163,9 +171,13 @@ contract TokenBridge { internal fn _call_mint_on_token(amount: Field, secret_hash: Field){ Token::at(storage.token.read()).mint_private(context, amount, secret_hash); } + // docs:end:call_mint_on_token + // docs:start:assert_token_is_same #[aztec(public)] internal fn _assert_token_is_same(token: Field) { assert(storage.token.read() == token, "Token address is not the same as seen in storage"); } + // docs:end:assert_token_is_same + } diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr index f4d89558415..6c44755b8a4 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr @@ -1,3 +1,4 @@ +// docs:start:token_brodge_token_interface use dep::aztec::{ context::{ PrivateContext, PublicContext, Context }, selector::compute_selector, @@ -49,3 +50,4 @@ impl Token { } // docs:end:private_burn_interface } +// docs:end:token_brodge_token_interface diff --git a/yarn-project/noir-contracts/src/contracts/token_portal_content_hash_lib/src/lib.nr b/yarn-project/noir-contracts/src/contracts/token_portal_content_hash_lib/src/lib.nr index f2a634b9f8c..98cf094aabc 100644 --- a/yarn-project/noir-contracts/src/contracts/token_portal_content_hash_lib/src/lib.nr +++ b/yarn-project/noir-contracts/src/contracts/token_portal_content_hash_lib/src/lib.nr @@ -1,5 +1,5 @@ -use dep::std::hash::pedersen_with_separator; // docs:start:mint_public_content_hash_nr +use dep::std::hash::pedersen_with_separator; use dep::aztec::hash::{sha256_to_field}; // Computes a content hash of a deposit/mint_public message. @@ -28,6 +28,7 @@ pub fn get_mint_public_content_hash(owner_address: Field, amount: Field, cancell } // docs:end:mint_public_content_hash_nr +// docs:start:get_mint_private_content_hash // Computes a content hash of a deposit/mint_private message. // Refer TokenPortal.sol for reference on L1. pub fn get_mint_private_content_hash(secret_hash_for_redeeming_minted_notes: Field, amount: Field, canceller: Field) -> Field { @@ -51,7 +52,9 @@ pub fn get_mint_private_content_hash(secret_hash_for_redeeming_minted_notes: Fie let content_hash = sha256_to_field(hash_bytes); content_hash } +// docs:end:get_mint_private_content_hash +// docs:start:get_withdraw_content_hash // Computes a content hash of a withdraw message. pub fn get_withdraw_content_hash(recipient: Field, amount: Field, callerOnL1: Field) -> Field { // Compute the content hash @@ -76,4 +79,5 @@ pub fn get_withdraw_content_hash(recipient: Field, amount: Field, callerOnL1: Fi } let content_hash = sha256_to_field(hash_bytes); content_hash -} \ No newline at end of file +} +// docs:end:get_withdraw_content_hash diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr index 82c3a996114..aabf2735702 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr @@ -1,3 +1,4 @@ +// docs:start:interfaces use dep::aztec::{ context::{ PrivateContext, PublicContext, Context }, selector::compute_selector, @@ -52,3 +53,4 @@ impl TokenBridge { ); } } +// docs:end:interfaces diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr index 2d372eca1cd..35472e07ff4 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr @@ -1,3 +1,4 @@ +// docs:start:uniswap_setup mod interfaces; mod util; @@ -49,7 +50,9 @@ contract Uniswap { #[aztec(private)] fn constructor() {} +// docs:end:uniswap_setup +// docs:start:swap_public #[aztec(public)] fn swap_public( sender: AztecAddress, @@ -117,7 +120,9 @@ contract Uniswap { 1 } +// docs:end:swap_public +// docs:start:swap_private #[aztec(private)] fn swap_private( input_asset: AztecAddress, // since private, we pass here and later assert that this is as expected by input_bridge @@ -181,6 +186,7 @@ contract Uniswap { 1 } + // docs:end:swap_private // docs:start:authwit_uniswap_get // Since the token bridge burns funds on behalf of this contract, this contract has to tell the token contract if the signature is valid @@ -229,10 +235,12 @@ contract Uniswap { } // docs:end:authwit_uniswap_set + // docs:start:assert_token_is_same #[aztec(public)] internal fn _assert_token_is_same(token: AztecAddress, token_bridge: AztecAddress) { assert(token.eq(TokenBridge::at(token_bridge.address).token(context)), "input_asset address is not the same as seen in the bridge contract"); } + // docs:end:assert_token_is_same // /// Unconstrained /// @@ -240,5 +248,4 @@ contract Uniswap { unconstrained fn nonce_for_burn_approval() -> Field { storage.nonce_for_burn_approval.read() } - } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr index 4d9a1cd845f..cc39741944b 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr @@ -1,14 +1,15 @@ +// docs:start:uniswap_public_content_hash use dep::aztec::hash::sha256_to_field; -// This method computes the L2 to L1 message content hash for the private +// This method computes the L2 to L1 message content hash for the public // refer `l1-contracts/test/portals/UniswapPortal.sol` on how L2 to L1 message is expected -pub fn compute_swap_private_content_hash( +pub fn compute_swap_public_content_hash( input_asset_bridge_portal_address: Field, input_amount: Field, uniswap_fee_tier: Field, output_asset_bridge_portal_address: Field, minimum_output_amount: Field, - secret_hash_for_redeeming_minted_notes: Field, + aztec_recipient: Field, secret_hash_for_L1_to_l2_message: Field, deadline_for_L1_to_l2_message: Field, canceller_for_L1_to_L2_message: Field, @@ -21,17 +22,17 @@ pub fn compute_swap_private_content_hash( let uniswap_fee_tier_bytes = uniswap_fee_tier.to_be_bytes(32); let output_token_portal_bytes = output_asset_bridge_portal_address.to_be_bytes(32); let amount_out_min_bytes = minimum_output_amount.to_be_bytes(32); - let secret_hash_for_redeeming_minted_notes_bytes = secret_hash_for_redeeming_minted_notes.to_be_bytes(32); + let aztec_recipient_bytes = aztec_recipient.to_be_bytes(32); let secret_hash_for_L1_to_l2_message_bytes = secret_hash_for_L1_to_l2_message.to_be_bytes(32); let deadline_for_L1_to_l2_message_bytes = deadline_for_L1_to_l2_message.to_be_bytes(32); let canceller_bytes = canceller_for_L1_to_L2_message.to_be_bytes(32); let caller_on_L1_bytes = caller_on_L1.to_be_bytes(32); - // function selector: 0xbd87d14b keccak256("swap_private(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)") - hash_bytes[0] = 0xbd; - hash_bytes[1] = 0x87; - hash_bytes[2] = 0xd1; - hash_bytes[3] = 0x4b; + // function selector: 0xf3068cac keccak256("swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)") + hash_bytes[0] = 0xf3; + hash_bytes[1] = 0x06; + hash_bytes[2] = 0x8c; + hash_bytes[3] = 0xac; for i in 0..32 { hash_bytes[i + 4] = input_token_portal_bytes[i]; @@ -39,25 +40,28 @@ pub fn compute_swap_private_content_hash( hash_bytes[i + 68] = uniswap_fee_tier_bytes[i]; hash_bytes[i + 100] = output_token_portal_bytes[i]; hash_bytes[i + 132] = amount_out_min_bytes[i]; - hash_bytes[i + 164] = secret_hash_for_redeeming_minted_notes_bytes[i]; + hash_bytes[i + 164] = aztec_recipient_bytes[i]; hash_bytes[i + 196] = secret_hash_for_L1_to_l2_message_bytes[i]; hash_bytes[i + 228] = deadline_for_L1_to_l2_message_bytes[i]; hash_bytes[i + 260] = canceller_bytes[i]; hash_bytes[i + 292] = caller_on_L1_bytes[i]; } + let content_hash = sha256_to_field(hash_bytes); content_hash } +// docs:end:uniswap_public_content_hash -// This method computes the L2 to L1 message content hash for the public +// docs:start:compute_swap_private_content_hash +// This method computes the L2 to L1 message content hash for the private // refer `l1-contracts/test/portals/UniswapPortal.sol` on how L2 to L1 message is expected -pub fn compute_swap_public_content_hash( +pub fn compute_swap_private_content_hash( input_asset_bridge_portal_address: Field, input_amount: Field, uniswap_fee_tier: Field, output_asset_bridge_portal_address: Field, minimum_output_amount: Field, - aztec_recipient: Field, + secret_hash_for_redeeming_minted_notes: Field, secret_hash_for_L1_to_l2_message: Field, deadline_for_L1_to_l2_message: Field, canceller_for_L1_to_L2_message: Field, @@ -70,17 +74,17 @@ pub fn compute_swap_public_content_hash( let uniswap_fee_tier_bytes = uniswap_fee_tier.to_be_bytes(32); let output_token_portal_bytes = output_asset_bridge_portal_address.to_be_bytes(32); let amount_out_min_bytes = minimum_output_amount.to_be_bytes(32); - let aztec_recipient_bytes = aztec_recipient.to_be_bytes(32); + let secret_hash_for_redeeming_minted_notes_bytes = secret_hash_for_redeeming_minted_notes.to_be_bytes(32); let secret_hash_for_L1_to_l2_message_bytes = secret_hash_for_L1_to_l2_message.to_be_bytes(32); let deadline_for_L1_to_l2_message_bytes = deadline_for_L1_to_l2_message.to_be_bytes(32); let canceller_bytes = canceller_for_L1_to_L2_message.to_be_bytes(32); let caller_on_L1_bytes = caller_on_L1.to_be_bytes(32); - // function selector: 0xf3068cac keccak256("swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)") - hash_bytes[0] = 0xf3; - hash_bytes[1] = 0x06; - hash_bytes[2] = 0x8c; - hash_bytes[3] = 0xac; + // function selector: 0xbd87d14b keccak256("swap_private(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)") + hash_bytes[0] = 0xbd; + hash_bytes[1] = 0x87; + hash_bytes[2] = 0xd1; + hash_bytes[3] = 0x4b; for i in 0..32 { hash_bytes[i + 4] = input_token_portal_bytes[i]; @@ -88,13 +92,13 @@ pub fn compute_swap_public_content_hash( hash_bytes[i + 68] = uniswap_fee_tier_bytes[i]; hash_bytes[i + 100] = output_token_portal_bytes[i]; hash_bytes[i + 132] = amount_out_min_bytes[i]; - hash_bytes[i + 164] = aztec_recipient_bytes[i]; + hash_bytes[i + 164] = secret_hash_for_redeeming_minted_notes_bytes[i]; hash_bytes[i + 196] = secret_hash_for_L1_to_l2_message_bytes[i]; hash_bytes[i + 228] = deadline_for_L1_to_l2_message_bytes[i]; hash_bytes[i + 260] = canceller_bytes[i]; hash_bytes[i + 292] = caller_on_L1_bytes[i]; } - let content_hash = sha256_to_field(hash_bytes); content_hash } +// docs:end:compute_swap_private_content_hash \ No newline at end of file