Skip to content

Commit aaaceb8

Browse files
committed
test fixed fee case
1 parent 47e5bae commit aaaceb8

File tree

8 files changed

+137
-34
lines changed

8 files changed

+137
-34
lines changed

core/tests/highlevel-test-tools/src/create-chain.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,26 +70,31 @@ const fileMutex = new FileMutex();
7070
/**
7171
* Reads the custom token address from the erc20.yaml configuration file
7272
*/
73-
export function getCustomTokenAddress(configPath: string = join(configsPath(), 'erc20.yaml')): string {
73+
export function getTokenAddress(
74+
tokenName: string = 'DAI',
75+
configPath: string = join(configsPath(), 'erc20.yaml')
76+
): string {
7477
try {
7578
if (!fs.existsSync(configPath)) {
7679
throw new Error(`Config file ${configPath} not found`);
7780
}
7881

7982
const fileContent = fs.readFileSync(configPath, 'utf8');
8083

81-
// Parse YAML as string and extract DAI token address using regex
82-
const daiAddressMatch = fileContent.match(/DAI:\s*\n\s*address:\s*(0x[a-fA-F0-9]{40})/);
84+
// Parse YAML as string and extract token address using regex
85+
const tokenAddressMatch = fileContent.match(
86+
new RegExp(`\\s*${tokenName}:\\s*\\n\\s*address:\\s*(0x[a-fA-F0-9]{40})`)
87+
);
8388

84-
if (daiAddressMatch && daiAddressMatch[1]) {
85-
const tokenAddress = daiAddressMatch[1];
86-
console.log(`✅ Found custom token address: ${tokenAddress}`);
89+
if (tokenAddressMatch && tokenAddressMatch[1]) {
90+
const tokenAddress = tokenAddressMatch[1];
91+
console.log(`✅ Found token address for ${tokenName}: ${tokenAddress}`);
8792
return tokenAddress;
8893
} else {
89-
throw new Error(`Custom token address not found in config file ${configPath}`);
94+
throw new Error(`Token address for ${tokenName} not found in config file ${configPath}`);
9095
}
9196
} catch (error) {
92-
console.error(`❌ Error reading custom token address from ${configPath}:`, error);
97+
console.error(`❌ Error reading token address for ${tokenName} from ${configPath}:`, error);
9398
throw error;
9499
}
95100
}
@@ -114,7 +119,7 @@ export async function createChainAndStartServer(chainType: ChainType, testSuiteN
114119

115120
const ethTokenAddress = '0x0000000000000000000000000000000000000001';
116121
// Get custom token address from config file
117-
const customTokenAddress = getCustomTokenAddress();
122+
const customTokenAddress = getTokenAddress();
118123

119124
// Chain-specific configurations
120125
const chainConfigs = {

core/tests/ts-integration/src/interop-setup.ts

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from './constants';
3838
import { RetryProvider } from './retry-provider';
3939
import { getInteropBundleData } from './temp-sdk';
40+
import { getTokenAddress } from 'highlevel-test-tools/src/create-chain';
4041

4142
const SHARED_STATE_FILE = path.join(__dirname, '../interop-shared-state.json');
4243
const 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

5658
export 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
}

core/tests/ts-integration/tests/interop-b-bundles.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,18 @@ describe('Interop-B Bundles behavior checks', () => {
170170
callAttributes: [ctx.interopCallValueAttr(baseAmount)]
171171
}
172172
];
173-
const msgValue = ctx.calculateMsgValue(execCallStarters.length, baseAmount);
173+
const msgValue = ctx.calculateMsgValue(execCallStarters.length, baseAmount, true);
174174
const receipt = await ctx.fromInterop1RequestInterop(
175175
execCallStarters,
176-
{ executionAddress: ctx.interop2RichWallet.address },
176+
{ executionAddress: ctx.interop2RichWallet.address, useFixedFee: true },
177177
{ value: msgValue }
178178
);
179179

180180
await ctx.assertInterop1BalanceChanges(receipt, before, {
181181
msgValue,
182182
baseTokenAmount: baseAmount,
183-
tokenAmount
183+
tokenAmount,
184+
zkTokenAmount: ctx.fixedFee * BigInt(execCallStarters.length)
184185
});
185186
bundles.mixed = {
186187
amounts: [baseAmount.toString(), tokenAmount.toString()],

core/tests/ts-integration/tests/interop-b-messages.test.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,22 @@ describe('Interop-B Messages behavior checks', () => {
3333
const amount = ctx.getTransferAmount();
3434
const before = await ctx.captureInterop1BalanceSnapshot();
3535

36-
const msgValue = ctx.calculateMsgValue(1, amount);
37-
const tx = await ctx.interop1InteropCenter.sendMessage(recipient, '0x', await ctx.directCallAttrs(amount), {
38-
value: msgValue
39-
});
36+
const msgValue = ctx.calculateMsgValue(1, amount, true);
37+
const tx = await ctx.interop1InteropCenter.sendMessage(
38+
recipient,
39+
'0x',
40+
await ctx.directCallAttrs(amount, true),
41+
{
42+
value: msgValue
43+
}
44+
);
4045
const receipt = await tx.wait();
4146

42-
await ctx.assertInterop1BalanceChanges(receipt, before, { msgValue, baseTokenAmount: amount });
47+
await ctx.assertInterop1BalanceChanges(receipt, before, {
48+
msgValue,
49+
baseTokenAmount: amount,
50+
zkTokenAmount: ctx.fixedFee
51+
});
4352
messages.baseToken = { amount: amount.toString(), receipt };
4453
}
4554

@@ -68,16 +77,16 @@ describe('Interop-B Messages behavior checks', () => {
6877
const amount = ctx.getTransferAmount();
6978
const before = await ctx.captureInterop1BalanceSnapshot();
7079

71-
const msgValue = ctx.interopFee + amount;
80+
const msgValue = amount;
7281
const tx = await ctx.interop1InteropCenter.sendMessage(
7382
assetRouterRecipient,
7483
ctx.getTokenTransferSecondBridgeData(ctx.baseToken1.assetId!, amount, ctx.interop2Recipient.address),
75-
await ctx.indirectCallAttrs(amount),
84+
await ctx.indirectCallAttrs(amount, true),
7685
{ value: msgValue }
7786
);
7887
const receipt = await tx.wait();
7988

80-
await ctx.assertInterop1BalanceChanges(receipt, before, { msgValue });
89+
await ctx.assertInterop1BalanceChanges(receipt, before, { msgValue, zkTokenAmount: ctx.fixedFee });
8190
messages.interop1BaseToken = { amount: amount.toString(), receipt };
8291
}
8392

zkstack_cli/crates/config/src/forge_interface/deploy_ctm/input.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub struct DeployCTMConfig {
1414
pub support_l2_legacy_shared_bridge_test: bool,
1515
pub contracts: ContractsDeployCTMConfig,
1616
pub is_zk_sync_os: bool,
17+
pub zk_token_asset_id: H256,
1718
}
1819

1920
impl FileConfigTrait for DeployCTMConfig {}
@@ -32,6 +33,7 @@ impl DeployCTMConfig {
3233
testnet_verifier,
3334
owner_address: wallets_config.governor.address,
3435
support_l2_legacy_shared_bridge_test,
36+
zk_token_asset_id: l1_network.zk_token_asset_id(),
3537
contracts: ContractsDeployCTMConfig {
3638
create2_factory_addr: initial_deployment_config.create2_factory_addr,
3739
create2_factory_salt: initial_deployment_config.create2_factory_salt,

zkstack_cli/crates/config/src/forge_interface/deploy_ecosystem/input.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ impl Default for Erc20DeploymentConfig {
7878
implementation: String::from("TestnetERC20Token.sol"),
7979
mint: U256::from_str("9000000000000000000000").unwrap(),
8080
},
81+
Erc20DeploymentTokensConfig {
82+
name: String::from("ZK"),
83+
symbol: String::from("ZK"),
84+
decimals: 18,
85+
implementation: String::from("TestnetERC20Token.sol"),
86+
mint: U256::from_str("9000000000000000000000").unwrap(),
87+
},
8188
],
8289
}
8390
}

zkstack_cli/crates/types/src/l1_network.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::str::FromStr;
22

33
use clap::ValueEnum;
4-
use ethers::types::Address;
4+
use ethers::types::{Address, H256};
55
use serde::{Deserialize, Serialize};
66
use strum::EnumIter;
77

@@ -48,4 +48,26 @@ impl L1Network {
4848
L1Network::Mainnet => None, // TODO: add mainnet address after it is known
4949
}
5050
}
51+
52+
pub fn zk_token_asset_id(&self) -> H256 {
53+
match self {
54+
L1Network::Localhost => {
55+
// When testing locally, we deploy the ZK token after ecosystem init, so we need to derive its asset id
56+
// The address where ZK will be deployed at is 0x57dba6a9b498265c4414b129a3b2ad6e80fca518
57+
H256::from_str("0x1da996953cdc5fb80dd9e37853dc8e02efb528dc0c7ca62965f6fb2afe867792")
58+
.unwrap()
59+
}
60+
L1Network::Sepolia => {
61+
// https://sepolia.etherscan.io/address/0x2569600E58850a0AaD61F7Dd2569516C3d909521#readProxyContract#F3
62+
H256::from_str("0x0d643837c76916220dfe0d5e971cfc3dc2c7569b3ce12851c8e8f17646d86bca")
63+
.unwrap()
64+
}
65+
L1Network::Mainnet => {
66+
// https://etherscan.io/address/0x66A5cFB2e9c529f14FE6364Ad1075dF3a649C0A5#readProxyContract#F3
67+
H256::from_str("0x83e2fbc0a739b3c765de4c2b4bf8072a71ea8fbb09c8cf579c71425d8bc8804a")
68+
.unwrap()
69+
}
70+
L1Network::Holesky => H256::zero(),
71+
}
72+
}
5173
}

0 commit comments

Comments
 (0)