diff --git a/yarn-project/archiver/src/archiver/config.ts b/yarn-project/archiver/src/archiver/config.ts index 46652800e26..292475dcdfb 100644 --- a/yarn-project/archiver/src/archiver/config.ts +++ b/yarn-project/archiver/src/archiver/config.ts @@ -63,6 +63,7 @@ export function getConfigEnvVars(): ArchiverConfig { SEARCH_START_BLOCK, API_KEY, INBOX_CONTRACT_ADDRESS, + OUTBOX_CONTRACT_ADDRESS, REGISTRY_CONTRACT_ADDRESS, DATA_DIRECTORY, } = process.env; @@ -71,7 +72,7 @@ export function getConfigEnvVars(): ArchiverConfig { rollupAddress: ROLLUP_CONTRACT_ADDRESS ? EthAddress.fromString(ROLLUP_CONTRACT_ADDRESS) : EthAddress.ZERO, registryAddress: REGISTRY_CONTRACT_ADDRESS ? EthAddress.fromString(REGISTRY_CONTRACT_ADDRESS) : EthAddress.ZERO, inboxAddress: INBOX_CONTRACT_ADDRESS ? EthAddress.fromString(INBOX_CONTRACT_ADDRESS) : EthAddress.ZERO, - outboxAddress: EthAddress.ZERO, + outboxAddress: OUTBOX_CONTRACT_ADDRESS ? EthAddress.fromString(OUTBOX_CONTRACT_ADDRESS) : EthAddress.ZERO, contractDeploymentEmitterAddress: CONTRACT_DEPLOYMENT_EMITTER_ADDRESS ? EthAddress.fromString(CONTRACT_DEPLOYMENT_EMITTER_ADDRESS) : EthAddress.ZERO, diff --git a/yarn-project/canary/package.json b/yarn-project/canary/package.json index d621c6008e8..4bdaf4e0495 100644 --- a/yarn-project/canary/package.json +++ b/yarn-project/canary/package.json @@ -22,11 +22,7 @@ }, "dependencies": { "@aztec/aztec.js": "workspace:^", - "@aztec/circuits.js": "workspace:^", - "@aztec/cli": "workspace:^", "@aztec/end-to-end": "workspace:^", - "@aztec/l1-artifacts": "workspace:^", - "@aztec/noir-contracts": "workspace:^", "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", "@types/koa-static": "^4.0.2", 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 a478d20a908..4b5d5256af6 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,439 +1,36 @@ -import { - AccountWallet, - AztecAddress, - EthAddress, - Fr, - NotePreimage, - TxHash, - TxStatus, - computeAuthWitMessageHash, - computeMessageSecretHash, - createDebugLogger, - createPXEClient, - getL1ContractAddresses, - getSandboxAccountsWallets, - sleep, - waitForSandbox, -} from '@aztec/aztec.js'; -import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; -import { TokenBridgeContract, TokenContract, UniswapContract } from '@aztec/noir-contracts/types'; +import { createDebugLogger, createPXEClient, getSandboxAccountsWallets, waitForSandbox } from '@aztec/aztec.js'; +import { UniswapSetupContext, uniswapL1L2TestSuite } from '@aztec/end-to-end'; -import { - HDAccount, - HttpTransport, - PublicClient, - WalletClient, - createPublicClient, - createWalletClient, - getContract, - http, - parseEther, -} from 'viem'; +import { createPublicClient, createWalletClient, http } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; -import { Chain, foundry } from 'viem/chains'; - -import { deployAndInitializeTokenAndBridgeContracts, deployL1Contract } from './utils.js'; - -const logger = createDebugLogger('aztec:canary'); +import { foundry } from 'viem/chains'; 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 WETH9_ADDRESS = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); -const DAI_ADDRESS = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); - -const EXPECTED_FORKED_BLOCK = 17514288; - -const pxeRpcUrl = PXE_URL; -const ethRpcUrl = ETHEREUM_HOST; - const hdAccount = mnemonicToAccount(MNEMONIC); - -const pxe = createPXEClient(pxeRpcUrl); - -const wethAmountToBridge: bigint = parseEther('1'); -const uniswapFeeTier = 3000; -const minimumOutputAmount = 0n; -const deadline = 2 ** 32 - 1; // max uint32 - 1 - -/** - * Deploys all l1 / l2 contracts - * @param owner - Owner address. - */ -async function deployAllContracts( - ownerWallet: AccountWallet, - ownerAddress: AztecAddress, - publicClient: PublicClient, - walletClient: WalletClient, -) { - const l1ContractsAddresses = await getL1ContractAddresses(pxeRpcUrl); - logger('Deploying DAI Portal, initializing and deploying l2 contract...'); - const daiContracts = await deployAndInitializeTokenAndBridgeContracts( - ownerWallet, - walletClient, - publicClient, - l1ContractsAddresses!.registryAddress, - ownerAddress, - DAI_ADDRESS, - ); - const daiL2Contract = daiContracts.token; - const daiL2Bridge = daiContracts.bridge; - const daiContract = daiContracts.underlyingERC20; - const daiTokenPortalAddress = daiContracts.tokenPortalAddress; - - logger('Deploying WETH Portal, initializing and deploying l2 contract...'); - const wethContracts = await deployAndInitializeTokenAndBridgeContracts( - ownerWallet, - walletClient, - publicClient, - l1ContractsAddresses!.registryAddress, - ownerAddress, - WETH9_ADDRESS, - ); - const wethL2Contract = wethContracts.token; - const wethL2Bridge = wethContracts.bridge; - const wethContract = wethContracts.underlyingERC20; - const wethTokenPortal = wethContracts.tokenPortal; - const wethTokenPortalAddress = wethContracts.tokenPortalAddress; - - logger('Deploy Uniswap portal on L1 and L2...'); - const uniswapPortalAddress = await deployL1Contract( - walletClient, - publicClient, - UniswapPortalAbi, - UniswapPortalBytecode, - ); - const uniswapPortal = getContract({ - address: uniswapPortalAddress.toString(), - abi: UniswapPortalAbi, - walletClient, - publicClient, +// This tests works on forked mainnet, configured on the CI. +const EXPECTED_FORKED_BLOCK = 17514288; +// 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), + }); + const publicClient = createPublicClient({ + chain: foundry, + transport: http(ETHEREUM_HOST), }); - // deploy l2 uniswap contract and attach to portal - const uniswapL2Contract = await UniswapContract.deploy(ownerWallet) - .send({ portalContract: uniswapPortalAddress }) - .deployed(); - - await uniswapPortal.write.initialize( - [l1ContractsAddresses!.registryAddress.toString(), uniswapL2Contract.address.toString()], - {} as any, - ); - - return { - daiL2Contract, - daiL2Bridge, - daiContract, - daiTokenPortalAddress, - wethL2Contract, - wethL2Bridge, - wethContract, - wethTokenPortal, - wethTokenPortalAddress, - uniswapL2Contract, - uniswapPortal, - uniswapPortalAddress, - }; -} - -const getL2PrivateBalanceOf = async (owner: AztecAddress, l2Contract: TokenContract) => { - return await l2Contract.methods.balance_of_private(owner).view({ from: owner }); -}; - -const getL2PublicBalanceOf = async (owner: AztecAddress, l2Contract: TokenContract) => { - return await l2Contract.methods.balance_of_public(owner).view(); -}; - -const expectPrivateBalanceOnL2 = async (owner: AztecAddress, expectedBalance: bigint, l2Contract: TokenContract) => { - const balance = await getL2PrivateBalanceOf(owner, l2Contract); - logger(`Account ${owner} balance: ${balance}. Expected to be: ${expectedBalance}`); - expect(balance).toBe(expectedBalance); -}; - -const expectPublicBalanceOnL2 = async (owner: AztecAddress, expectedBalance: bigint, l2Contract: TokenContract) => { - const balance = await getL2PublicBalanceOf(owner, l2Contract); - logger(`Account ${owner} balance: ${balance}. Expected to be: ${expectedBalance}`); - expect(balance).toBe(expectedBalance); -}; - -const generateClaimSecret = async () => { - const secret = Fr.random(); - const secretHash = await computeMessageSecretHash(secret); - return [secret, secretHash]; -}; - -const transferWethOnL2 = async ( - wethL2Contract: TokenContract, - ownerAddress: AztecAddress, - receiver: AztecAddress, - transferAmount: bigint, -) => { - const transferTx = wethL2Contract.methods.transfer_public(ownerAddress, receiver, transferAmount, 0).send(); - const receipt = await transferTx.wait(); - expect(receipt.status).toBe(TxStatus.MINED); -}; - -const consumeMessageOnAztecAndMintSecretly = async ( - l2Bridge: TokenBridgeContract, - bridgeAmount: bigint, - secretHashForRedeemingMintedNotes: Fr, - canceller: EthAddress, - messageKey: Fr, - secretForL2MessageConsumption: Fr, -) => { - logger('Consuming messages on L2 secretively'); - // Call the mint tokens function on the Aztec.nr contract - const consumptionTx = l2Bridge.methods - .claim_private( - bridgeAmount, - secretHashForRedeemingMintedNotes, - canceller, - messageKey, - secretForL2MessageConsumption, - ) - .send(); - const consumptionReceipt = await consumptionTx.wait(); - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - return consumptionReceipt.txHash; -}; - -const redeemShieldPrivatelyOnL2 = async ( - l2Contract: TokenContract, - to: AztecAddress, - shieldAmount: bigint, - secret: Fr, - secretHash: Fr, - txHash: TxHash, -) => { - // Add the note to the pxe. - const storageSlot = new Fr(5); - const preimage = new NotePreimage([new Fr(shieldAmount), secretHash]); - await pxe.addNote(to, l2Contract.address, storageSlot, preimage, txHash); + const [ownerWallet, sponsorWallet] = await getSandboxAccountsWallets(pxe); - logger('Spending commitment in private call'); - const privateTx = l2Contract.methods.redeem_shield(to, shieldAmount, secret).send(); - const privateReceipt = await privateTx.wait(); - expect(privateReceipt.status).toBe(TxStatus.MINED); + return { pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet }; }; -describe('uniswap_trade_on_l1_from_l2', () => { - let publicClient: PublicClient; - let walletClient: WalletClient; - let ownerWallet: AccountWallet; - let ownerAddress: AztecAddress; - let ownerEthAddress: EthAddress; - - beforeAll(async () => { - await waitForSandbox(pxe); - - walletClient = createWalletClient({ - account: hdAccount, - chain: foundry, - transport: http(ethRpcUrl), - }); - publicClient = createPublicClient({ - chain: foundry, - transport: http(ethRpcUrl), - }); - - 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'); - } - - ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); - }, 60_000); - it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { - logger('Running L1/L2 messaging test on HTTP interface.'); - - [ownerWallet] = await getSandboxAccountsWallets(pxe); - const accounts = await ownerWallet.getRegisteredAccounts(); - ownerAddress = accounts[0].address; - const receiver = accounts[1].address; - - const result = await deployAllContracts(ownerWallet, ownerAddress, publicClient, walletClient); - const { - daiL2Contract, - daiL2Bridge, - daiContract, - daiTokenPortalAddress, - wethL2Contract, - wethL2Bridge, - wethContract, - wethTokenPortal, - wethTokenPortalAddress, - uniswapL2Contract, - uniswapPortal, - } = result; - - const ownerInitialBalance = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); - logger(`Owner's initial L2 WETH balance: ${ownerInitialBalance}`); - - // 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 wethL1BeforeBalance = await wethContract.read.balanceOf([ownerEthAddress.toString()]); - // 1. Approve weth to be bridged - await wethContract.write.approve([wethTokenPortalAddress.toString(), wethAmountToBridge], {} as any); - - // 2. Deposit weth into the portal and move to L2 - // generate secret - const [secretForMintingWeth, secretHashForMintingWeth] = await generateClaimSecret(); - const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await generateClaimSecret(); - logger('Sending messages to L1 portal'); - const args = [ - wethAmountToBridge, - secretHashForRedeemingWeth.toString(true), - ownerEthAddress.toString(), - deadline, - secretHashForMintingWeth.toString(true), - ] as const; - const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztecPrivate(args, { - account: ownerEthAddress.toString(), - } as any); - await wethTokenPortal.write.depositToAztecPrivate(args, {} as any); - - const currentL1Balance = await wethContract.read.balanceOf([ownerEthAddress.toString()]); - logger(`Initial Balance: ${currentL1Balance}. Should be: ${wethL1BeforeBalance - wethAmountToBridge}`); - expect(currentL1Balance).toBe(wethL1BeforeBalance - wethAmountToBridge); - const messageKey = Fr.fromString(messageKeyHex); - - // Wait for the archiver to process the message - await sleep(5000); - // send a transfer tx to force through rollup with the message included - await transferWethOnL2(wethL2Contract, ownerAddress, receiver, 0n); - - // 3. Claim WETH on L2 - logger('Minting weth on L2'); - const redeemingWethTxHash = await consumeMessageOnAztecAndMintSecretly( - wethL2Bridge, - wethAmountToBridge, - secretHashForRedeemingWeth, - ownerEthAddress, - messageKey, - secretForMintingWeth, - ); - await redeemShieldPrivatelyOnL2( - wethL2Contract, - ownerAddress, - wethAmountToBridge, - secretForRedeemingWeth, - secretHashForRedeemingWeth, - redeemingWethTxHash, - ); - await expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge + BigInt(ownerInitialBalance), wethL2Contract); - - // Store balances - const wethL2BalanceBeforeSwap = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); - const daiL2BalanceBeforeSwap = await getL2PrivateBalanceOf(ownerAddress, daiL2Contract); - - // 4. 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(2n); - const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( - uniswapL2Contract.address, - wethL2Contract.methods - .unshield(ownerAddress, uniswapL2Contract.address, wethAmountToBridge, nonceForWETHUnshieldApproval) - .request(), - ); - await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); - - // 5. 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 generateClaimSecret(); - const [secretForRedeemingDai, secretHashForRedeemingDai] = await generateClaimSecret(); - - const withdrawReceipt = await uniswapL2Contract.methods - .swap_private( - wethL2Contract.address, - wethL2Bridge.address, - wethAmountToBridge, - daiL2Bridge.address, - nonceForWETHUnshieldApproval, - uniswapFeeTier, - minimumOutputAmount, - secretHashForRedeemingDai, - secretHashForDepositingSwappedDai, - deadline, - ownerEthAddress, - ownerEthAddress, - ) - .send() - .wait(); - expect(withdrawReceipt.status).toBe(TxStatus.MINED); - // ensure that user's funds were burnt - await expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge, wethL2Contract); - // ensure that uniswap contract didn't eat the funds. - await expectPublicBalanceOnL2(uniswapL2Contract.address, 0n, wethL2Contract); - - // 6. Consume L2 to L1 message by calling uniswapPortal.swap_private() - logger('Execute withdraw and swap on the uniswapPortal!'); - const daiL1BalanceOfPortalBeforeSwap = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); - const swapArgs = [ - wethTokenPortalAddress.toString(), - wethAmountToBridge, - uniswapFeeTier, - daiTokenPortalAddress.toString(), - minimumOutputAmount, - secretHashForRedeemingDai.toString(true), - secretHashForDepositingSwappedDai.toString(true), - deadline, - 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 daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); - logger( - `DAI balance in Portal: balance after (${daiL1BalanceOfPortalAfter}) should be bigger than balance before (${daiL1BalanceOfPortalBeforeSwap})`, - ); - expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); - const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); - - // Wait for the archiver to process the message - await sleep(5000); - // send a transfer tx to force through rollup with the message included - await transferWethOnL2(wethL2Contract, ownerAddress, receiver, 0n); - - // 7. claim dai on L2 - logger('Consuming messages to mint dai on L2'); - const redeemingDaiTxHash = await consumeMessageOnAztecAndMintSecretly( - daiL2Bridge, - daiAmountToBridge, - secretHashForRedeemingDai, - ownerEthAddress, - depositDaiMessageKey, - secretForDepositingSwappedDai, - ); - await redeemShieldPrivatelyOnL2( - daiL2Contract, - ownerAddress, - daiAmountToBridge, - secretForRedeemingDai, - secretHashForRedeemingDai, - redeemingDaiTxHash, - ); - await expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge, daiL2Contract); - - const wethL2BalanceAfterSwap = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); - const daiL2BalanceAfterSwap = await getL2PrivateBalanceOf(ownerAddress, daiL2Contract); - - 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()); - }, 140_000); -}); +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 4a05c6713f5..00000000000 --- a/yarn-project/canary/src/utils.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { AztecAddress, EthAddress, TxStatus, Wallet } from '@aztec/aztec.js'; -import { 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!); -} diff --git a/yarn-project/canary/tsconfig.json b/yarn-project/canary/tsconfig.json index d02659b19cd..7621a947004 100644 --- a/yarn-project/canary/tsconfig.json +++ b/yarn-project/canary/tsconfig.json @@ -22,21 +22,9 @@ { "path": "../aztec.js" }, - { - "path": "../circuits.js" - }, - { - "path": "../cli" - }, { "path": "../end-to-end" }, - { - "path": "../l1-artifacts" - }, - { - "path": "../noir-contracts" - } ], "include": ["src"] } 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 00000000000..09b896ea640 --- /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 + + 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/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 1f2b8209d66..55a2e988ef0 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -23,25 +23,14 @@ describe('e2e_cross_chain_messaging', () => { let outbox: any; beforeEach(async () => { - const { - aztecNode, - pxe, - deployL1ContractsValues, - accounts, - wallets, - logger: logger_, - cheatCodes, - teardown: teardown_, - } = await setup(2); + const { pxe, deployL1ContractsValues, wallets, logger: logger_, teardown: teardown_ } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( - aztecNode, pxe, - deployL1ContractsValues, - accounts, + deployL1ContractsValues.publicClient, + deployL1ContractsValues.walletClient, wallets[0], logger_, - cheatCodes, ); l2Token = crossChainTestHarness.l2Token; @@ -58,7 +47,6 @@ describe('e2e_cross_chain_messaging', () => { afterEach(async () => { await teardown(); - await crossChainTestHarness?.stop(); }); it('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { @@ -230,7 +218,7 @@ describe('e2e_cross_chain_messaging', () => { await delay(5000); /// waiting 5 seconds. // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. - await crossChainTestHarness.performL2Transfer(0n); + await crossChainTestHarness.mintTokensPublicOnL2(0n); // 3. Consume L1-> L2 message and try to mint publicly on L2 - should fail await expect( diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index 6d77f9826e7..329dc5b026d 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -23,24 +23,13 @@ describe('e2e_public_cross_chain_messaging', () => { let outbox: any; beforeEach(async () => { - const { - aztecNode: aztecNode_, - pxe, - deployL1ContractsValues, - accounts, - wallets, - logger: logger_, - teardown: teardown_, - cheatCodes, - } = await setup(2); + const { pxe, deployL1ContractsValues, wallets, logger: logger_, teardown: teardown_ } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( - aztecNode_, pxe, - deployL1ContractsValues, - accounts, + deployL1ContractsValues.publicClient, + deployL1ContractsValues.walletClient, wallets[0], logger_, - cheatCodes, ); l2Token = crossChainTestHarness.l2Token; l2Bridge = crossChainTestHarness.l2Bridge; @@ -57,7 +46,6 @@ describe('e2e_public_cross_chain_messaging', () => { afterEach(async () => { await teardown(); - await crossChainTestHarness?.stop(); }); it('Milestone 2: Deposit funds from L1 -> L2 and withdraw back to L1', async () => { @@ -184,7 +172,7 @@ describe('e2e_public_cross_chain_messaging', () => { await delay(5000); /// waiting 5 seconds. // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. - await crossChainTestHarness.performL2Transfer(0n); + await crossChainTestHarness.mintTokensPublicOnL2(0n); await expect( l2Bridge diff --git a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts index 3514744c0a7..df236df5ec8 100644 --- a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts @@ -18,24 +18,13 @@ describe('e2e_public_to_private_messaging', () => { let crossChainTestHarness: CrossChainTestHarness; beforeEach(async () => { - const { - aztecNode, - pxe, - deployL1ContractsValues, - accounts, - wallet, - logger: logger_, - cheatCodes, - teardown: teardown_, - } = await setup(2); + const { pxe, deployL1ContractsValues, wallet, logger: logger_, teardown: teardown_ } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( - aztecNode, pxe, - deployL1ContractsValues, - accounts, + deployL1ContractsValues.publicClient, + deployL1ContractsValues.walletClient, wallet, logger_, - cheatCodes, ); ethAccount = crossChainTestHarness.ethAccount; @@ -49,7 +38,6 @@ describe('e2e_public_to_private_messaging', () => { afterEach(async () => { await teardown(); - await crossChainTestHarness?.stop(); }); it('Milestone 5.4: Should be able to create a commitment in a public function and spend in a private function', async () => { diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 6e9c6077b41..6aba183bda5 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -1,14 +1,10 @@ -import { AztecNodeService } from '@aztec/aztec-node'; -import { CheatCodes, TxHash, Wallet, computeMessageSecretHash } from '@aztec/aztec.js'; -import { AztecAddress, CompleteAddress, EthAddress, Fr, PublicKey } from '@aztec/circuits.js'; -import { DeployL1Contracts } from '@aztec/ethereum'; -import { toBufferBE } from '@aztec/foundation/bigint-buffer'; +import { TxHash, Wallet, computeMessageSecretHash } from '@aztec/aztec.js'; +import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js'; import { sha256ToField } from '@aztec/foundation/crypto'; import { DebugLogger } from '@aztec/foundation/log'; import { OutboxAbi } from '@aztec/l1-artifacts'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; -import { PXEService } from '@aztec/pxe'; -import { AztecNode, NotePreimage, PXE, TxStatus } from '@aztec/types'; +import { NotePreimage, PXE, TxStatus } from '@aztec/types'; import { Chain, HttpTransport, PublicClient, getContract } from 'viem'; @@ -20,61 +16,41 @@ import { deployAndInitializeTokenAndBridgeContracts } from './utils.js'; */ export class CrossChainTestHarness { static async new( - aztecNode: AztecNode | undefined, pxeService: PXE, - deployL1ContractsValues: DeployL1Contracts, - accounts: CompleteAddress[], + publicClient: PublicClient, + walletClient: any, wallet: Wallet, logger: DebugLogger, - cheatCodes: CheatCodes, underlyingERC20Address?: EthAddress, - initialBalance?: bigint, ): Promise { - const walletClient = deployL1ContractsValues.walletClient; - const publicClient = deployL1ContractsValues.publicClient; const ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); - const [owner, receiver] = accounts; + const owner = wallet.getCompleteAddress(); + const l1ContractAddresses = (await pxeService.getNodeInfo()).l1ContractAddresses; const outbox = getContract({ - address: deployL1ContractsValues.l1ContractAddresses.outboxAddress!.toString(), + address: l1ContractAddresses.outboxAddress.toString(), abi: OutboxAbi, publicClient, }); // Deploy and initialize all required contracts logger('Deploying and initializing token, portal and its bridge...'); - const contracts = await deployAndInitializeTokenAndBridgeContracts( - wallet, - walletClient, - publicClient, - deployL1ContractsValues!.l1ContractAddresses.registryAddress!, - owner.address, - underlyingERC20Address, - ); - const l2Token = contracts.token; - const l2Bridge = contracts.bridge; - const underlyingERC20 = contracts.underlyingERC20; - const tokenPortal = contracts.tokenPortal; - const tokenPortalAddress = contracts.tokenPortalAddress; + 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.'); - if (initialBalance) { - logger(`Minting ${initialBalance} tokens to ${owner.address}...`); - const mintTx = l2Token.methods.mint_public(owner.address, initialBalance).send(); - const mintReceipt = await mintTx.wait(); - expect(mintReceipt.status).toBe(TxStatus.MINED); - expect(l2Token.methods.balance_of_public(owner.address).view()).toBe(initialBalance); - logger(`Minted ${initialBalance} tokens to ${owner.address}.`); - } - return new CrossChainTestHarness( - aztecNode, pxeService, - cheatCodes, - accounts, logger, - l2Token, - l2Bridge, + token, + bridge, ethAccount, tokenPortalAddress, tokenPortal, @@ -83,19 +59,12 @@ export class CrossChainTestHarness { publicClient, walletClient, owner.address, - receiver.address, - owner.publicKey, ); } + constructor( - /** AztecNode. */ - public aztecNode: AztecNode | undefined, /** Private eXecution Environment (PXE). */ public pxeService: PXE, - /** CheatCodes. */ - public cc: CheatCodes, - /** Accounts. */ - public accounts: CompleteAddress[], /** Logger. */ public logger: DebugLogger, @@ -122,10 +91,6 @@ export class CrossChainTestHarness { /** Aztec address to use in tests. */ public ownerAddress: AztecAddress, - /** Another Aztec Address to use in tests. */ - public receiver: AztecAddress, - /** The owners public key. */ - public ownerPub: PublicKey, ) {} async generateClaimSecret(): Promise<[Fr, Fr]> { @@ -150,7 +115,7 @@ export class CrossChainTestHarness { await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); // Deposit tokens to the TokenPortal - const deadline = 2 ** 32 - 1; // max uint32 - 1 + const deadline = 2 ** 32 - 1; // max uint32 this.logger('Sending messages to L1 portal to be consumed publicly'); const args = [ @@ -176,7 +141,7 @@ export class CrossChainTestHarness { await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); // Deposit tokens to the TokenPortal - const deadline = 2 ** 32 - 1; // max uint32 - 1 + const deadline = 2 ** 32 - 1; // max uint32 this.logger('Sending messages to L1 portal to be consumed privately'); const args = [ @@ -208,9 +173,11 @@ export class CrossChainTestHarness { await this.addPendingShieldNoteToPXE(amount, secretHash, receipt.txHash); } - async performL2Transfer(transferAmount: bigint) { + 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, this.receiver, transferAmount, 0).send(); + const transferTx = this.l2Token.methods + .transfer_public(this.ownerAddress, receiverAddress, transferAmount, 0) + .send(); const receipt = await transferTx.wait(); expect(receipt.status).toBe(TxStatus.MINED); } @@ -285,12 +252,11 @@ export class CrossChainTestHarness { async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise { this.logger('Ensure that the entry is not in outbox yet'); - const contractData = await this.pxeService.getContractData(this.l2Bridge.address); // 0xb460af94, selector for "withdraw(uint256,address,address)" const content = sha256ToField( Buffer.concat([ Buffer.from([0xb4, 0x60, 0xaf, 0x94]), - toBufferBE(withdrawAmount, 32), + new Fr(withdrawAmount).toBuffer(), this.ethAccount.toBuffer32(), callerOnL1.toBuffer32(), ]), @@ -299,7 +265,7 @@ export class CrossChainTestHarness { Buffer.concat([ this.l2Bridge.address.toBuffer(), new Fr(1).toBuffer(), // aztec version - contractData?.portalContractAddress.toBuffer32() ?? Buffer.alloc(32, 0), + this.tokenPortalAddress.toBuffer32() ?? Buffer.alloc(32, 0), new Fr(this.publicClient.chain.id).toBuffer(), // chain id content.toBuffer(), ]), @@ -356,11 +322,4 @@ export class CrossChainTestHarness { const unshieldReceipt = await unshieldTx.wait(); expect(unshieldReceipt.status).toBe(TxStatus.MINED); } - - async stop() { - if (this.aztecNode instanceof AztecNodeService) await this.aztecNode?.stop(); - if (this.pxeService instanceof PXEService) { - await this.pxeService?.stop(); - } - } } diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index ea37f3056c5..be53fabf8c2 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -289,6 +289,7 @@ export async function setup(numberOfAccounts = 1, opts: SetupOptions = {}): Prom config.l1Contracts.contractDeploymentEmitterAddress = deployL1ContractsValues.l1ContractAddresses.contractDeploymentEmitterAddress; config.l1Contracts.inboxAddress = deployL1ContractsValues.l1ContractAddresses.inboxAddress; + config.l1Contracts.outboxAddress = deployL1ContractsValues.l1ContractAddresses.outboxAddress; logger('Creating and synching an aztec node...'); const aztecNode = await AztecNodeService.createAndSync(config); diff --git a/yarn-project/end-to-end/src/index.ts b/yarn-project/end-to-end/src/index.ts index dd9005c49c9..da0a55181ff 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 8e26fb3a817..7d9c139a6c0 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,739 +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 { AztecNode, 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 aztecNode: AztecNode | undefined; - 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_, - aztecNode: aztecNode_, - pxe: pxe_, - deployL1ContractsValues, - accounts, - wallets, - logger: logger_, - cheatCodes, - } = 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'); - } - - aztecNode = aztecNode_; - 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( - aztecNode, - pxe, - deployL1ContractsValues, - accounts, - ownerWallet, - logger, - cheatCodes, - DAI_ADDRESS, - ); - - logger('Deploying WETH Portal, initializing and deploying l2 contract...'); - wethCrossChainHarness = await CrossChainTestHarness.new( - aztecNode, - pxe, - deployL1ContractsValues, - accounts, - ownerWallet, - logger, - cheatCodes, - 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('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.performL2Transfer(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.performL2Transfer(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); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 605dcd7c10a..ad79c7505a5 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -259,11 +259,7 @@ __metadata: resolution: "@aztec/canary@workspace:canary" dependencies: "@aztec/aztec.js": "workspace:^" - "@aztec/circuits.js": "workspace:^" - "@aztec/cli": "workspace:^" "@aztec/end-to-end": "workspace:^" - "@aztec/l1-artifacts": "workspace:^" - "@aztec/noir-contracts": "workspace:^" "@jest/globals": ^29.5.0 "@rushstack/eslint-patch": ^1.1.4 "@types/jest": ^29.5.0