From 69d53dc4c1e220ae8d1321ee9169fe6901594b7e Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Fri, 29 Sep 2023 14:59:22 +0000 Subject: [PATCH 1/2] public flow for uniswap --- .../src/uniswap_trade_on_l1_from_l2.test.ts | 173 +++++++++++++++++- .../contracts/uniswap_contract/src/main.nr | 109 ++++++++++- .../contracts/uniswap_contract/src/util.nr | 65 +++++++ 3 files changed, 336 insertions(+), 11 deletions(-) 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 4f25b6bbbc9..9ec6afaed36 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 @@ -39,6 +39,8 @@ describe('uniswap_trade_on_l1_from_l2', () => { let ownerWallet: AccountWallet; let ownerAddress: AztecAddress; let ownerEthAddress: EthAddress; + let user2Wallet: AccountWallet; + let user2Address: AztecAddress; let daiCrossChainHarness: CrossChainTestHarness; let wethCrossChainHarness: CrossChainTestHarness; @@ -58,8 +60,8 @@ describe('uniswap_trade_on_l1_from_l2', () => { pxe: pxe_, deployL1ContractsValues, accounts, + wallets, logger: logger_, - wallet, cheatCodes, } = await setup(2, dumpedState); const walletClient = deployL1ContractsValues.walletClient; @@ -73,8 +75,10 @@ describe('uniswap_trade_on_l1_from_l2', () => { pxe = pxe_; logger = logger_; teardown = teardown_; - ownerWallet = wallet; + ownerWallet = wallets[0]; + user2Wallet = wallets[1]; ownerAddress = accounts[0].address; + user2Address = accounts[1].address; ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); logger('Deploying DAI Portal, initializing and deploying l2 contract...'); @@ -83,7 +87,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { pxe, deployL1ContractsValues, accounts, - wallet, + ownerWallet, logger, cheatCodes, DAI_ADDRESS, @@ -95,7 +99,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { pxe, deployL1ContractsValues, accounts, - wallet, + ownerWallet, logger, cheatCodes, WETH9_ADDRESS, @@ -110,7 +114,9 @@ describe('uniswap_trade_on_l1_from_l2', () => { publicClient, }); // deploy l2 uniswap contract and attach to portal - uniswapL2Contract = await UniswapContract.deploy(wallet).send({ portalContract: uniswapPortalAddress }).deployed(); + uniswapL2Contract = await UniswapContract.deploy(ownerWallet) + .send({ portalContract: uniswapPortalAddress }) + .deployed(); await uniswapL2Contract.attach(uniswapPortalAddress); await uniswapPortal.write.initialize( @@ -271,4 +277,161 @@ describe('uniswap_trade_on_l1_from_l2', () => { logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); }, 140_000); + + it.only('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(2n); + const transferMessageHash = await hashPayload([ + uniswapL2Contract.address.toField(), + wethCrossChainHarness.l2Token.address.toField(), + FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)').toField(), + ownerAddress.toField(), + uniswapL2Contract.address.toField(), + new Fr(wethAmountToBridge), + nonceForWETHTransferApproval, + ]); + await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait(); + + // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. + const deadlineForDepositingSwappedDai = BigInt(2 ** 32 - 1); // max uint32 - 1 + const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = + await daiCrossChainHarness.generateClaimSecret(); + + // 4.1 Owner approves user to swap on their behalf: + const nonceForSwap = new Fr(3n); + const swapMessageHash = await hashPayload([ + user2Address.toField(), + uniswapL2Contract.address.toField(), + FunctionSelector.fromSignature( + 'swap_public((Field),(Field),Field,(Field),Field,Field,Field,(Field),Field,Field,(Field),(Field),Field)', + ).toField(), + ownerAddress.toField(), + wethCrossChainHarness.l2Bridge.address.toField(), + new Fr(wethAmountToBridge), + daiCrossChainHarness.l2Bridge.address.toField(), + nonceForWETHTransferApproval, + new Fr(uniswapFeeTier), + new Fr(minimumOutputAmount), + ownerAddress.toField(), + secretHashForDepositingSwappedDai, + new Fr(deadlineForDepositingSwappedDai), + ownerEthAddress.toField(), + ownerEthAddress.toField(), + nonceForSwap, + ]); + await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait(); + + // 4.2 Call swap_public from user2 + const withdrawReceipt = await uniswapL2Contract + .withWallet(user2Wallet) + .methods.swap_public( + ownerAddress, + wethCrossChainHarness.l2Bridge.address, + wethAmountToBridge, + daiCrossChainHarness.l2Bridge.address, + nonceForWETHTransferApproval, + uniswapFeeTier, + minimumOutputAmount, + ownerAddress, + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, + nonceForSwap, + ) + .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); + + // 5. Consume L2 to L1 message by calling uniswapPortal.swap() + 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()); + }, 140_000); }); diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr index ccca6825078..2b01f224802 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr @@ -6,7 +6,7 @@ mod util; // Uses the token bridge contract, which tells which input token we need to talk to and handles the exit funds to L1 contract Uniswap { use dep::aztec::{ - auth::IS_VALID_SELECTOR, + auth::{IS_VALID_SELECTOR, assert_valid_public_message_for}, context::{PrivateContext, PublicContext, Context}, oracle::compute_selector::compute_selector, oracle::context::get_portal_address, @@ -21,7 +21,7 @@ contract Uniswap { }; use crate::interfaces::{Token, TokenBridge}; - use crate::util::{compute_message_hash, compute_swap_private_content_hash}; + use crate::util::{compute_message_hash, compute_swap_private_content_hash, compute_swap_public_content_hash}; struct Storage { // like with account contracts, stores the approval message on a slot and tracks if they are active @@ -49,6 +49,96 @@ contract Uniswap { #[aztec(private)] fn constructor() {} + #[aztec(public)] + fn swap_public( + sender: AztecAddress, + input_asset_bridge: AztecAddress, + input_amount: Field, + output_asset_bridge: AztecAddress, + // params for using the transfer approval + nonce_for_transfer_approval: Field, + // params for the swap + uniswap_fee_tier: Field, + minimum_output_amount: Field, + // params for the depositing output_asset back to Aztec + recipient: AztecAddress, + secret_hash_for_L1_to_l2_message: Field, + deadline_for_L1_to_l2_message: Field, + canceller_for_L1_to_L2_message: EthereumAddress, + caller_on_L1: EthereumAddress, + // nonce for someone to call swap on sender's behalf + nonce_for_swap_approval: Field, + ) -> Field { + + if (sender.address != context.msg_sender()) { + // if someone else is calling on swap on sender's behalf, they need to have authorisation to do so: + let selector = compute_selector( + "swap_public((Field),(Field),Field,(Field),Field,Field,Field,(Field),Field,Field,(Field),(Field),Field)" + ); + let message_field = compute_message_hash([ + context.msg_sender(), + context.this_address(), + selector, + sender.address, + input_asset_bridge.address, + input_amount, + output_asset_bridge.address, + nonce_for_transfer_approval, + uniswap_fee_tier, + minimum_output_amount, + recipient.address, + secret_hash_for_L1_to_l2_message, + deadline_for_L1_to_l2_message, + canceller_for_L1_to_L2_message.address, + caller_on_L1.address, + nonce_for_swap_approval, + ]); + // this also emits a nullifier for the message + assert_valid_public_message_for(&mut context,sender.address,message_field); + } + + let input_asset = AztecAddress::new(TokenBridge::at(input_asset_bridge.address).token(context)); + + // Transfer funds to this contract + Token::at(input_asset.address).transfer_public( + context, + sender.address, + context.this_address(), + input_amount, + nonce_for_transfer_approval, + ); + + // Approve bridge to burn this contract's funds and exit to L1 Uniswap Portal + let _void = context.call_public_function( + context.this_address(), + compute_selector("_approve_bridge_and_exit_input_asset_to_L1((Field),(Field),Field)"), + [input_asset.address, input_asset_bridge.address, input_amount], + ); + + // Create swap message and send to Outbox for Uniswap Portal + // this ensures the integrity of what the user originally intends to do on L1. + let input_asset_bridge_portal_address = get_portal_address(input_asset_bridge.address); + let output_asset_bridge_portal_address = get_portal_address(output_asset_bridge.address); + assert(input_asset_bridge_portal_address != 0, "L1 portal address of input_asset's bridge is 0"); + assert(output_asset_bridge_portal_address != 0, "L1 portal address of output_asset's bridge is 0"); + + let content_hash = compute_swap_public_content_hash( + input_asset_bridge_portal_address, + input_amount, + uniswap_fee_tier, + output_asset_bridge_portal_address, + minimum_output_amount, + recipient.address, + secret_hash_for_L1_to_l2_message, + deadline_for_L1_to_l2_message, + canceller_for_L1_to_L2_message.address, + caller_on_L1.address, + ); + context.message_portal(content_hash); + + 1 + } + #[aztec(private)] fn swap( input_asset: AztecAddress, // since private, we pass here and later assert that this is as expected by input_bridge @@ -68,6 +158,10 @@ contract Uniswap { caller_on_L1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) ) -> Field { + // Assert that user provided token address is same as expected by token bridge. + // we can't directly use `input_asset_bridge.token` because that is a public method and public can't return data to private + context.call_public_function(context.this_address(), compute_selector("_assert_token_is_same(Field,Field)"), [input_asset.address, input_asset_bridge.address]); + // Transfer funds to this contract Token::at(input_asset.address).unshield( &mut context, @@ -122,7 +216,8 @@ contract Uniswap { } // This helper method approves the bridge to burn this contract's funds and exits the input asset to L1 - // Assumes contract already has funds + // Assumes contract already has funds. + // Assume `token` relates to `token_bridge` (ie token_bridge.token == token) // Note that private can't read public return values so created an internal public that handles everything // this method is used for both private and public swaps. #[aztec(public)] @@ -131,9 +226,6 @@ contract Uniswap { token_bridge: AztecAddress, amount: Field, ) { - // Assert that user provided token address is same as expected by token bridge. - assert(token.address == (TokenBridge::at(token_bridge.address).token(context)), "input_asset address is not the same as seen in the bridge contract"); - // approve bridge to burn this contract's funds (required when exiting on L1, as it burns funds on L2): let nonce_for_burn_approval = storage.nonce_for_burn_approval.read(); let selector = compute_selector("burn_public((Field),Field,Field)"); @@ -152,4 +244,9 @@ contract Uniswap { nonce_for_burn_approval, ); } + + #[aztec(public)] + internal fn _assert_token_is_same(token: Field, token_bridge: Field) { + assert(token == (TokenBridge::at(token_bridge).token(context)), "input_asset address is not the same as seen in the bridge contract"); + } } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr index da1361ab8e3..4e578587e4a 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr @@ -71,3 +71,68 @@ fn compute_swap_private_content_hash( content_hash } + +// This method computes the L2 to L1 message content hash for the public +// refer `l1-contracts/test/portals/UniswapPortal.sol` on how L2 to L1 message is expected +fn compute_swap_public_content_hash( + input_asset_bridge_portal_address: Field, + input_amount: Field, + uniswap_fee_tier: Field, + output_asset_bridge_portal_address: Field, + minimum_output_amount: Field, + aztec_recipient: Field, + secret_hash_for_L1_to_l2_message: Field, + deadline_for_L1_to_l2_message: Field, + canceller_for_L1_to_L2_message: Field, + caller_on_L1: Field, +) -> Field { + let mut hash_bytes: [u8; 324] = [0; 324]; // 10 fields of 32 bytes each + 4 bytes fn selector + + let input_token_portal_bytes = input_asset_bridge_portal_address.to_be_bytes(32); + let in_amount_bytes = input_amount.to_be_bytes(32); + let uniswap_fee_tier_bytes = uniswap_fee_tier.to_be_bytes(32); + let output_token_portal_bytes = output_asset_bridge_portal_address.to_be_bytes(32); + let amount_out_min_bytes = minimum_output_amount.to_be_bytes(32); + let aztec_recipient_bytes = aztec_recipient.to_be_bytes(32); + let secret_hash_for_L1_to_l2_message_bytes = secret_hash_for_L1_to_l2_message.to_be_bytes(32); + let deadline_for_L1_to_l2_message_bytes = deadline_for_L1_to_l2_message.to_be_bytes(32); + let canceller_bytes = canceller_for_L1_to_L2_message.to_be_bytes(32); + let caller_on_L1_bytes = caller_on_L1.to_be_bytes(32); + + // function selector: 0xf3068cac keccak256("swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)") + hash_bytes[0] = 0xf3; + hash_bytes[1] = 0x06; + hash_bytes[2] = 0x8c; + hash_bytes[3] = 0xac; + + for i in 0..32 { + hash_bytes[i + 4] = input_token_portal_bytes[i]; + hash_bytes[i + 36] = in_amount_bytes[i]; + hash_bytes[i + 68] = uniswap_fee_tier_bytes[i]; + hash_bytes[i + 100] = output_token_portal_bytes[i]; + hash_bytes[i + 132] = amount_out_min_bytes[i]; + hash_bytes[i + 164] = aztec_recipient_bytes[i]; + hash_bytes[i + 196] = secret_hash_for_L1_to_l2_message_bytes[i]; + hash_bytes[i + 228] = deadline_for_L1_to_l2_message_bytes[i]; + hash_bytes[i + 260] = canceller_bytes[i]; + hash_bytes[i + 292] = caller_on_L1_bytes[i]; + } + + let content_sha256 = sha256(hash_bytes); + + // Convert the content_sha256 to a field element + let mut v = 1; + let mut high = 0 as Field; + let mut low = 0 as Field; + + for i in 0..16 { + high = high + (content_sha256[15 - i] as Field) * v; + low = low + (content_sha256[16 + 15 - i] as Field) * v; + v = v * 256; + } + + // Abuse that a % p + b % p = (a + b) % p and that low < p + let content_hash = low + high * v; + + content_hash +} From ab067ffd8062a232861f2d330f53aa607261fdcd Mon Sep 17 00:00:00 2001 From: Rahul Kothari Date: Mon, 2 Oct 2023 10:27:26 +0000 Subject: [PATCH 2/2] respond to nits --- .../src/uniswap_trade_on_l1_from_l2.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 9ec6afaed36..583a692b84e 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 @@ -39,8 +39,9 @@ describe('uniswap_trade_on_l1_from_l2', () => { let ownerWallet: AccountWallet; let ownerAddress: AztecAddress; let ownerEthAddress: EthAddress; - let user2Wallet: AccountWallet; - let user2Address: AztecAddress; + // does transactions on behalf of owner on Aztec: + let sponsorWallet: AccountWallet; + let sponsorAddress: AztecAddress; let daiCrossChainHarness: CrossChainTestHarness; let wethCrossChainHarness: CrossChainTestHarness; @@ -76,9 +77,9 @@ describe('uniswap_trade_on_l1_from_l2', () => { logger = logger_; teardown = teardown_; ownerWallet = wallets[0]; - user2Wallet = wallets[1]; + sponsorWallet = wallets[1]; ownerAddress = accounts[0].address; - user2Address = accounts[1].address; + sponsorAddress = accounts[1].address; ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); logger('Deploying DAI Portal, initializing and deploying l2 contract...'); @@ -278,7 +279,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); }, 140_000); - it.only('should uniswap trade on L1 from L2 funds publicly (swaps WETH -> DAI)', async () => { + 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 @@ -334,7 +335,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { // 4.1 Owner approves user to swap on their behalf: const nonceForSwap = new Fr(3n); const swapMessageHash = await hashPayload([ - user2Address.toField(), + sponsorAddress.toField(), uniswapL2Contract.address.toField(), FunctionSelector.fromSignature( 'swap_public((Field),(Field),Field,(Field),Field,Field,Field,(Field),Field,Field,(Field),(Field),Field)', @@ -355,9 +356,9 @@ describe('uniswap_trade_on_l1_from_l2', () => { ]); await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait(); - // 4.2 Call swap_public from user2 + // 4.2 Call swap_public from user2 on behalf of owner const withdrawReceipt = await uniswapL2Contract - .withWallet(user2Wallet) + .withWallet(sponsorWallet) .methods.swap_public( ownerAddress, wethCrossChainHarness.l2Bridge.address, @@ -380,7 +381,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); - // 5. Consume L2 to L1 message by calling uniswapPortal.swap() + // 5. Perform the swap on L1 with the `uniswapPortal.swap()` (consuming L2 to L1 messages) logger('Execute withdraw and swap on the uniswapPortal!'); const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( daiCrossChainHarness.tokenPortalAddress,