diff --git a/noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr index 06de11e96cb0..9caf91dec4eb 100644 --- a/noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr @@ -102,6 +102,22 @@ contract TokenBridge { } // docs:end:claim_private + #[aztec(private)] + fn claim_private_for_caller( + amount: Field, + secret_for_L1_to_L2_message_consumption: Field // secret used to consume the L1 to L2 message + ) { + // Consume L1 to L2 message and emit nullifier + let content_hash = get_mint_private_content_hash(0, amount); + context.consume_l1_to_l2_message( + content_hash, + secret_for_L1_to_L2_message_consumption, + storage.portal_address.read_private() + ); + + Token::at(storage.token.read_private()).mint_private_for(context.msg_sender(), amount).call(&mut context); + } + // docs:start:exit_to_l1_private // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately // Requires `msg.sender` (caller of the method) to give approval to the bridge to burn tokens on their behalf using witness signatures diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 489b9fce80e0..b93327b8ecbd 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -214,6 +214,13 @@ contract Token { } // docs:end:mint_private + #[aztec(private)] + fn mint_private_for(to: AztecAddress, amount: Field) { + let minter = context.msg_sender(); + storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, minter, to)); + Token::at(context.this_address()).assert_minter_and_mint(minter, amount).enqueue(&mut context); + } + // TODO: Nuke this - test functions do not belong to token contract! #[aztec(private)] fn privately_mint_private_note(amount: Field) { diff --git a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts index bd9d7d1d957c..69fe981526c8 100644 --- a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts +++ b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts @@ -1,9 +1,9 @@ -import { type AztecAddress, type EthAddress } from '@aztec/circuits.js'; +import { type AztecAddress, type EthAddress, type Fr } from '@aztec/circuits.js'; import { createEthereumChain, createL1Clients } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { prettyPrintJSON } from '../../utils/commands.js'; -import { ERC20PortalManager } from '../../utils/portal_manager.js'; +import { L1PortalManager } from '../../utils/portal_manager.js'; export async function bridgeERC20( amount: bigint, @@ -14,6 +14,7 @@ export async function bridgeERC20( mnemonic: string, tokenAddress: EthAddress, portalAddress: EthAddress, + privateTransfer: boolean, mint: boolean, json: boolean, log: LogFn, @@ -24,14 +25,20 @@ export async function bridgeERC20( const { publicClient, walletClient } = createL1Clients(chain.rpcUrl, privateKey ?? mnemonic, chain.chainInfo); // Setup portal manager - const portal = await ERC20PortalManager.create(tokenAddress, portalAddress, publicClient, walletClient, debugLogger); - const { secret } = await portal.prepareTokensOnL1(amount, amount, recipient, mint); + // const portal = await ERC20PortalManager.create(tokenAddress, portalAddress, publicClient, walletClient, debugLogger); + const manager = new L1PortalManager(tokenAddress, portalAddress, publicClient, walletClient, debugLogger); + let claimSecret: Fr; + if (privateTransfer) { + ({ claimSecret } = await manager.bridgeTokensPrivate(recipient, amount, mint)); + } else { + ({ claimSecret } = await manager.bridgeTokensPublic(recipient, amount, mint)); + } if (json) { log( prettyPrintJSON({ claimAmount: amount, - claimSecret: secret, + claimSecret: claimSecret, }), ); } else { @@ -40,7 +47,7 @@ export async function bridgeERC20( } else { log(`Bridged ${amount} tokens to L2 portal`); } - log(`claimAmount=${amount},claimSecret=${secret}\n`); + log(`claimAmount=${amount},claimSecret=${claimSecret}\n`); log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); } } diff --git a/yarn-project/cli/src/utils/portal_manager.ts b/yarn-project/cli/src/utils/portal_manager.ts index 19a0829b2f0c..f223623c7c20 100644 --- a/yarn-project/cli/src/utils/portal_manager.ts +++ b/yarn-project/cli/src/utils/portal_manager.ts @@ -14,106 +14,107 @@ import { getContract, } from 'viem'; -/** - * A Class for testing cross chain interactions, contains common interactions - * shared between cross chain tests. - */ -abstract class PortalManager { - protected constructor( - /** Underlying token for portal tests. */ - public underlyingERC20Address: EthAddress, - /** Portal address. */ - public tokenPortalAddress: EthAddress, - public publicClient: PublicClient, - public walletClient: WalletClient, - /** Logger. */ - public logger: DebugLogger, - ) {} - - generateClaimSecret(): [Fr, Fr] { - this.logger.debug("Generating a claim secret using pedersen's hash function"); - const secret = Fr.random(); - const secretHash = computeSecretHash(secret); - this.logger.info('Generated claim secret: ' + secretHash.toString()); - return [secret, secretHash]; - } +export enum TransferType { + PRIVATE, + PUBLIC, +} + +export interface L2Claim { + claimSecret: Fr; + claimAmount: Fr; +} + +function stringifyEthAddress(address: EthAddress | Hex, name?: string) { + return name ? `${name} (${address.toString()})` : address.toString(); +} - getERC20Contract(): GetContractReturnType> { - return getContract({ - address: this.underlyingERC20Address.toString(), +function generateClaimSecret(): [Fr, Fr] { + const secret = Fr.random(); + const secretHash = computeSecretHash(secret); + return [secret, secretHash]; +} + +class L1TokenManager { + private contract: GetContractReturnType>; + + public constructor( + public readonly address: EthAddress, + private publicClient: PublicClient, + private walletClient: WalletClient, + private logger: DebugLogger, + ) { + this.contract = getContract({ + address: this.address.toString(), abi: PortalERC20Abi, client: this.walletClient, }); } - async mintTokensOnL1(amount: bigint) { - this.logger.info( - `Minting tokens on L1 for ${this.walletClient.account.address} in contract ${this.underlyingERC20Address}`, - ); - await this.publicClient.waitForTransactionReceipt({ - hash: await this.getERC20Contract().write.mint([this.walletClient.account.address, amount]), - }); - } - - async getL1TokenBalance(address: EthAddress) { - return await this.getERC20Contract().read.balanceOf([address.toString()]); + public async getL1TokenBalance(address: Hex) { + return await this.contract.read.balanceOf([address]); } - protected async sendTokensToPortalPublic(bridgeAmount: bigint, l2Address: AztecAddress, secretHash: Fr) { - this.logger.info(`Approving erc20 tokens for the TokenPortal at ${this.tokenPortalAddress.toString()}`); + public async mint(amount: bigint, address: Hex, addressName = '') { + this.logger.info(`Minting ${amount} tokens for ${stringifyEthAddress(address, addressName)}`); await this.publicClient.waitForTransactionReceipt({ - hash: await this.getERC20Contract().write.approve([this.tokenPortalAddress.toString(), bridgeAmount]), + hash: await this.contract.write.mint([address, amount]), }); - - const messageHash = await this.bridgeTokens(l2Address, bridgeAmount, secretHash); - return Fr.fromString(messageHash); } - protected abstract bridgeTokens(to: AztecAddress, amount: bigint, secretHash: Fr): Promise; - - async prepareTokensOnL1(l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress, mint = true) { - const [secret, secretHash] = this.generateClaimSecret(); - - // Mint tokens on L1 - if (mint) { - await this.mintTokensOnL1(l1TokenBalance); - } - - // Deposit tokens to the TokenPortal - const msgHash = await this.sendTokensToPortalPublic(bridgeAmount, owner, secretHash); - - return { secret, msgHash, secretHash }; + public async approve(amount: bigint, address: Hex, addressName = '') { + this.logger.info(`Minting ${amount} tokens for ${stringifyEthAddress(address, addressName)}`); + await this.publicClient.waitForTransactionReceipt({ + hash: await this.contract.write.approve([address, amount]), + }); } } -export class FeeJuicePortalManager extends PortalManager { - async bridgeTokens(to: AztecAddress, amount: bigint, secretHash: Fr): Promise { - const portal = getContract({ - address: this.tokenPortalAddress.toString(), +export class FeeJuicePortalManager { + tokenManager: L1TokenManager; + contract: GetContractReturnType>; + + constructor( + portalAddress: EthAddress, + tokenAddress: EthAddress, + private publicClient: PublicClient, + private walletClient: WalletClient, + /** Logger. */ + private logger: DebugLogger, + ) { + this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger); + this.contract = getContract({ + address: portalAddress.toString(), abi: FeeJuicePortalAbi, client: this.walletClient, }); + } - this.logger.info( - `Simulating token portal deposit configured for token ${await portal.read.l2TokenAddress()} with registry ${await portal.read.registry()} to retrieve message hash`, - ); + public async bridgeTokensPublic(to: AztecAddress, amount: bigint, mint = false): Promise { + const [claimSecret, claimSecretHash] = generateClaimSecret(); + if (mint) { + await this.tokenManager.mint(amount, this.walletClient.account.address); + } - const args = [to.toString(), amount, secretHash.toString()] as const; - const { result: messageHash } = await portal.simulate.depositToAztecPublic(args); - this.logger.info('Sending messages to L1 portal to be consumed publicly'); + await this.tokenManager.approve(amount, this.contract.address, 'FeeJuice Portal'); + this.logger.info('Sending L1 Fee Juice to L2 to be claimed publicly'); + const args = [to.toString(), amount, claimSecretHash.toString()] as const; await this.publicClient.waitForTransactionReceipt({ - hash: await portal.write.depositToAztecPublic(args), + hash: await this.contract.write.depositToAztecPublic(args), }); - return messageHash; + + return { + claimAmount: new Fr(amount), + claimSecret, + }; } - public static async create( + public static async new( pxe: PXE, publicClient: PublicClient, walletClient: WalletClient, logger: DebugLogger, - ): Promise { + ): Promise { const { l1ContractAddresses: { feeJuiceAddress, feeJuicePortalAddress }, } = await pxe.getNodeInfo(); @@ -122,39 +123,66 @@ export class FeeJuicePortalManager extends PortalManager { throw new Error('Portal or token not deployed on L1'); } - return new FeeJuicePortalManager(feeJuiceAddress, feeJuicePortalAddress, publicClient, walletClient, logger); + return new FeeJuicePortalManager(feeJuicePortalAddress, feeJuicePortalAddress, publicClient, walletClient, logger); } } -export class ERC20PortalManager extends PortalManager { - async bridgeTokens(to: AztecAddress, amount: bigint, secretHash: Fr): Promise { - const portal = getContract({ - address: this.tokenPortalAddress.toString(), +export class L1PortalManager { + contract: GetContractReturnType>; + private tokenManager: L1TokenManager; + + constructor( + portalAddress: EthAddress, + tokenAddress: EthAddress, + private publicClient: PublicClient, + private walletClient: WalletClient, + private logger: DebugLogger, + ) { + this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger); + this.contract = getContract({ + address: portalAddress.toString(), abi: TokenPortalAbi, client: this.walletClient, }); + } - this.logger.info( - `Simulating token portal deposit configured for token ${await portal.read.l2Bridge()} with registry ${await portal.read.registry()} to retrieve message hash`, - ); - - const args = [to.toString(), amount, secretHash.toString()] as const; - const { result: messageHash } = await portal.simulate.depositToAztecPublic(args); - this.logger.info('Sending messages to L1 portal to be consumed publicly'); + public bridgeTokensPublic(to: AztecAddress, amount: bigint, mint = false): Promise { + return this.bridgeTokens(to, amount, mint, /* privateTransfer */ false); + } - await this.publicClient.waitForTransactionReceipt({ - hash: await portal.write.depositToAztecPublic(args), - }); - return messageHash; + public bridgeTokensPrivate(to: AztecAddress, amount: bigint, mint = false): Promise { + return this.bridgeTokens(to, amount, mint, /* privateTransfer */ true); } - public static create( - tokenAddress: EthAddress, - portalAddress: EthAddress, - publicClient: PublicClient, - walletClient: WalletClient, - logger: DebugLogger, - ): Promise { - return Promise.resolve(new ERC20PortalManager(tokenAddress, portalAddress, publicClient, walletClient, logger)); + private async bridgeTokens( + to: AztecAddress, + amount: bigint, + mint: boolean, + privateTransfer: boolean, + ): Promise { + const [claimSecret, claimSecretHash] = generateClaimSecret(); + + if (mint) { + await this.tokenManager.mint(amount, this.walletClient.account.address); + } + + await this.tokenManager.approve(amount, this.contract.address, 'TokenPortal'); + + if (privateTransfer) { + this.logger.info('Sending L1 tokens to L2 to be claimed privately'); + await this.publicClient.waitForTransactionReceipt({ + hash: await this.contract.write.depositToAztecPrivate([Fr.ZERO.toString(), amount, claimSecretHash.toString()]), + }); + } else { + this.logger.info('Sending L1 tokens to L2 to be claimed publicly'); + await this.publicClient.waitForTransactionReceipt({ + hash: await this.contract.write.depositToAztecPublic([to.toString(), amount, claimSecretHash.toString()]), + }); + } + + return { + claimAmount: new Fr(amount), + claimSecret, + }; } }