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:
// 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
use dep::aztec::macros::aztec;
#[aztec]
contract TokenBridge {
use dep::aztec::prelude::{AztecAddress, EthAddress, PublicImmutable};
use dep::token_portal_content_hash_lib::{
get_mint_to_private_content_hash, get_mint_to_public_content_hash,
get_withdraw_content_hash,
};
use dep::token::Token;
use dep::aztec::macros::{
functions::{initializer, internal, private, public, view},
storage::storage,
};
}
Inside this block (before the last }
), paste this to initialize the constructor:
// Storage structure, containing all storage, and specifying what slots they use.
#[storage]
struct Storage<Context> {
token: PublicImmutable<AztecAddress, Context>,
portal_address: PublicImmutable<EthAddress, Context>,
}
// Constructs the contract.
#[public]
#[initializer]
fn constructor(token: AztecAddress, portal_address: EthAddress) {
storage.token.initialize(token);
storage.portal_address.initialize(portal_address);
}
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L27-L42
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
#[public]
fn claim_public(to: AztecAddress, amount: Field, secret: Field, message_leaf_index: Field) {
let content_hash = get_mint_to_public_content_hash(to, amount);
// Consume message and emit nullifier
context.consume_l1_to_l2_message(
content_hash,
secret,
storage.portal_address.read(),
message_leaf_index,
);
// Mint tokens
Token::at(storage.token.read()).mint_to_public(to, amount).call(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L54-L71
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_to_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_to_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_to_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
/// Claims the bridged tokens and makes them accessible in private. Note that recipient's address is not revealed
/// but the amount is. Hence it's most likely possible to determine to which L1 deposit this claim corresponds to
/// (unless there are multiple pending deposits of the same amount).
/// TODO(#8416): Consider creating a truly private claim flow.
#[private]
fn claim_private(
recipient: AztecAddress, // recipient of the bridged tokens
amount: Field,
secret_for_L1_to_L2_message_consumption: Field, // secret used to consume the L1 to L2 message
message_leaf_index: Field,
) {
// Consume L1 to L2 message and emit nullifier
let content_hash = get_mint_to_private_content_hash(amount);
context.consume_l1_to_l2_message(
content_hash,
secret_for_L1_to_L2_message_consumption,
storage.portal_address.read(),
message_leaf_index,
);
// Read the token address from storage
let token_address = storage.token.read();
// At last we mint the tokens
Token::at(token_address).mint_to_private(context.msg_sender(), recipient, amount).call(
&mut context,
);
}
Source code: noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr#L94-L125
The get_mint_to_private_content_hash
function is imported from the token_portal_content_hash_lib
.
If the content hashes were constructed similarly for mint_to_private
and mint_to_public
, 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.
Note that the TokenBridge
contract should be an authorized minter in the corresponding Token
contract so that it is able to complete the private mint to the intended recipient.
In the next step we will see how we can cancel a message.