For the below Arbitrum transaction, Tenderly shows a "Total Gas" of ~205k and an "Actual Gas Used" of ~8000k. How can this difference be explained? How can the actual gas used be higher than the total gas?
2 Answers
It's calling "Total Gas" the gas spent by executing the transaction (opcode by opcode, same as mainnet), while "Actual Gas Used" includes also the gas spent to publish the calldata on L1.
Using the notation from here:
Gas Limit (G) = Gas used on L2 (L2G) + Extra Buffer for L1 cost (B)
G
can be "Actual Gas Used" and L2G
"Total Gas".
You can look at this page on the explorer to confirm it (look at "L1 Gas Used" and "L2 Gas Used"): https://sepolia.arbiscan.io/tx/0xc98beb10bbfa1d088e401f2136b88a2431871711f7556e7388f8074f579eda1e#txninfo
It seems like you are doing normal 1559 fee estimation for an L2 which doesn't really work. Here is a proof of concept on Arbitrum fee estimation:
import { NODE_INTERFACE_ADDRESS } from '@arbitrum/sdk/dist/lib/dataEntities/constants';
import { NodeInterface__factory } from '@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory';
import { utils } from 'ethers';
// Instantiation of the NodeInterface object
const nodeInterface = NodeInterface__factory.connect(NODE_INTERFACE_ADDRESS, this.rpc);
const gasEstimateComponents = await nodeInterface.callStatic.gasEstimateComponents(to, false, utils.hexlify(data), {
blockTag: 'latest'
});
// Getting useful values for calculating the formula
const l1GasEstimated = gasEstimateComponents.gasEstimateForL1;
const l2GasUsed = gasEstimateComponents.gasEstimate.sub(gasEstimateComponents.gasEstimateForL1);
const l2EstimatedPrice = gasEstimateComponents.baseFee;
const l1EstimatedPrice = gasEstimateComponents.l1BaseFeeEstimate.mul(16);
console.log('gasEstimateComponents', gasEstimateComponents);
console.log('l1GasEstimated', l1GasEstimated.toNumber());
console.log('l2GasUsed', l2GasUsed.toNumber());
console.log('l2EstimatedPrice', l2EstimatedPrice.toNumber());
console.log('l1EstimatedPrice', l1EstimatedPrice.toNumber());
// Calculating some extra values to be able to apply all variables of the formula
// -------------------------------------------------------------------------------
// NOTE: This one might be a bit confusing, but l1GasEstimated (B in the formula) is calculated based on l2 gas fees
// const l1Cost = l1GasEstimated.mul(l2EstimatedPrice);
// NOTE: This is similar to 140 + utils.hexDataLength(txData);
// const l1Size = l1Cost.div(l1EstimatedPrice);
// Getting the result of the formula
// ---------------------------------
// Setting the basic variables of the formula
const P = l2EstimatedPrice;
const L2G = l2GasUsed;
const L1P = l1EstimatedPrice;
const L1S = 0;
// L1C (L1 Cost) = L1P * L1S
const L1C = L1P.mul(L1S);
// B (Extra Buffer) = L1C / P
const B = L1C.div(P);
// G (Gas Limit) = L2G + B
const G = L2G.add(B);
// TXFEES (Transaction fees) = P * G
const TXFEES = P.mul(G);
console.log('Gas estimation components');
console.log('-------------------');
console.log(`Full gas estimation = ${gasEstimateComponents.gasEstimate.toNumber()} units`);
console.log(`L2 Gas (L2G) = ${L2G.toNumber()} units`);
console.log(`L1 estimated Gas (L1G) = ${l1GasEstimated.toNumber()} units`);
console.log(`P (L2 Gas Price) = ${utils.formatUnits(P, 'gwei')} gwei`);
console.log(`L1P (L1 estimated calldata price per byte) = ${utils.formatUnits(L1P, 'gwei')} gwei`);
console.log(`L1S (L1 Calldata size in bytes) = ${L1S} bytes`);
console.log('-------------------');
console.log(`Transaction estimated fees to pay == ${this.network.tokenToWei(utils.formatEther(TXFEES), 18)} ETH`);
console.log(`Transaction estimated fees to pay unformatted = ${utils.formatEther(TXFEES)} ETH`);
return { fee: `${utils.formatEther(TXFEES)}`, raw: { baseFee: l2EstimatedPrice, maxPriorityFee: 1 } };