Minting tokens on Aztec
In this step we will start writing our Aztec.nr bridge smart contract and write a function to consume the message from the token portal to mint funds on Aztec
Initial contract setup
In our token-bridge
Aztec project in aztec-contracts
, under src
there is an example main.nr
file. Paste this to define imports and initialize the constructor:
// Minimal implementation of the token bridge that can move funds between L1 <> L2.
// The bridge has a corresponding Portal contract on L1 that it is attached to
// And corresponds to a Token on L2 that uses the `AuthWit` accounts pattern.
// Bridge has to be set as a minter on the token before it can be used
contract TokenBridge {
use dep::aztec::prelude::{FunctionSelector, AztecAddress, EthAddress, PublicMutable, SharedImmutable};
use dep::token_portal_content_hash_lib::{get_mint_public_content_hash, get_mint_private_content_hash, get_withdraw_content_hash};
use dep::token::Token;
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L1-L14
// Storage structure, containing all storage, and specifying what slots they use.
#[aztec(storage)]
struct Storage {
token: PublicMutable<AztecAddress>,
portal_address: SharedImmutable<EthAddress>,
}
// Constructs the contract.
#[aztec(public)]
#[aztec(initializer)]
fn constructor(token: AztecAddress, portal_address: EthAddress) {
storage.token.write(token);
storage.portal_address.initialize(portal_address);
}
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L16-L31
Consume the L1 message
In the previous step, we have moved our funds to the portal and created a L1->L2 message. Upon building the next rollup, the sequencer asks the inbox for any incoming messages and adds them to Aztec’s L1->L2 message tree, so an application on L2 can prove that the message exists and consumes it.
In main.nr
, now paste this claim_public
function:
// Consumes a L1->L2 message and calls the token contract to mint the appropriate amount publicly
#[aztec(public)]
fn claim_public(to: AztecAddress, amount: Field, secret: Field, message_leaf_index: Field) {
let content_hash = get_mint_public_content_hash(to, amount);
// Consume message and emit nullifier
context.consume_l1_to_l2_message(
content_hash,
secret,
storage.portal_address.read_public(),
message_leaf_index
);
// Mint tokens
Token::at(storage.token.read()).mint_public(to, amount).call(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L43-L60
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?
- 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 indepositToAztecPublic()
to create the content hash. - We then attempt to consume the L1->L2 message. Since we are depositing to Aztec publicly, all of the inputs are public.
context.consume_l1_to_l2_message()
takes in the few parameters:content_hash
: The content - which is reconstructed in theget_mint_public_content_hash()
secret
: The secret used for consumption, often 0 for public messagessender
: Who on L1 sent the message. Which should match the storedportal_address
in our case as we only want to allow messages from a specific sender.message_leaf_index
: The index in the message tree of the message.
- Note that the
content_hash
requiresto
andamount
. 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 thus not be in the tree, failing the consumption. This is why we add these parameters into the content.
- Then we call
Token::at(storage.token.read()).mint_public()
to mint the tokens to the to address.
Private flow
Now we will create a function to mint the amount privately. Paste this into your main.nr
// Consumes a L1->L2 message and calls the token contract to mint the appropriate amount in private assets
// User needs to call token.redeem_shield() to get the private assets
#[aztec(private)]
fn claim_private(
secret_hash_for_redeeming_minted_notes: Field, // secret hash used to redeem minted notes at a later time. This enables anyone to call this function and mint tokens to a user on their behalf
amount: Field,
secret_for_L1_to_L2_message_consumption: Field // secret used to consume the L1 to L2 message
) {
// Consume L1 to L2 message and emit nullifier
let content_hash = get_mint_private_content_hash(secret_hash_for_redeeming_minted_notes, amount);
context.consume_l1_to_l2_message(
content_hash,
secret_for_L1_to_L2_message_consumption,
storage.portal_address.read_private()
);
// Mint tokens on L2
// `mint_private` on token is public. So we call an internal public function
// which then calls the public method on the token contract.
// Since the secret_hash is passed, no secret is leaked.
TokenBridge::at(context.this_address())._call_mint_on_token(amount, secret_hash_for_redeeming_minted_notes).enqueue(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L80-L103
// This is a public call as we need to read from public storage.
// Also, note that user hashes their secret in private and only sends the hash in public
// meaning only user can `redeem_shield` at a later time with their secret.
#[aztec(public)]
#[aztec(internal)]
fn _call_mint_on_token(amount: Field, secret_hash: Field) {
Token::at(storage.token.read()).mint_private(amount, secret_hash).call(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L138-L147
The get_mint_private_content_hash
function is imported from the token_portal_content_hash_lib
.
If the content hashes were constructed similarly for mint_private
and mint_publicly
, then content intended for private execution could have been consumed by calling the claim_public
method. By making these two content hashes distinct, we prevent this scenario.
While we mint the tokens on L2, we still don’t actually mint them to a certain address. Instead we continue to pass the secret_hash_for_redeeming_minted_notes
like we did on L1. This means that a user could reveal their secret for L2 message consumption for anyone to mint tokens on L2 but they can redeem these notes at a later time. This enables a paradigm where an app can manage user’s secrets for L2 message consumption on their behalf. The app or any external party can also mint tokens on the user’s behalf should they be comfortable with leaking the secret for L2 Message consumption. This doesn’t leak any new information to the app because their smart contract on L1 knew that a user wanted to move some amount of tokens to L2. The app still doesn’t know which address on L2 the user wants these notes to be in, but they can mint tokens nevertheless on their behalf.
To mint tokens privately, claim_private
calls an internal function _call_mint_on_token()
which then calls token.mint_private().
In the next step we will see how we can cancel a message.