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 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.
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.
115 changes: 115 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,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";
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/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 {
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)

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.
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](../../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.

<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](../../../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.

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](../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.

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