Bẫy phí động Uniswap v4: Báo giá 0%, thu 12,8%

BlockbeatsBlockbeats

Tiêu đề gốc: "Báo giá 0%, thực thi 12,8%: Phục hồi bẫy phí động 'pool độc' Uniswap v4"

Tác giả gốc: Wang Zihao, BitsLab

Từ ngày 22 đến 23 tháng 8 năm 2026, một pool phí động USDC/WBNB trên Uniswap v4 trên BNB Chain hiển thị mức phí 0% trong báo giá của aggregator, nhưng người dùng thực tế bị thu phí LP 12,8%. Trong khoảng 29 giờ, pool này đã thực hiện 21.086 giao dịch, tích lũy 131.888 USD phí, sau đó bị hủy. Tất cả các giao dịch nạn nhân đều thành công: hợp đồng không bị xâm phạm, đầu ra cao hơn giới hạn tối thiểu do người dùng đặt, không có tín hiệu bất thường nào—khoản chênh lệch lặng lẽ biến thành phí.

Bài viết này bắt đầu với một giao dịch mẫu:

(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: người dùng nạp 5.926,90 USDT, nhận được ít hơn dự kiến 757,43 USDT), phân tích cách pool này đạt được "báo giá 0%, thực thi 12,8%": ba lớp phán đoán của hàm phí, bảo vệ trượt giá bị vô hiệu hóa, và danh sách trắng chặn các LP khác. Tất cả kết luận dựa trên dữ liệu on-chain và xác minh có kiểm soát cục bộ.

01 Chuỗi từ báo giá đến thực thi

Sau khi người dùng khởi tạo hoán đổi trên frontend aggregator, toàn bộ chuỗi diễn ra như sau:

Có hai bối cảnh trên chuỗi này, mỗi lớp phán đoán sau đều dựa trên hai điểm này.

Trước tiên xem phía v4: nó cho phép Hook ghi đè phí theo từng giao dịch. PoolKey.fee = 0x800000 khai báo pool phí động; Hook trả về giá trị thứ ba trong beforeSwap mang 0x400000 | fee. Mã nhận giá trị trả về này trong v4-core (Pool.sol:303-305):

isOverride() kiểm tra cờ ghi đè, removeOverrideFlagAndValidate() loại bỏ 0x400000 và xác minh không vượt quá MAX_LP_FEE = 1.000.000 (100%), sau đó thực thi swap này theo mức phí đó và ghi vào sự kiện Swap. Nói cách khác, Hook trả về mức phí nào, người dùng phải trả mức phí đó—miễn là đầu ra vẫn cao hơn amountOutMin. Đây là chức năng chính thức, điều chỉnh phí theo biến động hoặc tồn kho đều là mục đích hợp pháp.

Xem xét chính Hook này: nó chỉ khai báo hai quyền. v4 mã hóa quyền trong 14 bit thấp của địa chỉ hợp đồng (Hooks.sol:29-47), xác minh khi triển khai và không thể thay đổi sau đó. Định nghĩa cờ phía v4-core và giải mã địa chỉ đối chiếu:

Kiểm tra on-chain getHookPermissions() trả về khớp với bitmap: chỉ hai bit này là đúng.

Hook không có mã nguồn. Bài viết này dịch ngược toàn bộ hàm phí từ bytecode—dưới đây là phân tích hành vi của logic đã chạy on-chain này:

Tiếp theo xem từng lớp. Hai lớp đầu trả lời câu hỏi "đây có phải là mô phỏng không", lớp thứ ba quyết định giao dịch thực trả bao nhiêu.

02 Ba lớp phán đoán của hàm phí

Phán đoán 1: Phân biệt người gọi

Sau khi hàm phí được gọi, trước tiên so sánh tx.origin: nếu bằng bất kỳ trong ba địa chỉ hằng số, trả về trực tiếp mức dự phòng. Mức dự phòng hiện được cấu hình là 0%.

Ba địa chỉ không được chọn ngẫu nhiên. Khi eth_call không chỉ định from, origin của giao dịch rơi vào địa chỉ zero—truy vấn giá hàng loạt của aggregator thường không chỉ định. Xác minh trực tiếp trong fork cục bộ:

Lớp đầu tiên chỉ nhận ba địa chỉ này. Lớp tiếp theo xem sau khi chỉ định địa chỉ thông thường, Hook còn có thể phân biệt thế nào.

Phán đoán 2: Kiểm tra gas

Thay tx.origin bằng địa chỉ thông thường và thử lại, kết quả vẫn là 0%—lớp đầu tiên bị vượt qua, nhưng phí không thay đổi. Điều này cho thấy lớp đầu tiên không phải là phán đoán duy nhất, định vị đến lớp thứ hai: gasleft() * 10100 / 10000 >= gate.

Aggregator thường đặt gas rất cao khi báo giá để tránh các tuyến ứng viên thất bại trong mô phỏng do thiếu gas; trước khi giao dịch thực vào Hook, gas đã bị tiêu hao bởi Router, ủy quyền và các bước nhảy trước. Cùng một mã, ba mức gas:

Đường phân giới được đo thực tế nằm giữa 16,85 triệu – 16,9 triệu gas. Sự khác biệt giữa 30 triệu và 2,59 triệu chính là sự khác biệt giữa "báo giá" và "thực thi".

Phán đoán 3: Giả ngẫu nhiên theo từng giao dịch

Giao dịch thực vượt qua cả hai lớp đầu vào lớp thứ ba: băm dấu vân tay môi trường khối rồi lấy modulo 10.000, trả về mức phí tương ứng theo khoảng kết quả. Cấu hình hiện tại có ba mức 8% / 10% / 10%, nếu không khớp thì trả về 0%.

Lớp này có hai thiết kế đáng chú ý. Dấu vân tay trộn ba trường của calldata—calldata báo giá của aggregator và calldata định tuyến thực vốn đã khác nhau, dấu vân tay tự nhiên cũng khác. Ngoài ra, giả ngẫu nhiên theo từng giao dịch làm cho phí có phân bố, khó nhận ra quy luật khi chỉ xem vài giao dịch, việc liệt kê các quy tắc đơn giản không thể ngăn chặn được.

Trong tất cả 21.086 sự kiện on-chain xuất hiện 19 mức phí lịch sử (6,8%–28%), cho thấy các mức phí luôn được hàm quản trị cấu hình lại theo nhu cầu; tại thời điểm giao dịch mẫu, mức hiệu lực là 12,8%.

Tổng quan về ba lớp

Đến đây có thể định nghĩa "pool độc"—một pool v4 đồng thời thỏa mãn ba điều kiện: PoolKey.fee mang cờ phí động; quyết định phí đọc tín hiệu môi trường thực thi (gas, origin, môi trường khối) thay vì đọc trạng thái thị trường công khai; phí giữa mô phỏng báo giá và thực thi on-chain phân kỳ hệ thống, bên hưởng lợi là người triển khai pool độc. Ranh giới nằm ở chỗ phí đọc gì: trả về cùng một mức phí cho mọi người gọi là thiết kế thị trường lập trình được; trả về mức phí khác nhau theo "ai đang hỏi giá" là lừa dối hệ thống định tuyến.

Đến đây, cơ chế phân kỳ phí đã hoàn chỉnh. Nhưng sau khi phí bị thu, tại sao giao dịch vẫn thành công, là câu hỏi tiếp theo.

03 Tại sao bảo vệ trượt giá không hoàn tác

Trước tiên xem đường dẫn và số tiền đầy đủ của giao dịch này:

Sự kiện Swap của bước nhảy này ghi mức phí hiệu lực vào trường fee: 128000. Đơn vị phí của v4 lấy 1.000.000 là 100%, 128000 là 12,8%. Tổn thất của giao dịch này có thể đo từ hai góc độ: từ pool, phí thu trên đầu vào WBNB; từ toàn bộ tuyến, chênh lệch stablecoin vào ra là 757,43 USDT, tổn thất 12,7794%, trong đó còn bao gồm phí bước nhảy trước và độ lệch peg. Hai con số chỉ chênh 0,02 điểm phần trăm, cho thấy nguồn tổn thất chính là phí LP của pool này.

amountOutMin chỉ kiểm tra giới hạn đầu ra cuối cùng, không xem mỗi bước nhảy trung gian thu bao nhiêu phí:

Mức phí 12,8% nằm hoàn toàn trong vùng đệm 20,32%, đầu ra vẫn cao hơn giới hạn tối thiểu—giao dịch thành công, không hoàn tác. Mặc định thông thường của các tuyến stablecoin chính là 0,1%–1%, tuyến này cho không gian gấp hơn 20 lần. Người dùng nghĩ vùng đệm rộng là "an toàn hơn", thực tế là giao mọi hợp đồng trên đường dẫn cho sự tự giác của đối phương. Biểu diễn khoản lỗ này thành biểu đồ:

04 Thao tác của người triển khai "pool độc"

Danh sách trắng: chặn các LP khác

Logic danh sách trắng nằm trong beforeAddLiquidity, kết quả dịch ngược như sau:

Thêm thanh khoản phải qua ba cửa: trước tiên là kiểm tra gọi PoolManager mà mọi Hook v4 đều có, sau đó phải thông qua PositionManager chính thức, cuối cùng đối chiếu danh sách trắng của người nắm giữ vị thế. Địa chỉ không có trong danh sách bị hợp đồng hoàn tác tại đây, các LP khác không vào được, thu nhập phí không bị pha loãng, toàn bộ thuộc về người triển khai. Danh sách được duy trì theo địa chỉ bởi một hàm quản trị (selector 0xc4452e52).

Mức 0% và giao dịch rửa

Nhìn tổng thể 21.086 giao dịch, phân bố cho thấy mô hình rõ ràng: mức 0% có 6.946 giao dịch, khối lượng 12.947.751 USD, trung bình 1.864 USD mỗi giao dịch; mức thu phí có 14.140 giao dịch, khối lượng 1.120.106 USD, trung bình 79 USD mỗi giao dịch. Các giao dịch lớn tập trung ở mức 0%, phí tập trung vào giao dịch nhỏ. Giải thích hợp lý nhất cho các giao dịch lớn ở mức 0% là người triển khai tự giao dịch với gas cao: gas cao trùng với cùng phán đoán mà báo giá aggregator gặp phải, chi phí phí tự giao dịch gần bằng không. Khối lượng được rửa đẩy pool lên bảng xếp hạng của trang thị trường (ảnh chụp cho thấy khối lượng 24h khoảng 10,82 triệu USD, 16.671 giao dịch), trong mắt aggregator, đây là một pool có độ sâu tốt, phí thấp.

Tùy chỉnh mức phí theo nhu cầu

Phán đoán 3 đã đề cập 19 mức phí lịch sử, chúng đến từ hàm cấu hình phí được dịch ngược (selector 0x4d909a45, chưa được ghi nhận chữ ký công khai):

Bốn mức phí và ngưỡng được đóng gói tổng thể, ghi vào khe lưu trữ keccak(poolId, 2). Kiểm tra on-chain khe này đọc được 0x0138800186a00186a0, khớp từng đoạn với định dạng đóng gói của hàm. Các mức phí ở đây là tham số có thể điều chỉnh bất cứ lúc nào, không cố định khi triển khai.

Vòng đời hoạt động

Pool này được tạo lúc 06:57 ngày 22-08, giao dịch swap đầu tiên lúc 07:13, giao dịch mẫu của bài viết lúc 21:39; giao dịch cuối cùng lúc 12:25 ngày 23-08, sau đó bị hủy. Toàn bộ thời gian hoạt động khoảng 29 giờ, tổng khối lượng 14.067.857 USD, thu nhập phí 131.888 USD (chưa trừ chi phí người triển khai). Khi kiểm tra lại, cờ đăng ký đã trở lại trạng thái kích hoạt—pool vẫn còn, có thể kích hoạt lại bất cứ lúc nào.

05 Đường dẫn đầy đủ

Kết nối toàn bộ "chuỗi tấn công":

Toàn bộ quan hệ nhân quả của chuỗi nằm trong hình trên, ba điều kiện không thể thiếu: mô phỏng trúng hai lớp đầu, giao dịch thực rơi vào lớp thứ ba, vùng đệm lớn hơn phí. Bất kỳ điều kiện nào không thành lập, thủ đoạn này đều thất bại.

06 Khắc phục và khuyến nghị

Vấn đề đầu tiên là cách báo giá của aggregator. Nguyên nhân gốc là mô phỏng và thực thi không đi cùng một đường: báo giá dùng truy vấn cấp pool lý tưởng hóa, kèm địa chỉ zero và gas cao; giao dịch thực mang danh tính thật và gas đã bị tiêu hao. Hook vừa đọc được những khác biệt này, phí do đó phân kỳ. Cách khắc phục là làm cho mô phỏng gần với thực thi—dùng calldata Router thực tế sẽ gửi, from/to thật, gas gần với giao dịch on-chain để mô phỏng; sau khi giao dịch, giải mã sự kiện Swap để đối chiếu fee, nếu không khớp với báo giá thì giảm trọng số hoặc gỡ xuống.

Vấn đề thứ hai là cài đặt trượt giá của người dùng. Nguyên nhân gốc là min_out quá lỏng: vùng đệm 20,32% nuốt trọn mức phí 12,8%, giao dịch vẫn thành công. Các tuyến stablecoin chính 0,1%–1% đủ dùng hàng ngày, hãy nhìn con số này trước khi khởi tạo giao dịch; ví và frontend/backend siết chặt giá trị mặc định có thể chặn phần lớn rủi ro cho người dùng.

Vấn đề thứ ba là quyền truy cập định tuyến. Nguyên nhân gốc là bất kỳ pool phí động nào cũng có thể trực tiếp tham gia cạnh tranh báo giá: Hook không mã nguồn mở, không có hồ sơ kiểm toán vẫn có thể vào tuyến đề xuất nhờ khối lượng được rửa. Đối với pool phí động không có hồ sơ đáng tin cậy (profile), mặc định không vào định tuyến hoặc giảm trọng số đáng kể là cách xử lý an toàn nhất.

Dữ liệu và ghi chú

· Giao dịch mẫu:

https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (khối 117497524)

· Giao dịch khởi tạo pool:

https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05

· PoolManager:

0x28e2ea090877bf75740558f6bfb36a5ffee9e9df

· Pool ID:

0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea

· Hook:

0xd111b3ddd92e627f1864520c770e913ec04e0880 (không có mã nguồn, mã giả được dịch ngược độc lập cho bài viết này; quản trị viên 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)

· Khối lượng và phí: tất cả 21.086 sự kiện Swap của pool được cộng dồn từng giao dịch (eth_getLogs), WBNB quy đổi theo giá thực thi cùng giao dịch; thu nhập là giá trị gộp, chưa trừ chi phí người triển khai

Nội dung này chỉ mang tính chất tham khảo và cung cấp thông tin, không phải lời khuyên đầu tư liên quan đến BTCC. BTCC luôn cố gắng cung cấp thông tin chính xác, nhưng không đảm bảo tuyệt đối về tính xác thực, độ chính xác hoặc bản quyền nội dung trên.

Đề xuất

Báo cáo chuyên sâu: Giá hợp lý của PUMP là bao nhiêu?Sau sự cố Liquid Network 320 triệu USD: Một tuyến phòng thủ thất thủ, nền tảng tài sản số còn giữ được gì?ZEC vào top 10, mọi chuyện cũ bị lật lạiVương Thuần chỉ trích Zcash: Sự thổi phồng của Phố Wall hay một Bitcoin tốt hơn?Khối lượng giao dịch cổ phiếu token hóa trên Base đạt 100 triệu USD