From 4af81f3604f33b129c466fffb770e106a522635a Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Wed, 11 Oct 2023 12:15:14 +0000 Subject: [PATCH] e2e and canary use same suite for uniswap --- .../src/uniswap_trade_on_l1_from_l2.test.ts | 423 +--------- yarn-project/canary/src/utils.ts | 463 ----------- .../end-to-end/src/canary/uniswap_l1_l2.ts | 744 ++++++++++++++++++ yarn-project/end-to-end/src/index.ts | 1 + .../src/uniswap_trade_on_l1_from_l2.test.ts | 739 +---------------- 5 files changed, 791 insertions(+), 1579 deletions(-) delete mode 100644 yarn-project/canary/src/utils.ts create mode 100644 yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts 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 8adab2db4f70..4b5d5256af66 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 @@ -1,413 +1,36 @@ -import { - AccountWallet, - AztecAddress, - DebugLogger, - EthAddress, - Fr, - PXE, - TxStatus, - computeAuthWitMessageHash, - createDebugLogger, - createPXEClient, - sleep as delay, - getSandboxAccountsWallets, - waitForSandbox, -} from '@aztec/aztec.js'; -import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; -import { UniswapContract } from '@aztec/noir-contracts/types'; +import { createDebugLogger, createPXEClient, getSandboxAccountsWallets, waitForSandbox } from '@aztec/aztec.js'; +import { UniswapSetupContext, uniswapL1L2TestSuite } from '@aztec/end-to-end'; -import { jest } from '@jest/globals'; -import { createPublicClient, createWalletClient, getContract, http, parseEther } from 'viem'; +import { createPublicClient, createWalletClient, http } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; -import { CrossChainTestHarness, deployL1Contract } from './utils.js'; - const { PXE_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; - export const MNEMONIC = 'test test test test test test test test test test test junk'; const hdAccount = mnemonicToAccount(MNEMONIC); - +// This tests works on forked mainnet, configured on the CI. const EXPECTED_FORKED_BLOCK = 17514288; - -const TIMEOUT = 90_000; -describe('uniswap_trade_on_l1_from_l2', () => { - jest.setTimeout(TIMEOUT); - const WETH9_ADDRESS: EthAddress = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); - const DAI_ADDRESS: EthAddress = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); - - let pxe: PXE; - let logger: DebugLogger; - let walletClient: any; - - let ownerWallet: AccountWallet; - let ownerAddress: AztecAddress; - let ownerEthAddress: EthAddress; - // does transactions on behalf of owner on Aztec: - let sponsorWallet: AccountWallet; - let sponsorAddress: AztecAddress; - - let daiCrossChainHarness: CrossChainTestHarness; - let wethCrossChainHarness: CrossChainTestHarness; - - let uniswapPortal: any; - let uniswapPortalAddress: EthAddress; - let uniswapL2Contract: UniswapContract; - - const wethAmountToBridge = parseEther('1'); - const uniswapFeeTier = 3000n; - const minimumOutputAmount = 0n; - const deadlineForDepositingSwappedDai = BigInt(2 ** 32 - 1); // max uint32 - 1 - - beforeAll(async () => { - logger = createDebugLogger('aztec:canary'); - pxe = createPXEClient(PXE_URL); - await waitForSandbox(pxe); - - walletClient = createWalletClient({ - account: hdAccount, - chain: foundry, - transport: http(ETHEREUM_HOST), - }); - const publicClient = createPublicClient({ - chain: foundry, - transport: http(ETHEREUM_HOST), - }); - - 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'); - } - - [ownerWallet, sponsorWallet] = await getSandboxAccountsWallets(pxe); - ownerAddress = ownerWallet.getAddress(); - sponsorAddress = sponsorWallet.getAddress(); - ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); - - logger('Deploying DAI Portal, initializing and deploying l2 contract...'); - daiCrossChainHarness = await CrossChainTestHarness.new( - pxe, - publicClient, - walletClient, - ownerWallet, - logger, - DAI_ADDRESS, - ); - - logger('Deploying WETH Portal, initializing and deploying l2 contract...'); - wethCrossChainHarness = await CrossChainTestHarness.new( - pxe, - publicClient, - walletClient, - ownerWallet, - logger, - WETH9_ADDRESS, - ); - - logger('Deploy Uniswap portal on L1 and L2...'); - uniswapPortalAddress = await deployL1Contract(walletClient, publicClient, UniswapPortalAbi, UniswapPortalBytecode); - uniswapPortal = getContract({ - address: uniswapPortalAddress.toString(), - abi: UniswapPortalAbi, - walletClient, - publicClient, - }); - // deploy l2 uniswap contract and attach to portal - uniswapL2Contract = await UniswapContract.deploy(ownerWallet) - .send({ portalContract: uniswapPortalAddress }) - .deployed(); - - const registryAddress = (await pxe.getNodeInfo()).l1ContractAddresses.registryAddress; - await uniswapPortal.write.initialize([registryAddress.toString(), uniswapL2Contract.address.toString()], {} as any); +// We tell the archiver to only sync from this block. +process.env.SEARCH_START_BLOCK = EXPECTED_FORKED_BLOCK.toString(); + +const setupRPC = async (): Promise => { + const logger = createDebugLogger('aztec:canary_uniswap'); + const pxe = createPXEClient(PXE_URL); + await waitForSandbox(pxe); + + const walletClient = createWalletClient({ + account: hdAccount, + chain: foundry, + transport: http(ETHEREUM_HOST), }); - - beforeEach(async () => { - // Give me some WETH so I can deposit to L2 and do the swap... - logger('Getting some weth'); - await walletClient.sendTransaction({ to: WETH9_ADDRESS.toString(), value: parseEther('1') }); + const publicClient = createPublicClient({ + chain: foundry, + transport: http(ETHEREUM_HOST), }); - it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { - const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); - - // 1. Approve and deposit weth to the portal and move to L2 - const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret(); - const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); - - const messageKey = await wethCrossChainHarness.sendTokensToPortalPrivate( - wethAmountToBridge, - secretHashForMintingWeth, - secretHashForRedeemingWeth, - ); - // funds transferred from owner to token portal - expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe(wethL1BeforeBalance - wethAmountToBridge); - expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe( - wethAmountToBridge, - ); - - // Wait for the archiver to process the message - await delay(5000); - - // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 2. Claim WETH on L2 - logger('Minting weth on L2'); - await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly( - wethAmountToBridge, - secretHashForRedeemingWeth, - messageKey, - secretForMintingWeth, - ); - await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); - await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge); - - // Store balances - const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - - // before swap - check nonce_for_burn_approval stored on uniswap - // (which is used by uniswap to approve the bridge to burn funds on its behalf to exit to L1) - const nonceForBurnApprovalBeforeSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - - // 3. Owner gives uniswap approval to unshield funds to self on its behalf - logger('Approving uniswap to unshield funds to self on my behalf'); - const nonceForWETHUnshieldApproval = new Fr(1n); - const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) - .request(), - ); - await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); - - // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. - logger('Withdrawing weth to L1 and sending message to swap to dai'); - const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = - await daiCrossChainHarness.generateClaimSecret(); - const [secretForRedeemingDai, secretHashForRedeemingDai] = await daiCrossChainHarness.generateClaimSecret(); - - const withdrawReceipt = await uniswapL2Contract.methods - .swap_private( - wethCrossChainHarness.l2Token.address, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHUnshieldApproval, - uniswapFeeTier, - minimumOutputAmount, - secretHashForRedeemingDai, - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - ) - .send() - .wait(); - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - // ensure that user's funds were burnt - await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); - // ensure that uniswap contract didn't eat the funds. - await wethCrossChainHarness.expectPublicBalanceOnL2(uniswapL2Contract.address, 0n); - // check burn approval nonce incremented: - const nonceForBurnApprovalAfterSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - expect(nonceForBurnApprovalAfterSwap).toBe(nonceForBurnApprovalBeforeSwap + 1n); + const [ownerWallet, sponsorWallet] = await getSandboxAccountsWallets(pxe); - // 5. Consume L2 to L1 message by calling uniswapPortal.swap_private() - logger('Execute withdraw and swap on the uniswapPortal!'); - const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - const swapArgs = [ - wethCrossChainHarness.tokenPortalAddress.toString(), - wethAmountToBridge, - uniswapFeeTier, - daiCrossChainHarness.tokenPortalAddress.toString(), - minimumOutputAmount, - secretHashForRedeemingDai.toString(true), - secretHashForDepositingSwappedDai.toString(true), - deadlineForDepositingSwappedDai, - ownerEthAddress.toString(), - true, - ] as const; - const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPrivate(swapArgs, { - account: ownerEthAddress.toString(), - } as any); + return { pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet }; +}; - // this should also insert a message into the inbox. - await uniswapPortal.write.swapPrivate(swapArgs, {} as any); - const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); - - // weth was swapped to dai and send to portal - const daiL1BalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); - const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); - - // Wait for the archiver to process the message - await delay(5000); - // send a transfer tx to force through rollup with the message included - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 6. claim dai on L2 - logger('Consuming messages to mint dai on L2'); - await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly( - daiAmountToBridge, - secretHashForRedeemingDai, - depositDaiMessageKey, - secretForDepositingSwappedDai, - ); - await daiCrossChainHarness.redeemShieldPrivatelyOnL2(daiAmountToBridge, secretForRedeemingDai); - await daiCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); - - const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - - logger('WETH balance before swap: ' + wethL2BalanceBeforeSwap.toString()); - logger('DAI balance before swap : ' + daiL2BalanceBeforeSwap.toString()); - logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); - logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); - logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); - }); - - it('should uniswap trade on L1 from L2 funds publicly (swaps WETH -> DAI)', async () => { - const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); - - // 1. Approve and deposit weth to the portal and move to L2 - const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret(); - - const messageKey = await wethCrossChainHarness.sendTokensToPortalPublic( - wethAmountToBridge, - secretHashForMintingWeth, - ); - // funds transferred from owner to token portal - expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe(wethL1BeforeBalance - wethAmountToBridge); - expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe( - wethAmountToBridge, - ); - - // Wait for the archiver to process the message - await delay(5000); - - // Perform an unrelated transaction on L2 to progress the rollup. Here we transfer 0 tokens - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 2. Claim WETH on L2 - logger('Minting weth on L2'); - await wethCrossChainHarness.consumeMessageOnAztecAndMintPublicly( - wethAmountToBridge, - messageKey, - secretForMintingWeth, - ); - await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, wethAmountToBridge); - - // Store balances - const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - - // 3. Owner gives uniswap approval to transfer funds on its behalf - const nonceForWETHTransferApproval = new Fr(1n); - const transferMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) - .request(), - ); - await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); - - // before swap - check nonce_for_burn_approval stored on uniswap - // (which is used by uniswap to approve the bridge to burn funds on its behalf to exit to L1) - const nonceForBurnApprovalBeforeSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - - // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. - const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = - await daiCrossChainHarness.generateClaimSecret(); - - // 4.1 Owner approves user to swap on their behalf: - const nonceForSwap = new Fr(3n); - const action = uniswapL2Contract - .withWallet(sponsorWallet) - .methods.swap_public( - ownerAddress, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHTransferApproval, - uniswapFeeTier, - minimumOutputAmount, - ownerAddress, - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - nonceForSwap, - ); - const swapMessageHash = await computeAuthWitMessageHash(sponsorAddress, action.request()); - await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait(); - - // 4.2 Call swap_public from user2 on behalf of owner - const withdrawReceipt = await action.send().wait(); - 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.expectPublicBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); - - // check burn approval nonce incremented: - const nonceForBurnApprovalAfterSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - expect(nonceForBurnApprovalAfterSwap).toBe(nonceForBurnApprovalBeforeSwap + 1n); - - // 5. Perform the swap on L1 with the `uniswapPortal.swap_private()` (consuming L2 to L1 messages) - logger('Execute withdraw and swap on the uniswapPortal!'); - const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - const swapArgs = [ - wethCrossChainHarness.tokenPortalAddress.toString(), - wethAmountToBridge, - uniswapFeeTier, - daiCrossChainHarness.tokenPortalAddress.toString(), - minimumOutputAmount, - ownerAddress.toString(), - secretHashForDepositingSwappedDai.toString(true), - deadlineForDepositingSwappedDai, - ownerEthAddress.toString(), - true, - ] as const; - const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPublic(swapArgs, { - account: ownerEthAddress.toString(), - } as any); - - // this should also insert a message into the inbox. - await uniswapPortal.write.swapPublic(swapArgs, {} as any); - const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); - // weth was swapped to dai and send to portal - const daiL1BalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); - const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); - - // Wait for the archiver to process the message - await delay(5000); - // send a transfer tx to force through rollup with the message included - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 6. claim dai on L2 - logger('Consuming messages to mint dai on L2'); - await daiCrossChainHarness.consumeMessageOnAztecAndMintPublicly( - daiAmountToBridge, - depositDaiMessageKey, - secretForDepositingSwappedDai, - ); - await daiCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); - - const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - - logger('WETH balance before swap: ', wethL2BalanceBeforeSwap.toString()); - logger('DAI balance before swap : ', daiL2BalanceBeforeSwap.toString()); - logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); - logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); - logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); - }); -}); +uniswapL1L2TestSuite(setupRPC, () => Promise.resolve(), EXPECTED_FORKED_BLOCK); diff --git a/yarn-project/canary/src/utils.ts b/yarn-project/canary/src/utils.ts deleted file mode 100644 index ce24a41f5174..000000000000 --- a/yarn-project/canary/src/utils.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { - AztecAddress, - DebugLogger, - EthAddress, - Fr, - NotePreimage, - PXE, - TxHash, - TxStatus, - Wallet, - computeMessageSecretHash, -} from '@aztec/aztec.js'; -import { sha256ToField } from '@aztec/foundation/crypto'; -import { - OutboxAbi, - PortalERC20Abi, - PortalERC20Bytecode, - TokenPortalAbi, - TokenPortalBytecode, -} from '@aztec/l1-artifacts'; -import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; - -import type { Abi, Narrow } from 'abitype'; -import { Account, Chain, Hex, HttpTransport, PublicClient, WalletClient, getContract } from 'viem'; - -/** - * 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 deployAndInitializeTokenAndBridgeContracts( - 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, owner).send(); - - // 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); - - // deploy l2 token bridge and attach to the portal - const bridge = await TokenBridgeContract.deploy(wallet, token.address) - .send({ portalContract: tokenPortalAddress }) - .deployed(); - const bridgeAddress = bridge.address.toString(); - - // now we wait for the txs to be mined. This way we send all tx in the same rollup. - if ((await token.methods.admin().view()) !== owner.toBigInt()) throw new Error(`Token admin is not ${owner}`); - - 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(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(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 }; -} - -/** - * Helper function to deploy ETH contracts. - * @param walletClient - A viem WalletClient. - * @param publicClient - A viem PublicClient. - * @param abi - The ETH contract's ABI (as abitype's Abi). - * @param bytecode - The ETH contract's bytecode. - * @param args - Constructor arguments for the contract. - * @returns The ETH address the contract was deployed to. - */ -export async function deployL1Contract( - walletClient: WalletClient, - publicClient: PublicClient, - abi: Narrow, - bytecode: Hex, - args: readonly unknown[] = [], -): Promise { - const hash = await walletClient.deployContract({ - abi, - bytecode, - args, - } as any); - - const receipt = await publicClient.waitForTransactionReceipt({ hash }); - const contractAddress = receipt.contractAddress; - if (!contractAddress) { - throw new Error(`No contract address found in receipt: ${JSON.stringify(receipt)}`); - } - - return EthAddress.fromString(receipt.contractAddress!); -} - -/** - * A Class for testing cross chain interactions, contains common interactions - * shared between cross chain tests. - */ -export class CrossChainTestHarness { - static async new( - pxeService: PXE, - publicClient: PublicClient, - walletClient: any, - wallet: Wallet, - logger: DebugLogger, - underlyingERC20Address?: EthAddress, - ): Promise { - const ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); - const owner = wallet.getCompleteAddress(); - const l1ContractAddresses = (await pxeService.getNodeInfo()).l1ContractAddresses; - - const outbox = getContract({ - address: l1ContractAddresses.outboxAddress!.toString(), - abi: OutboxAbi, - publicClient, - }); - - // Deploy and initialize all required contracts - logger('Deploying and initializing token, portal and its bridge...'); - const { token, bridge, tokenPortalAddress, tokenPortal, underlyingERC20 } = - await deployAndInitializeTokenAndBridgeContracts( - wallet, - walletClient, - publicClient, - l1ContractAddresses.registryAddress, - owner.address, - underlyingERC20Address, - ); - logger('Deployed and initialized token, portal and its bridge.'); - - return new CrossChainTestHarness( - pxeService, - logger, - token, - bridge, - ethAccount, - tokenPortalAddress, - tokenPortal, - underlyingERC20, - outbox, - publicClient, - walletClient, - owner.address, - ); - } - - constructor( - /** Private eXecution Environment (PXE). */ - public pxeService: PXE, - /** Logger. */ - public logger: DebugLogger, - - /** L2 Token contract. */ - public l2Token: TokenContract, - /** L2 Token bridge contract. */ - public l2Bridge: TokenBridgeContract, - - /** Eth account to interact with. */ - public ethAccount: EthAddress, - - /** Portal address. */ - public tokenPortalAddress: EthAddress, - /** Token portal instance. */ - public tokenPortal: any, - /** Underlying token for portal tests. */ - public underlyingERC20: any, - /** Message Bridge Outbox. */ - public outbox: any, - /** Viem Public client instance. */ - public publicClient: PublicClient, - /** Viem Wallet Client instance. */ - public walletClient: any, - - /** Aztec address to use in tests. */ - public ownerAddress: AztecAddress, - ) {} - - async generateClaimSecret(): Promise<[Fr, Fr]> { - this.logger("Generating a claim secret using pedersen's hash function"); - const secret = Fr.random(); - const secretHash = await computeMessageSecretHash(secret); - this.logger('Generated claim secret: ' + secretHash.toString(true)); - return [secret, secretHash]; - } - - async mintTokensOnL1(amount: bigint) { - this.logger('Minting tokens on L1'); - await this.underlyingERC20.write.mint([this.ethAccount.toString(), amount], {} as any); - expect(await this.underlyingERC20.read.balanceOf([this.ethAccount.toString()])).toBe(amount); - } - - async getL1BalanceOf(address: EthAddress) { - return await this.underlyingERC20.read.balanceOf([address.toString()]); - } - - 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 publicly'); - const args = [ - bridgeAmount, - this.ownerAddress.toString(), - this.ethAccount.toString(), - deadline, - secretHash.toString(true), - ] as const; - 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, - secretHashForRedeemingMintedNotes.toString(true), - this.ethAccount.toString(), - deadline, - secretHashForL2MessageConsumption.toString(true), - ] as const; - const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztecPrivate(args, { - account: this.ethAccount.toString(), - } as any); - await this.tokenPortal.write.depositToAztecPrivate(args, {} as any); - - return Fr.fromString(messageKeyHex); - } - - async mintTokensPublicOnL2(amount: bigint) { - this.logger('Minting tokens on L2 publicly'); - const tx = this.l2Token.methods.mint_public(this.ownerAddress, amount).send(); - const receipt = await tx.wait(); - expect(receipt.status).toBe(TxStatus.MINED); - } - - async mintTokensPrivateOnL2(amount: bigint, secretHash: Fr) { - const tx = this.l2Token.methods.mint_private(amount, secretHash).send(); - const receipt = await tx.wait(); - expect(receipt.status).toBe(TxStatus.MINED); - await this.addPendingShieldNoteToPXE(amount, secretHash, receipt.txHash); - } - - async performL2Transfer(transferAmount: bigint, receiverAddress: AztecAddress) { - // send a transfer tx to force through rollup with the message included - const transferTx = this.l2Token.methods - .transfer_public(this.ownerAddress, receiverAddress, transferAmount, 0) - .send(); - const receipt = await transferTx.wait(); - expect(receipt.status).toBe(TxStatus.MINED); - } - - async consumeMessageOnAztecAndMintSecretly( - bridgeAmount: bigint, - secretHashForRedeemingMintedNotes: Fr, - messageKey: Fr, - secretForL2MessageConsumption: 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, - secretHashForRedeemingMintedNotes, - this.ethAccount, - messageKey, - secretForL2MessageConsumption, - ) - .send(); - const consumptionReceipt = await consumptionTx.wait(); - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - - await this.addPendingShieldNoteToPXE(bridgeAmount, secretHashForRedeemingMintedNotes, consumptionReceipt.txHash); - } - - 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 tx = this.l2Bridge.methods - .claim_public(this.ownerAddress, bridgeAmount, this.ethAccount, messageKey, secret) - .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 - .exit_to_l1_private(this.ethAccount, this.l2Token.address, withdrawAmount, EthAddress.ZERO, nonce) - .send(); - const withdrawReceipt = await withdrawTx.wait(); - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - } - - async withdrawPublicFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { - const withdrawTx = this.l2Bridge.methods - .exit_to_l1_public(this.ethAccount, withdrawAmount, EthAddress.ZERO, nonce) - .send(); - const withdrawReceipt = await withdrawTx.wait(); - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - } - - async getL2PrivateBalanceOf(owner: AztecAddress) { - return await this.l2Token.methods.balance_of_private(owner).view({ from: owner }); - } - - async expectPrivateBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { - const balance = await this.getL2PrivateBalanceOf(owner); - this.logger(`Account ${owner} balance: ${balance}`); - expect(balance).toBe(expectedBalance); - } - - async getL2PublicBalanceOf(owner: AztecAddress) { - return await this.l2Token.methods.balance_of_public(owner).view(); - } - - async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { - const balance = await this.getL2PublicBalanceOf(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'); - // 0xb460af94, selector for "withdraw(uint256,address,address)" - const content = sha256ToField( - Buffer.concat([ - Buffer.from([0xb4, 0x60, 0xaf, 0x94]), - new Fr(withdrawAmount).toBuffer(), - this.ethAccount.toBuffer32(), - callerOnL1.toBuffer32(), - ]), - ); - const entryKey = sha256ToField( - Buffer.concat([ - this.l2Bridge.address.toBuffer(), - new Fr(1).toBuffer(), // aztec version - this.tokenPortalAddress.toBuffer32() ?? Buffer.alloc(32, 0), - new Fr(this.publicClient.chain.id).toBuffer(), // chain id - content.toBuffer(), - ]), - ); - expect(await this.outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); - - return entryKey; - } - - async withdrawFundsFromBridgeOnL1(withdrawAmount: bigint, entryKey: Fr) { - this.logger('Send L1 tx to consume entry and withdraw funds'); - // Call function on L1 contract to consume the message - const { request: withdrawRequest, result: withdrawEntryKey } = await this.tokenPortal.simulate.withdraw([ - withdrawAmount, - this.ethAccount.toString(), - false, - ]); - - expect(withdrawEntryKey).toBe(entryKey.toString(true)); - expect(await this.outbox.read.contains([withdrawEntryKey])).toBeTruthy(); - - await this.walletClient.writeContract(withdrawRequest); - return withdrawEntryKey; - } - - async shieldFundsOnL2(shieldAmount: bigint, secretHash: Fr) { - this.logger('Shielding funds on L2'); - const shieldTx = this.l2Token.methods.shield(this.ownerAddress, shieldAmount, secretHash, 0).send(); - const shieldReceipt = await shieldTx.wait(); - expect(shieldReceipt.status).toBe(TxStatus.MINED); - - await this.addPendingShieldNoteToPXE(shieldAmount, secretHash, shieldReceipt.txHash); - } - - async addPendingShieldNoteToPXE(shieldAmount: bigint, secretHash: Fr, txHash: TxHash) { - this.logger('Adding note to PXE'); - const storageSlot = new Fr(5); - const preimage = new NotePreimage([new Fr(shieldAmount), secretHash]); - await this.pxeService.addNote(this.ownerAddress, this.l2Token.address, storageSlot, preimage, txHash); - } - - async redeemShieldPrivatelyOnL2(shieldAmount: bigint, secret: Fr) { - this.logger('Spending commitment in private call'); - const privateTx = this.l2Token.methods.redeem_shield(this.ownerAddress, shieldAmount, secret).send(); - const privateReceipt = await privateTx.wait(); - expect(privateReceipt.status).toBe(TxStatus.MINED); - } - - async unshieldTokensOnL2(unshieldAmount: bigint, nonce = Fr.ZERO) { - this.logger('Unshielding tokens'); - const unshieldTx = this.l2Token.methods - .unshield(this.ownerAddress, 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/canary/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts new file mode 100644 index 000000000000..0bffd63997de --- /dev/null +++ b/yarn-project/end-to-end/src/canary/uniswap_l1_l2.ts @@ -0,0 +1,744 @@ +import { AccountWallet, AztecAddress, computeAuthWitMessageHash } from '@aztec/aztec.js'; +import { deployL1Contract } from '@aztec/ethereum'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Fr } from '@aztec/foundation/fields'; +import { DebugLogger } from '@aztec/foundation/log'; +import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; +import { UniswapContract } from '@aztec/noir-contracts/types'; +import { PXE, TxStatus } from '@aztec/types'; + +import { jest } from '@jest/globals'; +import { Chain, HttpTransport, PublicClient, getContract, parseEther } from 'viem'; + +import { CrossChainTestHarness } from '../fixtures/cross_chain_test_harness.js'; +import { delay } 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. +// To generate a new dump, use the `dumpChainState` cheatcode. +// To start an actual fork, use the command: +// anvil --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c --fork-block-number 17514288 --chain-id 31337 +// For CI, this is configured in `run_tests.sh` and `docker-compose.yml` + +const TIMEOUT = 90_000; + +/** Objects to be returned by the uniswap setup function */ +export type UniswapSetupContext = { + /** The Private eXecution Environment (PXE). */ + pxe: PXE; + /** Logger instance named as the current test. */ + logger: DebugLogger; + /** Viem Public client instance. */ + publicClient: PublicClient; + /** Viem Wallet Client instance. */ + walletClient: any; + /** The owner wallet. */ + ownerWallet: AccountWallet; + /** The sponsor wallet. */ + sponsorWallet: AccountWallet; +}; + +export const uniswapL1L2TestSuite = ( + setup: () => Promise, + cleanup: () => Promise, + expectedForkBlockNumber = 17514288, +) => { + describe('uniswap_trade_on_l1_from_l2', () => { + jest.setTimeout(TIMEOUT); + + const WETH9_ADDRESS: EthAddress = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); + const DAI_ADDRESS: EthAddress = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); + + let pxe: PXE; + let logger: DebugLogger; + + let walletClient: any; + + let ownerWallet: AccountWallet; + let ownerAddress: AztecAddress; + let ownerEthAddress: EthAddress; + // does transactions on behalf of owner on Aztec: + let sponsorWallet: AccountWallet; + let sponsorAddress: AztecAddress; + + let daiCrossChainHarness: CrossChainTestHarness; + let wethCrossChainHarness: CrossChainTestHarness; + + let uniswapPortal: any; + let uniswapPortalAddress: EthAddress; + let uniswapL2Contract: UniswapContract; + + const wethAmountToBridge = parseEther('1'); + const uniswapFeeTier = 3000n; + const minimumOutputAmount = 0n; + const deadlineForDepositingSwappedDai = BigInt(2 ** 32 - 1); // max uint32 - 1 + + beforeAll(async () => { + let publicClient: PublicClient; + ({ pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet } = await setup()); + + // walletClient = deployL1ContractsValues.walletClient; + // const publicClient = deployL1ContractsValues.publicClient; + + if (Number(await publicClient.getBlockNumber()) < expectedForkBlockNumber) { + throw new Error('This test must be run on a fork of mainnet with the expected fork block'); + } + + ownerAddress = ownerWallet.getAddress(); + sponsorAddress = sponsorWallet.getAddress(); + ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); + + logger('Deploying DAI Portal, initializing and deploying l2 contract...'); + daiCrossChainHarness = await CrossChainTestHarness.new( + pxe, + publicClient, + walletClient, + ownerWallet, + logger, + DAI_ADDRESS, + ); + + logger('Deploying WETH Portal, initializing and deploying l2 contract...'); + wethCrossChainHarness = await CrossChainTestHarness.new( + pxe, + publicClient, + walletClient, + ownerWallet, + logger, + WETH9_ADDRESS, + ); + + logger('Deploy Uniswap portal on L1 and L2...'); + uniswapPortalAddress = await deployL1Contract( + walletClient, + publicClient, + UniswapPortalAbi, + UniswapPortalBytecode, + ); + uniswapPortal = getContract({ + address: uniswapPortalAddress.toString(), + abi: UniswapPortalAbi, + walletClient, + publicClient, + }); + // deploy l2 uniswap contract and attach to portal + uniswapL2Contract = await UniswapContract.deploy(ownerWallet) + .send({ portalContract: uniswapPortalAddress }) + .deployed(); + + const registryAddress = (await pxe.getNodeInfo()).l1ContractAddresses.registryAddress; + await uniswapPortal.write.initialize( + [registryAddress.toString(), uniswapL2Contract.address.toString()], + {} as any, + ); + }); + + beforeEach(async () => { + // Give me some WETH so I can deposit to L2 and do the swap... + logger('Getting some weth'); + await walletClient.sendTransaction({ to: WETH9_ADDRESS.toString(), value: parseEther('1') }); + }); + + afterAll(async () => { + await cleanup(); + }); + + it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { + const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); + + // 1. Approve and deposit weth to the portal and move to L2 + const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret(); + const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); + + const messageKey = await wethCrossChainHarness.sendTokensToPortalPrivate( + wethAmountToBridge, + secretHashForMintingWeth, + secretHashForRedeemingWeth, + ); + // funds transferred from owner to token portal + expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe( + wethL1BeforeBalance - wethAmountToBridge, + ); + expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe( + wethAmountToBridge, + ); + + // Wait for the archiver to process the message + await delay(5000); + + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. + await wethCrossChainHarness.mintTokensPublicOnL2(0n); + + // 2. Claim WETH on L2 + logger('Minting weth on L2'); + await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly( + wethAmountToBridge, + secretHashForRedeemingWeth, + messageKey, + secretForMintingWeth, + ); + await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge); + + // Store balances + const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + + // before swap - check nonce_for_burn_approval stored on uniswap + // (which is used by uniswap to approve the bridge to burn funds on its behalf to exit to L1) + const nonceForBurnApprovalBeforeSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); + + // 3. Owner gives uniswap approval to unshield funds to self on its behalf + logger('Approving uniswap to unshield funds to self on my behalf'); + const nonceForWETHUnshieldApproval = new Fr(1n); + const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) + .request(), + ); + await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); + + // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. + logger('Withdrawing weth to L1 and sending message to swap to dai'); + const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = + await daiCrossChainHarness.generateClaimSecret(); + const [secretForRedeemingDai, secretHashForRedeemingDai] = await daiCrossChainHarness.generateClaimSecret(); + + const withdrawReceipt = await uniswapL2Contract.methods + .swap_private( + wethCrossChainHarness.l2Token.address, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHUnshieldApproval, + uniswapFeeTier, + minimumOutputAmount, + secretHashForRedeemingDai, + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + ) + .send() + .wait(); + expect(withdrawReceipt.status).toBe(TxStatus.MINED); + // ensure that user's funds were burnt + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); + // ensure that uniswap contract didn't eat the funds. + await wethCrossChainHarness.expectPublicBalanceOnL2(uniswapL2Contract.address, 0n); + // check burn approval nonce incremented: + const nonceForBurnApprovalAfterSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); + expect(nonceForBurnApprovalAfterSwap).toBe(nonceForBurnApprovalBeforeSwap + 1n); + + // 5. Consume L2 to L1 message by calling uniswapPortal.swap_private() + logger('Execute withdraw and swap on the uniswapPortal!'); + const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( + daiCrossChainHarness.tokenPortalAddress, + ); + const swapArgs = [ + wethCrossChainHarness.tokenPortalAddress.toString(), + wethAmountToBridge, + uniswapFeeTier, + daiCrossChainHarness.tokenPortalAddress.toString(), + minimumOutputAmount, + secretHashForRedeemingDai.toString(true), + secretHashForDepositingSwappedDai.toString(true), + deadlineForDepositingSwappedDai, + ownerEthAddress.toString(), + true, + ] as const; + const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPrivate(swapArgs, { + account: ownerEthAddress.toString(), + } as any); + + // this should also insert a message into the inbox. + await uniswapPortal.write.swapPrivate(swapArgs, {} as any); + const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); + + // weth was swapped to dai and send to portal + const daiL1BalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf( + daiCrossChainHarness.tokenPortalAddress, + ); + expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); + const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); + + // Wait for the archiver to process the message + await delay(5000); + // send a transfer tx to force through rollup with the message included + await wethCrossChainHarness.mintTokensPublicOnL2(0n); + + // 6. claim dai on L2 + logger('Consuming messages to mint dai on L2'); + await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly( + daiAmountToBridge, + secretHashForRedeemingDai, + depositDaiMessageKey, + secretForDepositingSwappedDai, + ); + await daiCrossChainHarness.redeemShieldPrivatelyOnL2(daiAmountToBridge, secretForRedeemingDai); + await daiCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); + + const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + + logger('WETH balance before swap: ' + wethL2BalanceBeforeSwap.toString()); + logger('DAI balance before swap : ' + daiL2BalanceBeforeSwap.toString()); + logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); + logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); + logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); + }); + + it('should uniswap trade on L1 from L2 funds publicly (swaps WETH -> DAI)', async () => { + const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); + + // 1. Approve and deposit weth to the portal and move to L2 + const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret(); + + const messageKey = await wethCrossChainHarness.sendTokensToPortalPublic( + wethAmountToBridge, + secretHashForMintingWeth, + ); + // funds transferred from owner to token portal + expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe( + wethL1BeforeBalance - wethAmountToBridge, + ); + expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe( + wethAmountToBridge, + ); + + // Wait for the archiver to process the message + await delay(5000); + + // Perform an unrelated transaction on L2 to progress the rollup. Here we transfer 0 tokens + await wethCrossChainHarness.mintTokensPublicOnL2(0n); + + // 2. Claim WETH on L2 + logger('Minting weth on L2'); + await wethCrossChainHarness.consumeMessageOnAztecAndMintPublicly( + wethAmountToBridge, + messageKey, + secretForMintingWeth, + ); + await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, wethAmountToBridge); + + // Store balances + const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress); + const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress); + + // 3. Owner gives uniswap approval to transfer funds on its behalf + const nonceForWETHTransferApproval = new Fr(1n); + const transferMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) + .request(), + ); + await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); + + // before swap - check nonce_for_burn_approval stored on uniswap + // (which is used by uniswap to approve the bridge to burn funds on its behalf to exit to L1) + const nonceForBurnApprovalBeforeSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); + + // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. + const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = + await daiCrossChainHarness.generateClaimSecret(); + + // 4.1 Owner approves user to swap on their behalf: + const nonceForSwap = new Fr(3n); + const action = uniswapL2Contract + .withWallet(sponsorWallet) + .methods.swap_public( + ownerAddress, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHTransferApproval, + uniswapFeeTier, + minimumOutputAmount, + ownerAddress, + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + nonceForSwap, + ); + const swapMessageHash = await computeAuthWitMessageHash(sponsorAddress, action.request()); + await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait(); + + // 4.2 Call swap_public from user2 on behalf of owner + const withdrawReceipt = await action.send().wait(); + 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.expectPublicBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); + + // check burn approval nonce incremented: + const nonceForBurnApprovalAfterSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); + expect(nonceForBurnApprovalAfterSwap).toBe(nonceForBurnApprovalBeforeSwap + 1n); + + // 5. Perform the swap on L1 with the `uniswapPortal.swap_private()` (consuming L2 to L1 messages) + logger('Execute withdraw and swap on the uniswapPortal!'); + const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( + daiCrossChainHarness.tokenPortalAddress, + ); + const swapArgs = [ + wethCrossChainHarness.tokenPortalAddress.toString(), + wethAmountToBridge, + uniswapFeeTier, + daiCrossChainHarness.tokenPortalAddress.toString(), + minimumOutputAmount, + ownerAddress.toString(), + secretHashForDepositingSwappedDai.toString(true), + deadlineForDepositingSwappedDai, + ownerEthAddress.toString(), + true, + ] as const; + const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPublic(swapArgs, { + account: ownerEthAddress.toString(), + } as any); + + // this should also insert a message into the inbox. + await uniswapPortal.write.swapPublic(swapArgs, {} as any); + const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); + // weth was swapped to dai and send to portal + const daiL1BalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf( + daiCrossChainHarness.tokenPortalAddress, + ); + expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); + const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); + + // Wait for the archiver to process the message + await delay(5000); + // send a transfer tx to force through rollup with the message included + await wethCrossChainHarness.mintTokensPublicOnL2(0n); + + // 6. claim dai on L2 + logger('Consuming messages to mint dai on L2'); + await daiCrossChainHarness.consumeMessageOnAztecAndMintPublicly( + daiAmountToBridge, + depositDaiMessageKey, + secretForDepositingSwappedDai, + ); + await daiCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); + + const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress); + const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress); + + logger('WETH balance before swap: ', wethL2BalanceBeforeSwap.toString()); + logger('DAI balance before swap : ', daiL2BalanceBeforeSwap.toString()); + logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); + logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); + logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); + }); + + // Edge cases for the private flow: + // note - tests for uniswapPortal.sol and minting asset on L2 are covered in other tests. + + it('swap_private reverts without unshield approval', async () => { + // swap should fail since no withdraw approval to uniswap: + const nonceForWETHUnshieldApproval = new Fr(2n); + + const expectedMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) + .request(), + ); + + await expect( + uniswapL2Contract.methods + .swap_private( + wethCrossChainHarness.l2Token.address, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHUnshieldApproval, + uniswapFeeTier, + minimumOutputAmount, + Fr.random(), + Fr.random(), + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + ) + .simulate(), + ).rejects.toThrowError(`Unknown auth witness for message hash 0x${expectedMessageHash.toString('hex')}`); + }); + + it("can't swap if user passes a token different to what the bridge tracks", async () => { + // 1. give user private funds on L2: + const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); + await wethCrossChainHarness.mintTokensPrivateOnL2(wethAmountToBridge, secretHashForRedeemingWeth); + await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge); + + // 2. owner gives uniswap approval to unshield funds: + logger('Approving uniswap to unshield funds to self on my behalf'); + const nonceForWETHUnshieldApproval = new Fr(3n); + const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) + .request(), + ); + await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); + + // 3. Swap but send the wrong token address + logger('Swap but send the wrong token address'); + await expect( + uniswapL2Contract.methods + .swap_private( + wethCrossChainHarness.l2Token.address, // send weth token + daiCrossChainHarness.l2Bridge.address, // but dai bridge! + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHUnshieldApproval, + uniswapFeeTier, + minimumOutputAmount, + Fr.random(), + Fr.random(), + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + ) + .simulate(), + ).rejects.toThrowError('Assertion failed: input_asset address is not the same as seen in the bridge contract'); + }); + + // edge cases for public flow: + + it("I don't need approval to call swap_public if I'm swapping on my own behalf", async () => { + // 1. get tokens on l2 + await wethCrossChainHarness.mintTokensPublicOnL2(wethAmountToBridge); + + // 2. Give approval to uniswap to transfer funds to itself + const nonceForWETHTransferApproval = new Fr(2n); + const transferMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) + .request(), + ); + await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); + + // No approval to call `swap` but should work even without it: + const [_, secretHashForDepositingSwappedDai] = await daiCrossChainHarness.generateClaimSecret(); + + const withdrawReceipt = await uniswapL2Contract.methods + .swap_public( + ownerAddress, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHTransferApproval, + uniswapFeeTier, + minimumOutputAmount, + ownerAddress, + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + Fr.ZERO, // nonce for swap -> doesn't matter + ) + .send() + .wait(); + 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.expectPublicBalanceOnL2(ownerAddress, 0n); + }); + + it("someone can't call swap_public on my behalf without approval", async () => { + // Owner approves a a user to swap_public: + const approvedUser = AztecAddress.random(); + + const nonceForWETHTransferApproval = new Fr(3n); + const nonceForSwap = new Fr(3n); + const secretHashForDepositingSwappedDai = new Fr(4n); + const action = uniswapL2Contract + .withWallet(sponsorWallet) + .methods.swap_public( + ownerAddress, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHTransferApproval, + uniswapFeeTier, + minimumOutputAmount, + ownerAddress, + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + nonceForSwap, + ); + const swapMessageHash = await computeAuthWitMessageHash(approvedUser, action.request()); + await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait(); + + // Swap! + await expect(action.simulate()).rejects.toThrowError( + "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", + ); + }); + + it("uniswap can't pull funds without transfer approval", async () => { + // swap should fail since no transfer approval to uniswap: + const nonceForWETHTransferApproval = new Fr(4n); + + const transferMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) + .request(), + ); + await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); + + await expect( + uniswapL2Contract.methods + .swap_public( + ownerAddress, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + new Fr(420), // using a different nonce + uniswapFeeTier, + minimumOutputAmount, + ownerAddress, + Fr.random(), + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + Fr.ZERO, + ) + .simulate(), + ).rejects.toThrowError(`Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'`); + }); + + // tests when trying to mix private and public flows: + it("can't call swap_public on L1 if called swap_private on L2", async () => { + // get tokens on L2: + const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); + logger('minting weth on L2'); + await wethCrossChainHarness.mintTokensPrivateOnL2(wethAmountToBridge, secretHashForRedeemingWeth); + await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); + + // Owner gives uniswap approval to unshield funds to self on its behalf + logger('Approving uniswap to unshield funds to self on my behalf'); + const nonceForWETHUnshieldApproval = new Fr(4n); + + const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) + .request(), + ); + await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); + const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + + // Swap + logger('Withdrawing weth to L1 and sending message to swap to dai'); + const secretHashForDepositingSwappedDai = Fr.random(); + + const withdrawReceipt = await uniswapL2Contract.methods + .swap_private( + wethCrossChainHarness.l2Token.address, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHUnshieldApproval, + uniswapFeeTier, + minimumOutputAmount, + Fr.random(), + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + ) + .send() + .wait(); + expect(withdrawReceipt.status).toBe(TxStatus.MINED); + // ensure that user's funds were burnt + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); + + // On L1 call swap_public! + logger('call swap_public on L1'); + const swapArgs = [ + wethCrossChainHarness.tokenPortalAddress.toString(), + wethAmountToBridge, + uniswapFeeTier, + daiCrossChainHarness.tokenPortalAddress.toString(), + minimumOutputAmount, + ownerAddress.toString(), + secretHashForDepositingSwappedDai.toString(true), + deadlineForDepositingSwappedDai, + ownerEthAddress.toString(), + true, + ] as const; + await expect( + uniswapPortal.simulate.swapPublic(swapArgs, { + account: ownerEthAddress.toString(), + } as any), + ).rejects.toThrowError('The contract function "swapPublic" reverted.'); + }); + + it("can't call swap_private on L1 if called swap_public on L2", async () => { + // get tokens on L2: + await wethCrossChainHarness.mintTokensPublicOnL2(wethAmountToBridge); + + // Owner gives uniswap approval to transfer funds on its behalf + const nonceForWETHTransferApproval = new Fr(5n); + const transferMessageHash = await computeAuthWitMessageHash( + uniswapL2Contract.address, + wethCrossChainHarness.l2Token.methods + .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) + .request(), + ); + await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); + + // Call swap_public on L2 + const secretHashForDepositingSwappedDai = Fr.random(); + const withdrawReceipt = await uniswapL2Contract.methods + .swap_public( + ownerAddress, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHTransferApproval, + uniswapFeeTier, + minimumOutputAmount, + ownerAddress, + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + Fr.ZERO, + ) + .send() + .wait(); + 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.expectPublicBalanceOnL2(ownerAddress, 0n); + + // Call swap_private on L1 + const secretHashForRedeemingDai = Fr.random(); // creating my own secret hash + logger('Execute withdraw and swap on the uniswapPortal!'); + const swapArgs = [ + wethCrossChainHarness.tokenPortalAddress.toString(), + wethAmountToBridge, + uniswapFeeTier, + daiCrossChainHarness.tokenPortalAddress.toString(), + minimumOutputAmount, + secretHashForRedeemingDai.toString(true), + secretHashForDepositingSwappedDai.toString(true), + deadlineForDepositingSwappedDai, + ownerEthAddress.toString(), + true, + ] as const; + await expect( + uniswapPortal.simulate.swapPrivate(swapArgs, { + account: ownerEthAddress.toString(), + } as any), + ).rejects.toThrowError('The contract function "swapPrivate" reverted.'); + }); + }); +}; diff --git a/yarn-project/end-to-end/src/index.ts b/yarn-project/end-to-end/src/index.ts index dd9005c49c91..da0a55181ff0 100644 --- a/yarn-project/end-to-end/src/index.ts +++ b/yarn-project/end-to-end/src/index.ts @@ -2,3 +2,4 @@ export { cliTestSuite } from './canary/cli.js'; export { browserTestSuite } from './canary/browser.js'; +export { uniswapL1L2TestSuite, UniswapSetupContext } from './canary/uniswap_l1_l2.js'; 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 ec49e257ad06..7d9c139a6c0a 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,731 +1,38 @@ -import { AccountWallet, AztecAddress, computeAuthWitMessageHash } from '@aztec/aztec.js'; -import { deployL1Contract } from '@aztec/ethereum'; -import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr } from '@aztec/foundation/fields'; -import { DebugLogger } from '@aztec/foundation/log'; -import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; -import { UniswapContract } from '@aztec/noir-contracts/types'; -import { PXE, TxStatus } from '@aztec/types'; +import { UniswapSetupContext, uniswapL1L2TestSuite } from './canary/uniswap_l1_l2.js'; +import { setup as e2eSetup } from './fixtures/utils.js'; -import { jest } from '@jest/globals'; -import { getContract, parseEther } from 'viem'; - -import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.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 +// 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. -// To generate a new dump, use the `dumpChainState` cheatcode. -// To start an actual fork, use the command: -// anvil --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c --fork-block-number 17514288 --chain-id 31337 -// For CI, this is configured in `run_tests.sh` and `docker-compose.yml` - const dumpedState = 'src/fixtures/dumps/uniswap_state'; // When taking a dump use the block number of the fork to improve speed. const EXPECTED_FORKED_BLOCK = 0; //17514288; // We tell the archiver to only sync from this block. process.env.SEARCH_START_BLOCK = EXPECTED_FORKED_BLOCK.toString(); -const TIMEOUT = 90_000; - -// 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', () => { - jest.setTimeout(TIMEOUT); - - const WETH9_ADDRESS: EthAddress = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); - const DAI_ADDRESS: EthAddress = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); - - let pxe: PXE; - let logger: DebugLogger; - let teardown: () => Promise; - - let walletClient: any; - - let ownerWallet: AccountWallet; - let ownerAddress: AztecAddress; - let ownerEthAddress: EthAddress; - // does transactions on behalf of owner on Aztec: - let sponsorWallet: AccountWallet; - let sponsorAddress: AztecAddress; - - let daiCrossChainHarness: CrossChainTestHarness; - let wethCrossChainHarness: CrossChainTestHarness; - - let uniswapPortal: any; - let uniswapPortalAddress: EthAddress; - let uniswapL2Contract: UniswapContract; - - const wethAmountToBridge = parseEther('1'); - const uniswapFeeTier = 3000n; - const minimumOutputAmount = 0n; - const deadlineForDepositingSwappedDai = BigInt(2 ** 32 - 1); // max uint32 - 1 - - beforeAll(async () => { - const { - teardown: teardown_, - pxe: pxe_, - deployL1ContractsValues, - accounts, - wallets, - logger: logger_, - } = await setup(2, { stateLoad: dumpedState }); - 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'); - } - - pxe = pxe_; - logger = logger_; - teardown = teardown_; - ownerWallet = wallets[0]; - sponsorWallet = wallets[1]; - ownerAddress = accounts[0].address; - sponsorAddress = accounts[1].address; - ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); - - logger('Deploying DAI Portal, initializing and deploying l2 contract...'); - daiCrossChainHarness = await CrossChainTestHarness.new( - pxe, - deployL1ContractsValues.publicClient, - deployL1ContractsValues.walletClient, - ownerWallet, - logger, - DAI_ADDRESS, - ); - - logger('Deploying WETH Portal, initializing and deploying l2 contract...'); - wethCrossChainHarness = await CrossChainTestHarness.new( - pxe, - deployL1ContractsValues.publicClient, - deployL1ContractsValues.walletClient, - ownerWallet, - logger, - WETH9_ADDRESS, - ); - - logger('Deploy Uniswap portal on L1 and L2...'); - uniswapPortalAddress = await deployL1Contract(walletClient, publicClient, UniswapPortalAbi, UniswapPortalBytecode); - uniswapPortal = getContract({ - address: uniswapPortalAddress.toString(), - abi: UniswapPortalAbi, - walletClient, - publicClient, - }); - // deploy l2 uniswap contract and attach to portal - uniswapL2Contract = await UniswapContract.deploy(ownerWallet) - .send({ portalContract: uniswapPortalAddress }) - .deployed(); - - await uniswapPortal.write.initialize( - [deployL1ContractsValues!.l1ContractAddresses.registryAddress!.toString(), uniswapL2Contract.address.toString()], - {} as any, - ); - }); - - beforeEach(async () => { - // Give me some WETH so I can deposit to L2 and do the swap... - logger('Getting some weth'); - await walletClient.sendTransaction({ to: WETH9_ADDRESS.toString(), value: parseEther('1') }); - }); - - afterAll(async () => { - await teardown(); - // await wethCrossChainHarness.stop(); - // await daiCrossChainHarness.stop(); - }); - - it.only('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { - const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); - - // 1. Approve and deposit weth to the portal and move to L2 - const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret(); - const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); - - const messageKey = await wethCrossChainHarness.sendTokensToPortalPrivate( - wethAmountToBridge, - secretHashForMintingWeth, - secretHashForRedeemingWeth, - ); - // funds transferred from owner to token portal - expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe(wethL1BeforeBalance - wethAmountToBridge); - expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe( - wethAmountToBridge, - ); - - // Wait for the archiver to process the message - await delay(5000); - - // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 2. Claim WETH on L2 - logger('Minting weth on L2'); - await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly( - wethAmountToBridge, - secretHashForRedeemingWeth, - messageKey, - secretForMintingWeth, - ); - await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); - await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge); - - // Store balances - const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - - // before swap - check nonce_for_burn_approval stored on uniswap - // (which is used by uniswap to approve the bridge to burn funds on its behalf to exit to L1) - const nonceForBurnApprovalBeforeSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - - // 3. Owner gives uniswap approval to unshield funds to self on its behalf - logger('Approving uniswap to unshield funds to self on my behalf'); - const nonceForWETHUnshieldApproval = new Fr(1n); - const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) - .request(), - ); - await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); - - // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. - logger('Withdrawing weth to L1 and sending message to swap to dai'); - const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = - await daiCrossChainHarness.generateClaimSecret(); - const [secretForRedeemingDai, secretHashForRedeemingDai] = await daiCrossChainHarness.generateClaimSecret(); - - const withdrawReceipt = await uniswapL2Contract.methods - .swap_private( - wethCrossChainHarness.l2Token.address, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHUnshieldApproval, - uniswapFeeTier, - minimumOutputAmount, - secretHashForRedeemingDai, - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - ) - .send() - .wait(); - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - // ensure that user's funds were burnt - await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); - // ensure that uniswap contract didn't eat the funds. - await wethCrossChainHarness.expectPublicBalanceOnL2(uniswapL2Contract.address, 0n); - // check burn approval nonce incremented: - const nonceForBurnApprovalAfterSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - expect(nonceForBurnApprovalAfterSwap).toBe(nonceForBurnApprovalBeforeSwap + 1n); - - // 5. Consume L2 to L1 message by calling uniswapPortal.swap_private() - logger('Execute withdraw and swap on the uniswapPortal!'); - const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - const swapArgs = [ - wethCrossChainHarness.tokenPortalAddress.toString(), - wethAmountToBridge, - uniswapFeeTier, - daiCrossChainHarness.tokenPortalAddress.toString(), - minimumOutputAmount, - secretHashForRedeemingDai.toString(true), - secretHashForDepositingSwappedDai.toString(true), - deadlineForDepositingSwappedDai, - ownerEthAddress.toString(), - true, - ] as const; - const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPrivate(swapArgs, { - account: ownerEthAddress.toString(), - } as any); - - // this should also insert a message into the inbox. - await uniswapPortal.write.swapPrivate(swapArgs, {} as any); - const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); - - // weth was swapped to dai and send to portal - const daiL1BalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); - const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); - - // Wait for the archiver to process the message - await delay(5000); - // send a transfer tx to force through rollup with the message included - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 6. claim dai on L2 - logger('Consuming messages to mint dai on L2'); - await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly( - daiAmountToBridge, - secretHashForRedeemingDai, - depositDaiMessageKey, - secretForDepositingSwappedDai, - ); - await daiCrossChainHarness.redeemShieldPrivatelyOnL2(daiAmountToBridge, secretForRedeemingDai); - await daiCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); - - const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - - logger('WETH balance before swap: ' + wethL2BalanceBeforeSwap.toString()); - logger('DAI balance before swap : ' + daiL2BalanceBeforeSwap.toString()); - logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); - logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); - logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); - }); - - it('should uniswap trade on L1 from L2 funds publicly (swaps WETH -> DAI)', async () => { - const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); - - // 1. Approve and deposit weth to the portal and move to L2 - const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret(); - - const messageKey = await wethCrossChainHarness.sendTokensToPortalPublic( - wethAmountToBridge, - secretHashForMintingWeth, - ); - // funds transferred from owner to token portal - expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe(wethL1BeforeBalance - wethAmountToBridge); - expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe( - wethAmountToBridge, - ); - - // Wait for the archiver to process the message - await delay(5000); - - // Perform an unrelated transaction on L2 to progress the rollup. Here we transfer 0 tokens - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 2. Claim WETH on L2 - logger('Minting weth on L2'); - await wethCrossChainHarness.consumeMessageOnAztecAndMintPublicly( - wethAmountToBridge, - messageKey, - secretForMintingWeth, - ); - await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, wethAmountToBridge); - - // Store balances - const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - - // 3. Owner gives uniswap approval to transfer funds on its behalf - const nonceForWETHTransferApproval = new Fr(1n); - const transferMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) - .request(), - ); - await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); - - // before swap - check nonce_for_burn_approval stored on uniswap - // (which is used by uniswap to approve the bridge to burn funds on its behalf to exit to L1) - const nonceForBurnApprovalBeforeSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - - // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. - const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = - await daiCrossChainHarness.generateClaimSecret(); - - // 4.1 Owner approves user to swap on their behalf: - const nonceForSwap = new Fr(3n); - const action = uniswapL2Contract - .withWallet(sponsorWallet) - .methods.swap_public( - ownerAddress, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHTransferApproval, - uniswapFeeTier, - minimumOutputAmount, - ownerAddress, - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - nonceForSwap, - ); - const swapMessageHash = await computeAuthWitMessageHash(sponsorAddress, action.request()); - await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait(); - - // 4.2 Call swap_public from user2 on behalf of owner - const withdrawReceipt = await action.send().wait(); - 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.expectPublicBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); - - // check burn approval nonce incremented: - const nonceForBurnApprovalAfterSwap = await uniswapL2Contract.methods.nonce_for_burn_approval().view(); - expect(nonceForBurnApprovalAfterSwap).toBe(nonceForBurnApprovalBeforeSwap + 1n); - - // 5. Perform the swap on L1 with the `uniswapPortal.swap_private()` (consuming L2 to L1 messages) - logger('Execute withdraw and swap on the uniswapPortal!'); - const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - const swapArgs = [ - wethCrossChainHarness.tokenPortalAddress.toString(), - wethAmountToBridge, - uniswapFeeTier, - daiCrossChainHarness.tokenPortalAddress.toString(), - minimumOutputAmount, - ownerAddress.toString(), - secretHashForDepositingSwappedDai.toString(true), - deadlineForDepositingSwappedDai, - ownerEthAddress.toString(), - true, - ] as const; - const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPublic(swapArgs, { - account: ownerEthAddress.toString(), - } as any); - - // this should also insert a message into the inbox. - await uniswapPortal.write.swapPublic(swapArgs, {} as any); - const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); - // weth was swapped to dai and send to portal - const daiL1BalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf( - daiCrossChainHarness.tokenPortalAddress, - ); - expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); - const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); - - // Wait for the archiver to process the message - await delay(5000); - // send a transfer tx to force through rollup with the message included - await wethCrossChainHarness.mintTokensPublicOnL2(0n); - - // 6. claim dai on L2 - logger('Consuming messages to mint dai on L2'); - await daiCrossChainHarness.consumeMessageOnAztecAndMintPublicly( - daiAmountToBridge, - depositDaiMessageKey, - secretForDepositingSwappedDai, - ); - await daiCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); - - const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress); - - logger('WETH balance before swap: ', wethL2BalanceBeforeSwap.toString()); - logger('DAI balance before swap : ', daiL2BalanceBeforeSwap.toString()); - logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); - logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); - logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); - }); - - // Edge cases for the private flow: - // note - tests for uniswapPortal.sol and minting asset on L2 are covered in other tests. - - it('swap_private reverts without unshield approval', async () => { - // swap should fail since no withdraw approval to uniswap: - const nonceForWETHUnshieldApproval = new Fr(2n); - - const expectedMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) - .request(), - ); - - await expect( - uniswapL2Contract.methods - .swap_private( - wethCrossChainHarness.l2Token.address, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHUnshieldApproval, - uniswapFeeTier, - minimumOutputAmount, - Fr.random(), - Fr.random(), - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - ) - .simulate(), - ).rejects.toThrowError(`Unknown auth witness for message hash 0x${expectedMessageHash.toString('hex')}`); - }); - - it("can't swap if user passes a token different to what the bridge tracks", async () => { - // 1. give user private funds on L2: - const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); - await wethCrossChainHarness.mintTokensPrivateOnL2(wethAmountToBridge, secretHashForRedeemingWeth); - await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); - await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge); - - // 2. owner gives uniswap approval to unshield funds: - logger('Approving uniswap to unshield funds to self on my behalf'); - const nonceForWETHUnshieldApproval = new Fr(3n); - const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) - .request(), - ); - await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); - - // 3. Swap but send the wrong token address - logger('Swap but send the wrong token address'); - await expect( - uniswapL2Contract.methods - .swap_private( - wethCrossChainHarness.l2Token.address, // send weth token - daiCrossChainHarness.l2Bridge.address, // but dai bridge! - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHUnshieldApproval, - uniswapFeeTier, - minimumOutputAmount, - Fr.random(), - Fr.random(), - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - ) - .simulate(), - ).rejects.toThrowError('Assertion failed: input_asset address is not the same as seen in the bridge contract'); - }); - - // edge cases for public flow: - - it("I don't need approval to call swap_public if I'm swapping on my own behalf", async () => { - // 1. get tokens on l2 - await wethCrossChainHarness.mintTokensPublicOnL2(wethAmountToBridge); - - // 2. Give approval to uniswap to transfer funds to itself - const nonceForWETHTransferApproval = new Fr(2n); - const transferMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) - .request(), - ); - await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); - - // No approval to call `swap` but should work even without it: - const [_, secretHashForDepositingSwappedDai] = await daiCrossChainHarness.generateClaimSecret(); - - const withdrawReceipt = await uniswapL2Contract.methods - .swap_public( - ownerAddress, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHTransferApproval, - uniswapFeeTier, - minimumOutputAmount, - ownerAddress, - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - Fr.ZERO, // nonce for swap -> doesn't matter - ) - .send() - .wait(); - 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.expectPublicBalanceOnL2(ownerAddress, 0n); - }); - - it("someone can't call swap_public on my behalf without approval", async () => { - // Owner approves a a user to swap_public: - const approvedUser = AztecAddress.random(); - - const nonceForWETHTransferApproval = new Fr(3n); - const nonceForSwap = new Fr(3n); - const secretHashForDepositingSwappedDai = new Fr(4n); - const action = uniswapL2Contract - .withWallet(sponsorWallet) - .methods.swap_public( - ownerAddress, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHTransferApproval, - uniswapFeeTier, - minimumOutputAmount, - ownerAddress, - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - nonceForSwap, - ); - const swapMessageHash = await computeAuthWitMessageHash(approvedUser, action.request()); - await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait(); - - // Swap! - await expect(action.simulate()).rejects.toThrowError( - "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", - ); - }); - - it("uniswap can't pull funds without transfer approval", async () => { - // swap should fail since no transfer approval to uniswap: - const nonceForWETHTransferApproval = new Fr(4n); - - const transferMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) - .request(), - ); - await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); - - await expect( - uniswapL2Contract.methods - .swap_public( - ownerAddress, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - new Fr(420), // using a different nonce - uniswapFeeTier, - minimumOutputAmount, - ownerAddress, - Fr.random(), - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - Fr.ZERO, - ) - .simulate(), - ).rejects.toThrowError(`Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'`); - }); - - // tests when trying to mix private and public flows: - it("can't call swap_public on L1 if called swap_private on L2", async () => { - // get tokens on L2: - const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); - logger('minting weth on L2'); - await wethCrossChainHarness.mintTokensPrivateOnL2(wethAmountToBridge, secretHashForRedeemingWeth); - await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); - - // Owner gives uniswap approval to unshield funds to self on its behalf - logger('Approving uniswap to unshield funds to self on my behalf'); - const nonceForWETHUnshieldApproval = new Fr(4n); - const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) - .request(), - ); - await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); - const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); +let teardown: () => Promise; - // Swap - logger('Withdrawing weth to L1 and sending message to swap to dai'); - const secretHashForDepositingSwappedDai = Fr.random(); +const testSetup = async (): Promise => { + const { + teardown: teardown_, + pxe, + deployL1ContractsValues, + wallets, + logger, + } = await e2eSetup(2, { stateLoad: dumpedState }); - const withdrawReceipt = await uniswapL2Contract.methods - .swap_private( - wethCrossChainHarness.l2Token.address, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHUnshieldApproval, - uniswapFeeTier, - minimumOutputAmount, - Fr.random(), - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - ) - .send() - .wait(); - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - // ensure that user's funds were burnt - await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); + const walletClient = deployL1ContractsValues.walletClient; + const publicClient = deployL1ContractsValues.publicClient; - // On L1 call swap_public! - logger('call swap_public on L1'); - const swapArgs = [ - wethCrossChainHarness.tokenPortalAddress.toString(), - wethAmountToBridge, - uniswapFeeTier, - daiCrossChainHarness.tokenPortalAddress.toString(), - minimumOutputAmount, - ownerAddress.toString(), - secretHashForDepositingSwappedDai.toString(true), - deadlineForDepositingSwappedDai, - ownerEthAddress.toString(), - true, - ] as const; - await expect( - uniswapPortal.simulate.swapPublic(swapArgs, { - account: ownerEthAddress.toString(), - } as any), - ).rejects.toThrowError('The contract function "swapPublic" reverted.'); - }); + const ownerWallet = wallets[0]; + const sponsorWallet = wallets[1]; - it("can't call swap_private on L1 if called swap_public on L2", async () => { - // get tokens on L2: - await wethCrossChainHarness.mintTokensPublicOnL2(wethAmountToBridge); + teardown = teardown_; - // Owner gives uniswap approval to transfer funds on its behalf - const nonceForWETHTransferApproval = new Fr(5n); - const transferMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethCrossChainHarness.l2Token.methods - .transfer_public(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHTransferApproval) - .request(), - ); - await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); + return { pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet }; +}; - // Call swap_public on L2 - const secretHashForDepositingSwappedDai = Fr.random(); - const withdrawReceipt = await uniswapL2Contract.methods - .swap_public( - ownerAddress, - wethCrossChainHarness.l2Bridge.address, - wethAmountToBridge, - daiCrossChainHarness.l2Bridge.address, - nonceForWETHTransferApproval, - uniswapFeeTier, - minimumOutputAmount, - ownerAddress, - secretHashForDepositingSwappedDai, - deadlineForDepositingSwappedDai, - ownerEthAddress, - ownerEthAddress, - Fr.ZERO, - ) - .send() - .wait(); - 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.expectPublicBalanceOnL2(ownerAddress, 0n); +const testCleanup = async () => { + await teardown(); +}; - // Call swap_private on L1 - const secretHashForRedeemingDai = Fr.random(); // creating my own secret hash - logger('Execute withdraw and swap on the uniswapPortal!'); - const swapArgs = [ - wethCrossChainHarness.tokenPortalAddress.toString(), - wethAmountToBridge, - uniswapFeeTier, - daiCrossChainHarness.tokenPortalAddress.toString(), - minimumOutputAmount, - secretHashForRedeemingDai.toString(true), - secretHashForDepositingSwappedDai.toString(true), - deadlineForDepositingSwappedDai, - ownerEthAddress.toString(), - true, - ] as const; - await expect( - uniswapPortal.simulate.swapPrivate(swapArgs, { - account: ownerEthAddress.toString(), - } as any), - ).rejects.toThrowError('The contract function "swapPrivate" reverted.'); - }); -}); +uniswapL1L2TestSuite(testSetup, testCleanup, EXPECTED_FORKED_BLOCK);