diff --git a/.changeset/fix-operator-fee-isthmus.md b/.changeset/fix-operator-fee-isthmus.md new file mode 100644 index 0000000000..1e918ee67c --- /dev/null +++ b/.changeset/fix-operator-fee-isthmus.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Fixed Operator Fee estimation for Isthmus upgrade by using the `getOperatorFee` function from the Gas Price Oracle instead of manually computing from L1Block parameters. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd27629396..04cb6a221e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6275,8 +6275,8 @@ packages: typescript: optional: true - viem@2.45.0: - resolution: {integrity: sha512-iVA9qrAgRdtpWa80lCZ6Jri6XzmLOwwA1wagX2HnKejKeliFLpON0KOdyfqvcy+gUpBVP59LBxP2aKiL3aj8fg==} + viem@2.45.1: + resolution: {integrity: sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -8301,7 +8301,7 @@ snapshots: pino-pretty: 10.3.1 prom-client: 14.2.0 type-fest: 4.39.0 - viem: 2.45.0(typescript@5.9.3)(zod@3.23.8) + viem: 2.45.1(typescript@5.9.3)(zod@3.23.8) yargs: 17.7.2 zod: 3.23.8 zod-validation-error: 1.5.0(zod@3.23.8) @@ -13125,7 +13125,7 @@ snapshots: - utf-8-validate - zod - viem@2.45.0(typescript@5.9.3)(zod@3.23.8): + viem@2.45.1(typescript@5.9.3)(zod@3.23.8): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 diff --git a/src/op-stack/abis.ts b/src/op-stack/abis.ts index e208e7296f..fe6f22aea1 100644 --- a/src/op-stack/abis.ts +++ b/src/op-stack/abis.ts @@ -46,6 +46,13 @@ export const gasPriceOracleAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [{ internalType: 'uint256', name: '_gasUsed', type: 'uint256' }], + name: 'getOperatorFee', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'l1BaseFee', diff --git a/src/op-stack/actions/buildDepositTransaction.test.ts b/src/op-stack/actions/buildDepositTransaction.test.ts index 62e9c84772..2acf519a79 100644 --- a/src/op-stack/actions/buildDepositTransaction.test.ts +++ b/src/op-stack/actions/buildDepositTransaction.test.ts @@ -28,7 +28,7 @@ test('default', async () => { "account": undefined, "request": { "data": undefined, - "gas": 21000n, + "gas": 23660n, "isCreation": undefined, "mint": undefined, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", @@ -56,7 +56,7 @@ test('args: account', async () => { }, "request": { "data": undefined, - "gas": 21000n, + "gas": 23660n, "isCreation": undefined, "mint": undefined, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", @@ -85,7 +85,7 @@ test('args: chain', async () => { }, "request": { "data": undefined, - "gas": 21000n, + "gas": 23660n, "isCreation": undefined, "mint": undefined, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", @@ -114,7 +114,7 @@ test('args: data', async () => { }, "request": { "data": "0xdeadbeef", - "gas": 21160n, + "gas": 23838n, "isCreation": undefined, "mint": undefined, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", @@ -172,7 +172,7 @@ test('args: mint', async () => { }, "request": { "data": undefined, - "gas": 21000n, + "gas": 23660n, "isCreation": undefined, "mint": 1000000000000000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", @@ -201,7 +201,7 @@ test('args: value', async () => { }, "request": { "data": undefined, - "gas": 21000n, + "gas": 32646n, "isCreation": undefined, "mint": undefined, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", diff --git a/src/op-stack/actions/estimateContractL1Gas.test.ts b/src/op-stack/actions/estimateContractL1Gas.test.ts index 508c5d0879..43c4aebcd0 100644 --- a/src/op-stack/actions/estimateContractL1Gas.test.ts +++ b/src/op-stack/actions/estimateContractL1Gas.test.ts @@ -15,5 +15,5 @@ test('default', async () => { functionName: 'transfer', args: ['0xc8373edfad6d5c5f600b6b2507f78431c5271ff5', 1n], }), - ).toBe(2544n) + ).toBe(1600n) }) diff --git a/src/op-stack/actions/estimateL1Gas.test.ts b/src/op-stack/actions/estimateL1Gas.test.ts index ecae1cc5f2..da388a2e37 100644 --- a/src/op-stack/actions/estimateL1Gas.test.ts +++ b/src/op-stack/actions/estimateL1Gas.test.ts @@ -21,12 +21,12 @@ const baseTransaction = { test('default', async () => { const gas = await estimateL1Gas(optimismClientWithAccount, baseTransaction) - expect(gas).toBe(2004n) + expect(gas).toBe(1600n) }) test('minimal', async () => { const gas = await estimateL1Gas(optimismClientWithAccount, {}) - expect(gas).toBe(1604n) + expect(gas).toBe(1600n) }) test('args: account', async () => { @@ -34,7 +34,7 @@ test('args: account', async () => { ...baseTransaction, account: accounts[0].address, }) - expect(gas).toBe(2004n) + expect(gas).toBe(1600n) }) test('args: data', async () => { @@ -42,7 +42,7 @@ test('args: data', async () => { ...baseTransaction, data: '0x00000000000000000000000000000000000000000000000004fefa17b7240000', }) - expect(gas).toBe(2156n) + expect(gas).toBe(1600n) }) test('args: gasPriceOracleAddress', async () => { @@ -50,7 +50,7 @@ test('args: gasPriceOracleAddress', async () => { ...baseTransaction, gasPriceOracleAddress: '0x420000000000000000000000000000000000000F', }) - expect(gas).toBe(2004n) + expect(gas).toBe(1600n) }) test('args: nonce', async () => { @@ -58,7 +58,7 @@ test('args: nonce', async () => { ...baseTransaction, nonce: 69, }) - expect(gas).toBe(2004n) + expect(gas).toBe(1600n) }) test('args: nullish chain', async () => { @@ -67,7 +67,7 @@ test('args: nullish chain', async () => { account: accounts[0].address, chain: null, }) - expect(gas).toBe(2004n) + expect(gas).toBe(1600n) }) test('behavior: account with no funds', async () => { diff --git a/src/op-stack/actions/estimateOperatorFee.test.ts b/src/op-stack/actions/estimateOperatorFee.test.ts index ca09547b18..c1d432b576 100644 --- a/src/op-stack/actions/estimateOperatorFee.test.ts +++ b/src/op-stack/actions/estimateOperatorFee.test.ts @@ -45,10 +45,10 @@ test('args: data', async () => { expect(fee).toBeDefined() }) -test('args: l1BlockAddress', async () => { +test('args: gasPriceOracleAddress', async () => { const fee = await estimateOperatorFee(optimismClientWithAccount, { ...baseTransaction, - l1BlockAddress: '0x4200000000000000000000000000000000000015', + gasPriceOracleAddress: '0x420000000000000000000000000000000000000F', }) expect(fee).toBeDefined() }) diff --git a/src/op-stack/actions/estimateOperatorFee.ts b/src/op-stack/actions/estimateOperatorFee.ts index fa9f032e49..fc163e71f5 100644 --- a/src/op-stack/actions/estimateOperatorFee.ts +++ b/src/op-stack/actions/estimateOperatorFee.ts @@ -18,7 +18,7 @@ import type { TransactionRequestEIP1559 } from '../../types/transaction.js' import type { RequestErrorType } from '../../utils/buildRequest.js' import { getChainContractAddress } from '../../utils/chain/getChainContractAddress.js' import type { HexToNumberErrorType } from '../../utils/encoding/fromHex.js' -import { l1BlockAbi } from '../abis.js' +import { gasPriceOracleAbi } from '../abis.js' import { contracts } from '../contracts.js' export type EstimateOperatorFeeParameters< @@ -28,7 +28,12 @@ export type EstimateOperatorFeeParameters< > = Omit & GetAccountParameter & GetChainParameter & { - /** L1 block attributes contract address. */ + /** Gas price oracle address. */ + gasPriceOracleAddress?: Address | undefined + /** + * L1 block attributes contract address. + * @deprecated Use `gasPriceOracleAddress` instead. + */ l1BlockAddress?: Address | undefined } @@ -46,7 +51,6 @@ export type EstimateOperatorFeeErrorType = * * Operator fees are part of the Isthmus upgrade and allow OP Stack operators * to recover costs related to Alt-DA, ZK proving, or custom gas tokens. - * Returns 0 for pre-Isthmus chains or when operator fee functions don't exist. * * @param client - Client to use * @param parameters - {@link EstimateOperatorFeeParameters} @@ -75,45 +79,28 @@ export async function estimateOperatorFee< client: Client, args: EstimateOperatorFeeParameters, ): Promise { - const { chain = client.chain, l1BlockAddress: l1BlockAddress_ } = args + const { + chain = client.chain, + gasPriceOracleAddress: gasPriceOracleAddress_, + } = args - const l1BlockAddress = (() => { - if (l1BlockAddress_) return l1BlockAddress_ - if (chain?.contracts?.l1Block) + const gasPriceOracleAddress = (() => { + if (gasPriceOracleAddress_) return gasPriceOracleAddress_ + if (chain) return getChainContractAddress({ chain, - contract: 'l1Block', + contract: 'gasPriceOracle', }) - return contracts.l1Block.address - })() + return contracts.gasPriceOracle.address + })() as Address - // Try to get operator fee parameters. If any of these calls fail, - // it means this is a pre-Isthmus chain and operator fees don't apply - try { - // Get operator fee parameters first to fail fast if not supported - const [operatorFeeScalar, operatorFeeConstant] = await Promise.all([ - readContract(client, { - abi: l1BlockAbi, - address: l1BlockAddress, - functionName: 'operatorFeeScalar', - }), - readContract(client, { - abi: l1BlockAbi, - address: l1BlockAddress, - functionName: 'operatorFeeConstant', - }), - ]) + // Estimate gas for the transaction + const gasUsed = await estimateGas(client, args as EstimateGasParameters) - // Estimate gas for the actual transaction - const gasUsed = await estimateGas(client, args as EstimateGasParameters) - - // Calculate operator fee: saturatingAdd(saturatingMul(gasUsed, scalar) / 1e6, constant) - // Using saturating arithmetic to prevent overflow - const scaledFee = (gasUsed * BigInt(operatorFeeScalar)) / 1_000_000n - return scaledFee + BigInt(operatorFeeConstant) - } catch { - // If any call fails, this is likely a pre-Isthmus chain or the contract - // doesn't support these functions. Return 0 for operator fee. - return 0n - } + return readContract(client, { + abi: gasPriceOracleAbi, + address: gasPriceOracleAddress, + functionName: 'getOperatorFee', + args: [gasUsed], + }) } diff --git a/src/op-stack/actions/estimateTotalGas.test.ts b/src/op-stack/actions/estimateTotalGas.test.ts index 3bd9fdbe1f..a1bf1ce690 100644 --- a/src/op-stack/actions/estimateTotalGas.test.ts +++ b/src/op-stack/actions/estimateTotalGas.test.ts @@ -18,12 +18,12 @@ const baseTransaction = { test('default', async () => { const gas = await estimateTotalGas(optimismClientWithAccount, baseTransaction) - expect(gas).toBe(23028n) + expect(gas).toBe(34246n) }) test('minimal', async () => { const gas = await estimateTotalGas(optimismClientWithAccount, {}) - expect(gas).toBe(54605n) + expect(gas).toBe(54601n) }) test('args: account', async () => { @@ -31,7 +31,7 @@ test('args: account', async () => { ...baseTransaction, account: accounts[0].address, }) - expect(gas).toBe(23028n) + expect(gas).toBe(34246n) }) test('args: data', async () => { @@ -39,7 +39,7 @@ test('args: data', async () => { ...baseTransaction, data: '0x00000000000000000000000000000000000000000000000004fefa17b7240000', }) - expect(gas).toBe(23760n) + expect(gas).toBe(34560n) }) test('args: gasPriceOracleAddress', async () => { @@ -47,7 +47,7 @@ test('args: gasPriceOracleAddress', async () => { ...baseTransaction, gasPriceOracleAddress: '0x420000000000000000000000000000000000000F', }) - expect(gas).toBe(23028n) + expect(gas).toBe(34246n) }) test('args: nonce', async () => { @@ -55,7 +55,7 @@ test('args: nonce', async () => { ...baseTransaction, nonce: 69, }) - expect(gas).toBe(23028n) + expect(gas).toBe(34246n) }) test('args: nullish chain', async () => { @@ -64,5 +64,5 @@ test('args: nullish chain', async () => { account: accounts[0].address, chain: null, }) - expect(gas).toBe(23028n) + expect(gas).toBe(34246n) }) diff --git a/test/src/anvil.ts b/test/src/anvil.ts index 0980bc007a..0c244b78d1 100644 --- a/test/src/anvil.ts +++ b/test/src/anvil.ts @@ -41,7 +41,7 @@ export const anvilOptimism = defineAnvil({ 'VITE_ANVIL_FORK_URL_OPTIMISM', 'https://mainnet.optimism.io', ), - forkBlockNumber: 113624777n, + forkBlockNumber: 147000000n, port: 8645, })