The Analysis of sky-high gas fee (I’m not the real man of rich)

0x00 Event Background

The analysis is derived from a transaction with a transfer amount of 100,000 USD, but the transaction fee is as high as 7,676 ETH.
https://etherscan.io/tx/0x2c9931793876db33b1a9aad123ad4921dfb9cd5e59dbb78ce78f277759587115

0x01 Core

The core technical issue is that ethjs-util’s intToBuffer does not support incoming floating-point data. Ethereumjs uses ethjs-util’s intToBuffer.

In short: when DApp uses ethereumjs to construct a transaction, if the incoming commission has a decimal number, it may return a large number in the browser as a commission due to a bug in the type conversion. And the hardware wallet is not displayed clearly, causing the user to directly authorize and sign the transaction with the sky-high gas fee.

0x02 Key Code Analysis

SlowMist Security Team started the analysis according to the description in this issue: https://github.com/ethereumjs/ethereumjs-monorepo/issues/1497.

The core problem is that the intToBuffer of ethjs-util does not support incoming floating-point data type.

Let’s first look at the key code. The most mentioned is the problem of ethereumjs. The main focus of discussion is the value of the two parameters maxPriorityFeePerGas and maxFeePerGas. Due to the incoming floating-point type, the calculation error is caused and the wrong procedure is obtained. Thus the event of sky-high gas fee procedures occurred.

After analysis, both parameters were processed by the toBuffer, so the analysis of the toBuffer is started.
https://github.com/ethereumjs/ethereumjs- monorepo/blob/cf95e04c6a/packages/tx/src/eip1559Transaction.ts#L200-L201

this.maxFeePerGas = new BN(toBuffer(maxFeePerGas === ‘’ ? ‘0x’ : maxFeePerGas))
this.maxPriorityFeePerGas = new BN(
toBuffer(maxPriorityFeePerGas === ‘’ ? ‘0x’ : maxPriorityFeePerGas)
)

The toBuffer will call the intToBuffer function of ethjs-util. This function mainly deals with two things.
https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1950

function intToBuffer(i) {
var hex = intToHex(i);

return new Buffer(padToEven(hex.slice(2)), ‘hex’);
}

1.Convert int to Hex
https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1939

function intToHex(i) {
var hex = i.toString(16); // eslint-disable-line

return ‘0x’ + hex;
}

2. Determine whether the data can be divisible by 2. If not, add a 0 at the beginning of the character in order to successfully write two groups of 1 into the buffer.
https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1920

function padToEven(value) {
var a = value; // eslint-disable-line

if (typeof a !== ‘string’) {
throw new Error(‘[ethjs-util] while padding to even, value must be string, is currently ‘ + typeof a + ‘, while padToEven.’);
}

if (a.length % 2) {
a = ‘0’ + a;
}

return a;
}

Analyze the wrong sample data: ‘33974229950.550003’, after processing intToHex and padToEven in the intToBuffer function, 7e9059bbe.8ccd is obtained. The results of this part of the browser js and nodejs are the same.
The inconsistency is in the operation of new Buffer: ‘new Buffer(padToEven(hex.slice(2)),’hex’);’

0x03 Processing Method: Browser JS

Pack the JS file through webpack and reference the file, and then debug and analyze it on the browser. The first input example character ‘33974229950.550003’ will enter the intToBuffer function for processing.

Analyze the process of intToBuffer synchronously, this part of the code logic is the same as 0x02, and the result of the conversion part is 7e9059bbe.8ccd.

Next, analyze how to fill the converted characters into the buffer. Through this step, the contents of the buffer can be obtained as ‘126, 144, 89, 187, 14, 140, 205’, corresponding to ‘7e, 90, 59, bb, e, 8c, cd’.

> 0x7e -> 126
> 0x90 -> 144
> 0x59 -> 89
> 0xbb -> 187
> 0xe -> 14
> 0x8c -> 140
> 0xcd -> 205

It was found here that e. The decimal point in this part has disappeared, so started to look for the mystery of the disappearance of the decimal point, and traced it to the hexWrite function, which will divide the obtained data into two groups. Then use parseInt to parse the segmented data.

However, ‘parseInt(‘e.’,16) -> 14 === parseInt(‘e’,16) -> 14' the missing decimal point was eaten by ‘parseInt’, resulting in an error in the data written into the buffer. Write The value of the input buffer is ‘7e9059bbe8ccd’.

0x04 Processing Method Analysis: Nodejs

The issue with the browser is that the decimal point of ‘7e9059bbe.8ccd’ was eaten by ‘parseInt’ when writing the buffer, causing data errors. However, after analysis, the node data is also wrong, and the cause of the error is different from the browser.

Firstly, let’s take a look at the following example:

The result of filling three different groups of data into the buffer of node is actually the same. After analysis, there is a special feature in the buffer of node, that is, the data after two sets of segmentation. If it cannot be parsed normally by hex, the data and the subsequent data will not be processed, and the data that can be processed normally will be returned directly. It can be understood as being truncated. This part can refer to the code logic in ‘node_buffer.cc’ of node.

> new Buffer(‘7e9059bbe’, ‘hex’)
<Buffer 7e 90 59 bb>

> new Buffer(‘7e9059bbe.8ccd’, ‘hex’)
<Buffer 7e 90 59 bb>

> new Buffer(‘7e9059bb’, ‘hex’)
<Buffer 7e 90 59 bb>

0x05 Comparison of Execution Results

Since node will truncate the ‘e.’ and subsequent data in the original data ‘7e9059bbe.8ccd’, the final error value is ‘7e9059bb’, which is smaller than the correct value ‘07e9059bbe’.

The execution result of node:

The browser will swallow the’.’ in the original data ‘7e9059bbe.8ccd’, so the final wrong value is ‘7e9059bbe8ccd’, which is much larger than the correct value ‘07e9059bbe’.

The execution result of the browser:

0x06 Cause of the issue

The ‘intToBuffer’ function of ‘ethjs-util’ does not support floating-point data type, and the input variable type is not judged in this function to ensure that the variable type is expected.Since the ‘toBuffer‘ of ’ethereumjs‘ references the ‘intToBuffer’ of ‘ethjs-util’ for processing, the data is not checked. The way that led to this incident, fortunately, the sky-high gas fee was eventually returned to ‘7626 ETH’ by the kind miners.

0x07 Lessons Learned

From the perspective of third-party libraries, reliable and safe coding practices should be followed during the coding process. Legitimate checks should be carried out at the beginning of functions to ensure that data and code logic are executed as expected.

From the perspective of library users, users should read the development documents and docking documents of the third-party library by themselves, and also test the logic of accessing the third-party library in the code, and test by constructing a large amount of data. Ensure that the business can be executed normally as expected, and ensure the coverage of high-standard test cases.

Reference:
https://github.com/ethereumjs/ethereumjs-monorepo/issues/1497
https://blog.deversifi.com/23-7-million-dollar-ethereum-transaction-fee-post-mortem/
https://www.chainnews.com/news/611706276133.htm

Focuses on Blockchain Ecosystem Security, have served over 1k+ customers.