@aztec/ethereum 3.0.0-nightly.20251001 → 3.0.0-nightly.20251002

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,6 +45,7 @@ import { RegistryContract } from './contracts/registry.js';
45
45
  import { RollupContract, SlashingProposerType } from './contracts/rollup.js';
46
46
  import {
47
47
  CoinIssuerArtifact,
48
+ DateGatedRelayerArtifact,
48
49
  FeeAssetArtifact,
49
50
  FeeAssetHandlerArtifact,
50
51
  GSEArtifact,
@@ -164,6 +165,8 @@ export interface DeployL1ContractsArgs extends Omit<L1ContractsConfig, keyof L1T
164
165
  realVerifier: boolean;
165
166
  /** The zk passport args */
166
167
  zkPassportArgs?: ZKPassportArgs;
168
+ /** If provided, use this token for BOTH fee and staking assets (skip deployments) */
169
+ existingTokenAddress?: EthAddress;
167
170
  }
168
171
 
169
172
  export interface ZKPassportArgs {
@@ -175,6 +178,80 @@ export interface ZKPassportArgs {
175
178
  zkPassportScope?: string;
176
179
  }
177
180
 
181
+ // Minimal ERC20 ABI for validation purposes. We only read view methods.
182
+ const ERC20_VALIDATION_ABI = [
183
+ {
184
+ type: 'function',
185
+ name: 'totalSupply',
186
+ stateMutability: 'view',
187
+ inputs: [],
188
+ outputs: [{ name: '', type: 'uint256' }],
189
+ },
190
+ {
191
+ type: 'function',
192
+ name: 'name',
193
+ stateMutability: 'view',
194
+ inputs: [],
195
+ outputs: [{ name: '', type: 'string' }],
196
+ },
197
+ {
198
+ type: 'function',
199
+ name: 'symbol',
200
+ stateMutability: 'view',
201
+ inputs: [],
202
+ outputs: [{ name: '', type: 'string' }],
203
+ },
204
+ {
205
+ type: 'function',
206
+ name: 'decimals',
207
+ stateMutability: 'view',
208
+ inputs: [],
209
+ outputs: [{ name: '', type: 'uint8' }],
210
+ },
211
+ ] as const;
212
+
213
+ /**
214
+ * Validates that the provided address points to a contract that resembles an ERC20 token.
215
+ * Checks for contract code and attempts common ERC20 view calls.
216
+ * Throws an error if validation fails.
217
+ */
218
+ export async function validateExistingErc20TokenAddress(
219
+ l1Client: ExtendedViemWalletClient,
220
+ tokenAddress: EthAddress,
221
+ logger: Logger,
222
+ ): Promise<void> {
223
+ const addressString = tokenAddress.toString();
224
+
225
+ // Ensure there is contract code at the address
226
+ const code = await l1Client.getCode({ address: addressString });
227
+ if (!code || code === '0x') {
228
+ throw new Error(`No contract code found at provided token address ${addressString}`);
229
+ }
230
+
231
+ const contract = getContract({
232
+ address: getAddress(addressString),
233
+ abi: ERC20_VALIDATION_ABI,
234
+ client: l1Client,
235
+ });
236
+
237
+ // Validate all required ERC20 methods in parallel
238
+ const checks = [
239
+ contract.read.totalSupply().then(total => typeof total === 'bigint'),
240
+ contract.read.name().then(() => true),
241
+ contract.read.symbol().then(() => true),
242
+ contract.read.decimals().then(dec => typeof dec === 'number' || typeof dec === 'bigint'),
243
+ ];
244
+
245
+ const results = await Promise.allSettled(checks);
246
+ const failedChecks = results.filter(result => result.status === 'rejected' || result.value !== true);
247
+
248
+ if (failedChecks.length > 0) {
249
+ throw new Error(`Address ${addressString} does not appear to implement ERC20 view methods`);
250
+ }
251
+
252
+ logger.verbose(`Validated existing token at ${addressString} appears to be ERC20-compatible`);
253
+ }
254
+
178
255
  export const deploySharedContracts = async (
179
256
  l1Client: ExtendedViemWalletClient,
180
257
  deployer: L1Deployer,
@@ -187,19 +264,22 @@ export const deploySharedContracts = async (
187
264
 
188
265
  const txHashes: Hex[] = [];
189
266
 
190
- const { address: feeAssetAddress } = await deployer.deploy(FeeAssetArtifact, [
191
- 'FeeJuice',
192
- 'FEE',
193
- l1Client.account.address,
194
- ]);
195
- logger.verbose(`Deployed Fee Asset at ${feeAssetAddress}`);
267
+ let feeAssetAddress: EthAddress;
268
+ let stakingAssetAddress: EthAddress;
269
+ if (args.existingTokenAddress) {
270
+ await validateExistingErc20TokenAddress(l1Client, args.existingTokenAddress, logger);
271
+ feeAssetAddress = args.existingTokenAddress;
272
+ stakingAssetAddress = args.existingTokenAddress;
273
+ logger.verbose(`Using existing token for fee and staking assets at ${args.existingTokenAddress}`);
274
+ } else {
275
+ const deployedFee = await deployer.deploy(FeeAssetArtifact, ['FeeJuice', 'FEE', l1Client.account.address]);
276
+ feeAssetAddress = deployedFee.address;
277
+ logger.verbose(`Deployed Fee Asset at ${feeAssetAddress}`);
196
278
 
197
- const { address: stakingAssetAddress } = await deployer.deploy(StakingAssetArtifact, [
198
- 'Staking',
199
- 'STK',
200
- l1Client.account.address,
201
- ]);
202
- logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`);
279
+ const deployedStaking = await deployer.deploy(StakingAssetArtifact, ['Staking', 'STK', l1Client.account.address]);
280
+ stakingAssetAddress = deployedStaking.address;
281
+ logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`);
282
+ }
203
283
 
204
284
  const gseAddress = (
205
285
  await deployer.deploy(GSEArtifact, [
@@ -287,8 +367,8 @@ export const deploySharedContracts = async (
287
367
  let stakingAssetHandlerAddress: EthAddress | undefined = undefined;
288
368
  let zkPassportVerifierAddress: EthAddress | undefined = undefined;
289
369
 
290
- // Only if not on mainnet will we deploy the handlers
291
- if (l1Client.chain.id !== 1) {
370
+ // Only if not on mainnet will we deploy the handlers, and only when we control the token
371
+ if (l1Client.chain.id !== 1 && !args.existingTokenAddress) {
292
372
  /* -------------------------------------------------------------------------- */
293
373
  /* CHEAT CODES START HERE */
294
374
  /* -------------------------------------------------------------------------- */
@@ -379,19 +459,23 @@ export const deploySharedContracts = async (
379
459
 
380
460
  const rewardDistributorAddress = await registry.getRewardDistributor();
381
461
 
382
- const blockReward = getRewardConfig(networkName).blockReward;
462
+ if (!args.existingTokenAddress) {
463
+ const blockReward = getRewardConfig(networkName).blockReward;
383
464
 
384
- const funding = blockReward * 200000n;
385
- const { txHash: fundRewardDistributorTxHash } = await deployer.sendTransaction({
386
- to: feeAssetAddress.toString(),
387
- data: encodeFunctionData({
388
- abi: FeeAssetArtifact.contractAbi,
389
- functionName: 'mint',
390
- args: [rewardDistributorAddress.toString(), funding],
391
- }),
392
- });
465
+ const funding = blockReward * 200000n;
466
+ const { txHash: fundRewardDistributorTxHash } = await deployer.sendTransaction({
467
+ to: feeAssetAddress.toString(),
468
+ data: encodeFunctionData({
469
+ abi: FeeAssetArtifact.contractAbi,
470
+ functionName: 'mint',
471
+ args: [rewardDistributorAddress.toString(), funding],
472
+ }),
473
+ });
393
474
 
394
- logger.verbose(`Funded reward distributor with ${funding} fee asset in ${fundRewardDistributorTxHash}`);
475
+ logger.verbose(`Funded reward distributor with ${funding} fee asset in ${fundRewardDistributorTxHash}`);
476
+ } else {
477
+ logger.verbose(`Skipping reward distributor funding as existing token is provided`);
478
+ }
395
479
 
396
480
  /* -------------------------------------------------------------------------- */
397
481
  /* FUND REWARD DISTRIBUTOR STOP */
@@ -610,21 +694,26 @@ export const deployRollup = async (
610
694
  logger.verbose(`All core contracts have been deployed`);
611
695
 
612
696
  if (args.feeJuicePortalInitialBalance && args.feeJuicePortalInitialBalance > 0n) {
613
- const feeJuicePortalAddress = await rollupContract.getFeeJuicePortal();
697
+ // Skip funding when using an external token, as we likely don't have mint permissions
698
+ if (!('existingTokenAddress' in args) || !args.existingTokenAddress) {
699
+ const feeJuicePortalAddress = await rollupContract.getFeeJuicePortal();
614
700
 
615
- // In fast mode, use the L1TxUtils to send transactions with nonce management
616
- const { txHash: mintTxHash } = await deployer.sendTransaction({
617
- to: addresses.feeJuiceAddress.toString(),
618
- data: encodeFunctionData({
619
- abi: FeeAssetArtifact.contractAbi,
620
- functionName: 'mint',
621
- args: [feeJuicePortalAddress.toString(), args.feeJuicePortalInitialBalance],
622
- }),
623
- });
624
- logger.verbose(
625
- `Funding fee juice portal with ${args.feeJuicePortalInitialBalance} fee juice in ${mintTxHash} (accelerated test deployments)`,
626
- );
627
- txHashes.push(mintTxHash);
701
+ // In fast mode, use the L1TxUtils to send transactions with nonce management
702
+ const { txHash: mintTxHash } = await deployer.sendTransaction({
703
+ to: addresses.feeJuiceAddress.toString(),
704
+ data: encodeFunctionData({
705
+ abi: FeeAssetArtifact.contractAbi,
706
+ functionName: 'mint',
707
+ args: [feeJuicePortalAddress.toString(), args.feeJuicePortalInitialBalance],
708
+ }),
709
+ });
710
+ logger.verbose(
711
+ `Funding fee juice portal with ${args.feeJuicePortalInitialBalance} fee juice in ${mintTxHash} (accelerated test deployments)`,
712
+ );
713
+ txHashes.push(mintTxHash);
714
+ } else {
715
+ logger.verbose('Skipping fee juice portal funding due to external token usage');
716
+ }
628
717
  }
629
718
 
630
719
  const slashFactoryAddress = (await deployer.deploy(SlashFactoryArtifact, [rollupAddress.toString()])).address;
@@ -747,6 +836,7 @@ export const handoverToGovernance = async (
747
836
  governanceAddress: EthAddress,
748
837
  logger: Logger,
749
838
  acceleratedTestDeployments: boolean | undefined,
839
+ useExternalToken: boolean = false,
750
840
  ) => {
751
841
  // We need to call a function on the registry to set the various contract addresses.
752
842
  const registryContract = getContract({
@@ -812,7 +902,10 @@ export const handoverToGovernance = async (
812
902
  txHashes.push(transferOwnershipTxHash);
813
903
  }
814
904
 
815
- if (acceleratedTestDeployments || (await feeAsset.read.owner()) !== coinIssuerAddress.toString()) {
905
+ if (
906
+ !useExternalToken &&
907
+ (acceleratedTestDeployments || (await feeAsset.read.owner()) !== coinIssuerAddress.toString())
908
+ ) {
816
909
  const { txHash } = await deployer.sendTransaction(
817
910
  {
818
911
  to: feeAssetAddress.toString(),
@@ -839,23 +932,28 @@ export const handoverToGovernance = async (
839
932
  );
840
933
  logger.verbose(`Accept ownership of fee asset in ${acceptTokenOwnershipTxHash}`);
841
934
  txHashes.push(acceptTokenOwnershipTxHash);
935
+ } else if (useExternalToken) {
936
+ logger.verbose('Skipping fee asset ownership transfer due to external token usage');
842
937
  }
843
938
 
939
+ // Either deploy or at least predict the address of the date gated relayer
940
+ const dateGatedRelayer = await deployer.deploy(DateGatedRelayerArtifact, [
941
+ governanceAddress.toString(),
942
+ 1798761600n, // 2027-01-01 00:00:00 UTC
943
+ ]);
944
+
844
945
  // If the owner is not the Governance contract, transfer ownership to the Governance contract
845
- if (
846
- acceleratedTestDeployments ||
847
- (await coinIssuerContract.read.owner()) !== getAddress(governanceAddress.toString())
848
- ) {
946
+ if (acceleratedTestDeployments || (await coinIssuerContract.read.owner()) === deployer.client.account.address) {
849
947
  const { txHash: transferOwnershipTxHash } = await deployer.sendTransaction({
850
948
  to: coinIssuerContract.address,
851
949
  data: encodeFunctionData({
852
950
  abi: CoinIssuerArtifact.contractAbi,
853
951
  functionName: 'transferOwnership',
854
- args: [getAddress(governanceAddress.toString())],
952
+ args: [getAddress(dateGatedRelayer.address.toString())],
855
953
  }),
856
954
  });
857
955
  logger.verbose(
858
- `Transferring the ownership of the coin issuer contract at ${coinIssuerAddress} to the Governance ${governanceAddress} in tx ${transferOwnershipTxHash}`,
956
+ `Transferring the ownership of the coin issuer contract at ${coinIssuerAddress} to the DateGatedRelayer ${dateGatedRelayer.address} in tx ${transferOwnershipTxHash}`,
859
957
  );
860
958
  txHashes.push(transferOwnershipTxHash);
861
959
  }
@@ -863,6 +961,8 @@ export const handoverToGovernance = async (
863
961
  // Wait for all actions to be mined
864
962
  await deployer.waitForDeployments();
865
963
  await Promise.all(txHashes.map(txHash => extendedClient.waitForTransactionReceipt({ hash: txHash })));
964
+
965
+ return { dateGatedRelayerAddress: dateGatedRelayer.address };
866
966
  };
867
967
 
868
968
  /*
@@ -1079,6 +1179,13 @@ export const deployL1Contracts = async (
1079
1179
  logger.info(`Deploying L1 contracts with config: ${jsonStringify(args)}`);
1080
1180
  validateConfig(args);
1081
1181
 
1182
+ if (args.initialValidators && args.initialValidators.length > 0 && args.existingTokenAddress) {
1183
+ throw new Error(
1184
+ 'Cannot deploy with both initialValidators and existingTokenAddress. ' +
1185
+ 'Initial validator funding requires minting tokens, which is not possible with an external token.',
1186
+ );
1187
+ }
1188
+
1082
1189
  const l1Client = createExtendedL1Client(rpcUrls, account, chain);
1083
1190
 
1084
1191
  // Deploy multicall3 if it does not exist in this network
@@ -1148,7 +1255,7 @@ export const deployL1Contracts = async (
1148
1255
  await deployer.waitForDeployments();
1149
1256
 
1150
1257
  // Now that the rollup has been deployed and added to the registry, transfer ownership to governance
1151
- await handoverToGovernance(
1258
+ const { dateGatedRelayerAddress } = await handoverToGovernance(
1152
1259
  l1Client,
1153
1260
  deployer,
1154
1261
  registryAddress,
@@ -1158,6 +1265,7 @@ export const deployL1Contracts = async (
1158
1265
  governanceAddress,
1159
1266
  logger,
1160
1267
  args.acceleratedTestDeployments,
1268
+ !!args.existingTokenAddress,
1161
1269
  );
1162
1270
 
1163
1271
  logger.info(`Handing over to governance complete`);
@@ -1342,6 +1450,7 @@ export const deployL1Contracts = async (
1342
1450
  stakingAssetHandlerAddress,
1343
1451
  zkPassportVerifierAddress,
1344
1452
  coinIssuerAddress,
1453
+ dateGatedRelayerAddress,
1345
1454
  },
1346
1455
  };
1347
1456
  };
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  CoinIssuerAbi,
3
3
  CoinIssuerBytecode,
4
+ DateGatedRelayerAbi,
5
+ DateGatedRelayerBytecode,
4
6
  EmpireSlasherDeploymentExtLibAbi,
5
7
  EmpireSlasherDeploymentExtLibBytecode,
6
8
  EmpireSlashingProposerAbi,
@@ -149,6 +151,12 @@ export const CoinIssuerArtifact = {
149
151
  contractBytecode: CoinIssuerBytecode as Hex,
150
152
  };
151
153
 
154
+ export const DateGatedRelayerArtifact = {
155
+ name: 'DateGatedRelayer',
156
+ contractAbi: DateGatedRelayerAbi,
157
+ contractBytecode: DateGatedRelayerBytecode as Hex,
158
+ };
159
+
152
160
  export const GovernanceProposerArtifact = {
153
161
  name: 'GovernanceProposer',
154
162
  contractAbi: GovernanceProposerAbi,
@@ -32,6 +32,7 @@ export type L1ContractAddresses = {
32
32
  stakingAssetHandlerAddress?: EthAddress | undefined;
33
33
  zkPassportVerifierAddress?: EthAddress | undefined;
34
34
  gseAddress?: EthAddress | undefined;
35
+ dateGatedRelayerAddress?: EthAddress | undefined;
35
36
  };
36
37
 
37
38
  export const L1ContractAddressesSchema = z.object({
@@ -51,12 +52,13 @@ export const L1ContractAddressesSchema = z.object({
51
52
  stakingAssetHandlerAddress: schemas.EthAddress.optional(),
52
53
  zkPassportVerifierAddress: schemas.EthAddress.optional(),
53
54
  gseAddress: schemas.EthAddress.optional(),
55
+ dateGatedRelayerAddress: schemas.EthAddress.optional(),
54
56
  }) satisfies ZodFor<L1ContractAddresses>;
55
57
 
56
58
  const parseEnv = (val: string) => EthAddress.fromString(val);
57
59
 
58
60
  export const l1ContractAddressesMapping: ConfigMappingsType<
59
- Omit<L1ContractAddresses, 'gseAddress' | 'zkPassportVerifierAddress'>
61
+ Omit<L1ContractAddresses, 'gseAddress' | 'zkPassportVerifierAddress' | 'dateGatedRelayerAddress'>
60
62
  > = {
61
63
  registryAddress: {
62
64
  env: 'REGISTRY_CONTRACT_ADDRESS',