This repository has been archived by the owner on Sep 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #185 from getwax/fee-measurer
Fee measurer
- Loading branch information
Showing
20 changed files
with
6,410 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
artifacts | ||
cache | ||
node_modules | ||
typechain-types |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
node_modules | ||
.env | ||
coverage | ||
coverage.json | ||
typechain | ||
typechain-types | ||
|
||
# Hardhat files | ||
cache | ||
artifacts | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"editor.rulers": [80], | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll.eslint": "explicit" | ||
}, | ||
"eslint.rules.customizations": [ | ||
{ | ||
"rule": "*", | ||
"severity": "warn" | ||
} | ||
], | ||
"editor.tabSize": 2 | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
# Fee Measurer | ||
|
||
This tool facilitates direct measurement of L2 fees. | ||
|
||
Every L2 needs to extend Ethereum's system of charging fees to account for their | ||
L1 costs. The method tends to be different for each L2, which creates a lot of | ||
confusion and makes it difficult to compare L2s to each other. | ||
|
||
This tool helps unify L2 fees by approximating them with this model: | ||
|
||
``` | ||
l1GasUsed = fixedL1Gas + l1GasPerEffectiveDataByte * effectiveDataBytes | ||
l2GasUsed = (ordinary gas defined by protocol / same on L1 and local dev) | ||
fee = l1GasUsed * l1GasPrice + l2GasUsed * l2GasPrice | ||
``` | ||
|
||
To start using this model for an L2, we need to find the values of `fixedL1Gas` | ||
and `l1GasPerEffectiveDataByte` for the L2. Once these are measured, the same | ||
values should continue to accurately predict fees (<5% error) as L1 and L2 gas | ||
prices change. Reassessment should only be needed periodically to stay up to | ||
date with network upgrades. | ||
|
||
## How to Measure the Model Parameters | ||
|
||
```sh | ||
yarn | ||
``` | ||
|
||
The main script is `scripts/measure.ts`. You can try this out in the test | ||
environment with: | ||
|
||
```sh | ||
yarn hardhat run scripts/measure.ts | ||
``` | ||
|
||
However, because the test environment is not an L2, it should measure the L2's | ||
extra fees as zero. Gas prices can also fluctuate dramatically in the test | ||
environment, which throws off the calculations. | ||
|
||
For a real measurement, you'll need: | ||
1. Choose the network you want to measure, and find its network name (or add it) | ||
in `hardhat.config.ts` | ||
1. Set up an account with about 0.002 ETH and have a mnemonic phrase for it (the | ||
exact amount spent should be closer to 0.001 ETH but you'll want a buffer to | ||
avoid needing to re-run the test from running out of funds) | ||
|
||
Optionally, you can also consider changing this hardcoded number: | ||
|
||
```ts | ||
const maxPriorityFeePerGas = 100_000n; // 0.0001 gwei | ||
``` | ||
|
||
then: | ||
|
||
```sh | ||
# Tip: Include a leading space to reduce the chance the command is written to | ||
# history somewhere. | ||
MNEMONIC="(your mnemonic)" yarn hardhat \ | ||
--network "(network name)" \ | ||
run scripts/measure.ts | ||
``` | ||
|
||
This generates significant output, but the key values of interest are | ||
`baselineExtraWei` and `extraWeiPerByte`. Also of interest are the | ||
`weiUsedRelErrors` results (there are 2 sets). These should be as close to zero | ||
as possible, ideally below 0.05 (5% error). If they are higher, it could be due | ||
to significant gas price movements during the test, or because the L2's pricing | ||
model is too far outside the assumptions of this model. You'll need to see | ||
errors that you consider acceptable for future estimation to proceed, which | ||
might be resolved by re-running the test at a later time. (Gas changes are only | ||
an issue during the test - you can still use the resulting model to do | ||
estimations when gas prices change later.) | ||
|
||
Once you're happy with your numbers, you need to find the applicable L1 gas | ||
price. You can get a ballpark number by just independently looking at what L1 | ||
recently charged for gas, but for best results you need to use a method specific | ||
to your L2. For example: | ||
- Arbitrum: There are precompiles available ([more info]( | ||
https://hackmd.io/@voltrevo/H15SBijOa#Calculating-the-Actual-L1-Gas)) | ||
- Optimism: The applicable L1 gas price is provided on [optimism's block | ||
explorer](https://optimistic.etherscan.io/) | ||
|
||
With the L1 gas price value (in wei (if you have gwei, multiply by 10^9 to get | ||
the wei value)), you can calculate: | ||
|
||
``` | ||
fixedL1Gas = baselineExtraWei / l1GasPrice | ||
l1GasPerEffectiveDataByte = extraWeiPerByte / l1GasPrice | ||
``` | ||
|
||
Example results (measured Jan 2024): | ||
|
||
| | Arbitrum One | Optimism | | ||
| --------------------------- | ------------ | -------- | | ||
| `fixedL1Gas` | 1816 | 1302 | | ||
| `l1GasPerEffectiveDataByte` | 16.2 | 11.0 | | ||
|
||
## How to Apply the Model | ||
|
||
**Step 1: Measure L2 gas** | ||
|
||
Use a test environment like hardhat to measure the amount of ordinary gas (ie | ||
the L2's gas when run on the L2) for the transaction you're interested in. | ||
Depending on the complexity of your transaction, some care might be needed to | ||
set up the right kind of state so that your transaction follows the | ||
relevant/'normal' code path. This value is often higher for the first instance | ||
of the transaction, because it writes to new storage. It's up to you to figure | ||
out whether this first-time cost or ongoing cost is the number you need (or you | ||
might be interested in both). | ||
|
||
You can also get this value by running transactions directly on the target | ||
chain, but be careful to interpret L2 gas correctly. This can be tricky because | ||
L2s can report gas in unexpected ways, [for example on Arbitrum]( | ||
https://hackmd.io/@voltrevo/H15SBijOa). | ||
|
||
**Step 2: Measure Effective Data Bytes** | ||
|
||
This is about the number of bytes in your transaction's `data` field and how | ||
they are treated by your specific L2. If your transaction data is a custom and | ||
efficient format that is mostly random and incompressible, then you can use the | ||
actual length of the data field as its effective length. | ||
|
||
If you have a large data field based on the solidity ABI, the effective length | ||
is probably about 40% of the actual length (based on Jan 2024 experiments). | ||
|
||
Otherwise, you'll need to do some investigation for your transaction and L2 to | ||
find the right value. For example: | ||
- Arbitrum: Uses a complex system based on brotli, and appears to charge for | ||
small data fields (eg ERC20 transfer) as though they were incompressible. The | ||
most practical way to measure effective bytes is probably to send your actual | ||
bytes to the chain and find the length that generates the correct fee when you | ||
plug it into the model. | ||
- Optimism: Ultimately uses gzip to post L1 data, but for fee calculation | ||
purposes it uses the number of non-zero bytes plus 1/4 * the number of | ||
zero-bytes. | ||
|
||
**Step 3: Final Calculation** | ||
|
||
From previous steps, you should have `l2Gas` and `effectiveDataBytes` values for | ||
your transaction, as well as the `fixedL1Gas` and `l1GasPerEffectiveDataByte` | ||
values that are built into the model. | ||
|
||
With those values, you can calculate: | ||
|
||
``` | ||
l1GasUsed = fixedL1Gas + l1GasPerEffectiveDataByte * effectiveDataBytes | ||
l2GasUsed = (the number measured in step 1) | ||
fee = l1GasUsed * l1GasPrice + l2GasUsed * l2GasPrice | ||
``` | ||
|
||
This gives a number in wei, which you can divide by 10^18 for a value in ETH, | ||
and multiply by the price of ETH to get a value in your preferred currency. | ||
|
||
## Median Gas Price | ||
|
||
To give an objective estimate that isn't tied exactly to the current gas price | ||
conditions, the median gas price can be used. | ||
|
||
This script will measure the median basefee: | ||
|
||
``` | ||
yarn hardhat run --network "(your network)" scripts/estimateMedianBasefee.ts | ||
``` | ||
|
||
Add a modest/applicable priority fee to get an overall gas price (eg 0.0001 | ||
gwei). This depends on the L2 (eg Arbitrum doesn't take a priority fee even if | ||
you offer it). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
//SPDX-License-Identifier: Unlicense | ||
pragma solidity ^0.8.0; | ||
|
||
contract FeeMeasurer { | ||
// Note: Linter wants to restrict this to pure but that prevents calling this | ||
// in an actual transaction. | ||
function useGas(uint8 size) external { | ||
require(size != 0, "Size zero works differently due to zero-byte"); | ||
|
||
uint256 iterations = 100 * uint256(size); | ||
|
||
for (uint256 i = 0; i < iterations; i++) {} | ||
} | ||
|
||
function useGasOrdinaryGasUsed(uint8 size) public pure returns (uint256) { | ||
require(size != 0, "Size zero works differently due to zero-byte"); | ||
|
||
return 21629 + 11100 * uint256(size); | ||
} | ||
|
||
function fallbackOrdinaryGasUsed( | ||
uint256 nonZeroDataLen | ||
) public pure returns (uint256) { | ||
uint256 result = 21064 + 16 * nonZeroDataLen; | ||
|
||
if (nonZeroDataLen >= 4) { | ||
// I think this is because a different branch is used if the data | ||
// might match a method id. Less than 4 bytes and it can't match. | ||
result += 78; | ||
} | ||
|
||
return result; | ||
} | ||
|
||
fallback() external {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { HardhatUserConfig, task, types } from 'hardhat/config'; | ||
import '@nomicfoundation/hardhat-toolbox'; | ||
|
||
const config: HardhatUserConfig = { | ||
solidity: { | ||
compilers: [ | ||
{ | ||
version: '0.8.21', | ||
settings: { | ||
optimizer: { | ||
enabled: true, | ||
runs: 1_000_000, | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
|
||
networks: { | ||
hardhat: { | ||
gasPrice: 20_000_000_000, | ||
}, | ||
|
||
mainnet: { | ||
url: 'https://rpc.ankr.com/eth' | ||
}, | ||
|
||
arbitrumOne: { | ||
url: 'https://arb1.arbitrum.io/rpc', | ||
}, | ||
|
||
optimism: { | ||
url: 'https://mainnet.optimism.io', | ||
}, | ||
}, | ||
}; | ||
|
||
task('sendEth', 'Sends ETH to an address') | ||
.addParam('address', 'Address to send ETH to', undefined, types.string) | ||
.addOptionalParam('amount', 'Amount of ETH to send', '1.0') | ||
.setAction( | ||
async ({ address, amount }: { address: string; amount: string }, hre) => { | ||
const wallet = hre.ethers.Wallet.fromPhrase( | ||
`${'test '.repeat(11)}junk`, | ||
hre.ethers.provider, | ||
); | ||
|
||
console.log(`${wallet.address} -> ${address} ${amount} ETH`); | ||
|
||
const txnRes = await wallet.sendTransaction({ | ||
to: address, | ||
value: hre.ethers.parseEther(amount), | ||
}); | ||
|
||
await txnRes.wait(); | ||
}, | ||
); | ||
|
||
export default config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"name": "fee-measurer", | ||
"version": "0.0.0", | ||
"main": "index.js", | ||
"private": true, | ||
"dependencies": { | ||
"account-abstraction": "github:eth-infinitism/account-abstraction#0.6.0", | ||
"hardhat": "^2.17.3", | ||
"openzeppelin-contracts": "github:openzeppelin/openzeppelin-contracts#v4.9.3", | ||
"safe-contracts": "github:safe-global/safe-contracts#v1.4.1" | ||
}, | ||
"devDependencies": { | ||
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0", | ||
"@nomicfoundation/hardhat-ethers": "^3.0.0", | ||
"@nomicfoundation/hardhat-network-helpers": "^1.0.0", | ||
"@nomicfoundation/hardhat-toolbox": "^3.0.0", | ||
"@nomicfoundation/hardhat-verify": "^1.0.0", | ||
"@typechain/ethers-v6": "^0.4.0", | ||
"@typechain/hardhat": "^8.0.0", | ||
"@types/chai": "^4.2.0", | ||
"@types/mocha": ">=9.1.0", | ||
"@types/node": ">=16.0.0", | ||
"chai": "^4.2.0", | ||
"ethers": "^6.4.0", | ||
"hardhat-gas-reporter": "^1.0.8", | ||
"solidity-coverage": "^0.8.0", | ||
"ts-node": ">=8.0.0", | ||
"typechain": "^8.1.0", | ||
"typescript": ">=4.5.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* eslint-disable no-console */ | ||
|
||
import { ethers } from 'hardhat'; | ||
import { FeeMeasurer__factory } from '../typechain-types'; | ||
import SafeSingletonFactory from '../src/SafeSingletonFactory'; | ||
import { Signer } from 'ethers'; | ||
|
||
async function main() { | ||
let signer: Signer; | ||
|
||
if (process.env.MNEMONIC) { | ||
signer = ethers.Wallet.fromPhrase(process.env.MNEMONIC, ethers.provider); | ||
} else { | ||
signer = (await ethers.getSigners())[0]; | ||
} | ||
|
||
const ssf = await SafeSingletonFactory.init(signer); | ||
const feeMeasurer = await ssf.connectOrDeploy(FeeMeasurer__factory, []); | ||
|
||
console.log('FeeMeasurer deployed to:', await feeMeasurer.getAddress()); | ||
} | ||
|
||
// We recommend this pattern to be able to use async/await everywhere | ||
// and properly handle errors. | ||
main().catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
Oops, something went wrong.