diff --git a/libs/metrics/src/l1/l1MetricsService.ts b/libs/metrics/src/l1/l1MetricsService.ts index 67d34ca..6eff42e 100644 --- a/libs/metrics/src/l1/l1MetricsService.ts +++ b/libs/metrics/src/l1/l1MetricsService.ts @@ -7,6 +7,7 @@ import { encodeFunctionData, erc20Abi, formatUnits, + Hex, parseEther, parseUnits, zeroAddress, @@ -18,7 +19,7 @@ import { L1MetricsServiceException, } from "@zkchainhub/metrics/exceptions"; import { bridgeHubAbi, diamondProxyAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis"; -import { AssetTvl, GasInfo } from "@zkchainhub/metrics/types"; +import { AssetTvl, FeeParams, feeParamsFieldLengths, GasInfo } from "@zkchainhub/metrics/types"; import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing"; import { EvmProviderService } from "@zkchainhub/providers"; import { @@ -39,6 +40,7 @@ import { } from "@zkchainhub/shared/tokens/tokens"; const ONE_ETHER = parseEther("1"); +const FEE_PARAMS_SLOT: Hex = `0x26`; /** * Acts as a wrapper around Viem library to provide methods to interact with an EVM-based blockchain. @@ -324,20 +326,56 @@ export class L1MetricsService { } } - //TODO: Implement feeParams. - async feeParams(_chainId: ChainId): Promise<{ - batchOverheadL1Gas: number; - maxPubdataPerBatch: number; - maxL2GasPerBatch: number; - priorityTxMaxPubdata: number; - minimalL2GasPrice: number; - }> { + /** + * Retrieves the fee parameters for a specific chain. + * + * @param chainId - The ID of the chain. + * @returns A Promise that resolves to a FeeParams object containing the fee parameters. + * @throws {L1MetricsServiceException} If the fee parameters cannot be retrieved from L1. + */ + async feeParams(chainId: ChainId): Promise { + const diamondProxyAddress = await this.fetchDiamondProxyAddress(chainId); + + // Read the storage at the target slot; + const feeParamsData = await this.evmProviderService.getStorageAt( + diamondProxyAddress, + FEE_PARAMS_SLOT, + ); + if (!feeParamsData) { + throw new L1MetricsServiceException("Failed to get fee params from L1."); + } + + const strippedParamsData = feeParamsData.slice(2); // Remove the 0x prefix + let cursor = strippedParamsData.length; + const values: string[] = []; + + for (const value of Object.values(feeParamsFieldLengths)) { + values.push(strippedParamsData.slice(cursor - value, cursor)); + cursor -= value; + } + + assert( + values.length === Object.keys(feeParamsFieldLengths).length, + "Error parsing fee params", + ); + + const [ + pubdataPricingMode, + batchOverheadL1Gas, + maxPubdataPerBatch, + maxL2GasPerBatch, + priorityTxMaxPubdata, + minimalL2GasPrice, + ] = values as [string, string, string, string, string, string]; + + // Convert hex to decimal return { - batchOverheadL1Gas: 50000, - maxPubdataPerBatch: 120000, - maxL2GasPerBatch: 10000000, - priorityTxMaxPubdata: 15000, - minimalL2GasPrice: 10000000, + pubdataPricingMode: parseInt(pubdataPricingMode, 16), + batchOverheadL1Gas: parseInt(batchOverheadL1Gas, 16), + maxPubdataPerBatch: parseInt(maxPubdataPerBatch, 16), + maxL2GasPerBatch: parseInt(maxL2GasPerBatch, 16), + priorityTxMaxPubdata: parseInt(priorityTxMaxPubdata, 16), + minimalL2GasPrice: BigInt(`0x${minimalL2GasPrice}`), }; } } diff --git a/libs/metrics/src/types/feeParams.type.ts b/libs/metrics/src/types/feeParams.type.ts new file mode 100644 index 0000000..b177298 --- /dev/null +++ b/libs/metrics/src/types/feeParams.type.ts @@ -0,0 +1,19 @@ +//See: https://github.com/matter-labs/era-contracts/blob/8a70bbbc48125f5bde6189b4e3c6a3ee79631678/l1-contracts/contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol#L52 +export type FeeParams = { + pubdataPricingMode: number; + batchOverheadL1Gas: number; + maxPubdataPerBatch: number; + maxL2GasPerBatch: number; + priorityTxMaxPubdata: number; + minimalL2GasPrice?: bigint; +}; + +// Define the lengths for each field (in hex digits, each byte is 2 hex digits) +export const feeParamsFieldLengths = { + pubdataPricingMode: 2, // uint8 -> 1 byte -> 2 hex digits + batchOverheadL1Gas: 8, // uint32 -> 4 bytes -> 8 hex digits + maxPubdataPerBatch: 8, // uint32 -> 4 bytes -> 8 hex digits + maxL2GasPerBatch: 8, // uint32 -> 4 bytes -> 8 hex digits + priorityTxMaxPubdata: 8, // uint32 -> 4 bytes -> 8 hex digits + minimalL2GasPrice: 16, // uint64 -> 8 bytes -> 16 hex digits +} as const; diff --git a/libs/metrics/src/types/index.ts b/libs/metrics/src/types/index.ts index b2f1a4d..e3a10a4 100644 --- a/libs/metrics/src/types/index.ts +++ b/libs/metrics/src/types/index.ts @@ -1,2 +1,3 @@ export * from "./gasInfo.type"; export * from "./tvl.type"; +export * from "./feeParams.type"; diff --git a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts index 17089a1..421be82 100644 --- a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts +++ b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts @@ -717,15 +717,48 @@ describe("L1MetricsService", () => { }); describe("feeParams", () => { - it("return feeParams", async () => { - const result = await l1MetricsService.feeParams(1n); - expect(result).toEqual({ - batchOverheadL1Gas: 50000, + it("should retrieve the fee parameters for a specific chain", async () => { + // Mock the dependencies + const chainId = 324n; // this is ZKsyncEra chain id + const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890"; + const mockFeeParamsRawData = + "0x00000000000000000000000ee6b280000182b804c4b4000001d4c0000f424000"; + + const mockFeeParams = { + pubdataPricingMode: 0, + batchOverheadL1Gas: 1000000, maxPubdataPerBatch: 120000, - maxL2GasPerBatch: 10000000, - priorityTxMaxPubdata: 15000, - minimalL2GasPrice: 10000000, - }); + maxL2GasPerBatch: 80000000, + priorityTxMaxPubdata: 99000, + minimalL2GasPrice: 250000000n, + }; + + l1MetricsService["diamondContracts"].set(chainId, mockedDiamondProxyAddress); + jest.spyOn(mockEvmProviderService, "getStorageAt").mockResolvedValue( + mockFeeParamsRawData, + ); + + const result = await l1MetricsService.feeParams(chainId); + + expect(mockEvmProviderService.getStorageAt).toHaveBeenCalledWith( + mockedDiamondProxyAddress, + `0x26`, + ); + + expect(result).toEqual(mockFeeParams); + }); + + it("should throw an exception if the fee parameters cannot be retrieved from L1", async () => { + // Mock the dependencies + const chainId = 324n; // this is ZKsyncEra chain id + const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890"; + l1MetricsService["diamondContracts"].set(chainId, mockedDiamondProxyAddress); + + jest.spyOn(mockEvmProviderService, "getStorageAt").mockResolvedValue(undefined); + + await expect(l1MetricsService.feeParams(chainId)).rejects.toThrow( + L1MetricsServiceException, + ); }); }); }); diff --git a/libs/providers/src/providers/evmProvider.service.ts b/libs/providers/src/providers/evmProvider.service.ts index 470b674..52babb0 100644 --- a/libs/providers/src/providers/evmProvider.service.ts +++ b/libs/providers/src/providers/evmProvider.service.ts @@ -93,8 +93,8 @@ export class EvmProviderService { * @returns {Promise} A Promise that resolves to the value of the storage slot. * @throws {InvalidArgumentException} If the slot is not a positive integer. */ - async getStorageAt(address: Address, slot: number): Promise { - if (slot <= 0 || !Number.isInteger(slot)) { + async getStorageAt(address: Address, slot: number | Hex): Promise { + if (typeof slot === "number" && (slot <= 0 || !Number.isInteger(slot))) { throw new InvalidArgumentException( `Slot must be a positive integer number. Received: ${slot}`, ); @@ -102,7 +102,7 @@ export class EvmProviderService { return this.client.getStorageAt({ address, - slot: toHex(slot), + slot: typeof slot === "string" ? slot : toHex(slot), }); } diff --git a/libs/providers/test/unit/providers/evmProvider.service.spec.ts b/libs/providers/test/unit/providers/evmProvider.service.spec.ts index 72a6122..0cbdf0b 100644 --- a/libs/providers/test/unit/providers/evmProvider.service.spec.ts +++ b/libs/providers/test/unit/providers/evmProvider.service.spec.ts @@ -131,6 +131,18 @@ describe("EvmProviderService", () => { expect(mockClient.getStorageAt).toHaveBeenCalledWith({ address, slot: "0x1" }); }); + it("should return the value of the storage slot at the given address and slot value", async () => { + const address = "0x123456789"; + const slot = "0x12"; + const expectedValue = "0xabcdef"; + jest.spyOn(mockClient, "getStorageAt").mockResolvedValue(expectedValue); + + const value = await viemProvider.getStorageAt(address, slot); + + expect(value).toBe(expectedValue); + expect(mockClient.getStorageAt).toHaveBeenCalledWith({ address, slot: "0x12" }); + }); + it("should throw an error if the slot is not a positive integer", async () => { const address = "0x123456789"; const slot = -1;