Bitfinex 的一個主要錢包在九月底以 7676 ETH (約2,300 萬美元)的天價手續費向非託管交易所 DeversiFi 匯了一筆 10 萬美元 USDT 的轉帳。動區專欄作者 慢霧科技 Slow Mist 以此案例來做關鍵分析,已從中發掘出問題發生的原因,並總結出應該注意的重點教訓。
(前情提要:Bitfinex以「2,300萬鎂的ETH當手續費」轉帳10萬USDT!DeversiFi不明原因導致、遭駭為謠言)
事件背景
分析源自一筆轉帳金額 100,000 USDT,手續費卻高達 7,676 枚 ETH 的天價手續費交易。
關鍵程式碼分析
根據此 Issue 中的描述,開始了分析。
我們以倒敘的方式來說明問題,這樣更方便理解。核心問題是 `ethjs-util` 的 `intToBuffer` 不支持傳入浮點型的數據。
首先看看關鍵的程式碼,提得比較多的是 `ethereumjs` 的問題,主要聚焦討論的是 `maxPriorityFeePerGas` 和 `maxFeePerGas` 這兩個參數的值,由於傳入的浮點型,導致計算錯誤,得到了錯的手續,從而發生了「天價手續的事件」。
經過分析後,這兩個參數都是經過 `toBuffer` 進行處理的,所以開始分析 `toBuffer`。(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)
)
`toBuffer` 會去調用 `ethjs-util` 的 `intToBuffer` 函數,這個函數主要處理了兩件事情。
(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. 將 int 轉成 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. 判斷是否可以被 2 整除,如果不行需要在字符開頭添加一個 0 ,這裡主要是為了能夠成功的將數據 2 個 1 組寫入到 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;
}
以出錯的示例數據:`33974229950.550003` 進行分析,經過 `intToBuffer` 函數中的 `intToHex` 和 `padToEven` 處理後得到 `7e9059bbe.8ccd` ,這部分瀏覽器 js 和 nodejs 的結果都是一致的。
不一致的地方是在 new Buffer 的操作:
`new Buffer(padToEven(hex.slice(2)), ‘hex’);`
處理方式分析:瀏覽器 js
通過 webpack 打包好 js 文件並對文件進行引用,然後在瀏覽器上進行調試分析。
首先輸入的示例字符 `33974229950.550003` 會進入到 intToBuffer 的函數中進行處理。
同步分析 intToBuffer 的處理過程,這部分和「關鍵程式碼分析」部分的程式碼邏輯是一樣的,處理轉換部分得到的結果是 `7e9059bbe.8ccd`。
接下來分析如何將轉換後的字符填充進入的 buffer 中,通過這步可以得到 buffer 的內容是 `126, 144, 89, 187, 14, 140, 205` 對應的是 `7e, 90, 59, bb, e, 8c, cd`。
> 0x7e -> 126
> 0x90 -> 144
> 0x59 -> 89
> 0xbb -> 187
> 0xe -> 14
> 0x8c -> 140
> 0xcd -> 205
處理方式分析:nodejs
由於瀏覽器上出問題的是 `7e9059bbe.8ccd` 在寫入 buffer 的時候小數點被 `parseInt` 吃掉了導致數據出錯,但是經過分析,node 的數據也是錯誤的,且產生錯誤的原因是和瀏覽器的不一樣。
首先我們先看下如下的示例:
node 三組不同的數據填充到 buffer 得到的結果居然是一樣的,經過分析 node 的 buffer 有個小特性,就是 2 個一組切分後的數據,如果沒法正常通過 hex 解析的,就會把那一組數據以及之後的數據都不處理了,直接返回前面可以被正常處理的那部分數據。可以理解為被截斷了。這部分可以參考 node 底層的 buffer 中 `node_buffer.cc` 中的程式碼邏輯。
執行結果的比較
node 由於會將原始數據 `7e9059bbe.8ccd` 中的 `e.` 及之後的數據進行截斷,所以最終錯誤的值是 `7e9059bb`,相比正確的值 `07e9059bbe` 小。
node 的執行結果:
瀏覽器由於會將原始數據 `7e9059bbe.8ccd` 中的 `.` 吃掉,所以最終錯誤的值是 `7e9059bbe8ccd`,相比正確的值 `07e9059bbe` 大很多。
瀏覽器的執行結果:
問題的原因
`ethjs-util` 的 `intToBuffer` 函數不支持浮點型的數據,且在這個函數中沒有判斷傳入的變量類型,來確保變量類型是預期內的。由於 `ethereumjs` 的 `toBuffer` 引用了 `ethjs-util` 的 `intToBuffer` 進行處理,也沒有對數據進行檢查。導致了這次事件的發生,所幸最終善良的礦工歸還了「天價手續費 7626 ETH」。
吸取的教訓
從第三方的庫的角度來看,在編碼過程中應該要遵循可靠的安全的編碼規範,在函數的開頭要對傳入的數據進行合法性的檢查,確保數據和程式碼邏輯是按照預期內執行。
從庫的使用者的角度來看,使用者應該要自行閱讀第三方庫的開發文檔和對接文檔,並且也要對程式碼中接入第三方庫的邏輯進行測試,通過構造大量的數據進行測試,確保業務上能夠正常按照期望執行,保證高標準的測試用例的覆蓋率。
📍相關報導📍
完整解析|以太坊天價手續費的真相: 資金盤直銷騙局GoodCycle上演誤殺瞞天記!
「我只是付了 1 美元USDT,為什麼其他幣都沒了?」— 小心轉帳權限騙局
最狂胖手指!有人轉帳「133美元」,卻付了「265萬美元」手續費,幸運礦工是星火礦池
讓動區 Telegram 新聞頻道再次強大!!立即加入獲得第一手區塊鏈、加密貨幣新聞報導。
LINE 與 Messenger 不定期為大家服務