Shambolic Banana Barbel
High
GasPriceOracle.sol
underprices the cost of calling commitBatch()
, so the l1DataCost
paid by users will be substantially less than the true cost to the sequencer. This is made worse when transactions are heavily weighted towards L1 cost, in which case the sequencer can be responsible for payments 100X as large as the revenue collected.
When new L2 transactions are submitted, the transaction cost is calculated as the L2 gas price plus an l1DataFee
, which is intended to cover the cost of posting the data to L1.
The l1DataFee
is actually calculated in go-ethereum rollup/fees/rollup_fee.go#L153, but is equivalent to this calculation in GasPriceOracle.sol
as:
function getL1FeeCurie(bytes memory _data) internal view returns (uint256) {
// We have bounded the value of `commitScalar` and `blobScalar`, the whole expression won't overflow.
return (commitScalar * l1BaseFee + blobScalar * _data.length * l1BlobBaseFee) / PRECISION;
}
We can summarize as:
- We pay 1 gas per byte that goes into a blob (because GAS_PER_BLOB == 2**17), so we can calculate the cost of the blob as
blobScalar * _data.length * l1BlobBaseFee
. - We have to call
commitBatch()
as well, so we take the gas cost of that call and multiply by thel1BaseFee
.
The config specifies that the blobScalar = 0.4e9
and the commitScalar = 230e9
.
This commitScalar
is underpriced by a margin of 100X, as it should be 230_000e9
, not 230e9
.
The result is that transactions that are largely weighted towards the L1 fee will cost the sequencer way more to commit to than the user paid.
In an extreme, we can imagine an attack where users performed the most L1 data intensive transactions possible, which consist of filling a full batch with a single transaction that uses 128kb of calldata.
Let's look at the relative costs for the attacker and the sequencer of such a transaction:
- The attacker's fee is calculated as
intrinsic gas + calldata + l1DataFee
. - Since blobs are 128kb, we need
1024 * 128 = 131,072
bytes of calldata to fill the blob. - Assuming we need half non-zero to avoid compression, we can calculate the calldata cost as:
1024 * 128 * (4 + 16 / 2) = 1,310,720
. - Therefore, the total gas used by the attacker is
21_000 + 1,310,720 = 1,331,720
. - The l1DataFee is calculated as above. If we assume an l1BaseFee of 30 gwei, this gives a cost of
52,428 + (230 * 30) = 59,328 gwei
. - Assuming an L2 gas price of 0.001 gwei (estimate from L1 message queue), our total cost is
(1,331,720 * 0.001) + 59,328 = 60,659 gwei = $0.13
.
On the other hands, let's look at the sequencer's cost:
- The blob cost is the same as the attacker's l1DataFee, as that is calculated correctly at
52,428 gwei
. - The transaction cost, based on the Foundry estimate, is 230,000 gas. At 30 gwei per gas, that equals
230_000 * 30 = 6,900,000 gwei
. - The total cost to the sequencer is
6,900,000 + 52,428 = 6,952,428 gwei = $15.98
.
This mismatch requires sequencers to spend more than 100X to progress the chain, when only X was collected in sequencer fees from the user.
None
None
Note that this mispricing will cause the chain to slowly bleed revenue, regardless of any "attack". However, the most accute version of this attack would be as follows:
- User send a transfer of 0 value on L2 with 128kb of calldata.
- This costs them approximately $0.13 in L2 fees, but will require the sequencer to spent $15.98 in L1 gas to commit the batch.
- The user can repeat this attack indefinitely, causing the sequencer to spend more money than it receives.
Sequencers can be forced to spend 100X more than the fee revenue received in order to progress the chain, making the chain uneconomical.
N/A
commitBatch()
should be set to 230_000e9
.
Note that it appears that this value was pulled from Scroll's current on chain implementation. This issue has been reported directly to Scroll as well, and has been deemed valid and awarded with a bounty via their Immunefi bounty program.