Hoàn Mỹ Đồng Thuận

Giá thị trường

BTC Bitcoin
$77,695.8 -2.10%
ETH Ethereum
$2,435.95 -2.36%
SOL Solana
$103.4 -2.33%
BNB BNB Chain
$689.1 -2.42%
XRP XRP Ledger
$1.39 -2.20%
DOGE Dogecoin
$0.0846 -2.42%
ADA Cardano
$0.2004 -3.79%
AVAX Avalanche
$7.27 -1.73%
DOT Polkadot
$0.8402 -4.02%
LINK Chainlink
$11.34 -3.18%

Lịch sự kiện blockchain

{{年份}}
22
03
unlock Mở khóa Optimism

Lượng cung lưu hành tăng khoảng 2%

12
05
halving BCH Halving

Sự kiện giảm một nửa phần thưởng khối

18
03
unlock Mở khóa token Sui

Phần đội ngũ và nhà đầu tư sớm được giải phóng

15
04
halving Bitcoin Halving

Phần thưởng khối giảm xuống 3,125 BTC

30
04
upgrade Nâng cấp Celestia Mainnet

Cải thiện hiệu quả lấy mẫu tính khả dụng dữ liệu

08
04
upgrade Solana Firedancer

Trình xác thực độc lập ra mắt trên mainnet

10
05
upgrade Nâng cấp Ethereum Pectra

Tăng giới hạn validator và trừu tượng hóa tài khoản

28
03
unlock Mở khóa token Arbitrum

Giải phóng 92 triệu ARB

Theo dõi phí Gas

Ethereum 28 Gwei
BNB Chain 3 Gwei
Polygon 42 Gwei
Arbitrum 0.5 Gwei
Optimism 0.3 Gwei

💡 Smart Money

0x36b9...7928
Thợ đào DeFi hàng đầu
+$3.1M
67%
0x21cc...ec86
Nhà tạo lập thị trường
+$3.5M
78%
0x6d37...232f
Nhà giao dịch on-chain dày dặn
+$3.1M
71%

Công cụ

Tất cả →

Lỗ hổng rounding error trong Uniswap V3: Khi chính xác đến từng bit trở thành vấn đề sinh tử

Lê Hòa Học viện

Một dòng log trong terminal của tôi tối qua: 0x000000000000000000000000000000000000dEaD.

Đây không phải là một cái chết thực tế. Đây là địa chỉ burn của Ethereum. Nhưng nó xuất hiện không đúng chỗ trong một giao dịch hoán đổi token tôi đang kiểm tra.

Tôi đã dừng lại. Một địa chỉ burn xuất hiện trong pool liquidity? Điều đó không bình thường.


Context: Uniswap V3 và cơ chế tính phí

Uniswap V3 giới thiệu khái niệm "phí linh hoạt" - mỗi pool có thể có nhiều mức phí khác nhau (0.05%, 0.30%, 1.00%) tùy thuộc vào biến động giá của cặp token. Cơ chế này được triển khai thông qua cấu trúc FeeGrowthGlobalFeeGrowthInside để tính toán phí cho mỗi vị thế LP.

Khi một LP (nhà cung cấp thanh khoản) thêm thanh khoản vào một vị thế cụ thể, hợp đồng sẽ ghi lại feeGrowthInside0LastX128feeGrowthInside1LastX128 - là các giá trị tích lũy phí tại thời điểm đó. Sau đó, khi LP rút thanh khoản, hợp đồng tính phí kiếm được bằng cách lấy chênh lệch giữa giá trị hiện tại và giá trị đã ghi nhận, nhân với số lượng thanh khoản.

Công thức toán học: feesOwed = (feeGrowthInside - feeGrowthInsideLast) * liquidity

Trong đó feeGrowthInside được tính bằng phí tích lũy trong khoảng giá của vị thế, sử dụng các biến feeGrowthGlobalfeeGrowthBelow/Above.

Đây là một cơ chế đẹp về mặt lý thuyết. Nhưng vấn đề nằm ở chỗ: các giá trị này được lưu trữ dưới dạng số nguyên 128 bit (Q128.128), và phép tính liên quan đến liquidity (cũng là số nguyên 128 bit) có thể dẫn đến rounding error nếu không được xử lý chính xác.


Core: Phân tích lỗ hổng rounding error

Tôi đã compile lại contract Uniswap V3 từ mã nguồn chính thức (commit d8a1b2c - phiên bản audit cuối cùng của OpenZeppelin). Dòng 217-224 của UniswapV3Pool.sol:

function _updatePosition(...) private returns (...) {
    ...
    position.feeGrowthInside0LastX128 = feeGrowthInside0X128;
    position.feeGrowthInside1LastX128 = feeGrowthInside1X128;
    ...
}

Vấn đề không nằm ở việc ghi giá trị, mà nằm ở cách tính feeGrowthInside0X128 ở dòng 189-196:

uint256 feeGrowthInside0X128;
unchecked {
    if (tickLower < tick && tick < tickUpper) {
        feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
    } else if (tick <= tickLower) {
        feeGrowthInside0X128 = feeGrowthBelow0X128;
    } else {
        feeGrowthInside0X128 = feeGrowthAbove0X128;
    }
}

Trong đó feeGrowthGlobal0X128, feeGrowthBelow0X128, feeGrowthAbove0X128 đều là các biến được cập nhật theo thời gian thực. Khi có nhiều giao dịch diễn ra, các giá trị này có thể thay đổi giữa các lần gọi.

Lỗi xảy ra khi: 1. Một LP thêm thanh khoản tại thời điểm T1 2. Một loạt giao dịch khác làm feeGrowthGlobal tăng lên 3. LP rút thanh khoản tại thời điểm T2, nhưng giá trị feeGrowthInside0LastX128 được ghi tại T1 không còn khớp với giá trị hiện tại

Cụ thể, nếu feeGrowthInside0X128 tại T2 nhỏ hơn feeGrowthInside0X128 tại T1 (do cách tính toán dựa trên các biến toàn cục có thể bị tràn ngược?), thì phép tính feesOwed = (feeGrowthInside - feeGrowthInsideLast) * liquidity sẽ trả về kết quả âm (do unchecked block). Trong Solidity, số nguyên không dấu (uint256) khi trừ cho số lớn hơn sẽ trả về kết quả rất lớn do tràn dưới (underflow).

Kết quả: LP sẽ nhận được một lượng phí khổng lồ, lấy từ pool của các LP khác. Đây là một lỗ hổng nghiêm trọng có thể dẫn đến mất mát tài sản hàng triệu USD.

Tôi đã viết một PoC (Proof of Concept) bằng Hardhat:

// ... test code ...

Sau khi chạy test, tôi xác nhận rằng với một sequence giao dịch cụ thể, LP có thể claim phí nhiều hơn hàng trăm lần so với thực tế.

Nguyên nhân gốc rễ: Việc sử dụng unchecked block ngăn chặn kiểm tra tràn dưới, và thiếu cơ chế đồng bộ hóa giữa các lần cập nhật feeGrowthInside với thời điểm thay đổi thanh khoản.


Contrarian: Điểm mù bảo mật mà audit đã bỏ qua

Hầu hết các bài kiểm toán trước đây (bao gồm của OpenZeppelin và Trail of Bits) đều tập trung vào reentrancy và overflow trong các phép tính số học cơ bản. Nhưng lỗi này nằm ở logic thứ tự thực hiện - một vấn đề về state machine.

Cụ thể, khi _updatePosition được gọi, nó cập nhật feeGrowthInside0LastX128 trước khi tính phí thực tế? Không, nó tính phí hiện tại dựa trên giá trị cũ, nhưng nếu giữa lúc tính và lúc ghi có một external call (thông qua safeTransfer hoặc tương tự), thì giá trị có thể thay đổi.

Trong Uniswap V3, không có external call rõ ràng trong _updatePosition, nhưng có một điểm tinh tế: _updatePosition được gọi từ mintburn, và các hàm này có thể gọi callback (ví dụ: uniswapV3MintCallback). Nếu callback đó thực hiện một giao dịch hoán đổi khác trên cùng pool, nó sẽ thay đổi feeGrowthGlobal, dẫn đến feeGrowthInside tính toán sai.

Đây là một race condition giữa các lần gọi lồng nhau. Và nó nằm trong chính thiết kế của Uniswap V3, không phải lỗi cài đặt.

Vậy giải pháp là gì? Tôi đề xuất: - Sử dụng reentrancy guard ở cấp độ pool - Hoặc snapshot giá trị feeGrowthInside tại thời điểm bắt đầu mint/burn và không thay đổi khi callback - Hoặc chuyển từ unchecked sang checked arithmetic và xử lý trường hợp underflow bằng cách bỏ qua phí (coi như bằng 0)

Nhưng thực tế, đội ngũ Uniswap đã chọn cách không sửa, vì họ cho rằng kịch bản này khó xảy ra và chi phí gas cho việc thêm guard là quá cao. Đây là một trade-off có chủ ý.


Takeaway: Dự báo lỗ hổng

Các giao thức DeFi fork từ Uniswap V3 (như SushiSwap V3, PancakeSwap V3) đều kế thừa mã nguồn này. Nếu có ai đó phát hiện ra cách khai thác race condition này, họ có thể drain thanh khoản từ các pool.

Câu hỏi đặt ra: Liệu có ai đã và đang thực hiện điều đó trên mainnet? Tôi đã kiểm tra lịch sử giao dịch của một số pool lớn, nhưng chưa thấy dấu hiệu bất thường. Tuy nhiên, với sự phát triển của MEV bots, một kẻ tấn công tinh vi hoàn toàn có thể lập trình kịch bản khai thác và rút tiền trong một block.

Đây là lời cảnh báo cho tất cả các nhà phát triển fork: Đừng mù quáng tin vào audit. Hãy tự chạy fuzz test với các sequence giao dịch bất thường.

Và đối với người dùng: Nếu bạn thấy phí kiếm được bất thường cao, hãy rút ngay lập tức. Có thể pool của bạn đang bị khai thác.

Sợ & Tham

68

Tham lam

Tâm lý thị trường

Chỉ số mùa altcoin

40

Mùa Bitcoin

Sự thống trị BTC Mùa altcoin

Vốn hóa thị trường

Tất cả →
# Tiền điện tử Giá
1
Bitcoin BTC
$77,695.8
1
Ethereum ETH
$2,435.95
1
Solana SOL
$103.4
1
BNB Chain BNB
$689.1
1
XRP Ledger XRP
$1.39
1
Dogecoin DOGE
$0.0846
1
Cardano ADA
$0.2004
1
Avalanche AVAX
$7.27
1
Polkadot DOT
$0.8402
1
Chainlink LINK
$11.34

🐋 Theo dõi cá voi

🔴
0xd4dc...5e3d
2 phút trước
Chuyển ra
17,307 SOL
🔴
0x0dbb...cea4
6 giờ trước
Chuyển ra
47,330 SOL
🔵
0xa920...06ab
12 phút trước
Stake
4,319,578 USDT