diff --git a/yarn-project/end-to-end/src/cross_chain/test_harness.ts b/yarn-project/end-to-end/src/cross_chain/test_harness.ts index a9e56bcb95a5..20c97fe4dbb2 100644 --- a/yarn-project/end-to-end/src/cross_chain/test_harness.ts +++ b/yarn-project/end-to-end/src/cross_chain/test_harness.ts @@ -4,7 +4,7 @@ import { AztecAddress, EthAddress, Fr, Point } from '@aztec/circuits.js'; import { DeployL1Contracts } from '@aztec/ethereum'; import { DebugLogger } from '@aztec/foundation/log'; import { PublicClient, HttpTransport, Chain, getContract } from 'viem'; -import { deployAndInitializeNonNativeL2TokenContracts, pointToPublicKey } from '../utils.js'; +import { deployAndInitializeNonNativeL2TokenContracts, expectStorageSlot, pointToPublicKey } from '../utils.js'; import { OutboxAbi } from '@aztec/l1-artifacts'; import { sha256ToField } from '@aztec/foundation/crypto'; import { toBufferBE } from '@aztec/foundation/bigint-buffer'; @@ -120,6 +120,10 @@ export class CrossChainTestHarness { expect(await this.underlyingERC20.read.balanceOf([this.ethAccount.toString()])).toBe(amount); } + async getL1BalanceOf(address: EthAddress) { + return await this.underlyingERC20.read.balanceOf([address.toString()]); + } + async sendTokensToPortal(bridgeAmount: bigint, secretHash: Fr) { await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); @@ -158,6 +162,53 @@ export class CrossChainTestHarness { expect(transferReceipt.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 noir contract + const consumptionTx = this.l2Contract.methods + .mint(bridgeAmount, this.ownerPub, this.ownerAddress, messageKey, secret, this.ethAccount.toField()) + .send({ from: this.ownerAddress }); + + await consumptionTx.isMined(0, 0.1); + const consumptionReceipt = await consumptionTx.getReceipt(); + 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 noir contract + const consumptionTx = this.l2Contract.methods + .mintPublic(bridgeAmount, this.ownerAddress, messageKey, secret, this.ethAccount.toField()) + .send({ from: this.ownerAddress }); + + await consumptionTx.isMined(0, 0.1); + const consumptionReceipt = await consumptionTx.getReceipt(); + expect(consumptionReceipt.status).toBe(TxStatus.MINED); + } + + async getL2BalanceOf(owner: AztecAddress) { + const ownerPublicKey = await this.aztecRpcServer.getAccountPublicKey(owner); + const [balance] = await this.l2Contract.methods.getBalance(pointToPublicKey(ownerPublicKey)).view({ from: owner }); + return balance; + } + + async expectBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { + const balance = await this.getL2BalanceOf(owner); + this.logger(`Account ${owner} balance: ${balance}`); + expect(balance).toBe(expectedBalance); + } + + async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint, publicBalanceSlot: bigint) { + await expectStorageSlot( + this.logger, + this.aztecNode, + this.l2Contract, + publicBalanceSlot, + owner.toField(), + expectedBalance, + ); + } + async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise { this.logger('Ensure that the entry is not in outbox yet'); const contractInfo = await this.aztecNode.getContractInfo(this.l2Contract.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 31fa7bdc30ef..23d13a2d340d 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 @@ -2,7 +2,7 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress, AztecRPCServer, Contract, TxStatus } from '@aztec/aztec.js'; import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr, Point } from '@aztec/foundation/fields'; +import { Point } from '@aztec/foundation/fields'; import { DebugLogger } from '@aztec/foundation/log'; import { delay, pointToPublicKey, setup } from './utils.js'; import { CrossChainTestHarness } from './cross_chain/test_harness.js'; @@ -65,19 +65,6 @@ describe('e2e_cross_chain_messaging', () => { expect(balance).toBe(expectedBalance); }; - const consumeMessageOnAztecAndMint = async (bridgeAmount: bigint, messageKey: Fr, secret: Fr) => { - logger('Consuming messages on L2 secretively'); - // Call the mint tokens function on the noir contract - const consumptionTx = l2Contract.methods - .mint(bridgeAmount, ownerPub, ownerAddress, messageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - - await consumptionTx.isMined(0, 0.1); - const consumptionReceipt = await consumptionTx.getReceipt(); - - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - }; - const withdrawFundsFromAztec = async (withdrawAmount: bigint) => { logger('Send L2 tx to withdraw funds'); const withdrawTx = l2Contract.methods @@ -108,7 +95,7 @@ describe('e2e_cross_chain_messaging', () => { const transferAmount = 1n; await crossChainTestHarness.performL2Transfer(transferAmount); - await consumeMessageOnAztecAndMint(bridgeAmount, messageKey, secret); + await crossChainTestHarness.consumeMessageOnAztecAndMintSecretly(bridgeAmount, messageKey, secret); await expectBalance(ownerAddress, bridgeAmount + initialBalance - transferAmount); // time to withdraw the funds again! 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 9ebb9f97c5e2..f5ad44d3eec6 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 @@ -2,9 +2,8 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress, AztecRPCServer, Contract, TxStatus } from '@aztec/aztec.js'; import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr } from '@aztec/foundation/fields'; import { DebugLogger } from '@aztec/foundation/log'; -import { delay, expectStorageSlot, setup } from './utils.js'; +import { delay, setup } from './utils.js'; import { CrossChainTestHarness } from './cross_chain/test_harness.js'; describe('e2e_public_cross_chain_messaging', () => { @@ -58,19 +57,6 @@ describe('e2e_public_cross_chain_messaging', () => { await crossChainTestHarness?.stop(); }); - const consumeMessageOnAztec = async (bridgeAmount: bigint, messageKey: Fr, secret: Fr) => { - logger('Consuming messages on L2 Publicly'); - // Call the mint tokens function on the noir contract - const consumptionTx = l2Contract.methods - .mintPublic(bridgeAmount, ownerAddress, messageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - - await consumptionTx.isMined(0, 0.1); - const consumptionReceipt = await consumptionTx.getReceipt(); - - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - }; - const withdrawFundsFromAztec = async (withdrawAmount: bigint) => { logger('Send L2 tx to withdraw funds'); const withdrawTx = l2Contract.methods @@ -102,22 +88,15 @@ describe('e2e_public_cross_chain_messaging', () => { const transferAmount = 1n; await crossChainTestHarness.performL2Transfer(transferAmount); - await consumeMessageOnAztec(bridgeAmount, messageKey, secret); - await expectStorageSlot(logger, aztecNode, l2Contract, publicBalanceSlot, ownerAddress.toField(), bridgeAmount); + await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, messageKey, secret); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount, publicBalanceSlot); // time to withdraw the funds again! logger('Withdrawing funds from L2'); const withdrawAmount = 9n; const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); await withdrawFundsFromAztec(withdrawAmount); - await expectStorageSlot( - logger, - aztecNode, - l2Contract, - publicBalanceSlot, - ownerAddress.toField(), - bridgeAmount - withdrawAmount, - ); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount, publicBalanceSlot); // Check balance before and after exit. expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount); 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 c70fbbfb19d0..dcca1928fe86 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 @@ -1,19 +1,12 @@ import { AztecNodeService } from '@aztec/aztec-node'; -import { - AztecAddress, - AztecRPCServer, - Contract, - ContractDeployer, - Fr, - TxStatus, - computeMessageSecretHash, -} from '@aztec/aztec.js'; +import { AztecAddress, AztecRPCServer, Contract, ContractDeployer, Fr, TxStatus } from '@aztec/aztec.js'; import { deployL1Contract } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; import { delay, deployAndInitializeNonNativeL2TokenContracts, pointToPublicKey, setup } from './utils.js'; +import { CrossChainTestHarness } from './cross_chain/test_harness.js'; import { DebugLogger } from '@aztec/foundation/log'; -import { Chain, HttpTransport, PublicClient, getContract, parseEther } from 'viem'; +import { getContract, parseEther } from 'viem'; import { DeployL1Contracts } from '@aztec/ethereum'; import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; import { UniswapContractAbi } from '@aztec/noir-contracts/examples'; @@ -36,9 +29,6 @@ describe('uniswap_trade_on_l1_from_l2', () => { let accounts: AztecAddress[]; let logger: DebugLogger; - let publicClient: PublicClient; - let walletClient: any; - let ethAccount: EthAddress; let ownerAddress: AztecAddress; let receiver: AztecAddress; @@ -46,25 +36,19 @@ describe('uniswap_trade_on_l1_from_l2', () => { const initialBalance = 10n; const wethAmountToBridge = parseEther('1'); + let daiCrossChainHarness: CrossChainTestHarness; + let wethCrossChainHarness: CrossChainTestHarness; + let uniswapPortal: any; let uniswapPortalAddress: EthAddress; let uniswapL2Contract: Contract; - let wethContract: any; - let wethTokenPortalAddress: EthAddress; - let wethTokenPortal: any; - let wethL2Contract: Contract; - - let daiContract: any; - let daiTokenPortalAddress: EthAddress; - let daiL2Contract: Contract; - beforeEach(async () => { let deployL1ContractsValues: DeployL1Contracts; ({ aztecNode, aztecRpcServer, deployL1ContractsValues, accounts, logger } = await setup(2)); - walletClient = deployL1ContractsValues.walletClient; - publicClient = deployL1ContractsValues.publicClient; + const walletClient = deployL1ContractsValues.walletClient; + const publicClient = deployL1ContractsValues.publicClient; if (Number(await publicClient.getBlockNumber()) < EXPECTED_FORKED_BLOCK) { throw new Error('This test must be run on a fork of mainnet with the expected fork block'); @@ -72,7 +56,8 @@ describe('uniswap_trade_on_l1_from_l2', () => { ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); [ownerAddress, receiver] = accounts; - ownerPub = pointToPublicKey(await aztecRpcServer.getAccountPublicKey(ownerAddress)); + const ownerPubPoint = await aztecRpcServer.getAccountPublicKey(ownerAddress); + ownerPub = pointToPublicKey(ownerPubPoint); logger('Deploying DAI Portal, initializing and deploying l2 contract...'); const daiContracts = await deployAndInitializeNonNativeL2TokenContracts( @@ -84,9 +69,23 @@ describe('uniswap_trade_on_l1_from_l2', () => { ownerPub, DAI_ADDRESS, ); - daiL2Contract = daiContracts.l2Contract; - daiContract = daiContracts.underlyingERC20; - daiTokenPortalAddress = daiContracts.tokenPortalAddress; + daiCrossChainHarness = new CrossChainTestHarness( + aztecNode, + aztecRpcServer, + accounts, + logger, + daiContracts.l2Contract, + ethAccount, + daiContracts.tokenPortalAddress, + daiContracts.tokenPortal, + daiContracts.underlyingERC20, + null, + publicClient, + walletClient, + ownerAddress, + receiver, + ownerPubPoint, + ); logger('Deploying WETH Portal, initializing and deploying l2 contract...'); const wethContracts = await deployAndInitializeNonNativeL2TokenContracts( @@ -98,10 +97,23 @@ describe('uniswap_trade_on_l1_from_l2', () => { ownerPub, WETH9_ADDRESS, ); - wethL2Contract = wethContracts.l2Contract; - wethContract = wethContracts.underlyingERC20; - wethTokenPortal = wethContracts.tokenPortal; - wethTokenPortalAddress = wethContracts.tokenPortalAddress; + wethCrossChainHarness = new CrossChainTestHarness( + aztecNode, + aztecRpcServer, + accounts, + logger, + wethContracts.l2Contract, + ethAccount, + wethContracts.tokenPortalAddress, + wethContracts.tokenPortal, + wethContracts.underlyingERC20, + null, + publicClient, + walletClient, + ownerAddress, + receiver, + ownerPubPoint, + ); logger('Deploy Uniswap portal on L1 and L2...'); uniswapPortalAddress = await deployL1Contract(walletClient, publicClient, UniswapPortalAbi, UniswapPortalBytecode); @@ -130,91 +142,50 @@ describe('uniswap_trade_on_l1_from_l2', () => { }, 100_000); afterEach(async () => { - await aztecNode?.stop(); - await aztecRpcServer?.stop(); + await aztecNode.stop(); + await aztecRpcServer.stop(); + await wethCrossChainHarness.stop(); + await daiCrossChainHarness.stop(); }); - const getL2BalanceOf = async (owner: AztecAddress, l2Contract: any) => { - const ownerPublicKey = await aztecRpcServer.getAccountPublicKey(owner); - const [balance] = await l2Contract.methods.getBalance(pointToPublicKey(ownerPublicKey)).view({ from: owner }); - return balance; - }; - - const expectBalanceOnL2 = async (owner: AztecAddress, expectedBalance: bigint, l2Contract: any) => { - const balance = await getL2BalanceOf(owner, l2Contract); - logger(`Account ${owner} balance: ${balance}`); - expect(balance).toBe(expectedBalance); - }; - - const transferWethOnL2 = async (transferAmount: bigint) => { - const transferTx = wethL2Contract.methods - .transfer( - transferAmount, - pointToPublicKey(await aztecRpcServer.getAccountPublicKey(ownerAddress)), - pointToPublicKey(await aztecRpcServer.getAccountPublicKey(receiver)), - ) - .send({ from: accounts[0] }); - await transferTx.isMined(0, 0.1); - const transferReceipt = await transferTx.getReceipt(); - expect(transferReceipt.status).toBe(TxStatus.MINED); - }; - it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { - const meBeforeBalance = await wethContract.read.balanceOf([ethAccount.toString()]); - // 1. Approve weth to be bridged - await wethContract.write.approve([wethTokenPortalAddress.toString(), wethAmountToBridge], {} as any); + const meBeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ethAccount); - // 2. Deposit weth into the portal and move to L2 - // generate secret - const secret = Fr.random(); - const secretHash = await computeMessageSecretHash(secret); - const secretString = `0x${secretHash.toBuffer().toString('hex')}` as `0x${string}`; - const deadline = 2 ** 32 - 1; // max uint32 - 1 - logger('Sending messages to L1 portal'); - const args = [ownerAddress.toString(), wethAmountToBridge, deadline, secretString, ethAccount.toString()] as const; - const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztec(args, { - account: ethAccount.toString(), - } as any); - await wethTokenPortal.write.depositToAztec(args, {} as any); - expect(await wethContract.read.balanceOf([ethAccount.toString()])).toBe(meBeforeBalance - wethAmountToBridge); - const messageKey = Fr.fromString(messageKeyHex); + // 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); + expect(await wethCrossChainHarness.getL1BalanceOf(ethAccount)).toBe(meBeforeBalance - wethAmountToBridge); // Wait for the archiver to process the message await delay(5000); + // send a transfer tx to force through rollup with the message included const transferAmount = 1n; - await transferWethOnL2(transferAmount); + await wethCrossChainHarness.performL2Transfer(transferAmount); // 3. Claim WETH on L2 logger('Minting weth on L2'); - // Call the mint tokens function on the noir contract - const consumptionTx = wethL2Contract.methods - .mint(wethAmountToBridge, ownerPub, ownerAddress, messageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - await consumptionTx.isMined(0, 0.1); - const consumptionReceipt = await consumptionTx.getReceipt(); - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - await expectBalanceOnL2(ownerAddress, wethAmountToBridge + initialBalance - transferAmount, wethL2Contract); + await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly(wethAmountToBridge, messageKey, secret); + await wethCrossChainHarness.expectBalanceOnL2(ownerAddress, wethAmountToBridge + initialBalance - transferAmount); // Store balances - const wethBalanceBeforeSwap = await getL2BalanceOf(ownerAddress, wethL2Contract); - const daiBalanceBeforeSwap = await getL2BalanceOf(ownerAddress, daiL2Contract); + const wethBalanceBeforeSwap = await wethCrossChainHarness.getL2BalanceOf(ownerAddress); + const daiBalanceBeforeSwap = await daiCrossChainHarness.getL2BalanceOf(ownerAddress); // 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'); - // recipient is the uniswap portal - const selector = Fr.fromBuffer(wethL2Contract.methods.withdraw.selector); + const selector = Fr.fromBuffer(wethCrossChainHarness.l2Contract.methods.withdraw.selector); const minimumOutputAmount = 0; const withdrawTx = uniswapL2Contract.methods .swap( selector, - wethL2Contract.address.toField(), - wethTokenPortalAddress.toField(), + wethCrossChainHarness.l2Contract.address.toField(), + wethCrossChainHarness.tokenPortalAddress.toField(), wethAmountToBridge, new Fr(3000), - daiL2Contract.address.toField(), - daiTokenPortalAddress.toField(), + daiCrossChainHarness.l2Contract.address.toField(), + daiCrossChainHarness.tokenPortalAddress.toField(), new Fr(minimumOutputAmount), ownerPub, ownerAddress, @@ -230,19 +201,20 @@ describe('uniswap_trade_on_l1_from_l2', () => { expect(withdrawReceipt.status).toBe(TxStatus.MINED); // check weth balance of owner on L2 (we first briedged `wethAmountToBridge` into L2 and now withdrew it!) - await expectBalanceOnL2(ownerAddress, initialBalance - transferAmount, wethL2Contract); + await wethCrossChainHarness.expectBalanceOnL2(ownerAddress, initialBalance - transferAmount); // 5. Consume L2 to L1 message by calling uniswapPortal.swap() logger('Execute withdraw and swap on the uniswapPortal!'); - const daiBalanceOfPortalBefore = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); + const daiBalanceOfPortalBefore = await daiCrossChainHarness.getL1BalanceOf(daiCrossChainHarness.tokenPortalAddress); + const deadline = 2 ** 32 - 1; // max uint32 - 1 const swapArgs = [ - wethTokenPortalAddress.toString(), + wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, 3000, - daiTokenPortalAddress.toString(), + daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, ownerAddress.toString(), - secretString, + secretHash.toString(true), deadline, ethAccount.toString(), true, @@ -254,28 +226,22 @@ describe('uniswap_trade_on_l1_from_l2', () => { await uniswapPortal.write.swap(swapArgs, {} as any); const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); // weth was swapped to dai and send to portal - const daiBalanceOfPortalAfter = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); + const daiBalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf(daiCrossChainHarness.tokenPortalAddress); expect(daiBalanceOfPortalAfter).toBeGreaterThan(daiBalanceOfPortalBefore); - const daiAmountToBridge = daiBalanceOfPortalAfter - daiBalanceOfPortalBefore; + const daiAmountToBridge = BigInt(daiBalanceOfPortalAfter - daiBalanceOfPortalBefore); // Wait for the archiver to process the message await delay(5000); // send a transfer tx to force through rollup with the message included - await transferWethOnL2(transferAmount); + await wethCrossChainHarness.performL2Transfer(transferAmount); // 6. claim dai on L2 logger('Consuming messages to mint dai on L2'); - // Call the mint tokens function on the noir contract - const daiMintTx = daiL2Contract.methods - .mint(daiAmountToBridge, ownerPub, ownerAddress, depositDaiMessageKey, secret, ethAccount.toField()) - .send({ from: ownerAddress }); - await daiMintTx.isMined(0, 0.1); - const daiMintTxReceipt = await daiMintTx.getReceipt(); - expect(daiMintTxReceipt.status).toBe(TxStatus.MINED); - await expectBalanceOnL2(ownerAddress, initialBalance + BigInt(daiAmountToBridge), daiL2Contract); + await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly(daiAmountToBridge, depositDaiMessageKey, secret); + await daiCrossChainHarness.expectBalanceOnL2(ownerAddress, initialBalance + daiAmountToBridge); - const wethBalanceAfterSwap = await getL2BalanceOf(ownerAddress, wethL2Contract); - const daiBalanceAfterSwap = await getL2BalanceOf(ownerAddress, daiL2Contract); + const wethBalanceAfterSwap = await wethCrossChainHarness.getL2BalanceOf(ownerAddress); + const daiBalanceAfterSwap = await daiCrossChainHarness.getL2BalanceOf(ownerAddress); logger('WETH balance before swap: ', wethBalanceBeforeSwap.toString()); logger('DAI balance before swap : ', daiBalanceBeforeSwap.toString());