├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── assets └── banner.png ├── contracts └── ExampleContract.sol ├── hardhat.config.ts ├── package.json ├── scripts ├── gasEstimation.ts └── helpers │ ├── oracle.ts │ ├── transactions.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | RPC_SCROLL=https://alpha-rpc.scroll.io/l2 2 | EXAMPLE_CONTRACT_ADDRESS=0xc37ee92c73753B46eB865Ee156d89741ad0a8777 3 | ORACLE_PRECOMPILE_ADDRESS=0x5300000000000000000000000000000000000002 4 | 5 | PRIVATE_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | 8 | # Hardhat files 9 | cache 10 | artifacts 11 | 12 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Scroll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | scroll-banner 2 | 3 | # Scroll Gas Estimation Example 4 | This project demonstrates how to use estimate gas on Scroll 5 | 6 | ## What do I need to have before I start? 7 | To run the project, you need: 8 | - [Node.js](https://nodejs.org/en/) (Version 16 works) 9 | - [Yarn](https://yarnpkg.com/) 10 | 11 | ## How to run 12 | 1. Navigate to the project root 13 | 2. Run `yarn` to install the dependencies 14 | 3. Fill the `.env` values with yours 15 | 4. Run the script using `yarn gas:estimate` 16 | 17 | ## Deployments 18 | 📜 Scroll Alpha Testnet: 19 | `ExampleContract`: [0xc37ee92c73753B46eB865Ee156d89741ad0a8777](https://blockscout.scroll.io/address/0xc37ee92c73753B46eB865Ee156d89741ad0a8777) -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scroll-tech/gas-estimation-example/2cdc32f34ad7c9ad7e8ef1d051a1fda011337070/assets/banner.png -------------------------------------------------------------------------------- /contracts/ExampleContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.17; 2 | 3 | contract ExampleContract { 4 | uint public exampleVariable; 5 | 6 | function setExampleVariable(uint _exampleVariable) external { 7 | exampleVariable = _exampleVariable; 8 | } 9 | } -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import "hardhat-dependency-compiler"; 4 | 5 | require("dotenv").config(); 6 | const { 7 | RPC_SCROLL, 8 | PRIVATE_KEY 9 | } = process.env; 10 | 11 | const config: HardhatUserConfig = { 12 | solidity: { 13 | version: "0.8.17", 14 | settings: { 15 | optimizer: { 16 | enabled: true, 17 | runs: 200 18 | }, 19 | evmVersion: "london" 20 | } 21 | }, 22 | networks: { 23 | scroll: { 24 | url: RPC_SCROLL, 25 | accounts: [`0x${PRIVATE_KEY}`] 26 | } 27 | }, 28 | defaultNetwork: "scroll", 29 | dependencyCompiler: { 30 | paths: [ 31 | "@scroll-tech/contracts/L2/predeploys/IL1GasPriceOracle.sol" 32 | ] 33 | } 34 | }; 35 | 36 | export default config; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gas-estimation-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", 8 | "@nomicfoundation/hardhat-ethers": "^3.0.0", 9 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 10 | "@nomicfoundation/hardhat-toolbox": "^3.0.0", 11 | "@nomicfoundation/hardhat-verify": "^1.0.0", 12 | "@typechain/ethers-v6": "^0.4.0", 13 | "@typechain/hardhat": "^8.0.0", 14 | "@types/chai": "^4.2.0", 15 | "@types/mocha": ">=9.1.0", 16 | "@types/node": ">=12.0.0", 17 | "chai": "^4.2.0", 18 | "dotenv": "^16.1.4", 19 | "ethers": "^6.4.0", 20 | "hardhat": "^2.15.0", 21 | "hardhat-dependency-compiler": "^1.1.3", 22 | "hardhat-gas-reporter": "^1.0.8", 23 | "solidity-coverage": "^0.8.0", 24 | "ts-node": ">=8.0.0", 25 | "typechain": "^8.1.0", 26 | "typescript": ">=4.5.0" 27 | }, 28 | "scripts": { 29 | "test": "hardhat test", 30 | "compile": "hardhat compile", 31 | "gas:estimate": "hardhat run ./scripts/gasEstimation.ts" 32 | }, 33 | "dependencies": { 34 | "@ethersproject/transactions": "^5.7.0", 35 | "@scroll-tech/contracts": "^0.0.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/gasEstimation.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | import dotenv from "dotenv"; 4 | import { 5 | getSerializedTransaction, 6 | buildUnsignedTransaction, 7 | buildPopulatedExampleContractTransaction 8 | } from "./helpers/transactions"; 9 | import { estimateL1Fee, estimateL2Fee } from "./helpers/oracle"; 10 | import { getPercentageDifference, getRandomInt } from "./helpers/utils"; 11 | import { ContractTransaction } from "ethers"; 12 | 13 | dotenv.config(); 14 | 15 | const { 16 | EXAMPLE_CONTRACT_ADDRESS, 17 | ORACLE_PRECOMPILE_ADDRESS 18 | } = process.env; 19 | 20 | async function getEstimatedFees(gasOracleAddress: string, populatedTransaction: ContractTransaction, serializedTransaction: string) { 21 | const estimatedL1Fee = await estimateL1Fee(gasOracleAddress, serializedTransaction); 22 | const estimatedL2Fee = await estimateL2Fee(populatedTransaction); 23 | const estimatedTotalFee = estimatedL1Fee + estimatedL2Fee; 24 | 25 | return { 26 | estimatedL1Fee, 27 | estimatedL2Fee, 28 | estimatedTotalFee 29 | } 30 | } 31 | 32 | async function main() { 33 | if (!EXAMPLE_CONTRACT_ADDRESS || !ORACLE_PRECOMPILE_ADDRESS) { 34 | throw new Error("Please fill the .env file with all values"); 35 | } 36 | 37 | const { getSigners } = ethers; 38 | const [ signer ] = await getSigners(); 39 | const signerAddress = await signer.getAddress() 40 | 41 | const newValueToSetOnExampleContract = getRandomInt(100); 42 | 43 | // Building the transaction and getting the estimated fees 44 | const populatedTransaction = await buildPopulatedExampleContractTransaction(EXAMPLE_CONTRACT_ADDRESS, newValueToSetOnExampleContract); 45 | const unsignedTransaction = await buildUnsignedTransaction(signer, populatedTransaction); 46 | const serializedTransaction = getSerializedTransaction(unsignedTransaction); 47 | const estimatedFees = await getEstimatedFees(ORACLE_PRECOMPILE_ADDRESS, populatedTransaction, serializedTransaction); 48 | 49 | console.log("Estimated L1 fee (wei):", estimatedFees.estimatedL1Fee.toString()); 50 | console.log("Estimated L2 fee (wei):", estimatedFees.estimatedL2Fee.toString()); 51 | console.log("Estimated total fee (wei): ", estimatedFees.estimatedTotalFee.toString()); 52 | console.log("\n"); 53 | 54 | // Executing the transaction on-chain and verifying actual values 55 | const signerBalanceBefore = await ethers.provider.getBalance(signerAddress); 56 | const exampleContract = await ethers.getContractAt("ExampleContract", EXAMPLE_CONTRACT_ADDRESS, signer); 57 | const tx = await exampleContract.setExampleVariable(newValueToSetOnExampleContract); 58 | const txReceipt = await tx.wait(5); 59 | const signerBalanceAfter = await ethers.provider.getBalance(signerAddress); 60 | 61 | if (!txReceipt) { 62 | throw new Error("Failed fetching the tx receipt, please try running the script again"); 63 | } 64 | 65 | const totalFee = signerBalanceBefore - signerBalanceAfter; 66 | const l2Fee = txReceipt.gasUsed * txReceipt.gasPrice; 67 | const l1Fee = totalFee - l2Fee; 68 | 69 | console.log("Actual L1 fee (wei):", l1Fee.toString()); 70 | console.log("Actual L2 fee (wei):", l2Fee.toString()); 71 | console.log("Actual total fee (wei): ", totalFee.toString()); 72 | 73 | console.log("\n"); 74 | 75 | console.log("(actual fee - estimated fee)"); 76 | console.log(`Difference L1 fee (wei): ${l1Fee - estimatedFees.estimatedL1Fee} (${getPercentageDifference(l1Fee, estimatedFees.estimatedL1Fee)}%)`); 77 | console.log(`Difference L2 fee (wei): ${l2Fee - estimatedFees.estimatedL2Fee} (${getPercentageDifference(l2Fee, estimatedFees.estimatedL2Fee)}%)`); 78 | console.log(`Difference total fee (wei): ${totalFee - estimatedFees.estimatedTotalFee} (${getPercentageDifference(totalFee, estimatedFees.estimatedTotalFee)}%)`); 79 | } 80 | 81 | main().catch((error) => { 82 | console.error(error); 83 | process.exitCode = 1; 84 | }); -------------------------------------------------------------------------------- /scripts/helpers/oracle.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { ContractTransaction } from "ethers"; 3 | 4 | export async function estimateL1Fee(gasOraclePrecompileAddress: string, unsignedSerializedTransaction: string): Promise { 5 | const l1GasOracle = await ethers.getContractAt("IL1GasPriceOracle", gasOraclePrecompileAddress); 6 | 7 | return l1GasOracle.getL1Fee(unsignedSerializedTransaction); 8 | } 9 | 10 | export async function estimateL2Fee(tx: ContractTransaction): Promise { 11 | const gasToUse = await ethers.provider.estimateGas(tx); 12 | const feeData = await ethers.provider.getFeeData(); 13 | const gasPrice = feeData.gasPrice; 14 | 15 | if (!gasPrice) { 16 | throw new Error("There was an error estimating L2 fee"); 17 | } 18 | 19 | return gasToUse * gasPrice; 20 | } -------------------------------------------------------------------------------- /scripts/helpers/transactions.ts: -------------------------------------------------------------------------------- 1 | import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; 2 | import { ethers } from "hardhat"; 3 | import { serialize, UnsignedTransaction } from "@ethersproject/transactions"; 4 | import { ContractTransaction } from "ethers"; 5 | 6 | export async function buildPopulatedExampleContractTransaction(exampleContractAddress: string, newValueToSet: number): Promise { 7 | const exampleContract = await ethers.getContractAt("ExampleContract", exampleContractAddress); 8 | 9 | return exampleContract.setExampleVariable.populateTransaction(newValueToSet); 10 | } 11 | 12 | export async function buildUnsignedTransaction(signer: HardhatEthersSigner, populatedTransaction: ContractTransaction): Promise { 13 | const nonce = await signer.getNonce(); 14 | 15 | return { 16 | data: populatedTransaction.data, 17 | to: populatedTransaction.to, 18 | gasPrice: populatedTransaction.gasPrice, 19 | gasLimit: populatedTransaction.gasLimit, 20 | value: populatedTransaction.value, 21 | nonce, 22 | }; 23 | } 24 | 25 | export function getSerializedTransaction(tx: UnsignedTransaction) { 26 | return serialize(tx); 27 | } -------------------------------------------------------------------------------- /scripts/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | export function getRandomInt(max: number) { 2 | return Math.floor(Math.random() * max); 3 | } 4 | 5 | export function getBigNumberDivision(a: bigint, b: bigint): number { 6 | return Number(a) / Number(b); 7 | } 8 | 9 | export function getPercentageDifference(a: bigint, b: bigint): number { 10 | let largerNum, smallerNum, sign = 1; 11 | 12 | if ((a - b) === 0n) { 13 | return 0; 14 | } 15 | 16 | if (a == 0n || b == 0n) { 17 | return 100; 18 | } 19 | 20 | if (a > b) { 21 | largerNum = a; 22 | smallerNum = b; 23 | } else { 24 | largerNum = b; 25 | smallerNum = a; 26 | sign = -1; 27 | } 28 | 29 | const difference = getBigNumberDivision(largerNum, smallerNum); 30 | 31 | return ((difference - 1) * 100) * sign; 32 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------