@@ -37,6 +37,7 @@ import {
3737} from './constants' ;
3838import { RetryProvider } from './retry-provider' ;
3939import { getInteropBundleData } from './temp-sdk' ;
40+ import { getTokenAddress } from 'highlevel-test-tools/src/create-chain' ;
4041
4142const SHARED_STATE_FILE = path . join ( __dirname , '../interop-shared-state.json' ) ;
4243const LOCK_DIR = path . join ( __dirname , '../interop-setup.lock' ) ;
@@ -51,6 +52,7 @@ type BalanceSnapshot = {
5152 native : bigint ;
5253 baseToken2 ?: bigint ;
5354 token ?: bigint ;
55+ zkToken ?: bigint ;
5456} ;
5557
5658export class InteropTestContext {
@@ -84,7 +86,10 @@ export class InteropTestContext {
8486 public interop1InteropCenter ! : zksync . Contract ;
8587 public interop1NativeTokenVault ! : zksync . Contract ;
8688 public interop1TokenA ! : zksync . Contract ;
89+ // Interop 1 fee variables
8790 public interopFee ! : bigint ;
91+ public zkTokenAddress ! : string ;
92+ public fixedFee ! : bigint ;
8893
8994 // Interop2 (Second Chain) Variables
9095 public baseToken2 ! : Token ;
@@ -394,6 +399,36 @@ export class InteropTestContext {
394399 const setInteropFeeCmd = `zkstack chain set-interop-fee --fee ${ this . interopFee } --chain ${ fileConfig . chain } ` ;
395400 await utils . spawn ( setInteropFeeCmd ) ;
396401
402+ // Fund the wallet with ZK token for paying the fixed interop fee
403+ const zkTokenAddressL1 = getTokenAddress ( 'ZK' ) ;
404+ const zkToken = new ethers . Contract (
405+ zkTokenAddressL1 ,
406+ ArtifactMintableERC20 . abi ,
407+ this . interop1RichWallet . ethWallet ( )
408+ ) ;
409+ await ( await zkToken . mint ( this . interop1RichWallet . address , ethers . parseEther ( '1000' ) ) ) . wait ( ) ;
410+ await this . interop1RichWallet . deposit ( {
411+ token : zkTokenAddressL1 ,
412+ amount : ethers . parseEther ( '1000' ) ,
413+ to : this . interop1Wallet . address ,
414+ approveERC20 : true
415+ } ) ;
416+ // Get the fixed fee amount
417+ this . fixedFee = await this . interop1InteropCenter . ZK_INTEROP_FEE ( ) ;
418+ // Approve the interop center to spend the ZK tokens
419+ this . zkTokenAddress = ethers . ZeroAddress ;
420+ while ( this . zkTokenAddress === ethers . ZeroAddress ) {
421+ this . zkTokenAddress = await this . interop1InteropCenter . getZKTokenAddress ( ) ;
422+ await zksync . utils . sleep ( this . interop1Wallet . provider . pollingInterval ) ;
423+ }
424+ const zkTokenInterop1 = new zksync . Contract (
425+ this . zkTokenAddress ,
426+ ArtifactMintableERC20 . abi ,
427+ this . interop1Wallet
428+ ) ;
429+ await ( await zkTokenInterop1 . approve ( L2_INTEROP_CENTER_ADDRESS , ethers . parseEther ( '1000' ) ) ) . wait ( ) ;
430+
431+ // Deploy test token on interop1 chain
397432 const tokenADeploy = await deployContract ( this . interop1Wallet , ArtifactMintableERC20 , [
398433 this . tokenA . name ,
399434 this . tokenA . symbol ,
@@ -432,7 +467,9 @@ export class InteropTestContext {
432467 l2AddressSecondChain : this . tokenA . l2AddressSecondChain ,
433468 assetId : this . tokenA . assetId
434469 } ,
435- interopFee : this . interopFee . toString ( )
470+ interopFee : this . interopFee . toString ( ) ,
471+ zkTokenAddress : this . zkTokenAddress ,
472+ fixedFee : this . fixedFee . toString ( )
436473 } ;
437474
438475 this . loadState ( newState ) ;
@@ -452,6 +489,8 @@ export class InteropTestContext {
452489 ) ;
453490
454491 this . interopFee = BigInt ( state . interopFee ) ;
492+ this . zkTokenAddress = state . zkTokenAddress ;
493+ this . fixedFee = BigInt ( state . fixedFee ) ;
455494 }
456495
457496 async deinitialize ( ) {
@@ -471,25 +510,32 @@ export class InteropTestContext {
471510 ] ) ;
472511 }
473512
513+ /**
514+ * Helper to create the useFixedFee attribute
515+ */
516+ async useFixedFeeAttr ( useFixedFee : boolean = false ) {
517+ return this . erc7786AttributeDummy . interface . encodeFunctionData ( 'useFixedFee' , [ useFixedFee ] ) ;
518+ }
519+
474520 /**
475521 * Helper to create attributes with interopCallValue
476522 */
477- async directCallAttrs ( amount : bigint , executionAddress ?: string ) {
523+ async directCallAttrs ( amount : bigint , useFixedFee : boolean = false , executionAddress ?: string ) {
478524 return [
479525 await this . erc7786AttributeDummy . interface . encodeFunctionData ( 'interopCallValue' , [ amount ] ) ,
480526 await this . executionAddressAttr ( executionAddress ) ,
481- this . erc7786AttributeDummy . interface . encodeFunctionData ( ' useFixedFee' , [ false ] )
527+ await this . useFixedFeeAttr ( useFixedFee )
482528 ] ;
483529 }
484530
485531 /**
486532 * Helper to create attributes with indirectCall
487533 */
488- async indirectCallAttrs ( callValue : bigint = 0n , executionAddress ?: string ) {
534+ async indirectCallAttrs ( callValue : bigint = 0n , useFixedFee : boolean = false , executionAddress ?: string ) {
489535 return [
490536 await this . erc7786AttributeDummy . interface . encodeFunctionData ( 'indirectCall' , [ callValue ] ) ,
491537 await this . executionAddressAttr ( executionAddress ) ,
492- this . erc7786AttributeDummy . interface . encodeFunctionData ( ' useFixedFee' , [ false ] )
538+ await this . useFixedFeeAttr ( useFixedFee )
493539 ] ;
494540 }
495541
@@ -513,7 +559,7 @@ export class InteropTestContext {
513559 */
514560 async fromInterop1RequestInterop (
515561 execCallStarters : InteropCallStarter [ ] ,
516- bundleOptions ?: { executionAddress ?: string ; unbundlerAddress ?: string } ,
562+ bundleOptions ?: { executionAddress ?: string ; unbundlerAddress ?: string ; useFixedFee ?: boolean } ,
517563 overrides : ethers . Overrides = { }
518564 ) {
519565 const bundleAttributes = [ ] ;
@@ -524,17 +570,20 @@ export class InteropTestContext {
524570 ] )
525571 ) ;
526572 }
573+ // Note: The InteropCenter will automatically set the unbundler address to msg.sender if not provided
527574 if ( bundleOptions ?. unbundlerAddress ) {
528575 bundleAttributes . push (
529576 await this . erc7786AttributeDummy . interface . encodeFunctionData ( 'unbundlerAddress' , [
530577 formatEvmV1Address ( bundleOptions . unbundlerAddress , this . interop2ChainId )
531578 ] )
532579 ) ;
533580 }
534-
535- // Note: The InteropCenter will automatically set the unbundler address to msg.sender if not provided
536- // We only need to provide the required useFixedFee attribute
537- bundleAttributes . push ( this . erc7786AttributeDummy . interface . encodeFunctionData ( 'useFixedFee' , [ false ] ) ) ;
581+ // The `useFixedFee` attribute is required for all interop calls to ensure explicit fee payment choice
582+ bundleAttributes . push (
583+ await this . erc7786AttributeDummy . interface . encodeFunctionData ( 'useFixedFee' , [
584+ bundleOptions ?. useFixedFee ?? false
585+ ] )
586+ ) ;
538587
539588 const txFinalizeReceipt = (
540589 await this . interop1InteropCenter . sendBundle (
@@ -676,8 +725,8 @@ export class InteropTestContext {
676725 * Calculates the message value needed for an interop transaction.
677726 * Includes interop fees and optionally the base token amount if chains share the same base token.
678727 */
679- calculateMsgValue ( numCalls : number , baseTokenAmount : bigint = 0n ) : bigint {
680- const interopFeesTotal = this . interopFee * BigInt ( numCalls ) ;
728+ calculateMsgValue ( numCalls : number , baseTokenAmount : bigint = 0n , useFixedFee : boolean = false ) : bigint {
729+ const interopFeesTotal = useFixedFee ? 0n : this . interopFee * BigInt ( numCalls ) ;
681730 const baseTokenIncluded = this . isSameBaseToken ? baseTokenAmount : 0n ;
682731 return interopFeesTotal + baseTokenIncluded ;
683732 }
@@ -702,6 +751,8 @@ export class InteropTestContext {
702751 snapshot . token = await this . getTokenBalance ( this . interop1Wallet , tokenAddress ) ;
703752 }
704753
754+ snapshot . zkToken = await this . getTokenBalance ( this . interop1Wallet , this . zkTokenAddress ) ;
755+
705756 return snapshot ;
706757 }
707758
@@ -715,6 +766,7 @@ export class InteropTestContext {
715766 msgValue : bigint ;
716767 baseTokenAmount ?: bigint ;
717768 tokenAmount ?: bigint ;
769+ zkTokenAmount ?: bigint ;
718770 }
719771 ) {
720772 const feePaid = BigInt ( receipt . gasUsed ) * BigInt ( receipt . gasPrice ) ;
@@ -735,5 +787,10 @@ export class InteropTestContext {
735787 const afterToken = await this . getTokenBalance ( this . interop1Wallet , tokenAddress ) ;
736788 expect ( afterToken . toString ( ) ) . toBe ( ( beforeSnapshot . token - expected . tokenAmount ) . toString ( ) ) ;
737789 }
790+
791+ if ( expected . zkTokenAmount !== undefined && beforeSnapshot . zkToken !== undefined ) {
792+ const afterZkToken = await this . getTokenBalance ( this . interop1Wallet , this . zkTokenAddress ) ;
793+ expect ( afterZkToken . toString ( ) ) . toBe ( ( beforeSnapshot . zkToken - expected . zkTokenAmount ) . toString ( ) ) ;
794+ }
738795 }
739796}
0 commit comments