From 4024a1cfe0a9057d589341b87221e1e6c843aa03 Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Thu, 12 Oct 2023 14:38:34 +0000 Subject: [PATCH] finish uniswap portal --- .../token_portal/typescript_glue_code.md | 2 +- .../uniswap/execute_private_swap_on_l1.md | 11 +-- ..._on_l1.md => execute_public_swap_on_l1.md} | 8 ++- .../dev_docs/tutorials/uniswap/l1_portal.md | 17 ++++- .../tutorials/uniswap/l2_contract_setup.md | 13 +++- .../uniswap/redeeming_swapped_assets_on_l2.md | 12 ++++ docs/docs/dev_docs/tutorials/uniswap/setup.md | 32 ++++----- .../tutorials/uniswap/swap_privately.md | 19 +++-- .../tutorials/uniswap/swap_publicly.md | 21 +++--- .../tutorials/uniswap/typescript_glue_code.md | 72 ++++++++++++++++++- docs/sidebars.js | 3 +- l1-contracts/test/external/ISwapRouter.sol | 1 - l1-contracts/test/portals/UniswapPortal.sol | 15 ++-- .../src/uniswap_trade_on_l1_from_l2.test.ts | 6 +- .../end-to-end/src/canary/uniswap_l1_l2.ts | 24 +++++-- .../contracts/uniswap_contract/src/util.nr | 48 +++++++------ 16 files changed, 211 insertions(+), 93 deletions(-) rename docs/docs/dev_docs/tutorials/uniswap/{execute_swap_on_l1.md => execute_public_swap_on_l1.md} (81%) create mode 100644 docs/docs/dev_docs/tutorials/uniswap/redeeming_swapped_assets_on_l2.md 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 index 0662e5896435..b630fe20d14c 100644 --- a/docs/docs/dev_docs/tutorials/token_portal/typescript_glue_code.md +++ b/docs/docs/dev_docs/tutorials/token_portal/typescript_glue_code.md @@ -82,7 +82,7 @@ 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(120_000); + jest.setTimeout(90_000); let logger: DebugLogger; // include code: 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 index 6f71eb8cf673..efbcee0ecd5a 100644 --- 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 @@ -1,13 +1,8 @@ --- title: Executing Private Swap on L1 --- - -This works very similarly to the public flow. - -In the public flow, you can call `claim_public()` 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 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 your `UniswapPortal.sol`, paste this: +To execute the swaps on L1, go back to the `UniswapPortal.sol` we [created earlier](./l1_portal.md) in `packages/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_swap_on_l1.md b/docs/docs/dev_docs/tutorials/uniswap/execute_public_swap_on_l1.md similarity index 81% rename from docs/docs/dev_docs/tutorials/uniswap/execute_swap_on_l1.md rename to docs/docs/dev_docs/tutorials/uniswap/execute_public_swap_on_l1.md index 9d5a383c2c45..65a052b896ec 100644 --- a/docs/docs/dev_docs/tutorials/uniswap/execute_swap_on_l1.md +++ b/docs/docs/dev_docs/tutorials/uniswap/execute_public_swap_on_l1.md @@ -1,12 +1,12 @@ --- -title: Executing Swap on L1 +title: Solidity Code to Execute Swap on L1 --- -To execute the swaps on L1, go back to the `TokenPortal.sol` we [created earlier](./l1_portal.md). +To execute the swaps on L1, go back to the `UniswapPortal.sol` we [created earlier](./l1_portal.md) in `packages/l1-contracts`. Under the struct, paste this code that will manage the public flow: -#include_code solidity_uniswap_swap l1-contracts/test/portals/UniswapPortal.sol solidity +#include_code solidity_uniswap_swap_public l1-contracts/test/portals/UniswapPortal.sol solidity **What’s happening here?** @@ -23,4 +23,6 @@ Under the struct, paste this code that will manage the public flow: 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 index ece530917e77..8d656d7afd6e 100644 --- a/docs/docs/dev_docs/tutorials/uniswap/l1_portal.md +++ b/docs/docs/dev_docs/tutorials/uniswap/l1_portal.md @@ -7,13 +7,24 @@ In this step we will set up our Solidity portal contract. In `l1-tokens` create a new file called `UniswapPortal.sol` ```bash -cd l1-tokens && touch UniswapPortal.sol +cd packages/l1-tokens && touch UniswapPortal.sol ``` and paste this inside: +```solidity +pragma solidity >=0.8.18; -#include_code setup l1-contracts/test/portals/UniswapPortal.sol solidity +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -In this set up we define the `initialize()` function and a struct (`LocalSwapVars`) to manage assets being swapped. +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! \ No newline at end of file diff --git a/docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md b/docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md index ecf7af820d48..0e451114328d 100644 --- a/docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md +++ b/docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md @@ -12,14 +12,23 @@ Our main contract will live inside `uniswap/src/main.nr`. In `main.nr`, paste th **What’s happening here?** -Because Uniswap is the one approving, 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. +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. -Under the storage struct, paste this function: +## 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/redeeming_swapped_assets_on_l2.md b/docs/docs/dev_docs/tutorials/uniswap/redeeming_swapped_assets_on_l2.md new file mode 100644 index 000000000000..befecf8b2263 --- /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 index 5a6035b106d7..df88b5008c6c 100644 --- a/docs/docs/dev_docs/tutorials/uniswap/setup.md +++ b/docs/docs/dev_docs/tutorials/uniswap/setup.md @@ -7,26 +7,25 @@ This tutorial builds on top of the project created in the previous tutorial. It If you don’t have this, you can find the code for it [here]. // TODO full code in dev-rels rouper -# L1 contracts - -We will need one more L1 contract - _ISwapRouter_ - which you can find [here](https://github.com/AztecProtocol/aztec-packages/blob/c794533754a9706d362d0374209df9eb5b6bfdc7/l1-contracts/test/external/ISwapRouter.sol). Add this to `l1-contracts/external`: +# Uniswap contract +To interact with uniswap we need to add it's interface: ```bash -cd l1-contracts && mkdir external && touch ISwapRouter.sol +cd packages/l1-contracts && mkdir external && touch ISwapRouter.sol ``` Inside `ISwapRouter.sol` paste this: -"#include_code iswaprouter l1-contracts/test/external/ISwapRouter.sol solidity +#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 nargo project +# Create another nargo project In `aztec-packages` create a new nargo project. ```bash -cd aztec-packages && nargo new --contract uniswap +cd packages/aztec-packages && nargo new --contract uniswap ``` Now your `aztec-contracts` will look like this: @@ -43,7 +42,7 @@ aztec-contracts ├── main.nr ``` -Inside the new `Nargo.toml` paste this in `[dependencies]`: +Inside `uniswap/Nargo.toml` paste this in `[dependencies]`: ```json [dependencies] @@ -60,21 +59,14 @@ The `main.nr` will utilize a few helper functions that are outside the scope of cd uniswap/src && touch util.nr && touch interface.nr ``` -Inside `util.nr` paste this: - -#include_code uniswap_util -yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr rust - -This file contains two functions, `compute_swap_private_content_hash` and `compute_swap_public_content_hash`, which generate content hashes for L2 to L1 messages representing swap transactions. - -and inside `interface.nr` paste this: +Inside `interface.nr` paste this: #include_code interfaces yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr rust -This defines two structs: `Token` and `TokenBridge.` +This creates interfaces for the `Token` contract and `TokenBridge` contract -- `Token` represents an Aztec token, allowing for public transfers (`transfer_public`) and private-to-public conversions (`unshield`). -- The `TokenBridge` struct facilitates interactions with our bridge contract, enabling getting the associated token (`token`), claiming tokens in a public context (`claim_public`), and exiting tokens to L1. (`exit_to_l1_public`). +- `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 @@ -83,3 +75,5 @@ You will need a running sandbox. ```bash /bin/bash -c "$(curl -fsSL 'https://sandbox.aztec.network')" ``` + +Next we will write the L1 Uniswap Portal \ No newline at end of file diff --git a/docs/docs/dev_docs/tutorials/uniswap/swap_privately.md b/docs/docs/dev_docs/tutorials/uniswap/swap_privately.md index 155a32cea32e..a94906241828 100644 --- a/docs/docs/dev_docs/tutorials/uniswap/swap_privately.md +++ b/docs/docs/dev_docs/tutorials/uniswap/swap_privately.md @@ -2,15 +2,22 @@ title: Swapping Privately --- -In the `main.nr` contract we created [previously](./l2_contract_setup.md), paste these functions: +In the `uniswap/src/main.nr` contract we created [previously](./l2_contract_setup.md) in `packages/aztec-contracts/uniswap`, paste these functions: #include_code swap_private yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr rust -#include_code authwit_uniswap_set 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 flow works very similarly to the public flow except: +This uses a util function `compute_swap_private_content_hash()` - let's add that. -- Contracts on Aztec can’t directly hold notes. Since private tokens are basically notes, it isn’t possible for the Uniiswap contract to hold the notes and then approve the token bridge to burn them (since the Uniswap contract would then need to have a private key associated with it that can sign the payload for approval.) -- To work around this, the user can unshield their private tokens into Uniswap L2 contract. Unshielding 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. -- As a result `swap_private()` calls the internal public method which approves the input token bridge to burn Uniswap’s tokens and creates an L2 → L1 message to exit to L1. +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. \ No newline at end of file diff --git a/docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md b/docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md index 310daaab1c8c..fb4d8bb9b1d8 100644 --- a/docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md +++ b/docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md @@ -2,24 +2,27 @@ title: Swapping Publicly --- -In this function we will create the flow for allowing a user to swap their tokens publicly on L1. In this function we have to add functionality of letting anyone call this method on behalf of the user, assuming they have appropriate approvals. This facilitates Aztec Connect-style aggregation and means that an operator can pay gas fees. +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! -Under the storage struct in `main.nr` paste this: +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 compute_swap_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. We hash all the arguments together and check against this hash. This means that the sender can’t change any param other than what they got approved for. This is our standard auth-witness check. +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. This emits a L2 → L1 `withdraw()` message like we saw with the token bridge. - - 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 needs: +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 @@ -34,4 +37,4 @@ Under the storage struct in `main.nr` paste this: 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 execute this swap 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 index 3af85c528d46..a9ef754344da 100644 --- a/docs/docs/dev_docs/tutorials/uniswap/typescript_glue_code.md +++ b/docs/docs/dev_docs/tutorials/uniswap/typescript_glue_code.md @@ -2,4 +2,74 @@ title: Deploy & Call Contracts with Typescript --- -Still todo +So far we have written our solidity and aztec-nr functions to swap L2 tokens on L1 and get emit a message to deposit assets back on L2. But we haven't yet interacted with the sandbox to actually execute the code and see the tokens being bridged and swapped. We will now write a test to interact with the sandbox and see the expected results! + +In the root folder, go to `packages/src` folder where we added `jest`: +```bash +cd packages/src +mkdir test && cd test +touch uniswap.test.ts +``` + +Open `uniswap.test.ts`: + +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 + +## Test imports and setup +This is exactly the same as the setup for the tests in the token bridge tutorial. Copy the `utils.ts` and `cross_chain_test_harness.ts` we have defined that tutorial [here](../token_portal/typescript_glue_code.md#test-imports-and-setup). + +In `utils.ts`, also add: +```typescript +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= +``` + +### 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 "@aztec/noir-contracts/types"; +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 +Run the sandbox: +```bash +cd ~/.aztec && docker-compose up +``` + +In a separate terminal: +```bash +cd packages/src +yarn test uniswap +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index eec51acf8489..f8934b0138cb 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -256,9 +256,10 @@ const sidebars = { "dev_docs/tutorials/uniswap/l1_portal", "dev_docs/tutorials/uniswap/l2_contract_setup", "dev_docs/tutorials/uniswap/swap_publicly", - "dev_docs/tutorials/uniswap/execute_swap_on_l1", + "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", ], }, diff --git a/l1-contracts/test/external/ISwapRouter.sol b/l1-contracts/test/external/ISwapRouter.sol index f90d4fe50282..daf041a19f0f 100644 --- a/l1-contracts/test/external/ISwapRouter.sol +++ b/l1-contracts/test/external/ISwapRouter.sol @@ -77,4 +77,3 @@ interface ISwapRouter { returns (uint256 amountIn); } // docs:end:iswaprouter - diff --git a/l1-contracts/test/portals/UniswapPortal.sol b/l1-contracts/test/portals/UniswapPortal.sol index 6b6cbee33c13..75e32f22c43f 100644 --- a/l1-contracts/test/portals/UniswapPortal.sol +++ b/l1-contracts/test/portals/UniswapPortal.sol @@ -1,13 +1,14 @@ -// docs:start:setup 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 @@ -36,8 +37,7 @@ contract UniswapPortal { } // 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. @@ -129,7 +129,7 @@ contract UniswapPortal { _aztecRecipient, amountOut, _canceller, _deadlineForL1ToL2Message, _secretHashForL1ToL2Message ); } - // docs:end:solidity_uniswap_swap + // docs:end:solidity_uniswap_swap_public // docs:start:solidity_uniswap_swap_private /** @@ -228,5 +228,4 @@ contract UniswapPortal { ); } } - // docs:end:solidity_uniswap_swap_private - +// 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 16a890575d84..6b5ceed4b86c 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); \ No newline at end of file +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 6ff3289d3c80..c89735d66d63 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:start: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:start: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/noir-contracts/src/contracts/uniswap_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr index dfaad972ce89..cc39741944b3 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,15 +1,15 @@ -// docs:start:uniswap_util +// 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, @@ -22,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]; @@ -40,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, @@ -71,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]; @@ -89,14 +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:uniswap_util +// docs:end:compute_swap_private_content_hash \ No newline at end of file