From a133cf0026ba7cd0e2b9c2401c3620caab0906b9 Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Sun, 17 Sep 2023 14:07:41 +0000 Subject: [PATCH 1/6] portal contract --- .../end-to-end/src/cli_docs_sandbox.test.ts | 1 + yarn-project/noir-contracts/Nargo.toml | 1 + .../token_bridge_contract/Nargo.toml | 8 + .../token_bridge_contract/src/main.nr | 170 ++++++++++++++++++ .../src/token_interface.nr | 47 +++++ .../token_bridge_contract/src/types.nr | 44 +++++ .../token_bridge_contract/src/util.nr | 87 +++++++++ 7 files changed, 358 insertions(+) create mode 100644 yarn-project/noir-contracts/src/contracts/token_bridge_contract/Nargo.toml create mode 100644 yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr create mode 100644 yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr create mode 100644 yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/types.nr create mode 100644 yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr diff --git a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts index 055d605e0dc..4769d27b327 100644 --- a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts +++ b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts @@ -104,6 +104,7 @@ SchnorrSingleKeyAccountContractAbi StatefulTestContractAbi TestContractAbi TokenContractAbi +TokenBridgeContractAbi UniswapContractAbi // docs:end:example-contracts `; diff --git a/yarn-project/noir-contracts/Nargo.toml b/yarn-project/noir-contracts/Nargo.toml index 2d5cfebf585..fc837a45a5a 100644 --- a/yarn-project/noir-contracts/Nargo.toml +++ b/yarn-project/noir-contracts/Nargo.toml @@ -23,5 +23,6 @@ members = [ "src/contracts/stateful_test_contract", "src/contracts/test_contract", "src/contracts/token_contract", + "src/contracts/token_bridge_contract", "src/contracts/uniswap_contract", ] diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/Nargo.toml b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/Nargo.toml new file mode 100644 index 00000000000..cdad0512395 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "token_bridge_contract" +authors = [""] +compiler_version = "0.1" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr new file mode 100644 index 00000000000..adb0cc274cc --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr @@ -0,0 +1,170 @@ +mod types; +mod util; +mod token_interface; + +// 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 sued + +contract TokenBridge { + use dep::aztec::{ + context::{Context}, + state_vars::{public_state::PublicState}, + types::type_serialisation::field_serialisation::{ + FieldSerialisationMethods, FIELD_SERIALISED_LEN, + }, + oracle::compute_selector::compute_selector, + }; + + use crate::types::{AztecAddress, EthereumAddress}; + use crate::token_interface::Token; + use crate::util::{get_mint_content_hash, get_withdraw_content_hash, compute_secret_hash}; + + // Storage structure, containing all storage, and specifying what slots they use. + struct Storage { + token: PublicState, + } + + impl Storage { + fn init(context: Context) -> pub Self { + Storage { + token: PublicState::new( + context, + 1, + FieldSerialisationMethods, + ), + } + } + } + + // Constructs the contract. + #[aztec(private)] + fn constructor() { + // Currently not possible to execute public calls from constructor as code not yet available to sequencer. + // let selector = compute_selector("_initialize((Field))"); + // let _callStackItem = context.call_public_function(context.this_address(), selector, [context.msg_sender()]); + } + + // Consumes a L1->L2 message and calls the token contract to mint the appropriate amount publicly + #[aztec(public)] + fn deposit_public( + amount: Field, + msg_key: Field, + secret: Field, + canceller: EthereumAddress, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + + let content_hash = get_mint_content_hash(amount, context.msg_sender(), canceller.address); + // Consume message and emit nullifier + context.consume_l1_to_l2_message(msg_key, content_hash, secret); + + // Mint tokens + Token::at(storage.token.read()).mint_public(context, context.msg_sender(), amount); + + 1 + } + + // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message publicly + // Requires `from` to give approval to the bridge to burn tokens on their behalf using witness signatures + #[aztec(public)] + fn withdraw_public( + amount: Field, + recipient: EthereumAddress, // ethereum address to withdraw to + callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) + nonce: Field, + ) -> Field { + let storage = Storage::init(Context::public(&mut context)); + + // Burn tokens + Token::at(storage.token.read()).burn_public(context, context.msg_sender(), amount, nonce); + + // Send an L2 to L1 message + let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address); + context.message_portal(content); + 1 + } + + // 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 deposit_private( + amount: Field, + msg_key: Field, // L1 to L2 message key as derived from the inbox contract + secret: Field, + canceller: EthereumAddress, + ) -> Field { + // Consume L1 to L2 message and emit nullifier + let content_hash = get_mint_content_hash(amount, context.msg_sender(), canceller.address); + context.consume_l1_to_l2_message(inputs, msg_key, content_hash, secret); + + // Mint tokens on L2 + // We hash the secret privately and send the hash in a public call so the secret isn't leaked + // Furthermore, `mint_private` on token is public. So we can an internal public function + // which then calls the token contract + let secret_hash = compute_secret_hash(secret); + context.call_public_function( + context.this_address(), + compute_selector("_call_mint_on_token(Field,Field)"), + [amount, secret_hash], + ); + + 1 + } + + // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately + // Requires `from` to give approval to the bridge to burn tokens on their behalf using witness signatures + #[aztec(private)] + fn withdraw_private( + token: AztecAddress, + amount: Field, + recipient: EthereumAddress, // ethereum address to withdraw to + callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) + nonce: Field, + ) -> Field { + // Burn tokens + Token::at(token.address).burn(&mut context, context.msg_sender(), amount, nonce); + + // Send an L2 to L1 message + let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address); + context.message_portal(content); + + // Assert that user provided token address is same as seen in storage. + context.call_public_function(context.this_address(), compute_selector("_assert_token_is_same(Field)"), [token.address]); + + 1 + } + + // /// Unconstrained /// + + unconstrained fn token() -> Field { + let storage = Storage::init(Context::none()); + storage.token.read() + } + + /// SHOULD BE Internal /// + + // We cannot do this from the constructor currently + // Since this should be internal, for now, we ignore the safety checks of it, as they are + // enforced by it being internal and only called from the constructor. + #[aztec(public)] + fn _initialize(token: AztecAddress) { + let storage = Storage::init(Context::public(&mut context)); + storage.token.write(token.address); + } + + // This way, 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)] + internal fn _call_mint_on_token(amount: Field, secret_hash: Field){ + let storage = Storage::init(Context::public(&mut context)); + Token::at(storage.token.read()).mint_private(context, amount, secret_hash); + } + + #[aztec(public)] + internal fn _assert_token_is_same(token: Field) { + let storage = Storage::init(Context::public(&mut context)); + assert(storage.token.read() == token, "Token address is not the same as seen in storage"); + } +} diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr new file mode 100644 index 00000000000..45c11562d86 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr @@ -0,0 +1,47 @@ +use dep::aztec::{ + context::{ PrivateContext, PublicContext, Context }, + oracle::compute_selector::compute_selector, +}; + +struct Token { + address: Field, +} + +impl Token { + fn at(address: Field) -> Self { + Self { address } + } + + fn mint_public(self: Self, context: PublicContext, to: Field, amount: Field) { + let _return_values = context.call_public_function( + self.address, + compute_selector("mint_public((Field),Field)"), + [to, amount] + ); + } + + fn burn_public(self: Self, context: PublicContext, from: Field, amount: Field, nonce: Field) { + let _return_values = context.call_public_function( + self.address, + compute_selector("burn_public((Field),Field,Field)"), + [from, amount, nonce] + ); + } + + // Private + fn mint_private(self: Self, context: PublicContext, amount: Field, secret_hash: Field) { + let _return_values = context.call_public_function( + self.address, + compute_selector("mint_private(Field,Field)"), + [amount, secret_hash] + ); + } + + fn burn(self: Self, context: &mut PrivateContext, from: Field, amount: Field, nonce: Field) { + let _return_values = context.call_private_function( + self.address, + compute_selector("burn((Field),Field,Field)"), + [from, amount, nonce] + ); + } +} diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/types.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/types.nr new file mode 100644 index 00000000000..6c7f165c347 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/types.nr @@ -0,0 +1,44 @@ +struct AztecAddress { + address: Field +} + +impl AztecAddress { + fn new(address: Field) -> Self { + Self { + address + } + } + + fn serialize(self: Self) -> [Field; 1] { + [self.address] + } + + fn deserialize(fields: [Field; 1]) -> Self { + Self { + address: fields[0] + } + } +} + +struct EthereumAddress { + address: Field +} + +impl EthereumAddress { + fn new(address: Field) -> Self { + Self { + address + } + } + + + fn serialize(self: Self) -> [Field; 1] { + [self.address] + } + + fn deserialize(fields: [Field; 1]) -> Self { + Self { + address: fields[0] + } + } +} \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr new file mode 100644 index 00000000000..9e2cfd194e4 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr @@ -0,0 +1,87 @@ +use dep::std::hash::{pedersen_with_separator, sha256}; +use dep::aztec::constants_gen::{ + GENERATOR_INDEX__SIGNATURE_PAYLOAD, + GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET, +}; + +fn compute_secret_hash(secret: Field) -> Field { + // TODO(#1205) This is probably not the right index to use + pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] +} + +// Computes a content hash of a deposit/mint message. +fn get_mint_content_hash(amount: Field, owner_address: Field, canceller: Field) -> Field { + let mut hash_bytes: [u8; 100] = [0; 100]; + let amount_bytes = amount.to_be_bytes(32); + let recipient_bytes = owner_address.to_be_bytes(32); + let canceller_bytes = canceller.to_be_bytes(32); + + for i in 0..32 { + hash_bytes[i + 4] = amount_bytes[i]; + hash_bytes[i + 36] = recipient_bytes[i]; + hash_bytes[i + 68] = canceller_bytes[i]; + } + + // Function selector: 0xeeb73071 keccak256('mint(uint256,bytes32,address)') + hash_bytes[0] = 0xee; + hash_bytes[1] = 0xb7; + hash_bytes[2] = 0x30; + hash_bytes[3] = 0x71; + + let content_sha256 = sha256(hash_bytes); + + // // Convert the content_sha256 to a field element + let mut v = 1; + let mut high = 0 as Field; + let mut low = 0 as Field; + + for i in 0..16 { + high = high + (content_sha256[15 - i] as Field) * v; + low = low + (content_sha256[16 + 15 - i] as Field) * v; + v = v * 256; + } + + // Abuse that a % p + b % p = (a + b) % p and that low < p + let content_hash = low + high * v; + content_hash +} + +// Computes a content hash of a withdraw message. +fn get_withdraw_content_hash(amount: Field, recipient: Field, callerOnL1: Field) -> Field { + // Compute the content hash + // Compute sha256(selector || amount || recipient) + // then convert to a single field element + // add that to the l2 to l1 messages + let mut hash_bytes: [u8; 100] = [0; 100]; + let amount_bytes = amount.to_be_bytes(32); + let recipient_bytes = recipient.to_be_bytes(32); + let callerOnL1_bytes = callerOnL1.to_be_bytes(32); + + // 0xb460af94, selector for "withdraw(uint256,address,address)" + hash_bytes[0] = 0xb4; + hash_bytes[1] = 0x60; + hash_bytes[2] = 0xaf; + hash_bytes[3] = 0x94; + + for i in 0..32 { + hash_bytes[i + 4] = amount_bytes[i]; + hash_bytes[i + 36] = recipient_bytes[i]; + hash_bytes[i + 68] = callerOnL1_bytes[i]; + } + let content_sha256 = sha256(hash_bytes); + + // Convert the content_sha256 to a field element + let mut v = 1; + let mut high = 0 as Field; + let mut low = 0 as Field; + + for i in 0..16 { + high = high + (content_sha256[15 - i] as Field) * v; + low = low + (content_sha256[16 + 15 - i] as Field) * v; + v = v * 256; + } + + // Abuse that a % p + b % p = (a + b) % p and that low < p + let content = low + high * v; + content +} \ No newline at end of file From 55ca3e85b8065383280219768be46bdc596d219b Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Sun, 17 Sep 2023 14:08:05 +0000 Subject: [PATCH 2/6] update cross chain tests --- .../src/e2e_cross_chain_messaging.test.ts | 84 ++++++----- .../e2e_public_cross_chain_messaging.test.ts | 86 ++++++----- .../e2e_public_to_private_messaging.test.ts | 19 ++- .../src/fixtures/cross_chain_test_harness.ts | 135 +++++++++++------- yarn-project/end-to-end/src/fixtures/utils.ts | 131 ++++++++++++++++- .../src/uniswap_trade_on_l1_from_l2.test.ts | 80 ++++------- 6 files changed, 335 insertions(+), 200 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 8861c6477dd..88a8b250ab7 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -1,29 +1,28 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecRPCServer } from '@aztec/aztec-rpc'; -import { AztecAddress } from '@aztec/aztec.js'; +import { AccountWallet, AztecAddress } from '@aztec/aztec.js'; +import { Fr, FunctionSelector } from '@aztec/circuits.js'; import { EthAddress } from '@aztec/foundation/eth-address'; import { DebugLogger } from '@aztec/foundation/log'; -import { NonNativeTokenContract } from '@aztec/noir-contracts/types'; -import { AztecRPC, TxStatus } from '@aztec/types'; +import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; +import { AztecRPC } from '@aztec/types'; import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; -import { delay, setup } from './fixtures/utils.js'; +import { delay, hashPayload, setup } from './fixtures/utils.js'; describe('e2e_cross_chain_messaging', () => { let aztecNode: AztecNodeService; let aztecRpcServer: AztecRPC; let logger: DebugLogger; - let l2Contract: NonNativeTokenContract; + let wallet: AccountWallet; let ethAccount: EthAddress; - - let underlyingERC20: any; - let outbox: any; - - const initialBalance = 10n; let ownerAddress: AztecAddress; let crossChainTestHarness: CrossChainTestHarness; + let l2Token: TokenContract; + let l2Bridge: TokenBridgeContract; + let outbox: any; beforeEach(async () => { const { @@ -31,28 +30,27 @@ describe('e2e_cross_chain_messaging', () => { aztecRpcServer: aztecRpcServer_, deployL1ContractsValues, accounts, - wallet, + wallet: wallet_, logger: logger_, cheatCodes, } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( - initialBalance, aztecNode, aztecRpcServer_, deployL1ContractsValues, accounts, - wallet, + wallet_, logger_, cheatCodes, ); - l2Contract = crossChainTestHarness.l2Contract; + l2Token = crossChainTestHarness.l2Token; + l2Bridge = crossChainTestHarness.l2Bridge; ethAccount = crossChainTestHarness.ethAccount; ownerAddress = crossChainTestHarness.ownerAddress; - underlyingERC20 = crossChainTestHarness.underlyingERC20; outbox = crossChainTestHarness.outbox; aztecRpcServer = crossChainTestHarness.aztecRpcServer; - + wallet = wallet_; logger = logger_; logger('Successfully deployed contracts and initialized portal'); }, 100_000); @@ -65,24 +63,6 @@ describe('e2e_cross_chain_messaging', () => { await crossChainTestHarness?.stop(); }); - const expectBalance = async (owner: AztecAddress, expectedBalance: bigint) => { - const balance = await l2Contract.methods.getBalance(owner).view({ from: owner }); - logger(`Account ${owner} balance: ${balance}`); - expect(balance).toBe(expectedBalance); - }; - - const withdrawFundsFromAztec = async (withdrawAmount: bigint) => { - logger('Send L2 tx to withdraw funds'); - const withdrawTx = l2Contract.methods - .withdraw(withdrawAmount, ownerAddress, ethAccount, EthAddress.ZERO.toField()) - .send(); - - await withdrawTx.isMined({ interval: 0.1 }); - const withdrawReceipt = await withdrawTx.getReceipt(); - - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - }; - it('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { // Generate a claim secret using pedersen const l1TokenBalance = 1000000n; @@ -90,33 +70,49 @@ describe('e2e_cross_chain_messaging', () => { const [secret, secretHash] = await crossChainTestHarness.generateClaimSecret(); + // 1. Mint tokens on L1 await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); const messageKey = await crossChainTestHarness.sendTokensToPortal(bridgeAmount, secretHash); - expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. // Perform another unrelated transaction on L2 to progress the rollup. - const transferAmount = 1n; - await crossChainTestHarness.performL2Transfer(transferAmount); + const unrelatedMintAmount = 99n; + await crossChainTestHarness.mintTokensPublicOnL2(unrelatedMintAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); + // 3. Consume L1-> L2 message and mint private tokens on L2 await crossChainTestHarness.consumeMessageOnAztecAndMintSecretly(bridgeAmount, messageKey, secret); - await expectBalance(ownerAddress, bridgeAmount + initialBalance - transferAmount); + await crossChainTestHarness.redeemShieldPrivatelyOnL2(bridgeAmount, secret); + await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, bridgeAmount); // time to withdraw the funds again! logger('Withdrawing funds from L2'); + + // 4. Give approval to bridge to burn owner's funds: const withdrawAmount = 9n; + const nonce = Fr.random(); + const burnMessageHash = await hashPayload([ + l2Bridge.address.toField(), + l2Token.address.toField(), + FunctionSelector.fromSignature('burn((Field),Field,Field)').toField(), + ownerAddress.toField(), + new Fr(withdrawAmount), + nonce, + ]); + await wallet.createAuthWitness(burnMessageHash); + + // 5. Withdraw owner's funds from L2 to L1 const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); - await withdrawFundsFromAztec(withdrawAmount); - await expectBalance(ownerAddress, bridgeAmount + initialBalance - transferAmount - withdrawAmount); + await crossChainTestHarness.withdrawPrivateFromAztecToL1(withdrawAmount, nonce); + await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount); // Check balance before and after exit. - expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); await crossChainTestHarness.withdrawFundsFromBridgeOnL1(withdrawAmount, entryKey); - expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe( - l1TokenBalance - bridgeAmount + withdrawAmount, - ); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index 795101a72e6..bba6b576e19 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -1,29 +1,28 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecRPCServer } from '@aztec/aztec-rpc'; -import { AztecAddress } from '@aztec/aztec.js'; +import { AccountWallet, AztecAddress } from '@aztec/aztec.js'; +import { Fr, FunctionSelector } from '@aztec/circuits.js'; import { EthAddress } from '@aztec/foundation/eth-address'; import { DebugLogger } from '@aztec/foundation/log'; -import { NonNativeTokenContract } from '@aztec/noir-contracts/types'; -import { AztecRPC, TxStatus } from '@aztec/types'; +import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; +import { AztecRPC } from '@aztec/types'; import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; -import { delay, setup } from './fixtures/utils.js'; +import { delay, hashPayload, setup } from './fixtures/utils.js'; describe('e2e_public_cross_chain_messaging', () => { let aztecNode: AztecNodeService | undefined; let aztecRpcServer: AztecRPC; let logger: DebugLogger; - let l2Contract: NonNativeTokenContract; + let wallet: AccountWallet; let ethAccount: EthAddress; - - let underlyingERC20: any; - let outbox: any; - - const initialBalance = 10n; let ownerAddress: AztecAddress; let crossChainTestHarness: CrossChainTestHarness; + let l2Token: TokenContract; + let l2Bridge: TokenBridgeContract; + let outbox: any; beforeEach(async () => { const { @@ -31,28 +30,28 @@ describe('e2e_public_cross_chain_messaging', () => { aztecRpcServer: aztecRpcServer_, deployL1ContractsValues, accounts, - wallet, + wallet: wallet_, logger: logger_, cheatCodes, } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( - initialBalance, aztecNode_, aztecRpcServer_, deployL1ContractsValues, accounts, - wallet, + wallet_, logger_, cheatCodes, ); - l2Contract = crossChainTestHarness.l2Contract; + l2Token = crossChainTestHarness.l2Token; + l2Bridge = crossChainTestHarness.l2Bridge; ethAccount = crossChainTestHarness.ethAccount; ownerAddress = crossChainTestHarness.ownerAddress; - underlyingERC20 = crossChainTestHarness.underlyingERC20; outbox = crossChainTestHarness.outbox; aztecRpcServer = crossChainTestHarness.aztecRpcServer; aztecNode = aztecNode_; + wallet = wallet_; logger = logger_; logger('Successfully deployed contracts and initialized portal'); @@ -66,18 +65,6 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness?.stop(); }); - const withdrawFundsFromAztec = async (withdrawAmount: bigint) => { - logger('Send L2 tx to withdraw funds'); - const withdrawTx = l2Contract.methods - .withdrawPublic(withdrawAmount, ethAccount.toField(), EthAddress.ZERO.toField()) - .send(); - - await withdrawTx.isMined({ interval: 0.1 }); - const withdrawReceipt = await withdrawTx.getReceipt(); - - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - }; - it('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { // Generate a claim secret using pedersen const l1TokenBalance = 1000000n; @@ -85,34 +72,55 @@ describe('e2e_public_cross_chain_messaging', () => { const [secret, secretHash] = await crossChainTestHarness.generateClaimSecret(); + // 1. Mint tokens on L1 await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); + + // 2. Deposit tokens to the TokenPortal const messageKey = await crossChainTestHarness.sendTokensToPortal(bridgeAmount, secretHash); - expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. - // Perform another unrelated transaction on L2 to progress the rollup. - const transferAmount = 1n; - await crossChainTestHarness.performL2Transfer(transferAmount); + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint tokens to owner + const unrelatedMintAmount = 99n; + await crossChainTestHarness.mintTokensPublicOnL2(unrelatedMintAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); + const balanceBefore = unrelatedMintAmount; + // 3. Consume L1-> L2 message and mint public tokens on L2 await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, messageKey, secret); - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, balanceBefore + bridgeAmount); + const afterBalance = balanceBefore + bridgeAmount; - // time to withdraw the funds again! + // // time to withdraw the funds again! logger('Withdrawing funds from L2'); + + // 4. Give approval to bridge to burn owner's funds: const withdrawAmount = 9n; + const nonce = Fr.random(); + const burnMessageHash = await hashPayload([ + l2Bridge.address.toField(), + l2Token.address.toField(), + FunctionSelector.fromSignature('burn_public((Field),Field,Field)').toField(), + ownerAddress.toField(), + new Fr(withdrawAmount), + nonce, + ]); + await wallet.setPublicAuth(burnMessageHash, true).send().wait(); + + // 5. Withdraw owner's funds from L2 to L1 const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); - await withdrawFundsFromAztec(withdrawAmount); - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount); + await crossChainTestHarness.withdrawPublicFromAztecToL1(withdrawAmount, nonce); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, afterBalance - withdrawAmount); // Check balance before and after exit. - expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); await crossChainTestHarness.withdrawFundsFromBridgeOnL1(withdrawAmount, entryKey); - expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe( - l1TokenBalance - bridgeAmount + withdrawAmount, - ); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); + + // TODO: Fialure cases! }); diff --git a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts index 7e1988e6740..b7e815f51d8 100644 --- a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts @@ -16,7 +16,6 @@ describe('e2e_public_to_private_messaging', () => { let underlyingERC20: any; - const initialBalance = 10n; let ownerAddress: AztecAddress; let crossChainTestHarness: CrossChainTestHarness; @@ -32,7 +31,6 @@ describe('e2e_public_to_private_messaging', () => { cheatCodes, } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( - initialBalance, aztecNode_, aztecRpcServer_, deployL1ContractsValues, @@ -76,25 +74,24 @@ describe('e2e_public_to_private_messaging', () => { await delay(5000); /// waiting 5 seconds. // Perform another unrelated transaction on L2 to progress the rollup. - await crossChainTestHarness.expectBalanceOnL2(ownerAddress, initialBalance); - const transferAmount = 1n; - await crossChainTestHarness.performL2Transfer(transferAmount); - await crossChainTestHarness.expectBalanceOnL2(ownerAddress, initialBalance - transferAmount); + const initialBalance = 1n; + await crossChainTestHarness.mintTokensPublicOnL2(initialBalance); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, initialBalance); await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, messageKey, secret); - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, initialBalance + bridgeAmount); // Create the commitment to be spent in the private domain await crossChainTestHarness.shieldFundsOnL2(shieldAmount, secretHash); // Create the transaction spending the commitment await crossChainTestHarness.redeemShieldPrivatelyOnL2(shieldAmount, secret); - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - shieldAmount); - await crossChainTestHarness.expectBalanceOnL2(ownerAddress, initialBalance + shieldAmount - transferAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, initialBalance + bridgeAmount - shieldAmount); + await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, shieldAmount); // Unshield the tokens again, sending them to the same account, however this can be any account. await crossChainTestHarness.unshieldTokensOnL2(shieldAmount); - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount); - await crossChainTestHarness.expectBalanceOnL2(ownerAddress, initialBalance - transferAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, initialBalance + bridgeAmount); + await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, 0n); }, 200_000); }); diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 9084a112687..14066626781 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -7,12 +7,12 @@ import { toBufferBE } from '@aztec/foundation/bigint-buffer'; import { sha256ToField } from '@aztec/foundation/crypto'; import { DebugLogger } from '@aztec/foundation/log'; import { OutboxAbi } from '@aztec/l1-artifacts'; -import { NonNativeTokenContract } from '@aztec/noir-contracts/types'; +import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; import { AztecRPC, TxStatus } from '@aztec/types'; import { Chain, HttpTransport, PublicClient, getContract } from 'viem'; -import { deployAndInitializeNonNativeL2TokenContracts } from './utils.js'; +import { deployAndInitializeStandardizedTokenAndBridgeContracts } from './utils.js'; /** * A Class for testing cross chain interactions, contains common interactions @@ -20,7 +20,6 @@ import { deployAndInitializeNonNativeL2TokenContracts } from './utils.js'; */ export class CrossChainTestHarness { static async new( - initialBalance: bigint, aztecNode: AztecNodeService | undefined, aztecRpcServer: AztecRPC, deployL1ContractsValues: DeployL1Contracts, @@ -28,6 +27,8 @@ export class CrossChainTestHarness { wallet: Wallet, logger: DebugLogger, cheatCodes: CheatCodes, + underlyingERC20Address?: EthAddress, + initialBalance?: bigint, ): Promise { const walletClient = deployL1ContractsValues.walletClient; const publicClient = deployL1ContractsValues.publicClient; @@ -41,21 +42,30 @@ export class CrossChainTestHarness { }); // Deploy and initialize all required contracts - logger('Deploying Portal, initializing and deploying l2 contract...'); - const contracts = await deployAndInitializeNonNativeL2TokenContracts( + logger(`Deploying and initializing token, portal and its bridge...`); + const contracts = await deployAndInitializeStandardizedTokenAndBridgeContracts( wallet, walletClient, publicClient, deployL1ContractsValues!.registryAddress, - initialBalance, owner.address, + underlyingERC20Address, ); - const l2Contract = contracts.l2Contract; + const l2token = contracts.token; + const l2bridge = contracts.bridge; const underlyingERC20 = contracts.underlyingERC20; const tokenPortal = contracts.tokenPortal; const tokenPortalAddress = contracts.tokenPortalAddress; - // await expectBalance(accounts[0], initialBalance); - logger('Successfully deployed contracts and initialized portal'); + logger(`Deployed and initialized token, portal and its bridge.`); + + if (initialBalance) { + logger(`Minting ${initialBalance} tokens to ${owner.address}...`); + const mintTx = l2token.methods.mint_public({ address: owner.address }, initialBalance).send(); + const mintReceipt = await mintTx.wait(); + expect(mintReceipt.status).toBe(TxStatus.MINED); + expect(l2token.methods.balance_of_public({ address: owner.address }).view()).toBe(initialBalance); + logger(`Minted ${initialBalance} tokens to ${owner.address}.`); + } return new CrossChainTestHarness( aztecNode, @@ -63,7 +73,8 @@ export class CrossChainTestHarness { cheatCodes, accounts, logger, - l2Contract, + l2token, + l2bridge, ethAccount, tokenPortalAddress, tokenPortal, @@ -88,8 +99,11 @@ export class CrossChainTestHarness { /** Logger. */ public logger: DebugLogger, - /** Testing aztec contract. */ - public l2Contract: NonNativeTokenContract, + /** L2 Token contract. */ + public l2Token: TokenContract, + /** L2 Token bridge contract. */ + public l2Bridge: TokenBridgeContract, + /** Eth account to interact with. */ public ethAccount: EthAddress, @@ -154,58 +168,86 @@ export class CrossChainTestHarness { return Fr.fromString(messageKeyHex); } + async mintTokensPublicOnL2(amount: bigint) { + const tx = this.l2Token.methods.mint_public({ address: this.ownerAddress }, amount).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + } + async performL2Transfer(transferAmount: bigint) { // send a transfer tx to force through rollup with the message included - const transferTx = this.l2Contract.methods.transfer(transferAmount, this.receiver).send(); - - await transferTx.isMined({ interval: 0.1 }); - const transferReceipt = await transferTx.getReceipt(); - - expect(transferReceipt.status).toBe(TxStatus.MINED); + const transferTx = this.l2Token.methods + .transfer_public({ address: this.ownerAddress }, { address: this.receiver }, transferAmount, 0) + .send(); + const receipt = await transferTx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); } async consumeMessageOnAztecAndMintSecretly(bridgeAmount: bigint, messageKey: Fr, secret: Fr) { this.logger('Consuming messages on L2 secretively'); // Call the mint tokens function on the Aztec.nr contract - const consumptionTx = this.l2Contract.methods - .mint(bridgeAmount, this.ownerAddress, messageKey, secret, this.ethAccount.toField()) + const consumptionTx = this.l2Bridge.methods + .deposit_private(bridgeAmount, messageKey, secret, { address: this.ethAccount.toField() }) .send(); - - await consumptionTx.isMined({ interval: 0.1 }); - const consumptionReceipt = await consumptionTx.getReceipt(); + const consumptionReceipt = await consumptionTx.wait(); expect(consumptionReceipt.status).toBe(TxStatus.MINED); } async consumeMessageOnAztecAndMintPublicly(bridgeAmount: bigint, messageKey: Fr, secret: Fr) { this.logger('Consuming messages on L2 Publicly'); // Call the mint tokens function on the Aztec.nr contract - const consumptionTx = this.l2Contract.methods - .mintPublic(bridgeAmount, this.ownerAddress, messageKey, secret, this.ethAccount.toField()) + const tx = this.l2Bridge.methods + .deposit_public(bridgeAmount, messageKey, secret, { address: this.ethAccount.toField() }) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + } + + async withdrawPrivateFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { + const withdrawTx = this.l2Bridge.methods + .withdraw_private( + { address: this.l2Token.address }, + withdrawAmount, + { address: this.ethAccount.toField() }, + { address: EthAddress.ZERO.toField() }, + nonce, + ) .send(); + const withdrawReceipt = await withdrawTx.wait(); + expect(withdrawReceipt.status).toBe(TxStatus.MINED); + } - await consumptionTx.isMined({ interval: 0.1 }); - const consumptionReceipt = await consumptionTx.getReceipt(); - expect(consumptionReceipt.status).toBe(TxStatus.MINED); + async withdrawPublicFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { + const withdrawTx = this.l2Bridge.methods + .withdraw_public( + withdrawAmount, + { address: this.ethAccount.toField() }, + { address: EthAddress.ZERO.toField() }, + nonce, + ) + .send(); + const withdrawReceipt = await withdrawTx.wait(); + expect(withdrawReceipt.status).toBe(TxStatus.MINED); } - async getL2BalanceOf(owner: AztecAddress) { - return await this.l2Contract.methods.getBalance(owner).view({ from: owner }); + async getL2PrivateBalanceOf(owner: AztecAddress) { + return await this.l2Token.methods.balance_of_private({ address: owner }).view({ from: owner }); } - async expectBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { - const balance = await this.getL2BalanceOf(owner); + async expectPrivateBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { + const balance = await this.getL2PrivateBalanceOf(owner); this.logger(`Account ${owner} balance: ${balance}`); expect(balance).toBe(expectedBalance); } async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { - const balance = await this.l2Contract.methods.publicBalanceOf(owner.toField()).view({ from: owner }); + const balance = await this.l2Token.methods.balance_of_public({ address: owner }).view({ from: owner }); expect(balance).toBe(expectedBalance); } async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise { this.logger('Ensure that the entry is not in outbox yet'); - const contractData = await this.aztecRpcServer.getContractData(this.l2Contract.address); + const contractData = await this.aztecRpcServer.getContractData(this.l2Bridge.address); // 0xb460af94, selector for "withdraw(uint256,address,address)" const content = sha256ToField( Buffer.concat([ @@ -217,7 +259,7 @@ export class CrossChainTestHarness { ); const entryKey = sha256ToField( Buffer.concat([ - this.l2Contract.address.toBuffer(), + this.l2Bridge.address.toBuffer(), new Fr(1).toBuffer(), // aztec version contractData?.portalContractAddress.toBuffer32() ?? Buffer.alloc(32, 0), new Fr(this.publicClient.chain.id).toBuffer(), // chain id @@ -246,29 +288,24 @@ export class CrossChainTestHarness { } async shieldFundsOnL2(shieldAmount: bigint, secretHash: Fr) { - this.logger('Shielding funds on L2'); - const shieldTx = this.l2Contract.methods.shield(shieldAmount, secretHash).send(); - await shieldTx.isMined({ interval: 0.1 }); - const shieldReceipt = await shieldTx.getReceipt(); + const shieldTx = this.l2Token.methods.shield({ address: this.ownerAddress }, shieldAmount, secretHash, 0).send(); + const shieldReceipt = await shieldTx.wait(); expect(shieldReceipt.status).toBe(TxStatus.MINED); } async redeemShieldPrivatelyOnL2(shieldAmount: bigint, secret: Fr) { this.logger('Spending commitment in private call'); - const privateTx = this.l2Contract.methods.redeemShield(shieldAmount, secret, this.ownerAddress).send(); - - await privateTx.isMined(); - const privateReceipt = await privateTx.getReceipt(); - + const privateTx = this.l2Token.methods.redeem_shield({ address: this.ownerAddress }, shieldAmount, secret).send(); + const privateReceipt = await privateTx.wait(); expect(privateReceipt.status).toBe(TxStatus.MINED); } - async unshieldTokensOnL2(unshieldAmount: bigint) { + async unshieldTokensOnL2(unshieldAmount: bigint, nonce = Fr.ZERO) { this.logger('Unshielding tokens'); - const unshieldTx = this.l2Contract.methods.unshieldTokens(unshieldAmount, this.ownerAddress).send(); - await unshieldTx.isMined(); - const unshieldReceipt = await unshieldTx.getReceipt(); - + const unshieldTx = this.l2Token.methods + .unshield({ address: this.ownerAddress }, { address: this.ownerAddress }, unshieldAmount, nonce) + .send(); + const unshieldReceipt = await unshieldTx.wait(); expect(unshieldReceipt.status).toBe(TxStatus.MINED); } diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index d55774b1911..9624c2c12bc 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -13,12 +13,14 @@ import { getL1ContractAddresses, getSandboxAccountsWallets, } from '@aztec/aztec.js'; +import { CircuitsWasm, GeneratorIndex } from '@aztec/circuits.js'; +import { pedersenPlookupCompressWithHashIndex } from '@aztec/circuits.js/barretenberg'; import { DeployL1Contracts, deployL1Contract, deployL1Contracts } from '@aztec/ethereum'; import { Fr } from '@aztec/foundation/fields'; import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { retryUntil } from '@aztec/foundation/retry'; import { PortalERC20Abi, PortalERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } from '@aztec/l1-artifacts'; -import { NonNativeTokenContract } from '@aztec/noir-contracts/types'; +import { NonNativeTokenContract, TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; import { AztecRPC, L2BlockL2Logs, LogType, TxStatus } from '@aztec/types'; import { @@ -271,6 +273,119 @@ export function getLogger() { return createDebugLogger('aztec:' + describeBlockName); } +/** + * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract, its L2 bridge contract and attach is to the portal. + * @param wallet - the wallet instance + * @param walletClient - A viem WalletClient. + * @param publicClient - A viem PublicClient. + * @param rollupRegistryAddress - address of rollup registry to pass to initialize the token portal + * @param owner - owner of the L2 contract + * @param underlyingERC20Address - address of the underlying ERC20 contract to use (if none supplied, it deploys one) + * @returns l2 contract instance, bridge contract instance, token portal instance, token portal address and the underlying ERC20 instance + */ +export async function deployAndInitializeStandardizedTokenAndBridgeContracts( + wallet: Wallet, + walletClient: WalletClient, + publicClient: PublicClient, + rollupRegistryAddress: EthAddress, + owner: AztecAddress, + underlyingERC20Address?: EthAddress, +): Promise<{ + /** + * The L2 token contract instance. + */ + token: TokenContract; + /** + * The L2 bridge contract instance. + */ + bridge: TokenBridgeContract; + /** + * The token portal contract address. + */ + tokenPortalAddress: EthAddress; + /** + * The token portal contract instance + */ + tokenPortal: any; + /** + * The underlying ERC20 contract instance. + */ + underlyingERC20: any; +}> { + if (!underlyingERC20Address) { + underlyingERC20Address = await deployL1Contract(walletClient, publicClient, PortalERC20Abi, PortalERC20Bytecode); + } + const underlyingERC20 = getContract({ + address: underlyingERC20Address.toString(), + abi: PortalERC20Abi, + walletClient, + publicClient, + }); + + // deploy the token portal + const tokenPortalAddress = await deployL1Contract(walletClient, publicClient, TokenPortalAbi, TokenPortalBytecode); + const tokenPortal = getContract({ + address: tokenPortalAddress.toString(), + abi: TokenPortalAbi, + walletClient, + publicClient, + }); + + // deploy l2 token + const deployTx = TokenContract.deploy(wallet).send(); + + // deploy l2 token bridge and attach to the portal + const bridgeTx = TokenBridgeContract.deploy(wallet).send({ + portalContract: tokenPortalAddress, + contractAddressSalt: Fr.random(), + }); + + // now wait for the deploy txs to be mined. This way we send all tx in the same rollup. + const deployReceipt = await deployTx.wait(); + if (deployReceipt.status !== TxStatus.MINED) throw new Error(`Deploy token tx status is ${deployReceipt.status}`); + const token = await TokenContract.at(deployReceipt.contractAddress!, wallet); + + const bridgeReceipt = await bridgeTx.wait(); + if (bridgeReceipt.status !== TxStatus.MINED) throw new Error(`Deploy bridge tx status is ${bridgeReceipt.status}`); + const bridge = await TokenBridgeContract.at(bridgeReceipt.contractAddress!, wallet); + await bridge.attach(tokenPortalAddress); + const bridgeAddress = bridge.address.toString() as `0x${string}`; + + // initialize l2 token + const initializeTx = token.methods._initialize({ address: owner }).send(); + + // initialize bridge + const initializeBridgeTx = bridge.methods._initialize({ address: token.address }).send(); + + // now we wait for the txs to be mined. This way we send all tx in the same rollup. + const initializeReceipt = await initializeTx.wait(); + if (initializeReceipt.status !== TxStatus.MINED) + throw new Error(`Initialize token tx status is ${initializeReceipt.status}`); + if ((await token.methods.admin().view()) !== owner.toBigInt()) throw new Error(`Token admin is not ${owner}`); + + const initializeBridgeReceipt = await initializeBridgeTx.wait(); + if (initializeBridgeReceipt.status !== TxStatus.MINED) + throw new Error(`Initialize token bridge tx status is ${initializeBridgeReceipt.status}`); + if ((await bridge.methods.token().view()) !== token.address.toBigInt()) + throw new Error(`Bridge token is not ${token.address}`); + + // make the bridge a minter on the token: + const makeMinterTx = token.methods.set_minter({ address: bridge.address }, true).send(); + const makeMinterReceipt = await makeMinterTx.wait(); + if (makeMinterReceipt.status !== TxStatus.MINED) + throw new Error(`Make bridge a minter tx status is ${makeMinterReceipt.status}`); + if ((await token.methods.is_minter({ address: bridge.address }).view()) === 1n) + throw new Error(`Bridge is not a minter`); + + // initialize portal + await tokenPortal.write.initialize( + [rollupRegistryAddress.toString(), underlyingERC20Address.toString(), bridgeAddress], + {} as any, + ); + + return { token, bridge, tokenPortalAddress, tokenPortal, underlyingERC20 }; +} + /** * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract and attach is to the portal. * @param aztecRpcServer - the aztec rpc server instance @@ -282,6 +397,7 @@ export function getLogger() { * @param underlyingERC20Address - address of the underlying ERC20 contract to use (if none supplied, it deploys one) * @returns l2 contract instance, token portal instance, token portal address and the underlying ERC20 instance */ +// TODO (#2291) DELETE!!! export async function deployAndInitializeNonNativeL2TokenContracts( wallet: Wallet, walletClient: WalletClient, @@ -376,3 +492,16 @@ export const expectUnencryptedLogsFromLastBlockToBe = async (rpc: AztecRPC, logM expect(asciiLogs).toStrictEqual(logMessages); }; + +/** + * Hash a payload to generate a signature on an account contract + * @param payload - payload to hash + * @returns the hashed message + */ +export const hashPayload = async (payload: Fr[]) => { + return pedersenPlookupCompressWithHashIndex( + await CircuitsWasm.get(), + payload.map(fr => fr.toBuffer()), + GeneratorIndex.SIGNATURE_PAYLOAD, + ); +}; diff --git a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts index 118df9e14b7..fa168996aa7 100644 --- a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts @@ -11,7 +11,7 @@ import { AztecRPC, CompleteAddress, TxStatus } from '@aztec/types'; import { getContract, parseEther } from 'viem'; import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; -import { delay, deployAndInitializeNonNativeL2TokenContracts, setup } from './fixtures/utils.js'; +import { delay, setup } from './fixtures/utils.js'; // PSA: This tests works on forked mainnet. There is a dump of the data in `dumpedState` such that we // don't need to burn through RPC requests. @@ -27,7 +27,8 @@ const EXPECTED_FORKED_BLOCK = 0; //17514288; process.env.SEARCH_START_BLOCK = EXPECTED_FORKED_BLOCK.toString(); // Should mint WETH on L2, swap to DAI using L1 Uniswap and mint this DAI back on L2 -describe('uniswap_trade_on_l1_from_l2', () => { +// TODO(2167) - Fix this! Adapt to new portal standard and new cross chain harness. +describe.skip('uniswap_trade_on_l1_from_l2', () => { const WETH9_ADDRESS: EthAddress = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); const DAI_ADDRESS: EthAddress = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); @@ -67,65 +68,32 @@ describe('uniswap_trade_on_l1_from_l2', () => { ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); owner = accounts[0].address; - const ownerPublicKey = accounts[0].publicKey; receiver = accounts[1].address; logger('Deploying DAI Portal, initializing and deploying l2 contract...'); - const daiContracts = await deployAndInitializeNonNativeL2TokenContracts( - wallet, - walletClient, - publicClient, - deployL1ContractsValues!.registryAddress, - initialBalance, - owner, - DAI_ADDRESS, - ); - daiCrossChainHarness = new CrossChainTestHarness( + daiCrossChainHarness = await CrossChainTestHarness.new( aztecNode, aztecRpcServer, - cheatCodes, + deployL1ContractsValues, accounts, + wallet, logger, - daiContracts.l2Contract, - ethAccount, - daiContracts.tokenPortalAddress, - daiContracts.tokenPortal, - daiContracts.underlyingERC20, - null, - publicClient, - walletClient, - owner, - receiver, - ownerPublicKey, + cheatCodes, + DAI_ADDRESS, + initialBalance, ); logger('Deploying WETH Portal, initializing and deploying l2 contract...'); - const wethContracts = await deployAndInitializeNonNativeL2TokenContracts( - wallet, - walletClient, - publicClient, - deployL1ContractsValues!.registryAddress, - initialBalance, - owner, - WETH9_ADDRESS, - ); - wethCrossChainHarness = new CrossChainTestHarness( + wethCrossChainHarness = await CrossChainTestHarness.new( aztecNode, aztecRpcServer, - cheatCodes, + deployL1ContractsValues, accounts, + wallet, logger, - wethContracts.l2Contract, - ethAccount, - wethContracts.tokenPortalAddress, - wethContracts.tokenPortal, - wethContracts.underlyingERC20, - null, - publicClient, - walletClient, - owner, - receiver, - ownerPublicKey, + cheatCodes, + WETH9_ADDRESS, + initialBalance, ); logger('Deploy Uniswap portal on L1 and L2...'); @@ -181,11 +149,11 @@ describe('uniswap_trade_on_l1_from_l2', () => { // 3. Claim WETH on L2 logger('Minting weth on L2'); await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly(wethAmountToBridge, messageKey, secret); - await wethCrossChainHarness.expectBalanceOnL2(owner, wethAmountToBridge + initialBalance - transferAmount); + await wethCrossChainHarness.expectPrivateBalanceOnL2(owner, wethAmountToBridge + initialBalance - transferAmount); // Store balances - const wethBalanceBeforeSwap = await wethCrossChainHarness.getL2BalanceOf(owner); - const daiBalanceBeforeSwap = await daiCrossChainHarness.getL2BalanceOf(owner); + const wethBalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(owner); + const daiBalanceBeforeSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(owner); // 4. Send L2 to L1 message to withdraw funds and another message to swap assets. logger('Send L2 tx to withdraw WETH to uniswap portal and send message to swap assets on L1'); @@ -193,10 +161,10 @@ describe('uniswap_trade_on_l1_from_l2', () => { const withdrawTx = uniswapL2Contract.methods .swap( - wethCrossChainHarness.l2Contract.address.toField(), + wethCrossChainHarness.l2Token.address.toField(), wethAmountToBridge, new Fr(3000), - daiCrossChainHarness.l2Contract.address.toField(), + daiCrossChainHarness.l2Token.address.toField(), new Fr(minimumOutputAmount), owner, owner, @@ -211,7 +179,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { expect(withdrawReceipt.status).toBe(TxStatus.MINED); // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) - await wethCrossChainHarness.expectBalanceOnL2(owner, initialBalance - transferAmount); + await wethCrossChainHarness.expectPrivateBalanceOnL2(owner, initialBalance - transferAmount); // 5. Consume L2 to L1 message by calling uniswapPortal.swap() logger('Execute withdraw and swap on the uniswapPortal!'); @@ -248,10 +216,10 @@ describe('uniswap_trade_on_l1_from_l2', () => { // 6. claim dai on L2 logger('Consuming messages to mint dai on L2'); await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly(daiAmountToBridge, depositDaiMessageKey, secret); - await daiCrossChainHarness.expectBalanceOnL2(owner, initialBalance + daiAmountToBridge); + await daiCrossChainHarness.expectPrivateBalanceOnL2(owner, initialBalance + daiAmountToBridge); - const wethBalanceAfterSwap = await wethCrossChainHarness.getL2BalanceOf(owner); - const daiBalanceAfterSwap = await daiCrossChainHarness.getL2BalanceOf(owner); + const wethBalanceAfterSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(owner); + const daiBalanceAfterSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(owner); logger('WETH balance before swap: ', wethBalanceBeforeSwap.toString()); logger('DAI balance before swap : ', daiBalanceBeforeSwap.toString()); From 1868a4362f8ad97cba0ea69ab85ec54466040c0d Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Sun, 17 Sep 2023 14:27:49 +0000 Subject: [PATCH 3/6] move address types to aztec-nr and minor ts updates --- yarn-project/aztec-nr/aztec/src/types.nr | 1 + .../aztec/src/types/address.nr} | 0 .../end-to-end/src/cli_docs_sandbox.test.ts | 2 +- .../src/e2e_cross_chain_messaging.test.ts | 7 ++- .../e2e_public_cross_chain_messaging.test.ts | 4 +- .../src/fixtures/cross_chain_test_harness.ts | 16 +++---- .../src/uniswap_trade_on_l1_from_l2.test.ts | 2 - .../token_bridge_contract/src/main.nr | 3 +- .../src/token_interface.nr | 1 - .../src/contracts/token_contract/src/main.nr | 3 +- .../src/contracts/token_contract/src/types.nr | 48 ------------------- 11 files changed, 21 insertions(+), 66 deletions(-) rename yarn-project/{noir-contracts/src/contracts/token_bridge_contract/src/types.nr => aztec-nr/aztec/src/types/address.nr} (100%) diff --git a/yarn-project/aztec-nr/aztec/src/types.nr b/yarn-project/aztec-nr/aztec/src/types.nr index 81b7155d402..802ce648c49 100644 --- a/yarn-project/aztec-nr/aztec/src/types.nr +++ b/yarn-project/aztec-nr/aztec/src/types.nr @@ -1,3 +1,4 @@ +mod address; mod point; mod vec; // This can/should be moved out into an official noir library mod type_serialisation; \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/types.nr b/yarn-project/aztec-nr/aztec/src/types/address.nr similarity index 100% rename from yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/types.nr rename to yarn-project/aztec-nr/aztec/src/types/address.nr diff --git a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts index 4769d27b327..0f0eff61eb3 100644 --- a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts +++ b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts @@ -103,8 +103,8 @@ SchnorrHardcodedAccountContractAbi SchnorrSingleKeyAccountContractAbi StatefulTestContractAbi TestContractAbi -TokenContractAbi TokenBridgeContractAbi +TokenContractAbi UniswapContractAbi // docs:end:example-contracts `; diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 88a8b250ab7..da263d7569d 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -72,19 +72,22 @@ describe('e2e_cross_chain_messaging', () => { // 1. Mint tokens on L1 await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); + + // 2. Deposit tokens to the TokenPortal const messageKey = await crossChainTestHarness.sendTokensToPortal(bridgeAmount, secretHash); expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. - // Perform another unrelated transaction on L2 to progress the rollup. + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. const unrelatedMintAmount = 99n; await crossChainTestHarness.mintTokensPublicOnL2(unrelatedMintAmount); await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); // 3. Consume L1-> L2 message and mint private tokens on L2 await crossChainTestHarness.consumeMessageOnAztecAndMintSecretly(bridgeAmount, messageKey, secret); + // tokens were minted privately in a TransparentNote which the owner (person who knows the secret) must redeem: await crossChainTestHarness.redeemShieldPrivatelyOnL2(bridgeAmount, secret); await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, bridgeAmount); @@ -116,4 +119,6 @@ describe('e2e_cross_chain_messaging', () => { expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); + + // TODO: Fialure cases! }); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index bba6b576e19..d51b4ab0aa0 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -82,7 +82,7 @@ describe('e2e_public_cross_chain_messaging', () => { // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. - // Perform an unrelated transaction on L2 to progress the rollup. Here we mint tokens to owner + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. const unrelatedMintAmount = 99n; await crossChainTestHarness.mintTokensPublicOnL2(unrelatedMintAmount); await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); @@ -93,7 +93,7 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, balanceBefore + bridgeAmount); const afterBalance = balanceBefore + bridgeAmount; - // // time to withdraw the funds again! + // time to withdraw the funds again! logger('Withdrawing funds from L2'); // 4. Give approval to bridge to burn owner's funds: diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 14066626781..7c60c1de3ed 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -42,7 +42,7 @@ export class CrossChainTestHarness { }); // Deploy and initialize all required contracts - logger(`Deploying and initializing token, portal and its bridge...`); + logger('Deploying and initializing token, portal and its bridge...'); const contracts = await deployAndInitializeStandardizedTokenAndBridgeContracts( wallet, walletClient, @@ -51,19 +51,19 @@ export class CrossChainTestHarness { owner.address, underlyingERC20Address, ); - const l2token = contracts.token; - const l2bridge = contracts.bridge; + const l2Token = contracts.token; + const l2Bridge = contracts.bridge; const underlyingERC20 = contracts.underlyingERC20; const tokenPortal = contracts.tokenPortal; const tokenPortalAddress = contracts.tokenPortalAddress; - logger(`Deployed and initialized token, portal and its bridge.`); + logger('Deployed and initialized token, portal and its bridge.'); if (initialBalance) { logger(`Minting ${initialBalance} tokens to ${owner.address}...`); - const mintTx = l2token.methods.mint_public({ address: owner.address }, initialBalance).send(); + const mintTx = l2Token.methods.mint_public({ address: owner.address }, initialBalance).send(); const mintReceipt = await mintTx.wait(); expect(mintReceipt.status).toBe(TxStatus.MINED); - expect(l2token.methods.balance_of_public({ address: owner.address }).view()).toBe(initialBalance); + expect(l2Token.methods.balance_of_public({ address: owner.address }).view()).toBe(initialBalance); logger(`Minted ${initialBalance} tokens to ${owner.address}.`); } @@ -73,8 +73,8 @@ export class CrossChainTestHarness { cheatCodes, accounts, logger, - l2token, - l2bridge, + l2Token, + l2Bridge, ethAccount, tokenPortalAddress, tokenPortal, diff --git a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts index fa168996aa7..0985f0b3ca8 100644 --- a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts @@ -41,7 +41,6 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { let ethAccount: EthAddress; let owner: AztecAddress; - let receiver: AztecAddress; const initialBalance = 10n; const wethAmountToBridge = parseEther('1'); @@ -68,7 +67,6 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); owner = accounts[0].address; - receiver = accounts[1].address; logger('Deploying DAI Portal, initializing and deploying l2 contract...'); daiCrossChainHarness = await CrossChainTestHarness.new( diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr index adb0cc274cc..d28b8457979 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr @@ -1,4 +1,3 @@ -mod types; mod util; mod token_interface; @@ -14,10 +13,10 @@ contract TokenBridge { types::type_serialisation::field_serialisation::{ FieldSerialisationMethods, FIELD_SERIALISED_LEN, }, + types::address::{AztecAddress, EthereumAddress}, oracle::compute_selector::compute_selector, }; - use crate::types::{AztecAddress, EthereumAddress}; use crate::token_interface::Token; use crate::util::{get_mint_content_hash, get_withdraw_content_hash, compute_secret_hash}; diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr index 45c11562d86..647fca790ea 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr @@ -28,7 +28,6 @@ impl Token { ); } - // Private fn mint_private(self: Self, context: PublicContext, amount: Field, secret_hash: Field) { let _return_values = context.call_public_function( self.address, diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr index 8edb9dec257..30c1d55b3ae 100644 --- a/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr @@ -30,11 +30,12 @@ contract Token { types::type_serialisation::field_serialisation::{ FieldSerialisationMethods, FIELD_SERIALISED_LEN, }, + types::address::{AztecAddress}, oracle::compute_selector::compute_selector, auth::{assert_valid_message_for, assert_valid_public_message_for} }; - use crate::types::{AztecAddress, TransparentNote, TransparentNoteMethods, TRANSPARENT_NOTE_LEN}; + use crate::types::{TransparentNote, TransparentNoteMethods, TRANSPARENT_NOTE_LEN}; use crate::util::{compute_message_hash}; struct Storage { diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr index 9161fba1018..d2deceb268f 100644 --- a/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr @@ -10,54 +10,6 @@ use dep::aztec::constants_gen::GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET; global TRANSPARENT_NOTE_LEN: Field = 2; - -struct AztecAddress { - address: Field -} - -impl AztecAddress { - fn new(address: Field) -> Self { - Self { - address - } - } - - fn serialize(self: Self) -> [Field; 1] { - [self.address] - } - - fn deserialize(fields: [Field; 1]) -> Self { - Self { - address: fields[0] - } - } -} - -struct EthereumAddress { - address: Field -} - -impl EthereumAddress { - fn new(address: Field) -> Self { - Self { - address - } - } - - - fn serialize(self: Self) -> [Field; 1] { - [self.address] - } - - fn deserialize(fields: [Field; 1]) -> Self { - Self { - address: fields[0] - } - } -} - - - // Transparent note represents a note that is created in the clear (public execution), // but can only be spent by those that know the preimage of the "secret_hash" struct TransparentNote { From ea7beaa3134ff71d4542ac893fd2658fe386e34b Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Mon, 18 Sep 2023 14:05:02 +0000 Subject: [PATCH 4/6] create separate public, private mint paths, constain ethaddress to be 20 bytes, rename fns in bridge for clarity --- l1-contracts/test/portals/TokenPortal.sol | 96 ++++++++++- l1-contracts/test/portals/TokenPortal.t.sol | 157 ++++++++++++++++-- l1-contracts/test/portals/UniswapPortal.sol | 5 +- l1-contracts/test/portals/UniswapPortal.t.sol | 3 +- .../aztec-nr/aztec/src/types/address.nr | 5 + .../src/uniswap_trade_on_l1_from_l2.test.ts | 7 +- .../src/e2e_cross_chain_messaging.test.ts | 22 ++- .../e2e_public_cross_chain_messaging.test.ts | 4 +- .../e2e_public_to_private_messaging.test.ts | 2 +- .../src/fixtures/cross_chain_test_harness.ts | 53 +++++- .../src/integration_archiver_l1_to_l2.test.ts | 5 +- .../src/uniswap_trade_on_l1_from_l2.test.ts | 29 +++- .../token_bridge_contract/src/main.nr | 45 ++--- .../token_bridge_contract/src/util.nr | 53 +++++- 14 files changed, 408 insertions(+), 78 deletions(-) diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index 40bdada2423..c696b3ec09b 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -23,7 +23,7 @@ contract TokenPortal { } /** - * @notice Deposit funds into the portal and adds an L2 message. + * @notice Deposit funds into the portal and adds an L2 message which can only be consumed publicly on Aztec * @param _to - The aztec address of the recipient * @param _amount - The amount to deposit * @param _deadline - The timestamp after which the entry can be cancelled @@ -31,7 +31,7 @@ contract TokenPortal { * @param _canceller - The address that can cancel the L1 to L2 message. * @return The key of the entry in the Inbox */ - function depositToAztec( + function depositToAztecPublic( bytes32 _to, uint256 _amount, uint32 _deadline, @@ -45,7 +45,7 @@ contract TokenPortal { // Hash the message content to be reconstructed in the receiving contract bytes32 contentHash = Hash.sha256ToField( - abi.encodeWithSignature("mint(uint256,bytes32,address)", _amount, _to, _canceller) + abi.encodeWithSignature("mint_public(uint256,bytes32,address)", _amount, _to, _canceller) ); // Hold the tokens in the portal @@ -55,9 +55,49 @@ contract TokenPortal { return inbox.sendL2Message{value: msg.value}(actor, _deadline, contentHash, _secretHash); } + /** + * @notice Deposit funds into the portal and adds an L2 message which can only be consumed privately on Aztec + * @param _amount - The amount to deposit + * @param _deadline - The timestamp after which the entry can be cancelled + * @param _secretHashForL2MessageConsumption - The hash of the secret consumable L1 to L2 message. The hash should be 254 bits (so it can fit in a Field element) + * @param _secretHashForL2MessageConsumption - The hash of the secret to redeem minted notes privately on Aztec. The hash should be 254 bits (so it can fit in a Field element) + * @param _canceller - The address that can cancel the L1 to L2 message. + * @return The key of the entry in the Inbox + */ + function depositToAztecPrivate( + uint256 _amount, + uint32 _deadline, + bytes32 _secretHashForL2MessageConsumption, + bytes32 _secretHashForRedeemingMintedNotes, + address _canceller + ) external payable returns (bytes32) { + // Preamble + // @todo: (issue #624) handle different versions + IInbox inbox = registry.getInbox(); + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2TokenAddress, 1); + + // Hash the message content to be reconstructed in the receiving contract + bytes32 contentHash = Hash.sha256ToField( + abi.encodeWithSignature( + "mint_private(uint256,bytes32,address)", + _amount, + _secretHashForRedeemingMintedNotes, + _canceller + ) + ); + + // Hold the tokens in the portal + underlying.safeTransferFrom(msg.sender, address(this), _amount); + + // Send message to rollup + return inbox.sendL2Message{value: msg.value}( + actor, _deadline, contentHash, _secretHashForL2MessageConsumption + ); + } + // docs:start:token_portal_cancel /** - * @notice Cancel the L1 to L2 message + * @notice Cancel a public depositToAztec L1 to L2 message * @dev only callable by the `canceller` of the message * @param _to - The aztec address of the recipient in the original message * @param _amount - The amount to deposit per the original message @@ -66,7 +106,7 @@ contract TokenPortal { * @param _fee - The fee paid to the sequencer * @return The key of the entry in the Inbox */ - function cancelL1ToAztecMessage( + function cancelL1ToAztecMessagePublic( bytes32 _to, uint256 _amount, uint32 _deadline, @@ -81,7 +121,7 @@ contract TokenPortal { sender: l1Actor, recipient: l2Actor, content: Hash.sha256ToField( - abi.encodeWithSignature("mint(uint256,bytes32,address)", _amount, _to, msg.sender) + abi.encodeWithSignature("mint_public(uint256,bytes32,address)", _amount, _to, msg.sender) ), secretHash: _secretHash, deadline: _deadline, @@ -93,6 +133,50 @@ contract TokenPortal { underlying.transfer(msg.sender, _amount); return entryKey; } + + /** + * @notice Cancel a private depositToAztec L1 to L2 message + * @dev only callable by the `canceller` of the message + * @param _amount - The amount to deposit per the original message + * @param _deadline - The timestamp after which the entry can be cancelled + * @param _secretHashForL2MessageConsumption - The hash of the secret consumable L1 to L2 message + * @param _secretHashForL2MessageConsumption - The hash of the secret to redeem minted notes privately on Aztec + * @param _fee - The fee paid to the sequencer + * @return The key of the entry in the Inbox + */ + function cancelL1ToAztecMessagePrivate( + uint256 _amount, + uint32 _deadline, + bytes32 _secretHashForL2MessageConsumption, + bytes32 _secretHashForRedeemingMintedNotes, + uint64 _fee + ) external returns (bytes32) { + // @todo: (issue #624) handle different versions + IInbox inbox = registry.getInbox(); + DataStructures.L1Actor memory l1Actor = DataStructures.L1Actor(address(this), block.chainid); + DataStructures.L2Actor memory l2Actor = DataStructures.L2Actor(l2TokenAddress, 1); + DataStructures.L1ToL2Msg memory message = DataStructures.L1ToL2Msg({ + sender: l1Actor, + recipient: l2Actor, + content: Hash.sha256ToField( + abi.encodeWithSignature( + "mint_private(uint256,bytes32,address)", + _amount, + _secretHashForRedeemingMintedNotes, + msg.sender + ) + ), + secretHash: _secretHashForL2MessageConsumption, + deadline: _deadline, + fee: _fee + }); + bytes32 entryKey = inbox.cancelL2Message(message, address(this)); + // release the funds to msg.sender (since the content hash (& message key) is derived by hashing the caller, + // we confirm that msg.sender is same as `_canceller` supplied when creating the message) + underlying.transfer(msg.sender, _amount); + return entryKey; + } + // docs:end:token_portal_cancel // docs:start:token_portal_withdraw diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index abc7d98ac3b..93822b08c92 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -48,7 +48,10 @@ contract TokenPortalTest is Test { bytes32 internal to = bytes32(0x2d749407d8c364537cdeb799c1574929cb22ff1ece2b96d2a1c6fa287a0e0171); uint256 internal amount = 100; uint256 internal mintAmount = 1 ether; - bytes32 internal secretHash = 0x147e4fec49805c924e28150fc4b36824679bc17ecb1d7d9f6a9effb7fde6b6a0; + bytes32 internal secretHashForL2MessageConsumption = + 0x147e4fec49805c924e28150fc4b36824679bc17ecb1d7d9f6a9effb7fde6b6a0; + bytes32 internal secretHashForRedeemingMintedNotes = + 0x157e4fec49805c924e28150fc4b36824679bc17ecb1d7d9f6a9effb7fde6b6a0; uint64 internal bid = 1 ether; // params for withdraw: @@ -71,7 +74,7 @@ contract TokenPortalTest is Test { vm.deal(address(this), 100 ether); } - function _createExpectedL1ToL2Message(address _canceller) + function _createExpectedMintPrivateL1ToL2Message(address _canceller) internal view returns (DataStructures.L1ToL2Msg memory) @@ -80,21 +83,44 @@ contract TokenPortalTest is Test { sender: DataStructures.L1Actor(address(tokenPortal), block.chainid), recipient: DataStructures.L2Actor(l2TokenAddress, 1), content: Hash.sha256ToField( - abi.encodeWithSignature("mint(uint256,bytes32,address)", amount, to, _canceller) + abi.encodeWithSignature( + "mint_private(uint256,bytes32,address)", + amount, + secretHashForRedeemingMintedNotes, + _canceller + ) ), - secretHash: secretHash, + secretHash: secretHashForL2MessageConsumption, deadline: deadline, fee: bid }); } - function testDeposit() public returns (bytes32) { + function _createExpectedMintPublicL1ToL2Message(address _canceller) + internal + view + returns (DataStructures.L1ToL2Msg memory) + { + return DataStructures.L1ToL2Msg({ + sender: DataStructures.L1Actor(address(tokenPortal), block.chainid), + recipient: DataStructures.L2Actor(l2TokenAddress, 1), + content: Hash.sha256ToField( + abi.encodeWithSignature("mint_public(uint256,bytes32,address)", amount, to, _canceller) + ), + secretHash: secretHashForL2MessageConsumption, + deadline: deadline, + fee: bid + }); + } + + function testDepositPrivate() public returns (bytes32) { // mint token and approve to the portal portalERC20.mint(address(this), mintAmount); portalERC20.approve(address(tokenPortal), mintAmount); // Check for the expected message - DataStructures.L1ToL2Msg memory expectedMessage = _createExpectedL1ToL2Message(address(this)); + DataStructures.L1ToL2Msg memory expectedMessage = + _createExpectedMintPrivateL1ToL2Message(address(this)); bytes32 expectedEntryKey = inbox.computeEntryKey(expectedMessage); // Check the even was emitted @@ -113,8 +139,13 @@ contract TokenPortalTest is Test { ); // Perform op - bytes32 entryKey = - tokenPortal.depositToAztec{value: bid}(to, amount, deadline, secretHash, address(this)); + bytes32 entryKey = tokenPortal.depositToAztecPrivate{value: bid}( + amount, + deadline, + secretHashForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + address(this) + ); assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); @@ -125,28 +156,128 @@ contract TokenPortalTest is Test { return entryKey; } - function testCancel() public { - bytes32 expectedEntryKey = testDeposit(); + function testDepositPublic() public returns (bytes32) { + // mint token and approve to the portal + portalERC20.mint(address(this), mintAmount); + portalERC20.approve(address(tokenPortal), mintAmount); + + // Check for the expected message + DataStructures.L1ToL2Msg memory expectedMessage = + _createExpectedMintPublicL1ToL2Message(address(this)); + bytes32 expectedEntryKey = inbox.computeEntryKey(expectedMessage); + + // Check the even was emitted + vm.expectEmit(true, true, true, true); + // event we expect + emit MessageAdded( + expectedEntryKey, + expectedMessage.sender.actor, + expectedMessage.recipient.actor, + expectedMessage.sender.chainId, + expectedMessage.recipient.version, + expectedMessage.deadline, + expectedMessage.fee, + expectedMessage.content, + expectedMessage.secretHash + ); + + // Perform op + bytes32 entryKey = tokenPortal.depositToAztecPublic{value: bid}( + to, amount, deadline, secretHashForL2MessageConsumption, address(this) + ); + + assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); + + // Check that the message is in the inbox + DataStructures.Entry memory entry = inbox.get(entryKey); + assertEq(entry.count, 1); + + return entryKey; + } + + function testCancelPublic() public { + bytes32 expectedEntryKey = testDepositPublic(); // now cancel the message - move time forward (post deadline) vm.warp(deadline + 1 days); // ensure no one else can cancel the message: vm.startPrank(address(0xdead)); bytes32 expectedWrongEntryKey = - inbox.computeEntryKey(_createExpectedL1ToL2Message(address(0xdead))); + inbox.computeEntryKey(_createExpectedMintPublicL1ToL2Message(address(0xdead))); vm.expectRevert( abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) ); - tokenPortal.cancelL1ToAztecMessage(to, amount, deadline, secretHash, bid); + tokenPortal.cancelL1ToAztecMessagePublic( + to, amount, deadline, secretHashForL2MessageConsumption, bid + ); vm.stopPrank(); + // ensure cant cancel with cancelPrivate (since deposit was public) + expectedWrongEntryKey = + inbox.computeEntryKey(_createExpectedMintPrivateL1ToL2Message(address(this))); + vm.expectRevert( + abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) + ); + tokenPortal.cancelL1ToAztecMessagePrivate( + amount, deadline, secretHashForL2MessageConsumption, secretHashForRedeemingMintedNotes, bid + ); + // actually cancel the message // check event was emitted vm.expectEmit(true, false, false, false); // expected event: emit L1ToL2MessageCancelled(expectedEntryKey); // perform op - bytes32 entryKey = tokenPortal.cancelL1ToAztecMessage(to, amount, deadline, secretHash, bid); + bytes32 entryKey = tokenPortal.cancelL1ToAztecMessagePublic( + to, amount, deadline, secretHashForL2MessageConsumption, bid + ); + + assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); + assertFalse(inbox.contains(entryKey), "entry still in inbox"); + assertEq( + portalERC20.balanceOf(address(this)), + mintAmount, + "assets should be transferred back to this contract" + ); + assertEq(portalERC20.balanceOf(address(tokenPortal)), 0, "portal should have no assets"); + } + + function testCancelPrivate() public { + bytes32 expectedEntryKey = testDepositPrivate(); + // now cancel the message - move time forward (post deadline) + vm.warp(deadline + 1 days); + + // ensure no one else can cancel the message: + vm.startPrank(address(0xdead)); + bytes32 expectedWrongEntryKey = + inbox.computeEntryKey(_createExpectedMintPrivateL1ToL2Message(address(0xdead))); + vm.expectRevert( + abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) + ); + tokenPortal.cancelL1ToAztecMessagePrivate( + amount, deadline, secretHashForL2MessageConsumption, secretHashForRedeemingMintedNotes, bid + ); + vm.stopPrank(); + + // ensure cant cancel with cancelPublic (since deposit was private) + expectedWrongEntryKey = + inbox.computeEntryKey(_createExpectedMintPublicL1ToL2Message(address(this))); + vm.expectRevert( + abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) + ); + tokenPortal.cancelL1ToAztecMessagePublic( + to, amount, deadline, secretHashForL2MessageConsumption, bid + ); + + // actually cancel the message + // check event was emitted + vm.expectEmit(true, false, false, false); + // expected event: + emit L1ToL2MessageCancelled(expectedEntryKey); + // perform op + bytes32 entryKey = tokenPortal.cancelL1ToAztecMessagePrivate( + amount, deadline, secretHashForL2MessageConsumption, secretHashForRedeemingMintedNotes, bid + ); assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); assertFalse(inbox.contains(entryKey), "entry still in inbox"); diff --git a/l1-contracts/test/portals/UniswapPortal.sol b/l1-contracts/test/portals/UniswapPortal.sol index 8d68ff8861a..f96f46bc4f0 100644 --- a/l1-contracts/test/portals/UniswapPortal.sol +++ b/l1-contracts/test/portals/UniswapPortal.sol @@ -121,8 +121,9 @@ contract UniswapPortal { // Note, safeApprove was deprecated from Oz vars.outputAsset.approve(address(_outputTokenPortal), amountOut); - // Deposit the output asset to the L2 via its portal - return TokenPortal(_outputTokenPortal).depositToAztec{value: msg.value}( + // Deposit the output asset to the L2 via its portal] + // TODO(2167) - Update UniswapPortal properly with new portal standard. + return TokenPortal(_outputTokenPortal).depositToAztecPublic{value: msg.value}( _aztecRecipient, amountOut, _deadlineForL1ToL2Message, _secretHash, _canceller ); } diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index 0ef1c4b133e..e2724611745 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -344,7 +344,8 @@ contract UniswapPortalTest is Test { // expected event: emit L1ToL2MessageCancelled(l1ToL2MessageKey); // perform op - bytes32 entryKey = wethTokenPortal.cancelL1ToAztecMessage( + // TODO(2167) - Update UniswapPortal properly with new portal standard. + bytes32 entryKey = wethTokenPortal.cancelL1ToAztecMessagePublic( aztecRecipient, wethAmountOut, deadlineForL1ToL2Message, secretHash, 1 ether ); assertEq(entryKey, l1ToL2MessageKey, "returned entry key and calculated entryKey should match"); diff --git a/yarn-project/aztec-nr/aztec/src/types/address.nr b/yarn-project/aztec-nr/aztec/src/types/address.nr index 6c7f165c347..bf216f0c15f 100644 --- a/yarn-project/aztec-nr/aztec/src/types/address.nr +++ b/yarn-project/aztec-nr/aztec/src/types/address.nr @@ -26,6 +26,11 @@ struct EthereumAddress { impl EthereumAddress { fn new(address: Field) -> Self { + // Check that it actually will fit. Spending a lot of constraints here :grimacing: + let bytes = address.to_be_bytes(32); + for i in 0..12 { + assert(bytes[i] == 0, "Value too large for SafeU120"); + } Self { address } diff --git a/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts index 63f73c858a5..d38b7638747 100644 --- a/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts @@ -157,7 +157,8 @@ const transferWethOnL2 = async ( logger(`WETH to L2 Transfer Receipt status: ${transferReceipt.status}`); }; -describe('uniswap_trade_on_l1_from_l2', () => { +// TODO(2167) - Fix this! Adapt to new portal standard and new cross chain harness. +describe.skip('uniswap_trade_on_l1_from_l2', () => { let ethAccount = EthAddress.ZERO; let publicClient: PublicClient; let walletClient: WalletClient; @@ -224,10 +225,10 @@ describe('uniswap_trade_on_l1_from_l2', () => { const deadline = 2 ** 32 - 1; // max uint32 - 1 logger('Sending messages to L1 portal'); const args = [owner.toString(), wethAmountToBridge, deadline, secretString, ethAccount.toString()] as const; - const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztec(args, { + const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztecPublic(args, { account: ethAccount.toString(), } as any); - await wethTokenPortal.write.depositToAztec(args, {} as any); + await wethTokenPortal.write.depositToAztecPublic(args, {} as any); const currentL1Balance = await wethContract.read.balanceOf([ethAccount.toString()]); logger(`Initial Balance: ${currentL1Balance}. Should be: ${meBeforeBalance - wethAmountToBridge}`); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index da263d7569d..2ea9a9ba243 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -68,13 +68,20 @@ describe('e2e_cross_chain_messaging', () => { const l1TokenBalance = 1000000n; const bridgeAmount = 100n; - const [secret, secretHash] = await crossChainTestHarness.generateClaimSecret(); + const [secretForL2MessageConsumption, secretHashForL2MessageConsumption] = + await crossChainTestHarness.generateClaimSecret(); + const [secretForRedeemingMintedNotes, secretHashForRedeemingMintedNotes] = + await crossChainTestHarness.generateClaimSecret(); // 1. Mint tokens on L1 await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); // 2. Deposit tokens to the TokenPortal - const messageKey = await crossChainTestHarness.sendTokensToPortal(bridgeAmount, secretHash); + const messageKey = await crossChainTestHarness.sendTokensToPortalPrivate( + bridgeAmount, + secretHashForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + ); expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message @@ -86,9 +93,14 @@ describe('e2e_cross_chain_messaging', () => { await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); // 3. Consume L1-> L2 message and mint private tokens on L2 - await crossChainTestHarness.consumeMessageOnAztecAndMintSecretly(bridgeAmount, messageKey, secret); + await crossChainTestHarness.consumeMessageOnAztecAndMintSecretly( + bridgeAmount, + messageKey, + secretForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + ); // tokens were minted privately in a TransparentNote which the owner (person who knows the secret) must redeem: - await crossChainTestHarness.redeemShieldPrivatelyOnL2(bridgeAmount, secret); + await crossChainTestHarness.redeemShieldPrivatelyOnL2(bridgeAmount, secretForRedeemingMintedNotes); await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, bridgeAmount); // time to withdraw the funds again! @@ -120,5 +132,5 @@ describe('e2e_cross_chain_messaging', () => { expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); - // TODO: Fialure cases! + // TODO: Failure cases! }); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index d51b4ab0aa0..5b3ef1060b5 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -76,7 +76,7 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); // 2. Deposit tokens to the TokenPortal - const messageKey = await crossChainTestHarness.sendTokensToPortal(bridgeAmount, secretHash); + const messageKey = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message @@ -122,5 +122,5 @@ describe('e2e_public_cross_chain_messaging', () => { expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); - // TODO: Fialure cases! + // TODO: Failure cases! }); diff --git a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts index b7e815f51d8..115b7f1f846 100644 --- a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts @@ -67,7 +67,7 @@ describe('e2e_public_to_private_messaging', () => { const [secret, secretHash] = await crossChainTestHarness.generateClaimSecret(); await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); - const messageKey = await crossChainTestHarness.sendTokensToPortal(bridgeAmount, secretHash); + const messageKey = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 7c60c1de3ed..0889a3d501a 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -146,13 +146,13 @@ export class CrossChainTestHarness { return await this.underlyingERC20.read.balanceOf([address.toString()]); } - async sendTokensToPortal(bridgeAmount: bigint, secretHash: Fr) { + async sendTokensToPortalPublic(bridgeAmount: bigint, secretHash: Fr) { await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); // Deposit tokens to the TokenPortal const deadline = 2 ** 32 - 1; // max uint32 - 1 - this.logger('Sending messages to L1 portal to be consumed privately'); + this.logger('Sending messages to L1 portal to be consumed publicly'); const args = [ this.ownerAddress.toString(), bridgeAmount, @@ -160,10 +160,36 @@ export class CrossChainTestHarness { secretHash.toString(true), this.ethAccount.toString(), ] as const; - const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztec(args, { + const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztecPublic(args, { + account: this.ethAccount.toString(), + } as any); + await this.tokenPortal.write.depositToAztecPublic(args, {} as any); + + return Fr.fromString(messageKeyHex); + } + + async sendTokensToPortalPrivate( + bridgeAmount: bigint, + secretHashForL2MessageConsumption: Fr, + secretHashForRedeemingMintedNotes: Fr, + ) { + await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); + + // Deposit tokens to the TokenPortal + const deadline = 2 ** 32 - 1; // max uint32 - 1 + + this.logger('Sending messages to L1 portal to be consumed privately'); + const args = [ + bridgeAmount, + deadline, + secretHashForL2MessageConsumption.toString(true), + secretHashForRedeemingMintedNotes.toString(true), + this.ethAccount.toString(), + ] as const; + const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztecPrivate(args, { account: this.ethAccount.toString(), } as any); - await this.tokenPortal.write.depositToAztec(args, {} as any); + await this.tokenPortal.write.depositToAztecPrivate(args, {} as any); return Fr.fromString(messageKeyHex); } @@ -183,11 +209,18 @@ export class CrossChainTestHarness { expect(receipt.status).toBe(TxStatus.MINED); } - async consumeMessageOnAztecAndMintSecretly(bridgeAmount: bigint, messageKey: Fr, secret: Fr) { + async consumeMessageOnAztecAndMintSecretly( + bridgeAmount: bigint, + messageKey: Fr, + secretForL2MessageConsumption: Fr, + secretHashForL2MessageConsumption: Fr, + ) { this.logger('Consuming messages on L2 secretively'); // Call the mint tokens function on the Aztec.nr contract const consumptionTx = this.l2Bridge.methods - .deposit_private(bridgeAmount, messageKey, secret, { address: this.ethAccount.toField() }) + .claim_private(bridgeAmount, messageKey, secretForL2MessageConsumption, secretHashForL2MessageConsumption, { + address: this.ethAccount.toField(), + }) .send(); const consumptionReceipt = await consumptionTx.wait(); expect(consumptionReceipt.status).toBe(TxStatus.MINED); @@ -197,7 +230,9 @@ export class CrossChainTestHarness { this.logger('Consuming messages on L2 Publicly'); // Call the mint tokens function on the Aztec.nr contract const tx = this.l2Bridge.methods - .deposit_public(bridgeAmount, messageKey, secret, { address: this.ethAccount.toField() }) + .claim_public({ address: this.ownerAddress }, bridgeAmount, messageKey, secret, { + address: this.ethAccount.toField(), + }) .send(); const receipt = await tx.wait(); expect(receipt.status).toBe(TxStatus.MINED); @@ -205,7 +240,7 @@ export class CrossChainTestHarness { async withdrawPrivateFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { const withdrawTx = this.l2Bridge.methods - .withdraw_private( + .exit_to_l1_private( { address: this.l2Token.address }, withdrawAmount, { address: this.ethAccount.toField() }, @@ -219,7 +254,7 @@ export class CrossChainTestHarness { async withdrawPublicFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { const withdrawTx = this.l2Bridge.methods - .withdraw_public( + .exit_to_l1_public( withdrawAmount, { address: this.ethAccount.toField() }, { address: EthAddress.ZERO.toField() }, diff --git a/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts b/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts index d1ee8dbd3fc..771502116e5 100644 --- a/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts @@ -12,6 +12,7 @@ import { Chain, HttpTransport, PublicClient } from 'viem'; import { delay, deployAndInitializeNonNativeL2TokenContracts, setNextBlockTimestamp, setup } from './fixtures/utils.js'; +// TODO (#2291) - Replace with token bridge standard describe('archiver integration with l1 to l2 messages', () => { let aztecNode: AztecNodeService | undefined; let aztecRpcServer: AztecRPC; @@ -99,7 +100,7 @@ describe('archiver integration with l1 to l2 messages', () => { logger('Sending messages to L1 portal'); const args = [owner.toString(), mintAmount, deadline, secretString, ethAccount.toString()] as const; - await tokenPortal.write.depositToAztec(args, {} as any); + await tokenPortal.write.depositToAztecPublic(args, {} as any); expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(1000000n - mintAmount); // Wait for the archiver to process the message @@ -111,7 +112,7 @@ describe('archiver integration with l1 to l2 messages', () => { // cancel the message logger('cancelling the l1 to l2 message'); const argsCancel = [owner.toString(), 100n, deadline, secretString, 0n] as const; - await tokenPortal.write.cancelL1ToAztecMessage(argsCancel, { gas: 1_000_000n } as any); + await tokenPortal.write.cancelL1ToAztecMessagePublic(argsCancel, { gas: 1_000_000n } as any); expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(1000000n); // let archiver sync up await delay(5000); diff --git a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts index 0985f0b3ca8..b8482b98f7c 100644 --- a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts @@ -133,8 +133,15 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { const meBeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ethAccount); // 1. Approve and deposit weth to the portal and move to L2 - const [secret, secretHash] = await wethCrossChainHarness.generateClaimSecret(); - const messageKey = await wethCrossChainHarness.sendTokensToPortal(wethAmountToBridge, secretHash); + const [secretForL2MessageConsumption, secretHashForL2MessageConsumption] = + await wethCrossChainHarness.generateClaimSecret(); + const [, secretHashForRedeemingMintedNotes] = await wethCrossChainHarness.generateClaimSecret(); + + const messageKey = await wethCrossChainHarness.sendTokensToPortalPrivate( + wethAmountToBridge, + secretHashForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + ); expect(await wethCrossChainHarness.getL1BalanceOf(ethAccount)).toBe(meBeforeBalance - wethAmountToBridge); // Wait for the archiver to process the message @@ -146,7 +153,12 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { // 3. Claim WETH on L2 logger('Minting weth on L2'); - await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly(wethAmountToBridge, messageKey, secret); + await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly( + wethAmountToBridge, + messageKey, + secretForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + ); await wethCrossChainHarness.expectPrivateBalanceOnL2(owner, wethAmountToBridge + initialBalance - transferAmount); // Store balances @@ -166,7 +178,7 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { new Fr(minimumOutputAmount), owner, owner, - secretHash, + secretHashForL2MessageConsumption, new Fr(2 ** 32 - 1), ethAccount.toField(), ethAccount.toField(), @@ -190,7 +202,7 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, owner.toString(), - secretHash.toString(true), + secretHashForL2MessageConsumption.toString(true), deadline, ethAccount.toString(), true, @@ -213,7 +225,12 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { // 6. claim dai on L2 logger('Consuming messages to mint dai on L2'); - await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly(daiAmountToBridge, depositDaiMessageKey, secret); + await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly( + daiAmountToBridge, + depositDaiMessageKey, + secretForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + ); await daiCrossChainHarness.expectPrivateBalanceOnL2(owner, initialBalance + daiAmountToBridge); const wethBalanceAfterSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(owner); diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr index d28b8457979..afad9569c5d 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr @@ -18,7 +18,7 @@ contract TokenBridge { }; use crate::token_interface::Token; - use crate::util::{get_mint_content_hash, get_withdraw_content_hash, compute_secret_hash}; + use crate::util::{get_mint_public_content_hash, get_mint_private_content_hash, get_withdraw_content_hash, compute_secret_hash}; // Storage structure, containing all storage, and specifying what slots they use. struct Storage { @@ -47,7 +47,8 @@ contract TokenBridge { // Consumes a L1->L2 message and calls the token contract to mint the appropriate amount publicly #[aztec(public)] - fn deposit_public( + fn claim_public( + to: AztecAddress, amount: Field, msg_key: Field, secret: Field, @@ -55,20 +56,20 @@ contract TokenBridge { ) -> Field { let storage = Storage::init(Context::public(&mut context)); - let content_hash = get_mint_content_hash(amount, context.msg_sender(), canceller.address); + let content_hash = get_mint_public_content_hash(amount, to.address, canceller.address); // Consume message and emit nullifier context.consume_l1_to_l2_message(msg_key, content_hash, secret); // Mint tokens - Token::at(storage.token.read()).mint_public(context, context.msg_sender(), amount); + Token::at(storage.token.read()).mint_public(context, to.address, amount); 1 } // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message publicly - // Requires `from` to give approval to the bridge to burn tokens on their behalf using witness signatures + // Requires `msg.sender` to give approval to the bridge to burn tokens on their behalf using witness signatures #[aztec(public)] - fn withdraw_public( + fn exit_to_l1_public( amount: Field, recipient: EthereumAddress, // ethereum address to withdraw to callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) @@ -76,37 +77,38 @@ contract TokenBridge { ) -> Field { let storage = Storage::init(Context::public(&mut context)); - // Burn tokens - Token::at(storage.token.read()).burn_public(context, context.msg_sender(), amount, nonce); - // Send an L2 to L1 message let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address); - context.message_portal(content); + context.message_portal(content); + + // Burn tokens + Token::at(storage.token.read()).burn_public(context, context.msg_sender(), amount, nonce); + 1 } // 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 deposit_private( + fn claim_private( amount: Field, msg_key: Field, // L1 to L2 message key as derived from the inbox contract - secret: Field, + secret_for_L1_to_L2_message_consumption: Field, // secret used to consume the L1 to L2 message + 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 canceller: EthereumAddress, ) -> Field { // Consume L1 to L2 message and emit nullifier - let content_hash = get_mint_content_hash(amount, context.msg_sender(), canceller.address); - context.consume_l1_to_l2_message(inputs, msg_key, content_hash, secret); + let content_hash = get_mint_private_content_hash(amount, secret_hash_for_redeeming_minted_notes, canceller.address); + context.consume_l1_to_l2_message(msg_key, content_hash, secret_for_L1_to_L2_message_consumption); // Mint tokens on L2 // We hash the secret privately and send the hash in a public call so the secret isn't leaked // Furthermore, `mint_private` on token is public. So we can an internal public function // which then calls the token contract - let secret_hash = compute_secret_hash(secret); context.call_public_function( context.this_address(), compute_selector("_call_mint_on_token(Field,Field)"), - [amount, secret_hash], + [amount, secret_hash_for_redeeming_minted_notes], ); 1 @@ -115,16 +117,13 @@ contract TokenBridge { // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately // Requires `from` to give approval to the bridge to burn tokens on their behalf using witness signatures #[aztec(private)] - fn withdraw_private( + fn exit_to_l1_private( token: AztecAddress, amount: Field, recipient: EthereumAddress, // ethereum address to withdraw to callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) nonce: Field, ) -> Field { - // Burn tokens - Token::at(token.address).burn(&mut context, context.msg_sender(), amount, nonce); - // Send an L2 to L1 message let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address); context.message_portal(content); @@ -132,6 +131,9 @@ contract TokenBridge { // Assert that user provided token address is same as seen in storage. context.call_public_function(context.this_address(), compute_selector("_assert_token_is_same(Field)"), [token.address]); + // Burn tokens + Token::at(token.address).burn(&mut context, context.msg_sender(), amount, nonce); + 1 } @@ -153,7 +155,8 @@ contract TokenBridge { storage.token.write(token.address); } - // This way, user hashes their secret in private and only sends the hash in public + // 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)] internal fn _call_mint_on_token(amount: Field, secret_hash: Field){ diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr index 9e2cfd194e4..a9203f4fe24 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr @@ -9,8 +9,47 @@ fn compute_secret_hash(secret: Field) -> Field { pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] } -// Computes a content hash of a deposit/mint message. -fn get_mint_content_hash(amount: Field, owner_address: Field, canceller: Field) -> Field { +// Computes a content hash of a deposit/mint_private message. +// Refer TokenPortal.sol for reference on L1. +fn get_mint_private_content_hash(amount: Field, secret_hash_for_redeeming_minted_notes: Field, canceller: Field) -> Field { + let mut hash_bytes: [u8; 100] = [0; 100]; + let amount_bytes = amount.to_be_bytes(32); + let secret_hash_bytes = secret_hash_for_redeeming_minted_notes.to_be_bytes(32); + let canceller_bytes = canceller.to_be_bytes(32); + + for i in 0..32 { + hash_bytes[i + 4] = amount_bytes[i]; + hash_bytes[i + 36] = secret_hash_bytes[i]; + hash_bytes[i + 68] = canceller_bytes[i]; + } + + // Function selector: 0x25d46b0f keccak256('mint_private(uint256,bytes32,address)') + hash_bytes[0] = 0x25; + hash_bytes[1] = 0xd4; + hash_bytes[2] = 0x6b; + hash_bytes[3] = 0x0f; + + let content_sha256 = sha256(hash_bytes); + + // // Convert the content_sha256 to a field element + let mut v = 1; + let mut high = 0 as Field; + let mut low = 0 as Field; + + for i in 0..16 { + high = high + (content_sha256[15 - i] as Field) * v; + low = low + (content_sha256[16 + 15 - i] as Field) * v; + v = v * 256; + } + + // Abuse that a % p + b % p = (a + b) % p and that low < p + let content_hash = low + high * v; + content_hash +} + +// Computes a content hash of a deposit/mint_public message. +// Refer TokenPortal.sol for reference on L1. +fn get_mint_public_content_hash(amount: Field, owner_address: Field, canceller: Field) -> Field { let mut hash_bytes: [u8; 100] = [0; 100]; let amount_bytes = amount.to_be_bytes(32); let recipient_bytes = owner_address.to_be_bytes(32); @@ -22,11 +61,11 @@ fn get_mint_content_hash(amount: Field, owner_address: Field, canceller: Field) hash_bytes[i + 68] = canceller_bytes[i]; } - // Function selector: 0xeeb73071 keccak256('mint(uint256,bytes32,address)') - hash_bytes[0] = 0xee; - hash_bytes[1] = 0xb7; - hash_bytes[2] = 0x30; - hash_bytes[3] = 0x71; + // Function selector: 0x63c9440d keccak256('mint_public(uint256,bytes32,address)') + hash_bytes[0] = 0x63; + hash_bytes[1] = 0xc9; + hash_bytes[2] = 0x44; + hash_bytes[3] = 0x0d; let content_sha256 = sha256(hash_bytes); From 23bc9e6a9883e02309aab9e76339157b71eb0d40 Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Mon, 18 Sep 2023 16:43:02 +0000 Subject: [PATCH 5/6] add more unit tests --- .../src/e2e_cross_chain_messaging.test.ts | 109 ++++++++++++++++-- .../e2e_public_cross_chain_messaging.test.ts | 92 +++++++++++++-- 2 files changed, 181 insertions(+), 20 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 2ea9a9ba243..3e23f58f7b9 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -5,7 +5,7 @@ import { Fr, FunctionSelector } from '@aztec/circuits.js'; import { EthAddress } from '@aztec/foundation/eth-address'; import { DebugLogger } from '@aztec/foundation/log'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; -import { AztecRPC } from '@aztec/types'; +import { AztecRPC, TxStatus } from '@aztec/types'; import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; import { delay, hashPayload, setup } from './fixtures/utils.js'; @@ -15,7 +15,8 @@ describe('e2e_cross_chain_messaging', () => { let aztecRpcServer: AztecRPC; let logger: DebugLogger; - let wallet: AccountWallet; + let user1Wallet: AccountWallet; + let user2Wallet: AccountWallet; let ethAccount: EthAddress; let ownerAddress: AztecAddress; @@ -30,7 +31,7 @@ describe('e2e_cross_chain_messaging', () => { aztecRpcServer: aztecRpcServer_, deployL1ContractsValues, accounts, - wallet: wallet_, + wallets, logger: logger_, cheatCodes, } = await setup(2); @@ -39,7 +40,7 @@ describe('e2e_cross_chain_messaging', () => { aztecRpcServer_, deployL1ContractsValues, accounts, - wallet_, + wallets[0], logger_, cheatCodes, ); @@ -50,7 +51,8 @@ describe('e2e_cross_chain_messaging', () => { ownerAddress = crossChainTestHarness.ownerAddress; outbox = crossChainTestHarness.outbox; aztecRpcServer = crossChainTestHarness.aztecRpcServer; - wallet = wallet_; + user1Wallet = wallets[0]; + user2Wallet = wallets[1]; logger = logger_; logger('Successfully deployed contracts and initialized portal'); }, 100_000); @@ -63,7 +65,7 @@ describe('e2e_cross_chain_messaging', () => { await crossChainTestHarness?.stop(); }); - it('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { + it.skip('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { // Generate a claim secret using pedersen const l1TokenBalance = 1000000n; const bridgeAmount = 100n; @@ -117,7 +119,7 @@ describe('e2e_cross_chain_messaging', () => { new Fr(withdrawAmount), nonce, ]); - await wallet.createAuthWitness(burnMessageHash); + await user1Wallet.createAuthWitness(burnMessageHash); // 5. Withdraw owner's funds from L2 to L1 const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); @@ -132,5 +134,96 @@ describe('e2e_cross_chain_messaging', () => { expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); - // TODO: Failure cases! + // Unit tests for TokenBridge's private methods. + it('Someone else can mint funds to me on my behalf (privately)', async () => { + const l1TokenBalance = 1000000n; + const bridgeAmount = 100n; + const [secretForL2MessageConsumption, secretHashForL2MessageConsumption] = + await crossChainTestHarness.generateClaimSecret(); + const [secretForRedeemingMintedNotes, secretHashForRedeemingMintedNotes] = + await crossChainTestHarness.generateClaimSecret(); + + await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); + const messageKey = await crossChainTestHarness.sendTokensToPortalPrivate( + bridgeAmount, + secretHashForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + ); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); + + // Wait for the archiver to process the message + await delay(5000); /// waiting 5 seconds. + + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. + const unrelatedMintAmount = 99n; + await crossChainTestHarness.mintTokensPublicOnL2(unrelatedMintAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); + + // 3. Consume L1-> L2 message and mint private tokens on L2 + + // Sending wrong secret hashes should fail: + await expect( + l2Bridge + .withWallet(user2Wallet) + .methods.claim_private( + bridgeAmount, + messageKey, + secretForL2MessageConsumption, + secretHashForL2MessageConsumption, + { + address: ethAccount.toField(), + }, + ) + .simulate(), + ).rejects.toThrowError("Cannot satisfy constraint 'l1_to_l2_message_data.message.content == content"); + + // send the right one - + const consumptionTx = l2Bridge + .withWallet(user2Wallet) + .methods.claim_private( + bridgeAmount, + messageKey, + secretForL2MessageConsumption, + secretHashForRedeemingMintedNotes, + { + address: ethAccount.toField(), + }, + ) + .send(); + const consumptionReceipt = await consumptionTx.wait(); + expect(consumptionReceipt.status).toBe(TxStatus.MINED); + + // Now user1 can claim the notes that user2 minted on their behalf. + await crossChainTestHarness.redeemShieldPrivatelyOnL2(bridgeAmount, secretForRedeemingMintedNotes); + await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, bridgeAmount); + }, 50_000); + + it("Bridge can't withdraw my funds if I don't give approval", async () => { + const mintAmountToUser1 = 100n; + await crossChainTestHarness.mintTokensPublicOnL2(mintAmountToUser1); + + const withdrawAmount = 9n; + const nonce = Fr.random(); + const expectedBurnMessageHash = await hashPayload([ + l2Bridge.address.toField(), + l2Token.address.toField(), + FunctionSelector.fromSignature('burn((Field),Field,Field)').toField(), + user1Wallet.getAddress().toField(), + new Fr(withdrawAmount), + nonce, + ]); + // Should fail as owner has not given approval to bridge burn their funds. + await expect( + l2Bridge + .withWallet(user1Wallet) + .methods.exit_to_l1_private( + { address: l2Token.address }, + withdrawAmount, + { address: ethAccount.toField() }, + { address: EthAddress.ZERO.toField() }, + nonce, + ) + .simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${expectedBurnMessageHash.toString('hex')}`); + }); }); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index 5b3ef1060b5..c6dce6d5c11 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -5,7 +5,7 @@ import { Fr, FunctionSelector } from '@aztec/circuits.js'; import { EthAddress } from '@aztec/foundation/eth-address'; import { DebugLogger } from '@aztec/foundation/log'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; -import { AztecRPC } from '@aztec/types'; +import { AztecRPC, TxStatus } from '@aztec/types'; import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; import { delay, hashPayload, setup } from './fixtures/utils.js'; @@ -15,8 +15,9 @@ describe('e2e_public_cross_chain_messaging', () => { let aztecRpcServer: AztecRPC; let logger: DebugLogger; - let wallet: AccountWallet; - let ethAccount: EthAddress; + let ownerWallet: AccountWallet; + let user2Wallet: AccountWallet; + let ownerEthAddress: EthAddress; let ownerAddress: AztecAddress; let crossChainTestHarness: CrossChainTestHarness; @@ -30,7 +31,7 @@ describe('e2e_public_cross_chain_messaging', () => { aztecRpcServer: aztecRpcServer_, deployL1ContractsValues, accounts, - wallet: wallet_, + wallets, logger: logger_, cheatCodes, } = await setup(2); @@ -39,19 +40,20 @@ describe('e2e_public_cross_chain_messaging', () => { aztecRpcServer_, deployL1ContractsValues, accounts, - wallet_, + wallets[0], logger_, cheatCodes, ); l2Token = crossChainTestHarness.l2Token; l2Bridge = crossChainTestHarness.l2Bridge; - ethAccount = crossChainTestHarness.ethAccount; + ownerEthAddress = crossChainTestHarness.ethAccount; ownerAddress = crossChainTestHarness.ownerAddress; outbox = crossChainTestHarness.outbox; aztecRpcServer = crossChainTestHarness.aztecRpcServer; aztecNode = aztecNode_; - wallet = wallet_; + ownerWallet = wallets[0]; + user2Wallet = wallets[1]; logger = logger_; logger('Successfully deployed contracts and initialized portal'); @@ -77,7 +79,7 @@ describe('e2e_public_cross_chain_messaging', () => { // 2. Deposit tokens to the TokenPortal const messageKey = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe(l1TokenBalance - bridgeAmount); // Wait for the archiver to process the message await delay(5000); /// waiting 5 seconds. @@ -107,7 +109,7 @@ describe('e2e_public_cross_chain_messaging', () => { new Fr(withdrawAmount), nonce, ]); - await wallet.setPublicAuth(burnMessageHash, true).send().wait(); + await ownerWallet.setPublicAuth(burnMessageHash, true).send().wait(); // 5. Withdraw owner's funds from L2 to L1 const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); @@ -115,12 +117,78 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, afterBalance - withdrawAmount); // Check balance before and after exit. - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe(l1TokenBalance - bridgeAmount); await crossChainTestHarness.withdrawFundsFromBridgeOnL1(withdrawAmount, entryKey); - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); + expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe( + l1TokenBalance - bridgeAmount + withdrawAmount, + ); expect(await outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); }, 120_000); - // TODO: Failure cases! + // Unit tests for TokenBridge's public methods. + + it('Someone else can mint funds to me on my behalf (publicly)', async () => { + // Generate a claim secret using pedersen + const l1TokenBalance = 1000000n; + const bridgeAmount = 100n; + + const [secret, secretHash] = await crossChainTestHarness.generateClaimSecret(); + + await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); + const messageKey = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); + expect(await crossChainTestHarness.getL1BalanceOf(ownerEthAddress)).toBe(l1TokenBalance - bridgeAmount); + + // Wait for the archiver to process the message + await delay(5000); /// waiting 5 seconds. + + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. + const unrelatedMintAmount = 99n; + await crossChainTestHarness.mintTokensPublicOnL2(unrelatedMintAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); + + // user2 tries to consume this message and minting to itself -> should fail since the message is intended to be consumed only by owner. + await expect( + l2Bridge + .withWallet(user2Wallet) + .methods.claim_public({ address: user2Wallet.getAddress() }, bridgeAmount, messageKey, secret, { + address: ownerEthAddress.toField(), + }) + .simulate(), + ).rejects.toThrow(); + + // user2 consumes owner's L1-> L2 message on bridge contract and mints public tokens on L2 + logger("user2 consumes owner's message on L2 Publicly"); + const tx = l2Bridge + .withWallet(user2Wallet) + .methods.claim_public({ address: ownerAddress }, bridgeAmount, messageKey, secret, { + address: ownerEthAddress.toField(), + }) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + // ensure funds are gone to owner and not user2. + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount + unrelatedMintAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(user2Wallet.getAddress(), 0n); + }, 60_000); + + it("Bridge can't withdraw my funds if I don't give approval", async () => { + const mintAmountToOwner = 100n; + await crossChainTestHarness.mintTokensPublicOnL2(mintAmountToOwner); + + const withdrawAmount = 9n; + const nonce = Fr.random(); + // Should fail as owner has not given approval to bridge burn their funds. + await expect( + l2Bridge + .withWallet(ownerWallet) + .methods.exit_to_l1_public( + withdrawAmount, + { address: ownerEthAddress.toField() }, + { address: EthAddress.ZERO.toField() }, + nonce, + ) + .simulate(), + ).rejects.toThrowError('Assertion failed: Message not authorized by account'); + }); }); From e9e07ac207e8bca351033b97e7a7684cbc561764 Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Tue, 19 Sep 2023 09:59:53 +0000 Subject: [PATCH 6/6] respond to PR comments --- l1-contracts/test/portals/TokenPortal.sol | 11 ++----- l1-contracts/test/portals/TokenPortal.t.sol | 6 ++-- .../aztec-nr/aztec/src/types/address.nr | 2 +- .../src/e2e_cross_chain_messaging.test.ts | 14 ++++----- .../e2e_public_cross_chain_messaging.test.ts | 22 +++++++++----- .../src/fixtures/cross_chain_test_harness.ts | 26 ++++++++++------ .../token_bridge_contract/src/main.nr | 30 +++++++++---------- .../token_bridge_contract/src/util.nr | 4 +-- 8 files changed, 62 insertions(+), 53 deletions(-) diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index c696b3ec09b..3b74d240ec8 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -27,8 +27,8 @@ contract TokenPortal { * @param _to - The aztec address of the recipient * @param _amount - The amount to deposit * @param _deadline - The timestamp after which the entry can be cancelled - * @param _secretHash - The hash of the secret consumable message - * @param _canceller - The address that can cancel the L1 to L2 message. + * @param _secretHash - The hash of the secret consumable message. The hash should be 254 bits (so it can fit in a Field element) + * @param _canceller - The address that can cancel the L1 to L2 message * @return The key of the entry in the Inbox */ function depositToAztecPublic( @@ -39,7 +39,6 @@ contract TokenPortal { address _canceller ) external payable returns (bytes32) { // Preamble - // @todo: (issue #624) handle different versions IInbox inbox = registry.getInbox(); DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2TokenAddress, 1); @@ -61,7 +60,7 @@ contract TokenPortal { * @param _deadline - The timestamp after which the entry can be cancelled * @param _secretHashForL2MessageConsumption - The hash of the secret consumable L1 to L2 message. The hash should be 254 bits (so it can fit in a Field element) * @param _secretHashForL2MessageConsumption - The hash of the secret to redeem minted notes privately on Aztec. The hash should be 254 bits (so it can fit in a Field element) - * @param _canceller - The address that can cancel the L1 to L2 message. + * @param _canceller - The address that can cancel the L1 to L2 message * @return The key of the entry in the Inbox */ function depositToAztecPrivate( @@ -72,7 +71,6 @@ contract TokenPortal { address _canceller ) external payable returns (bytes32) { // Preamble - // @todo: (issue #624) handle different versions IInbox inbox = registry.getInbox(); DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2TokenAddress, 1); @@ -113,7 +111,6 @@ contract TokenPortal { bytes32 _secretHash, uint64 _fee ) external returns (bytes32) { - // @todo: (issue #624) handle different versions IInbox inbox = registry.getInbox(); DataStructures.L1Actor memory l1Actor = DataStructures.L1Actor(address(this), block.chainid); DataStructures.L2Actor memory l2Actor = DataStructures.L2Actor(l2TokenAddress, 1); @@ -151,7 +148,6 @@ contract TokenPortal { bytes32 _secretHashForRedeemingMintedNotes, uint64 _fee ) external returns (bytes32) { - // @todo: (issue #624) handle different versions IInbox inbox = registry.getInbox(); DataStructures.L1Actor memory l1Actor = DataStructures.L1Actor(address(this), block.chainid); DataStructures.L2Actor memory l2Actor = DataStructures.L2Actor(l2TokenAddress, 1); @@ -206,7 +202,6 @@ contract TokenPortal { ) }); - // @todo: (issue #624) handle different versions bytes32 entryKey = registry.getOutbox().consume(message); underlying.transfer(_recipient, _amount); diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 93822b08c92..d914124e607 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -48,8 +48,10 @@ contract TokenPortalTest is Test { bytes32 internal to = bytes32(0x2d749407d8c364537cdeb799c1574929cb22ff1ece2b96d2a1c6fa287a0e0171); uint256 internal amount = 100; uint256 internal mintAmount = 1 ether; + // this hash is just a random 32 byte string bytes32 internal secretHashForL2MessageConsumption = 0x147e4fec49805c924e28150fc4b36824679bc17ecb1d7d9f6a9effb7fde6b6a0; + // this hash is just a random 32 byte string bytes32 internal secretHashForRedeemingMintedNotes = 0x157e4fec49805c924e28150fc4b36824679bc17ecb1d7d9f6a9effb7fde6b6a0; uint64 internal bid = 1 ether; @@ -123,7 +125,7 @@ contract TokenPortalTest is Test { _createExpectedMintPrivateL1ToL2Message(address(this)); bytes32 expectedEntryKey = inbox.computeEntryKey(expectedMessage); - // Check the even was emitted + // Check the event was emitted vm.expectEmit(true, true, true, true); // event we expect emit MessageAdded( @@ -166,7 +168,7 @@ contract TokenPortalTest is Test { _createExpectedMintPublicL1ToL2Message(address(this)); bytes32 expectedEntryKey = inbox.computeEntryKey(expectedMessage); - // Check the even was emitted + // Check the event was emitted vm.expectEmit(true, true, true, true); // event we expect emit MessageAdded( diff --git a/yarn-project/aztec-nr/aztec/src/types/address.nr b/yarn-project/aztec-nr/aztec/src/types/address.nr index bf216f0c15f..6849767b8d0 100644 --- a/yarn-project/aztec-nr/aztec/src/types/address.nr +++ b/yarn-project/aztec-nr/aztec/src/types/address.nr @@ -29,7 +29,7 @@ impl EthereumAddress { // Check that it actually will fit. Spending a lot of constraints here :grimacing: let bytes = address.to_be_bytes(32); for i in 0..12 { - assert(bytes[i] == 0, "Value too large for SafeU120"); + assert(bytes[i] == 0, "Value too large for an ethereum address"); } Self { address diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 3e23f58f7b9..76b18022a0f 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -167,12 +167,10 @@ describe('e2e_cross_chain_messaging', () => { .withWallet(user2Wallet) .methods.claim_private( bridgeAmount, + secretHashForL2MessageConsumption, + { address: ethAccount.toField() }, messageKey, secretForL2MessageConsumption, - secretHashForL2MessageConsumption, - { - address: ethAccount.toField(), - }, ) .simulate(), ).rejects.toThrowError("Cannot satisfy constraint 'l1_to_l2_message_data.message.content == content"); @@ -182,12 +180,10 @@ describe('e2e_cross_chain_messaging', () => { .withWallet(user2Wallet) .methods.claim_private( bridgeAmount, + secretHashForRedeemingMintedNotes, + { address: ethAccount.toField() }, messageKey, secretForL2MessageConsumption, - secretHashForRedeemingMintedNotes, - { - address: ethAccount.toField(), - }, ) .send(); const consumptionReceipt = await consumptionTx.wait(); @@ -217,9 +213,9 @@ describe('e2e_cross_chain_messaging', () => { l2Bridge .withWallet(user1Wallet) .methods.exit_to_l1_private( + { address: ethAccount.toField() }, { address: l2Token.address }, withdrawAmount, - { address: ethAccount.toField() }, { address: EthAddress.ZERO.toField() }, nonce, ) diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index c6dce6d5c11..82afbdce887 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -151,9 +151,13 @@ describe('e2e_public_cross_chain_messaging', () => { await expect( l2Bridge .withWallet(user2Wallet) - .methods.claim_public({ address: user2Wallet.getAddress() }, bridgeAmount, messageKey, secret, { - address: ownerEthAddress.toField(), - }) + .methods.claim_public( + { address: user2Wallet.getAddress() }, + bridgeAmount, + { address: ownerEthAddress.toField() }, + messageKey, + secret, + ) .simulate(), ).rejects.toThrow(); @@ -161,9 +165,13 @@ describe('e2e_public_cross_chain_messaging', () => { logger("user2 consumes owner's message on L2 Publicly"); const tx = l2Bridge .withWallet(user2Wallet) - .methods.claim_public({ address: ownerAddress }, bridgeAmount, messageKey, secret, { - address: ownerEthAddress.toField(), - }) + .methods.claim_public( + { address: ownerAddress }, + bridgeAmount, + { address: ownerEthAddress.toField() }, + messageKey, + secret, + ) .send(); const receipt = await tx.wait(); expect(receipt.status).toBe(TxStatus.MINED); @@ -183,8 +191,8 @@ describe('e2e_public_cross_chain_messaging', () => { l2Bridge .withWallet(ownerWallet) .methods.exit_to_l1_public( - withdrawAmount, { address: ownerEthAddress.toField() }, + withdrawAmount, { address: EthAddress.ZERO.toField() }, nonce, ) diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 0889a3d501a..ca6866dafb0 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -211,16 +211,20 @@ export class CrossChainTestHarness { async consumeMessageOnAztecAndMintSecretly( bridgeAmount: bigint, + secretHashForRedeemingMintedNotes: Fr, messageKey: Fr, secretForL2MessageConsumption: Fr, - secretHashForL2MessageConsumption: Fr, ) { this.logger('Consuming messages on L2 secretively'); // Call the mint tokens function on the Aztec.nr contract const consumptionTx = this.l2Bridge.methods - .claim_private(bridgeAmount, messageKey, secretForL2MessageConsumption, secretHashForL2MessageConsumption, { - address: this.ethAccount.toField(), - }) + .claim_private( + bridgeAmount, + secretHashForRedeemingMintedNotes, + { address: this.ethAccount.toField() }, + messageKey, + secretForL2MessageConsumption, + ) .send(); const consumptionReceipt = await consumptionTx.wait(); expect(consumptionReceipt.status).toBe(TxStatus.MINED); @@ -230,9 +234,13 @@ export class CrossChainTestHarness { this.logger('Consuming messages on L2 Publicly'); // Call the mint tokens function on the Aztec.nr contract const tx = this.l2Bridge.methods - .claim_public({ address: this.ownerAddress }, bridgeAmount, messageKey, secret, { - address: this.ethAccount.toField(), - }) + .claim_public( + { address: this.ownerAddress }, + bridgeAmount, + { address: this.ethAccount.toField() }, + messageKey, + secret, + ) .send(); const receipt = await tx.wait(); expect(receipt.status).toBe(TxStatus.MINED); @@ -241,9 +249,9 @@ export class CrossChainTestHarness { async withdrawPrivateFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { const withdrawTx = this.l2Bridge.methods .exit_to_l1_private( + { address: this.ethAccount.toField() }, { address: this.l2Token.address }, withdrawAmount, - { address: this.ethAccount.toField() }, { address: EthAddress.ZERO.toField() }, nonce, ) @@ -255,8 +263,8 @@ export class CrossChainTestHarness { async withdrawPublicFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { const withdrawTx = this.l2Bridge.methods .exit_to_l1_public( - withdrawAmount, { address: this.ethAccount.toField() }, + withdrawAmount, { address: EthAddress.ZERO.toField() }, nonce, ) diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr index afad9569c5d..07ac537644c 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr @@ -4,7 +4,7 @@ mod token_interface; // 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 sued +// Bridge has to be set as a minter on the token before it can be used contract TokenBridge { use dep::aztec::{ @@ -50,13 +50,13 @@ contract TokenBridge { fn claim_public( to: AztecAddress, amount: Field, + canceller: EthereumAddress, msg_key: Field, secret: Field, - canceller: EthereumAddress, ) -> Field { let storage = Storage::init(Context::public(&mut context)); - let content_hash = get_mint_public_content_hash(amount, to.address, canceller.address); + let content_hash = get_mint_public_content_hash(to.address, amount, canceller.address); // Consume message and emit nullifier context.consume_l1_to_l2_message(msg_key, content_hash, secret); @@ -70,15 +70,15 @@ contract TokenBridge { // Requires `msg.sender` to give approval to the bridge to burn tokens on their behalf using witness signatures #[aztec(public)] fn exit_to_l1_public( - amount: Field, recipient: EthereumAddress, // ethereum address to withdraw to + amount: Field, callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) - nonce: Field, + nonce: Field, // nonce used in the approval message by `msg.sender` to let bridge burn their tokens on L2 ) -> Field { let storage = Storage::init(Context::public(&mut context)); // Send an L2 to L1 message - let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address); + let content = get_withdraw_content_hash(recipient.address, amount, callerOnL1.address); context.message_portal(content); // Burn tokens @@ -92,19 +92,19 @@ contract TokenBridge { #[aztec(private)] fn claim_private( amount: Field, - msg_key: Field, // L1 to L2 message key as derived from the inbox contract - secret_for_L1_to_L2_message_consumption: Field, // secret used to consume the L1 to L2 message 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 canceller: EthereumAddress, + msg_key: Field, // L1 to L2 message key as derived from the inbox contract + secret_for_L1_to_L2_message_consumption: Field, // secret used to consume the L1 to L2 message ) -> Field { // Consume L1 to L2 message and emit nullifier let content_hash = get_mint_private_content_hash(amount, secret_hash_for_redeeming_minted_notes, canceller.address); context.consume_l1_to_l2_message(msg_key, content_hash, secret_for_L1_to_L2_message_consumption); // Mint tokens on L2 - // We hash the secret privately and send the hash in a public call so the secret isn't leaked - // Furthermore, `mint_private` on token is public. So we can an internal public function - // which then calls the token contract + // `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. context.call_public_function( context.this_address(), compute_selector("_call_mint_on_token(Field,Field)"), @@ -115,17 +115,17 @@ contract TokenBridge { } // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately - // Requires `from` to give approval to the bridge to burn tokens on their behalf using witness signatures + // Requires `msg.sender` (caller of the method) to give approval to the bridge to burn tokens on their behalf using witness signatures #[aztec(private)] fn exit_to_l1_private( + recipient: EthereumAddress, // ethereum address to withdraw to token: AztecAddress, amount: Field, - recipient: EthereumAddress, // ethereum address to withdraw to callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) - nonce: Field, + nonce: Field, // nonce used in the approval message by `msg.sender` to let bridge burn their tokens on L2 ) -> Field { // Send an L2 to L1 message - let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address); + let content = get_withdraw_content_hash(recipient.address, amount, callerOnL1.address); context.message_portal(content); // Assert that user provided token address is same as seen in storage. diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr index a9203f4fe24..7ecb7d547cd 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr @@ -49,7 +49,7 @@ fn get_mint_private_content_hash(amount: Field, secret_hash_for_redeeming_minted // Computes a content hash of a deposit/mint_public message. // Refer TokenPortal.sol for reference on L1. -fn get_mint_public_content_hash(amount: Field, owner_address: Field, canceller: Field) -> Field { +fn get_mint_public_content_hash(owner_address: Field, amount: Field, canceller: Field) -> Field { let mut hash_bytes: [u8; 100] = [0; 100]; let amount_bytes = amount.to_be_bytes(32); let recipient_bytes = owner_address.to_be_bytes(32); @@ -86,7 +86,7 @@ fn get_mint_public_content_hash(amount: Field, owner_address: Field, canceller: } // Computes a content hash of a withdraw message. -fn get_withdraw_content_hash(amount: Field, recipient: Field, callerOnL1: Field) -> Field { +fn get_withdraw_content_hash(recipient: Field, amount: Field, callerOnL1: Field) -> Field { // Compute the content hash // Compute sha256(selector || amount || recipient) // then convert to a single field element