diff --git a/yarn-project/canary/package.json b/yarn-project/canary/package.json index d621c6008e87..a4119bc5b1b5 100644 --- a/yarn-project/canary/package.json +++ b/yarn-project/canary/package.json @@ -25,6 +25,7 @@ "@aztec/circuits.js": "workspace:^", "@aztec/cli": "workspace:^", "@aztec/end-to-end": "workspace:^", + "@aztec/foundation": "workspace:^", "@aztec/l1-artifacts": "workspace:^", "@aztec/noir-contracts": "workspace:^", "@jest/globals": "^29.5.0", 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 a478d20a908c..f4877810d3e5 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,365 +1,203 @@ import { AccountWallet, AztecAddress, + DebugLogger, EthAddress, Fr, - NotePreimage, - TxHash, + PXE, TxStatus, computeAuthWitMessageHash, - computeMessageSecretHash, createDebugLogger, createPXEClient, - getL1ContractAddresses, + sleep as delay, getSandboxAccountsWallets, - sleep, waitForSandbox, } from '@aztec/aztec.js'; import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; -import { TokenBridgeContract, TokenContract, UniswapContract } from '@aztec/noir-contracts/types'; +import { UniswapContract } from '@aztec/noir-contracts/types'; -import { - HDAccount, - HttpTransport, - PublicClient, - WalletClient, - createPublicClient, - createWalletClient, - getContract, - http, - parseEther, -} from 'viem'; +import { jest } from '@jest/globals'; +import { createPublicClient, createWalletClient, getContract, http, parseEther } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; -import { Chain, foundry } from 'viem/chains'; - -import { deployAndInitializeTokenAndBridgeContracts, deployL1Contract } from './utils.js'; +import { foundry } from 'viem/chains'; -const logger = createDebugLogger('aztec:canary'); +import { CrossChainTestHarness, deployL1Contract } from './utils.js'; const { PXE_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; export const MNEMONIC = 'test test test test test test test test test test test junk'; - -const WETH9_ADDRESS = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); -const DAI_ADDRESS = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); +const hdAccount = mnemonicToAccount(MNEMONIC); 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, - }); +const TIMEOUT = 90_000; +describe('uniswap_trade_on_l1_from_l2', () => { + jest.setTimeout(TIMEOUT); + const WETH9_ADDRESS: EthAddress = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); + const DAI_ADDRESS: EthAddress = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); - // 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); - - 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); -}; + let pxe: PXE; + let logger: DebugLogger; + let walletClient: any; -describe('uniswap_trade_on_l1_from_l2', () => { - let publicClient: PublicClient; - let walletClient: WalletClient; let ownerWallet: AccountWallet; let ownerAddress: AztecAddress; let ownerEthAddress: EthAddress; + // does transactions on behalf of owner on Aztec: + let sponsorWallet: AccountWallet; + let sponsorAddress: AztecAddress; + + let daiCrossChainHarness: CrossChainTestHarness; + let wethCrossChainHarness: CrossChainTestHarness; + + let uniswapPortal: any; + let uniswapPortalAddress: EthAddress; + let uniswapL2Contract: UniswapContract; + + const wethAmountToBridge = parseEther('1'); + const uniswapFeeTier = 3000n; + const minimumOutputAmount = 0n; + const deadlineForDepositingSwappedDai = BigInt(2 ** 32 - 1); // max uint32 - 1 beforeAll(async () => { + logger = createDebugLogger('aztec:canary'); + pxe = createPXEClient(PXE_URL); await waitForSandbox(pxe); - walletClient = createWalletClient({ + const walletClient = createWalletClient({ account: hdAccount, chain: foundry, - transport: http(ethRpcUrl), + transport: http(ETHEREUM_HOST), }); - publicClient = createPublicClient({ + const publicClient = createPublicClient({ chain: foundry, - transport: http(ethRpcUrl), + transport: http(ETHEREUM_HOST), }); if (Number(await publicClient.getBlockNumber()) < EXPECTED_FORKED_BLOCK) { throw new Error('This test must be run on a fork of mainnet with the expected fork block'); } + [ownerWallet, sponsorWallet] = await getSandboxAccountsWallets(pxe); + ownerAddress = ownerWallet.getAddress(); + sponsorAddress = sponsorWallet.getAddress(); ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); - }, 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}`); + 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'), - }); + 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); + it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { + const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); - // 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); + // 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 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); + 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 sleep(5000); - // send a transfer tx to force through rollup with the message included - await transferWethOnL2(wethL2Contract, ownerAddress, receiver, 0n); + await delay(5000); + + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. + await wethCrossChainHarness.mintTokensPublicOnL2(0n); - // 3. Claim WETH on L2 + // 2. Claim WETH on L2 logger('Minting weth on L2'); - const redeemingWethTxHash = await consumeMessageOnAztecAndMintSecretly( - wethL2Bridge, + await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly( wethAmountToBridge, secretHashForRedeemingWeth, - ownerEthAddress, messageKey, secretForMintingWeth, ); - await redeemShieldPrivatelyOnL2( - wethL2Contract, - ownerAddress, - wethAmountToBridge, - secretForRedeemingWeth, - secretHashForRedeemingWeth, - redeemingWethTxHash, - ); - await expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge + BigInt(ownerInitialBalance), wethL2Contract); + await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge); // Store balances - const wethL2BalanceBeforeSwap = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); - const daiL2BalanceBeforeSwap = await getL2PrivateBalanceOf(ownerAddress, daiL2Contract); + 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(); - // 4. Owner gives uniswap approval to unshield funds to self on its behalf + // 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(2n); + const nonceForWETHUnshieldApproval = new Fr(1n); const unshieldToUniswapMessageHash = await computeAuthWitMessageHash( uniswapL2Contract.address, - wethL2Contract.methods + wethCrossChainHarness.l2Token.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. + // 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 generateClaimSecret(); - const [secretForRedeemingDai, secretHashForRedeemingDai] = await generateClaimSecret(); + const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = + await daiCrossChainHarness.generateClaimSecret(); + const [secretForRedeemingDai, secretHashForRedeemingDai] = await daiCrossChainHarness.generateClaimSecret(); const withdrawReceipt = await uniswapL2Contract.methods .swap_private( - wethL2Contract.address, - wethL2Bridge.address, + wethCrossChainHarness.l2Token.address, + wethCrossChainHarness.l2Bridge.address, wethAmountToBridge, - daiL2Bridge.address, + daiCrossChainHarness.l2Bridge.address, nonceForWETHUnshieldApproval, uniswapFeeTier, minimumOutputAmount, secretHashForRedeemingDai, secretHashForDepositingSwappedDai, - deadline, + deadlineForDepositingSwappedDai, ownerEthAddress, ownerEthAddress, ) @@ -367,22 +205,27 @@ describe('uniswap_trade_on_l1_from_l2', () => { .wait(); expect(withdrawReceipt.status).toBe(TxStatus.MINED); // ensure that user's funds were burnt - await expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge, wethL2Contract); + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); // ensure that uniswap contract didn't eat the funds. - await expectPublicBalanceOnL2(uniswapL2Contract.address, 0n, wethL2Contract); + 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); - // 6. Consume L2 to L1 message by calling uniswapPortal.swap_private() + // 5. 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 daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( + daiCrossChainHarness.tokenPortalAddress, + ); const swapArgs = [ - wethTokenPortalAddress.toString(), + wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, uniswapFeeTier, - daiTokenPortalAddress.toString(), + daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, secretHashForRedeemingDai.toString(true), secretHashForDepositingSwappedDai.toString(true), - deadline, + deadlineForDepositingSwappedDai, ownerEthAddress.toString(), true, ] as const; @@ -395,45 +238,176 @@ describe('uniswap_trade_on_l1_from_l2', () => { 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})`, + 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 sleep(5000); + await delay(5000); // send a transfer tx to force through rollup with the message included - await transferWethOnL2(wethL2Contract, ownerAddress, receiver, 0n); + await wethCrossChainHarness.mintTokensPublicOnL2(0n); - // 7. claim dai on L2 + // 6. claim dai on L2 logger('Consuming messages to mint dai on L2'); - const redeemingDaiTxHash = await consumeMessageOnAztecAndMintSecretly( - daiL2Bridge, + await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly( daiAmountToBridge, secretHashForRedeemingDai, - ownerEthAddress, depositDaiMessageKey, secretForDepositingSwappedDai, ); - await redeemShieldPrivatelyOnL2( - daiL2Contract, - ownerAddress, - daiAmountToBridge, - secretForRedeemingDai, - secretHashForRedeemingDai, - redeemingDaiTxHash, - ); - await expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge, daiL2Contract); + await daiCrossChainHarness.redeemShieldPrivatelyOnL2(daiAmountToBridge, secretForRedeemingDai); + await daiCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); - const wethL2BalanceAfterSwap = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); - const daiL2BalanceAfterSwap = await getL2PrivateBalanceOf(ownerAddress, daiL2Contract); + 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()); - }, 140_000); + 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()); + }); }); diff --git a/yarn-project/canary/src/utils.ts b/yarn-project/canary/src/utils.ts index 4a05c6713f5f..ce24a41f5174 100644 --- a/yarn-project/canary/src/utils.ts +++ b/yarn-project/canary/src/utils.ts @@ -1,5 +1,23 @@ -import { AztecAddress, EthAddress, TxStatus, Wallet } from '@aztec/aztec.js'; -import { PortalERC20Abi, PortalERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } from '@aztec/l1-artifacts'; +import { + AztecAddress, + DebugLogger, + EthAddress, + Fr, + NotePreimage, + PXE, + TxHash, + TxStatus, + Wallet, + computeMessageSecretHash, +} from '@aztec/aztec.js'; +import { sha256ToField } from '@aztec/foundation/crypto'; +import { + OutboxAbi, + PortalERC20Abi, + PortalERC20Bytecode, + TokenPortalAbi, + TokenPortalBytecode, +} from '@aztec/l1-artifacts'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; import type { Abi, Narrow } from 'abitype'; @@ -129,3 +147,317 @@ export async function deployL1Contract( return EthAddress.fromString(receipt.contractAddress!); } + +/** + * A Class for testing cross chain interactions, contains common interactions + * shared between cross chain tests. + */ +export class CrossChainTestHarness { + static async new( + pxeService: PXE, + publicClient: PublicClient, + walletClient: any, + wallet: Wallet, + logger: DebugLogger, + underlyingERC20Address?: EthAddress, + ): Promise { + const ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); + const owner = wallet.getCompleteAddress(); + const l1ContractAddresses = (await pxeService.getNodeInfo()).l1ContractAddresses; + + const outbox = getContract({ + address: l1ContractAddresses.outboxAddress!.toString(), + abi: OutboxAbi, + publicClient, + }); + + // Deploy and initialize all required contracts + logger('Deploying and initializing token, portal and its bridge...'); + const { token, bridge, tokenPortalAddress, tokenPortal, underlyingERC20 } = + await deployAndInitializeTokenAndBridgeContracts( + wallet, + walletClient, + publicClient, + l1ContractAddresses.registryAddress, + owner.address, + underlyingERC20Address, + ); + logger('Deployed and initialized token, portal and its bridge.'); + + return new CrossChainTestHarness( + pxeService, + logger, + token, + bridge, + ethAccount, + tokenPortalAddress, + tokenPortal, + underlyingERC20, + outbox, + publicClient, + walletClient, + owner.address, + ); + } + + constructor( + /** Private eXecution Environment (PXE). */ + public pxeService: PXE, + /** Logger. */ + public logger: DebugLogger, + + /** L2 Token contract. */ + public l2Token: TokenContract, + /** L2 Token bridge contract. */ + public l2Bridge: TokenBridgeContract, + + /** Eth account to interact with. */ + public ethAccount: EthAddress, + + /** Portal address. */ + public tokenPortalAddress: EthAddress, + /** Token portal instance. */ + public tokenPortal: any, + /** Underlying token for portal tests. */ + public underlyingERC20: any, + /** Message Bridge Outbox. */ + public outbox: any, + /** Viem Public client instance. */ + public publicClient: PublicClient, + /** Viem Wallet Client instance. */ + public walletClient: any, + + /** Aztec address to use in tests. */ + public ownerAddress: AztecAddress, + ) {} + + async generateClaimSecret(): Promise<[Fr, Fr]> { + this.logger("Generating a claim secret using pedersen's hash function"); + const secret = Fr.random(); + const secretHash = await computeMessageSecretHash(secret); + this.logger('Generated claim secret: ' + secretHash.toString(true)); + return [secret, secretHash]; + } + + async mintTokensOnL1(amount: bigint) { + this.logger('Minting tokens on L1'); + await this.underlyingERC20.write.mint([this.ethAccount.toString(), amount], {} as any); + expect(await this.underlyingERC20.read.balanceOf([this.ethAccount.toString()])).toBe(amount); + } + + async getL1BalanceOf(address: EthAddress) { + return await this.underlyingERC20.read.balanceOf([address.toString()]); + } + + async sendTokensToPortalPublic(bridgeAmount: bigint, secretHash: Fr) { + await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); + + // Deposit tokens to the TokenPortal + const deadline = 2 ** 32 - 1; // max uint32 - 1 + + this.logger('Sending messages to L1 portal to be consumed publicly'); + const args = [ + bridgeAmount, + this.ownerAddress.toString(), + this.ethAccount.toString(), + deadline, + secretHash.toString(true), + ] as const; + const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztecPublic(args, { + account: this.ethAccount.toString(), + } as any); + await this.tokenPortal.write.depositToAztecPublic(args, {} as any); + + return Fr.fromString(messageKeyHex); + } + + async sendTokensToPortalPrivate( + bridgeAmount: bigint, + secretHashForL2MessageConsumption: Fr, + secretHashForRedeemingMintedNotes: Fr, + ) { + await this.underlyingERC20.write.approve([this.tokenPortalAddress.toString(), bridgeAmount], {} as any); + + // Deposit tokens to the TokenPortal + const deadline = 2 ** 32 - 1; // max uint32 - 1 + + this.logger('Sending messages to L1 portal to be consumed privately'); + const args = [ + bridgeAmount, + secretHashForRedeemingMintedNotes.toString(true), + this.ethAccount.toString(), + deadline, + secretHashForL2MessageConsumption.toString(true), + ] as const; + const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztecPrivate(args, { + account: this.ethAccount.toString(), + } as any); + await this.tokenPortal.write.depositToAztecPrivate(args, {} as any); + + return Fr.fromString(messageKeyHex); + } + + async mintTokensPublicOnL2(amount: bigint) { + this.logger('Minting tokens on L2 publicly'); + const tx = this.l2Token.methods.mint_public(this.ownerAddress, amount).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + } + + async mintTokensPrivateOnL2(amount: bigint, secretHash: Fr) { + const tx = this.l2Token.methods.mint_private(amount, secretHash).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + await this.addPendingShieldNoteToPXE(amount, secretHash, receipt.txHash); + } + + async performL2Transfer(transferAmount: bigint, receiverAddress: AztecAddress) { + // send a transfer tx to force through rollup with the message included + const transferTx = this.l2Token.methods + .transfer_public(this.ownerAddress, receiverAddress, transferAmount, 0) + .send(); + const receipt = await transferTx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + } + + async consumeMessageOnAztecAndMintSecretly( + bridgeAmount: bigint, + secretHashForRedeemingMintedNotes: Fr, + messageKey: Fr, + secretForL2MessageConsumption: Fr, + ) { + this.logger('Consuming messages on L2 secretively'); + // Call the mint tokens function on the Aztec.nr contract + const consumptionTx = this.l2Bridge.methods + .claim_private( + bridgeAmount, + secretHashForRedeemingMintedNotes, + this.ethAccount, + messageKey, + secretForL2MessageConsumption, + ) + .send(); + const consumptionReceipt = await consumptionTx.wait(); + expect(consumptionReceipt.status).toBe(TxStatus.MINED); + + await this.addPendingShieldNoteToPXE(bridgeAmount, secretHashForRedeemingMintedNotes, consumptionReceipt.txHash); + } + + async consumeMessageOnAztecAndMintPublicly(bridgeAmount: bigint, messageKey: Fr, secret: Fr) { + this.logger('Consuming messages on L2 Publicly'); + // Call the mint tokens function on the Aztec.nr contract + const tx = this.l2Bridge.methods + .claim_public(this.ownerAddress, bridgeAmount, this.ethAccount, messageKey, secret) + .send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + } + + async withdrawPrivateFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { + const withdrawTx = this.l2Bridge.methods + .exit_to_l1_private(this.ethAccount, this.l2Token.address, withdrawAmount, EthAddress.ZERO, nonce) + .send(); + const withdrawReceipt = await withdrawTx.wait(); + expect(withdrawReceipt.status).toBe(TxStatus.MINED); + } + + async withdrawPublicFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { + const withdrawTx = this.l2Bridge.methods + .exit_to_l1_public(this.ethAccount, withdrawAmount, EthAddress.ZERO, nonce) + .send(); + const withdrawReceipt = await withdrawTx.wait(); + expect(withdrawReceipt.status).toBe(TxStatus.MINED); + } + + async getL2PrivateBalanceOf(owner: AztecAddress) { + return await this.l2Token.methods.balance_of_private(owner).view({ from: owner }); + } + + async expectPrivateBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { + const balance = await this.getL2PrivateBalanceOf(owner); + this.logger(`Account ${owner} balance: ${balance}`); + expect(balance).toBe(expectedBalance); + } + + async getL2PublicBalanceOf(owner: AztecAddress) { + return await this.l2Token.methods.balance_of_public(owner).view(); + } + + async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { + const balance = await this.getL2PublicBalanceOf(owner); + expect(balance).toBe(expectedBalance); + } + + async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise { + this.logger('Ensure that the entry is not in outbox yet'); + // 0xb460af94, selector for "withdraw(uint256,address,address)" + const content = sha256ToField( + Buffer.concat([ + Buffer.from([0xb4, 0x60, 0xaf, 0x94]), + new Fr(withdrawAmount).toBuffer(), + this.ethAccount.toBuffer32(), + callerOnL1.toBuffer32(), + ]), + ); + const entryKey = sha256ToField( + Buffer.concat([ + this.l2Bridge.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + this.tokenPortalAddress.toBuffer32() ?? Buffer.alloc(32, 0), + new Fr(this.publicClient.chain.id).toBuffer(), // chain id + content.toBuffer(), + ]), + ); + expect(await this.outbox.read.contains([entryKey.toString(true)])).toBeFalsy(); + + return entryKey; + } + + async withdrawFundsFromBridgeOnL1(withdrawAmount: bigint, entryKey: Fr) { + this.logger('Send L1 tx to consume entry and withdraw funds'); + // Call function on L1 contract to consume the message + const { request: withdrawRequest, result: withdrawEntryKey } = await this.tokenPortal.simulate.withdraw([ + withdrawAmount, + this.ethAccount.toString(), + false, + ]); + + expect(withdrawEntryKey).toBe(entryKey.toString(true)); + expect(await this.outbox.read.contains([withdrawEntryKey])).toBeTruthy(); + + await this.walletClient.writeContract(withdrawRequest); + return withdrawEntryKey; + } + + async shieldFundsOnL2(shieldAmount: bigint, secretHash: Fr) { + this.logger('Shielding funds on L2'); + const shieldTx = this.l2Token.methods.shield(this.ownerAddress, shieldAmount, secretHash, 0).send(); + const shieldReceipt = await shieldTx.wait(); + expect(shieldReceipt.status).toBe(TxStatus.MINED); + + await this.addPendingShieldNoteToPXE(shieldAmount, secretHash, shieldReceipt.txHash); + } + + async addPendingShieldNoteToPXE(shieldAmount: bigint, secretHash: Fr, txHash: TxHash) { + this.logger('Adding note to PXE'); + const storageSlot = new Fr(5); + const preimage = new NotePreimage([new Fr(shieldAmount), secretHash]); + await this.pxeService.addNote(this.ownerAddress, this.l2Token.address, storageSlot, preimage, txHash); + } + + async redeemShieldPrivatelyOnL2(shieldAmount: bigint, secret: Fr) { + this.logger('Spending commitment in private call'); + const privateTx = this.l2Token.methods.redeem_shield(this.ownerAddress, shieldAmount, secret).send(); + const privateReceipt = await privateTx.wait(); + expect(privateReceipt.status).toBe(TxStatus.MINED); + } + + async unshieldTokensOnL2(unshieldAmount: bigint, nonce = Fr.ZERO) { + this.logger('Unshielding tokens'); + const unshieldTx = this.l2Token.methods + .unshield(this.ownerAddress, this.ownerAddress, unshieldAmount, nonce) + .send(); + const unshieldReceipt = await unshieldTx.wait(); + expect(unshieldReceipt.status).toBe(TxStatus.MINED); + } +} diff --git a/yarn-project/canary/tsconfig.json b/yarn-project/canary/tsconfig.json index d02659b19cdd..36e240e97259 100644 --- a/yarn-project/canary/tsconfig.json +++ b/yarn-project/canary/tsconfig.json @@ -31,6 +31,9 @@ { "path": "../end-to-end" }, + { + "path": "../foundation" + }, { "path": "../l1-artifacts" }, 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 8e26fb3a8173..bc084357caf1 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 @@ -264,7 +264,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { // 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); + await wethCrossChainHarness.mintTokensPublicOnL2(0n); // 6. claim dai on L2 logger('Consuming messages to mint dai on L2'); @@ -407,7 +407,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { // 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); + await wethCrossChainHarness.mintTokensPublicOnL2(0n); // 6. claim dai on L2 logger('Consuming messages to mint dai on L2'); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 6efd2976c7a6..88fb58d9cabd 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -262,6 +262,7 @@ __metadata: "@aztec/circuits.js": "workspace:^" "@aztec/cli": "workspace:^" "@aztec/end-to-end": "workspace:^" + "@aztec/foundation": "workspace:^" "@aztec/l1-artifacts": "workspace:^" "@aztec/noir-contracts": "workspace:^" "@jest/globals": ^29.5.0