diff --git a/ui-package/src/components/SubmitBuilderDepositsForm/BuilderDepositsTable.tsx b/ui-package/src/components/SubmitBuilderDepositsForm/BuilderDepositsTable.tsx index 44973f60e..fb769ca30 100644 --- a/ui-package/src/components/SubmitBuilderDepositsForm/BuilderDepositsTable.tsx +++ b/ui-package/src/components/SubmitBuilderDepositsForm/BuilderDepositsTable.tsx @@ -62,6 +62,10 @@ const BuilderDepositsTable = (props: IBuilderDepositsTableProps): React.ReactEle }, [dataSourceKey, blsReady]); // Compute the predeploy queue fee (shared across all rows). + // Unlike EIP-7002/7251, the EIP-8282 contract charges fees per write path: the fee + // numerator is the excess (slot 0) plus the requests already added in the current + // block (slot 1) beyond TARGET_PER_BLOCK, so the fee rises within a block. + const targetPerBlock = 8n; // TARGET_PER_BLOCK of the builder deposit contract let queueLength = 0n; let isPreFork = false; let requiredFee = 0n; @@ -73,12 +77,16 @@ const BuilderDepositsTable = (props: IBuilderDepositsTableProps): React.ReactEle if (queueLength === 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn) { isPreFork = true; } else { - requiredFee = getRequiredFee(queueLength); + let feeNumerator = queueLength; + if (queueData.blockCount > targetPerBlock) { + feeNumerator += queueData.blockCount - targetPerBlock; + } + requiredFee = getRequiredFee(feeNumerator); if (addExtraFee && cachedLogData) { for (let block in cachedLogData.logCount) avgRequestPerBlock += cachedLogData.logCount[block]; avgRequestPerBlock /= logLookbackRange; let extra = avgRequestPerBlock < 2 ? 3 : avgRequestPerBlock + 1; - requestFee = getRequiredFee(queueLength + BigInt(Math.ceil(extra))); + requestFee = getRequiredFee(feeNumerator + BigInt(Math.ceil(extra))); } else { requestFee = requiredFee; } diff --git a/ui-package/src/components/SubmitBuilderExitsForm/BuilderExitReview.tsx b/ui-package/src/components/SubmitBuilderExitsForm/BuilderExitReview.tsx index 8f16aa2f8..c8b8b617c 100644 --- a/ui-package/src/components/SubmitBuilderExitsForm/BuilderExitReview.tsx +++ b/ui-package/src/components/SubmitBuilderExitsForm/BuilderExitReview.tsx @@ -41,7 +41,14 @@ const BuilderExitReview = (props: IBuilderExitReviewProps) => { if (queueLength === 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn) { isPreFork = true; } else { - requiredFee = getRequiredFee(queueLength); + // Unlike EIP-7002/7251, the EIP-8282 contract charges fees per write path: the fee + // numerator is the excess (slot 0) plus the requests already added in the current + // block (slot 1) beyond TARGET_PER_BLOCK (2 for the builder exit contract). + let feeNumerator = queueLength; + if (queueData.blockCount > 2n) { + feeNumerator += queueData.blockCount - 2n; + } + requiredFee = getRequiredFee(feeNumerator); if (addExtraFee && cachedLogData) { for (let block in cachedLogData.logCount) { @@ -56,7 +63,7 @@ const BuilderExitReview = (props: IBuilderExitReviewProps) => { extraFeeForRequest++; } - requestFee = getRequiredFee(queueLength + BigInt(Math.ceil(extraFeeForRequest))); + requestFee = getRequiredFee(feeNumerator + BigInt(Math.ceil(extraFeeForRequest))); } else { requestFee = requiredFee; } diff --git a/ui-package/src/hooks/useQueueDataCache.ts b/ui-package/src/hooks/useQueueDataCache.ts index 31945bb45..fcb326928 100644 --- a/ui-package/src/hooks/useQueueDataCache.ts +++ b/ui-package/src/hooks/useQueueDataCache.ts @@ -3,6 +3,7 @@ import { useStorageAt, useBlockNumber, usePublicClient } from 'wagmi'; interface QueueData { queueLength: bigint; + blockCount: bigint; lastFetch: number; isLoading: boolean; error: Error | null; @@ -47,7 +48,16 @@ export const useQueueDataCache = (contractAddress: string, chainId?: number) => const storageCall = useStorageAt({ address: contractAddress as `0x${string}`, - slot: "0x00", + slot: "0x00", // excess requests (fee numerator base) + chainId, + query: { + enabled: false, // We'll manually control when to fetch + } + }); + + const countStorageCall = useStorageAt({ + address: contractAddress as `0x${string}`, + slot: "0x01", // requests added in the current block chainId, query: { enabled: false, // We'll manually control when to fetch @@ -88,12 +98,22 @@ export const useQueueDataCache = (contractAddress: string, chainId?: number) => fetchingContracts.add(cacheKey); try { - const result = await storageCall.refetch(); - + const [result, countResult] = await Promise.all([ + storageCall.refetch(), + countStorageCall.refetch(), + ]); + if (result.data) { + // refetch() resolves even on query errors; a missing slot-1 value must not + // silently degrade to 0n, as that would understate the quoted fee + if (countResult.data == null) { + throw countResult.error ?? new Error('Failed to read request count (slot 0x01)'); + } const queueLength = BigInt(result.data as string); + const blockCount = BigInt(countResult.data as string); const queueData: QueueData = { queueLength, + blockCount, lastFetch: Date.now(), isLoading: false, error: null, @@ -110,6 +130,7 @@ export const useQueueDataCache = (contractAddress: string, chainId?: number) => } catch (error) { const queueData: QueueData = { queueLength: 0n, + blockCount: 0n, lastFetch: Date.now(), isLoading: false, error: error as Error, @@ -125,7 +146,7 @@ export const useQueueDataCache = (contractAddress: string, chainId?: number) => } finally { fetchingContracts.delete(cacheKey); } - }, [cacheKey, storageCall]); + }, [cacheKey, storageCall, countStorageCall]); const fetchLogData = useCallback(async () => { if (!client || !blockNumber.data) return; @@ -205,6 +226,7 @@ export const useQueueDataCache = (contractAddress: string, chainId?: number) => if (fetchingContracts.has(cacheKey)) { return { queueLength: 0n, + blockCount: 0n, lastFetch: 0, isLoading: true, error: null,