diff --git a/scripts/deploy-predeploys.ts b/scripts/deploy-predeploys.ts index 06fdf47..312a862 100644 --- a/scripts/deploy-predeploys.ts +++ b/scripts/deploy-predeploys.ts @@ -203,12 +203,19 @@ function formatEther(wei: bigint): string { async function gasEstimateOnL2(params: { deployments: PreSignedDeployment[] rpcUrl: string -}): Promise { +}): Promise<{ results: GasEstimateResult[]; chainGasPrice: bigint }> { const { deployments, rpcUrl } = params const provider = new JsonRpcProvider(rpcUrl) const results: GasEstimateResult[] = [] + // Fetch the chain's current gas price so we can produce accurate cost estimates. + // Pre-signed txs embed a gas price (e.g. 100 gwei) that may differ wildly from the + // L2's actual pricing — especially on zkSync-style chains with a different gas model. + const feeData = await provider.getFeeData() + const chainGasPrice = feeData.gasPrice ?? BigInt(0) + console.log('\n[L2] Running gas estimation...') + console.log(` Chain gas price: ${formatUnits(chainGasPrice, 'gwei')} gwei`) for (const deployment of deployments) { console.log(`\n ${deployment.label}`) @@ -256,8 +263,13 @@ async function gasEstimateOnL2(params: { console.log(` ⚠ Gas estimation failed: ${err.message}`) } - const estimatedCost = estimatedGasUsed !== null ? estimatedGasUsed * txGasPrice : null - const expectedLeftover = estimatedCost !== null ? maxCost - estimatedCost : null + // Use the chain's actual gas price for cost estimates instead of the gas price + // embedded in the pre-signed tx. On L2s (especially zkSync-style chains) the + // gas model differs from L1 — estimateGas returns L2 gas units which should be + // priced at the L2 gas price, not the 100 gwei baked into the legacy tx. + const effectiveGasPrice = chainGasPrice > BigInt(0) ? chainGasPrice : txGasPrice + const estimatedCost = estimatedGasUsed !== null ? estimatedGasUsed * effectiveGasPrice : null + const expectedLeftover = estimatedCost !== null && maxCost > estimatedCost ? maxCost - estimatedCost : null results.push({ key: deployment.key, @@ -276,7 +288,7 @@ async function gasEstimateOnL2(params: { }) } - return results + return { results, chainGasPrice } } async function deployOnL2(params: { @@ -332,14 +344,21 @@ async function deployOnL2(params: { continue } - // Estimate actual gas cost for this deployment and use that instead of the - // hardcoded fundingAmount, so we don't over-fund the deployer address. + // The node requires the deployer to hold at least gasLimit × txGasPrice to + // accept the pre-signed transaction. We use gas estimation only to decide + // whether we can send *less* than the hardcoded amount — but we must never + // go below the node's minimum (gasLimit × txGasPrice). const parsedTx = Transaction.from(deployment.rawTransaction) const txGasPrice = parsedTx.gasPrice ?? BigInt(0) const txGasLimit = BigInt(parsedTx.gasLimit) + const nodeMinimum = txGasPrice * txGasLimit let requiredWei: bigint try { + const feeData = await provider.getFeeData() + const chainGasPrice = feeData.gasPrice ?? BigInt(0) + const effectiveGasPrice = chainGasPrice > BigInt(0) ? chainGasPrice : txGasPrice + const estimatedGas = await provider.estimateGas({ from: deployment.fundingAddress, data: parsedTx.data, @@ -347,7 +366,9 @@ async function deployOnL2(params: { }) // Apply the configurable buffer on top of the estimate const bufferedGas = estimatedGas * BigInt(100 + gasBuffer) / BigInt(100) - requiredWei = bufferedGas * txGasPrice + const estimatedCost = bufferedGas * effectiveGasPrice + // Never fund below the node's minimum acceptance threshold + requiredWei = estimatedCost > nodeMinimum ? estimatedCost : nodeMinimum console.log(` Gas estimate: ${estimatedGas.toLocaleString()} (+ ${gasBuffer}% buffer → funding ${formatEther(requiredWei)} ETH vs hardcoded ${deployment.fundingAmount} ETH)`) } catch (err: any) { // Fall back to the hardcoded amount if estimation fails @@ -493,7 +514,7 @@ async function main() { // Gas estimate mode: connect to L2, estimate actual costs, check deployer nonces if (options.gasEstimate) { - const estimates = await gasEstimateOnL2({ + const { results: estimates, chainGasPrice } = await gasEstimateOnL2({ deployments, rpcUrl: options.l2Rpc! }) @@ -533,13 +554,16 @@ async function main() { } console.log(` Gas price (tx): ${formatUnits(est.txGasPrice, 'gwei')} gwei`) + console.log(` Gas price (chain): ${formatUnits(chainGasPrice, 'gwei')} gwei`) console.log(` Gas limit (tx): ${est.txGasLimit.toLocaleString()}`) - if (est.estimatedGasUsed !== null && est.estimatedCost !== null && est.expectedLeftover !== null) { + if (est.estimatedGasUsed !== null && est.estimatedCost !== null) { if (!est.alreadyDeployed) totalEstimated += est.estimatedCost console.log(` Estimated gas: ${est.estimatedGasUsed.toLocaleString()} used`) - console.log(` Estimated cost: ${formatEther(est.estimatedCost)} ETH`) - console.log(` Expected refund: ${formatEther(est.expectedLeftover)} ETH left in deployer after tx`) + console.log(` Estimated cost: ${formatEther(est.estimatedCost)} ETH (estimatedGas × chain gas price)`) + if (est.expectedLeftover !== null) { + console.log(` Expected refund: ${formatEther(est.expectedLeftover)} ETH left in deployer after tx`) + } } else if (est.estimateError) { if (!est.alreadyDeployed) totalEstimated += est.maxCost // fall back to max cost if estimation failed console.log(` Gas estimate: ⚠ Failed (${est.estimateError})`) @@ -553,7 +577,7 @@ async function main() { console.log(` Hardcoded total: ${formatEther(totalHardcoded)} ETH`) console.log(` Min required: ${formatEther(totalMaxCost)} ETH (sum of gasPrice × gasLimit per tx)`) if (totalEstimated > BigInt(0)) { - console.log(` Estimated actual: ${formatEther(totalEstimated)} ETH (likely spend based on gas estimation)`) + console.log(` Estimated actual: ${formatEther(totalEstimated)} ETH (based on chain gas price: ${formatUnits(chainGasPrice, 'gwei')} gwei)`) } const hardcodedSavings = totalHardcoded - totalMaxCost if (hardcodedSavings > BigInt(0)) {