Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs_tutorials): Token Portal & Uniswap Tutorial #2726

Merged
merged 13 commits into from
Oct 13, 2023
21 changes: 21 additions & 0 deletions docs/docs/dev_docs/tutorials/token_portal/cancelling_deposits.md
Original file line number Diff line number Diff line change
@@ -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 with deposits we need to support cancelling messages for minting privately and publicly.
catmcgee marked this conversation as resolved.
Show resolved Hide resolved

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it not be cancelled before the deadline?


In the next step we will write L1 and L2 logic to withdraw funds from L2 to L1.
95 changes: 95 additions & 0 deletions docs/docs/dev_docs/tutorials/token_portal/depositing_to_aztec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Depositing Tokens to Aztec
---

In this step, we will write our token portal contract on L1.

## Initialize Solidity contract

In `l1-contracts/contracts` create a new file called `TokenPortal.sol` and paste this:

```solidity
pragma solidity >=0.8.18;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am getting import errors here. I also tried installing the contracts with yarn add -D @openzeppelin/contracts in ./l1-contracts/ but still seeing the error.
image

import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// Messaging
import {IRegistry} from "./aztec/interfaces/messagebridge/IRegistry.sol";
import {IInbox} from "./aztec/interfaces/messagebridge/IInbox.sol";
import {DataStructures} from "./aztec/libraries/DataStructures.sol";
import {Hash} from "./aztec/libraries/Hash.sol";

contract TokenPortal {
catmcgee marked this conversation as resolved.
Show resolved Hide resolved
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)

## 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, a struct:
catmcgee marked this conversation as resolved.
Show resolved Hide resolved
- 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
catmcgee marked this conversation as resolved.
Show resolved Hide resolved
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.
64 changes: 64 additions & 0 deletions docs/docs/dev_docs/tutorials/token_portal/main.md
Original file line number Diff line number Diff line change
@@ -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](https://docs.aztec.network/dev_docs/contracts/portals/main).
catmcgee marked this conversation as resolved.
Show resolved Hide resolved

## 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 trust assumptions across the L1 or Aztec network.
catmcgee marked this conversation as resolved.
Show resolved Hide resolved

That means your L1 application can have a sister application residing on L2 and both of them can work together across the two networks.

<Image img={require("/img/tutorials/portals_shilling.png")} />

### 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](https://docs.aztec.network/concepts/foundation/communication/cross_chain_calls).
catmcgee marked this conversation as resolved.
Show resolved Hide resolved

## 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.

catmcgee marked this conversation as resolved.
Show resolved Hide resolved
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](https://docs.aztec.network/dev_docs/tutorials/writing_token_contract)).
catmcgee marked this conversation as resolved.
Show resolved Hide resolved

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.
catmcgee marked this conversation as resolved.
Show resolved Hide resolved
- 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.
catmcgee marked this conversation as resolved.
Show resolved Hide resolved
- 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.

<Image img={require("/img/tutorials/token_bridge_diagram.png")} />

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.
52 changes: 52 additions & 0 deletions docs/docs/dev_docs/tutorials/token_portal/minting_on_aztec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: Minting tokens on Aztec
---

In this step we will start writing our Aztec.nr bridge smart contract!

## Consume the L1 message

We have now 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 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:

#include_code imports_and_constructor /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has mod token_interface;, which we haven't created yet. That should be added.


Then 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_bridge_contract/src/util.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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the msg_key? How is that generated and how do I know the key for the message I'm interested in?

- `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.mint()` to mint the tokens to the to address.
catmcgee marked this conversation as resolved.
Show resolved Hide resolved

## 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` that you created in [setup](./setup.md), paste this:

#include_code get_mint_private_content_hash /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr rust
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

say this ecists in util.nr and is another helper method.


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()](https://github.com/AztecProtocol/dev-rel/tree/main/tutorials/token-bridge#_call_mint_on_token) which then calls [token.mint_private()](https://github.com/AztecProtocol/dev-rel/blob/main/tutorials/token-contract/README.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)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this hyperlink seems to be left over due top copy-paste. remove them please

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought we wanted them

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove them or update them to link to the appropriate section in the docs.


In the next step we will see how we can cancel a message.
Loading
Loading