Skip to content

Commit

Permalink
finish uniswap portal
Browse files Browse the repository at this point in the history
  • Loading branch information
rahul-kothari committed Oct 12, 2023
1 parent 0e650af commit 2dddf3a
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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?**

Expand All @@ -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.
17 changes: 14 additions & 3 deletions docs/docs/dev_docs/tutorials/uniswap/l1_portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
13 changes: 11 additions & 2 deletions docs/docs/dev_docs/tutorials/uniswap/l2_contract_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
@@ -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!
32 changes: 13 additions & 19 deletions docs/docs/dev_docs/tutorials/uniswap/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]
Expand All @@ -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

Expand All @@ -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
19 changes: 13 additions & 6 deletions docs/docs/dev_docs/tutorials/uniswap/swap_privately.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
21 changes: 12 additions & 9 deletions docs/docs/dev_docs/tutorials/uniswap/swap_publicly.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Loading

0 comments on commit 2dddf3a

Please sign in to comment.