truffle
truffle copied to clipboard
Truffle displays an incorrect cost deploying to Arbitrum Rinkeby
- [x] I've opened a support ticket before filing this issue. (Ticket 617473)
Issue
When deploying a contract to Arbitrum, truffle displays a total cost that is different from the actual cost.
Steps to Reproduce
truffle unbox arbitrum
in .env, set an infura key and a mnemonic with Arbitrum Rinkeby ETH accessible here
npm run compile:arbitrum
npm run migrate:arbitrum --network=arbitrum_testnet
You will see something like:
Deploying 'SimpleStorage'
-------------------------
> transaction hash: 0xb3142a02eb78cc5f6f682100bc9a13cdc94c9cb3b45ddcae85739d3416266caf
...
> total cost: 0.000013137251486502 ETH
Take this transaction ID to https://testnet.arbiscan.io.
Expected Behavior
The cost that truffle reports should be the same as the cost arbiscan reports.
Actual Results
truffle total cost: 0.000013137251486502 ETH arbiscan total cost: 0.00001050980184 ETH
The support ticket example showed an even larger difference using a much larger contract:
truffle total cost: 2.69049662 ETH arbiscan total cost: 0.00271965395244 ETH.
Note
I confirmed that this is specific to Arbitrum and not other L2s like Optimism
Environment
- Operating System:
- Ethereum client:
- Truffle version (
truffle version
): v5.4.16 - node version (
node --version
): c14.18.0 - npm version (
npm --version
): 8.3.0
Ah! It looks like we aren't accounting for the fact that Arbitrum fees are surfaced differently than mainnet fees:
Arbitrum receipt:
{
"blockHash": "0x46b4605bb9c89f3f2c19b74b5c5aa1d77a4af090bba473b4d49396fd6b0cc564",
"blockNumber": 7794449,
"contractAddress": "0x0EA91c4d350458880BADf654031E1FcA30B3C2Eb",
"cumulativeGasUsed": 4053,
"effectiveGasPrice": "0xa02494",
"feeStats": {
"paid": {
"l1Calldata": "0x4bbb9c60620",
"l1Transaction": "0x4640cbf4d10",
"l2Computation": "0x9e7631b24",
"l2Storage": "0x0"
},
"prices": {
"l1Calldata": "0x47f06e8c",
"l1Transaction": "0x4640cbf4d10",
"l2Computation": "0xa02494",
"l2Storage": "0x1e8b7a11660"
},
"unitsUsed": {
"l1Calldata": "0x10d8",
"l1Transaction": "0x1",
"l2Computation": "0xfd5",
"l2Storage": "0x0"
}
},
"from": "0x61de0bbb6c8215af3f821fe4884a28bc737f98d3",
"gasUsed": 959933,
"l1BlockNumber": "0x965b70",
"l1InboxBatchInfo": null,
"logs": [],
"logsBloom": "0x0...0",
"returnCode": "0x0",
"returnData": "0x0...0",
"status": true,
"to": null,
"transactionHash": "0x5c6c6befbf30fec1643ca6acb00c6afb78759c9038d75fedfe7534b94d4bf6da",
"transactionIndex": 0,
"rawLogs": []
}
Local Ethereum receipt:
{
"transactionHash": "0xf2843b54d907a0eccfbec804699a4237857f7d8257ef55d1dc4c62b648c127f1",
"transactionIndex": 0,
"blockHash": "0x0d196855441c0aeb4fe04a0303f949a555acedd061cddbb6ee13749c6c58d89f",
"blockNumber": 9853902,
"from": "0x61de0bbb6c8215af3f821fe4884a28bc737f98d3",
"to": null,
"gasUsed": 96405,
"cumulativeGasUsed": 96405,
"contractAddress": "0xa6839894F879b3cb0FfB9F5Df467e1DFf1ED4Aa9",
"logs": [],
"status": true,
"logsBloom": "0x0...0",
"rawLogs": []
}
Whoa this is really weird. Good digging @kevinweaver! Is there any documentation for this receipt format?
There is some documentation on the tx receipt format for Arbitrum here.
However, I wonder if this is actually a problem that these receipts look different. @kevinweaver will try this again and run the exact same transaction to be sure we are comparing truffles to truffles, but it looks like on the Truffle side we report this based on the gasUsed
field which exists in both receipts (see line 80 here). Will be helpful to compare this value in both receipts when running the exact same tx I think.
It looks like the feeStats
metadata above is more of a symptom than the specific issue.
Truffle uses some data from the transaction response and some from the receipt we get back:
const gasPrice = new BN(tx.gasPrice);
const gas = new BN(receipt.gasUsed);
const value = new BN(tx.value);
to calculate the cost:
const cost = gasPrice.mul(gas).add(value);
Run with an example transaction, using these values gives us:
const gasPrice = 13671205
const gas = 959032
const value = 0
and the calculated cost:
const cost = 1.3111123e+13 // 13671205 * 959032 + 0;
// 0.00001311112307356 ETH formatted correctly
Meanwhile Arbiscan displays the real transaction fee as:
Transaction Fee: 0.00001048913955 ETH
Gas Price Bid: 0.000000000013671205 ETH
Gas Price Paid: 0.000000000010936964 ETH
Notice the “Gas Price Bid” is the same gasPrice
amount we rely on in our calculations. “Price Bid” does not exist on main net/rinkeby etherscan and doesn’t seem relevant to the price paid.
To solve this, we can’t rely on the gasPrice
in the transaction. If we pull from the new effectiveGasPrice
field in the receipt, we get a closer answer, but it’s still not 100%
effectiveGasPrice: '0xa6e284' // 10936964 in decimal
Replacing gasPrice with effectiveGasPrice in our calculations:
const cost = 1.0488898e+13 // 10936964 * 959032 + 0;
gives us something closer to the correct cost:
1.0488898e+13.
// 0.00001048913955 is the “correct” answer.
Currently looking through their docs to understand this better, I'm assuming it has to do with the complexity discussed here: https://developer.offchainlabs.com/docs/arbgas
Thanks for digging into this @kevinweaver! I suggest you reach out in their discord (not in the support channel though, ConsenSys runs that so it will just go to us!) with a question about this discrepancy in case they have any additional info.
I think it makes sense to dig more here and figure out what exactly goes into the calculation we see on Arbiscan. Looking forward to seeing what you find out!
Heya, you can find the custom tx receipt fields documented here
The gas fee market in Arbitrum works a bit differently to the L1 - it is inspired by EIP 1559 but adapted to fit the L2 requirements. There is a base cost of getting your tx processed. If demand for L2 resources are high, the base cost rises. If demand is low, it decreases.
The gas price submitted in a tx only represents the maximum you are willing to pay, the value actually charged will depend on network congestion, similar to a 2nd price auction.
The gasPrice in the tx response is the bid a user submitted. The value actually charged is available in the feeStats object under feeStats.price.l2Computation
. This is also the same value that is surfaced on the effectiveGasPrice
field.
For most accurate view on values charged users I would suggest you add the fields returned in the receipt's feeStats.paid
as these are the values that the user was charged for (all denominated in wei)
Understood, thanks for the clarification @fredlacs! I just tried adding those 4 feeStats.paid
values with my example transaction and came up with the accurate cost.
To support this from the truffle side, I see a few ways forward that I'm curious to hear your thoughts on @gnidan:
- Add support within truffle/interface-adapter. Something like this, but cleaner:
if("feeStats" in receipt && "paid" in receipt["feeStats"]){
//Arbitrum
cost = new BN(Object.values(receipt["feeStats"]["paid"]).reduce((sum, value) => sum + value));
} else {
cost = new BN(gasPrice.mul(gas).add(value));
}
A little strange to find L2 specific code in truffle and though this isn't the case with Optimism, it's potentially something we could continue to see in the future.
-
Add support within the arbitrum-box. I'm not sure what this looks like off the top of my head, perhaps some sort of migrate command wrapper that appends a warning with the correct value after? Very weird UX, unless there's a better way to approach from the box direction.
-
Create something like an Arbitrum plugin that overrides Truffle components with Arb specific logic. (I'm not familiar enough with truffle plugins to even say that's how they work).
Any other ideas?
If it's worth anything, here are my 2 cents: I think that the effectiveGasPrice
from the tx receipt should be used instead of the gasPrice
from the response when deriving this cost (regardless of the network).
The calculation using effectiveGasPrice
instead of the feeStats
in Arbitrum seems to be a bit off due to rounding on the node - which is something that we've flagged internally. While feeStats
will give you the exact cost, the effectiveGasPrice * gasUsed
will be pretty accurate.
If effectiveGasPrice
will eventually be updated to fix the inaccuracy, then this^ would definitely simplify things. And as per the spec, we should be migrating toward effectiveGasPrice
regardless as gasPrice
was deprecated post-EIP-1559.
Just chatted with @davidmurdoch: in ganache v7 eth_getTransactionReceipt returns effectiveGasPrice
, but no gasPrice
. In v6 we'd only have gasPrice
.
I think
const gasPrice = new BN(receipt.effectiveGasPrice ?? tx.gasPrice);
should work with no other changes, but I'll test on different ganache versions and networks.
@fainashalts says this is no longer true!