From d84ed3d5ef693ac9a52b7d29645ec126c0930be8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 03:58:32 +0000 Subject: [PATCH 1/2] fix(ui): include in-block request count in EIP-8282 queue fee calculation The builder deposit and builder exit system contracts (EIP-8282) charge request fees per write path, unlike EIP-7002/7251 where the fee is fixed within a block. Their user subroutine computes the fee numerator as excess (slot 0) plus the requests already added in the current block (slot 1) beyond TARGET_PER_BLOCK (8 for deposits, 2 for exits). The submit forms only read slot 0, so during bursts of requests the displayed fee was far below the fee the contract actually charged (e.g. 1 wei shown vs 48 wei required), making the submitted msg.value fall short of fee + stake and the transaction revert. Read the in-block count from slot 1 as well and fold it into the fee numerator, matching the contract logic. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UEDNv2UuHsNxitpTUGZf55 --- .../BuilderDepositsTable.tsx | 12 +++++++-- .../BuilderExitReview.tsx | 11 ++++++-- ui-package/src/hooks/useQueueDataCache.ts | 25 ++++++++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-) 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..568b91317 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,17 @@ 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) { const queueLength = BigInt(result.data as string); + const blockCount = countResult.data ? BigInt(countResult.data as string) : 0n; const queueData: QueueData = { queueLength, + blockCount, lastFetch: Date.now(), isLoading: false, error: null, @@ -110,6 +125,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 +141,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 +221,7 @@ export const useQueueDataCache = (contractAddress: string, chainId?: number) => if (fetchingContracts.has(cacheKey)) { return { queueLength: 0n, + blockCount: 0n, lastFetch: 0, isLoading: true, error: null, From 0b0d1527e53d67fca4f62a7ac7e26e9719357aae Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Thu, 20 Aug 2026 12:13:54 +0200 Subject: [PATCH 2/2] fix(ui): surface slot-1 read failure instead of silently quoting understated fee A failed 0x01 storage read previously degraded blockCount to 0n and cached it, quietly reinstating the understated EIP-8282 fee quote. Treat it as an error so consumers (which gate on queueData.error) don't quote a stale fee. --- ui-package/src/hooks/useQueueDataCache.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui-package/src/hooks/useQueueDataCache.ts b/ui-package/src/hooks/useQueueDataCache.ts index 568b91317..fcb326928 100644 --- a/ui-package/src/hooks/useQueueDataCache.ts +++ b/ui-package/src/hooks/useQueueDataCache.ts @@ -104,8 +104,13 @@ export const useQueueDataCache = (contractAddress: string, chainId?: number) => ]); 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 = countResult.data ? BigInt(countResult.data as string) : 0n; + const blockCount = BigInt(countResult.data as string); const queueData: QueueData = { queueLength, blockCount,