Tiêu đề gốc: "Báo giá 0%, thực thi 12,8%: Hồi tưởng về bẫy phí động 'bể độc' của Uniswap v4"
Tác giả gốc: Vương Tử Hào, BitsLab
Từ ngày 22 đến ngày 23 tháng 8 năm 2026, một bể phí động Uniswap v4 USDC/WBNB trên BNB Chain hiển thị phí 0% trong báo giá của aggregator, nhưng người dùng thực tế bị thu phí LP 12,8% khi khớp lệnh. Trong khoảng 29 giờ, bể này đã khớp 21.086 giao dịch, tổng phí tích lũy 131.888 USD, sau đó bị hủy đăng ký. Tất cả các giao dịch bị hại đều thành công: hợp đồng không bị đánh cắp, đầu ra cao hơn giới hạn dưới do người dùng đặt, không có bất kỳ tín hiệu bất thường nào — phần chênh lệch lặng lẽ biến thành phí.
Bài viết này lấy một giao dịch mẫu làm điểm vào:
(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: người dùng đầu tư 5.926,90 USDT, nhận được ít hơn dự kiến 757,43 USDT), phân tích cách bể này đạt được "báo giá 0%, thực thi 12,8%": ba tầng phán định của hàm phí, bảo vệ trượt giá bị vô hiệu hóa, danh sách trắng chặn các LP khác ngoài cửa. Tất cả kết luận đều dựa trên dữ liệu on-chain và xác minh có kiểm soát cục bộ.
Sau khi người dùng khởi tạo giao dịch hoán đổi trên giao diện aggregator, toàn bộ chuỗi diễn ra như sau:

Trên chuỗi này có hai sự thật nền tảng, mỗi tầng phán định sau đây đều được xây dựng 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 bể phí động; Hook mang 0x400000 | fee trong giá trị trả về thứ ba của beforeSwap. Đoạn 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 giao dịch hoán đổi này theo mức phí đó và ghi vào sự kiện Swap. Nói cách khác, Hook trả về bao nhiêu, người dùng trả bấy nhiêu — miễn là đầu ra vẫn cao hơn amountOutMin. Đây là tính năng chính thức, điều chỉnh phí theo biến động và tồn kho đều là mục đích sử dụng chính đáng.
Nhìn lại 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 thực khi triển khai, không thể thay đổi sau đó. Định nghĩa cờ phía v4-core và giải mã địa chỉ tương ứng với nhau:

Kiểm tra trên chuỗi thực tế 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 từ bytecode ra toàn bộ hàm phí — sau đây là phân tích hành vi của logic đã chạy trên chuỗi:

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ột lần mô phỏng không", lớp thứ ba quyết định giao dịch thực trả bao nhiêu.
Sau khi hàm phí được gọi, trước tiên so sánh tx.origin: nếu bằng bất kỳ địa chỉ nào trong ba địa chỉ hằng, trực tiếp trả về mức dự phòng. Mức dự phòng hiện được cấu hình là 0%.
Ba địa chỉ không phải chọn tùy tiện. Khi eth_call không chỉ định from, origin của giao dịch rơi vào địa chỉ zero — aggregator truy vấn hàng loạt 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 như thế nào.
Thay tx.origin bằng địa chỉ thông thường thử lại, trả về 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 định duy nhất, định vị đến lớp thứ hai: gasleft() * 10100 / 10000 >= gate.
Aggregator báo giá thường cấp gas rất cao, để ngăn tuyến ứng viên thất bại trong mô phỏng do thiếu gas; giao dịch thực trước khi vào Hook, gas đã bị Router, ủy quyền, bước nhảy trước tiêu thụ. Cùng một đoạn mã, ba loại gas:

Đường phân chia thực tế nằm giữa 16.85M–16.9M gas. Sự khác biệt giữa 30M và 2.59M chính là sự khác biệt giữa "báo giá" và "thực thi".
Các giao dịch thực sự vượt qua hai lớp đầu tiên sẽ 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 rơi ra ngoài thì quay về 0%.
Lớp này có hai thiết kế đáng chú ý. Dấu vân tay có trộn vào 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, nên dấu vân tay đương nhiên cũng khác. Ngoài ra, giả ngẫu nhiên từng giao dịch khiến mức phí mang tính phân phối, chỉ nhìn vài giao dịch thì rất khó phát hiện quy luật, các kiểu liệt kê quy tắc đơn giản không chặn được.
Trong toàn bộ 21.086 sự kiện trên chuỗi 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; mức có hiệu lực khi giao dịch mẫu khớp lệnh là 12,8%.

Đến đây có thể định nghĩa "hồ độc" — 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), chứ không đọc trạng thái thị trường công khai; phí khi mô phỏng báo giá và khi thực thi trên chuỗi xuất hiện phân kỳ mang tính hệ thống, bên hưởng lợi là người triển khai hồ độc. Ranh giới nằm ở việc phí đọc cái 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 tùy 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 đi, tại sao giao dịch vẫn thành công, là câu hỏi tiếp theo.
Trước tiên xem đường đi đầy đủ và số tiền của giao dịch này:

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

Mức phí 12,8% nằm trọn trong vùng đệm 20,32%, đầu ra vẫn cao hơn ngưỡng tối thiểu — giao dịch thành công, không rollback. Mặc định thông thường của các tuyến stablecoin chủ đạo là 0,1%–1%, tuyến này cho không gian hơn 20 lần. Người dùng tưởng vùng đệm rộng là "ổn định hơn", thực tế là giao phó từng hợp đồng trên đường đi cho sự tự giác của đối tác. Vẽ khoản này thành biểu đồ:

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 v4 Hook đều có, sau đó phải đi qua PositionManager chính thức, cuối cùng là đố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 rollback tại đây, các LP khác không vào được, doanh thu phí không bị pha loãng, tất cả thuộc về kẻ triển khai. Bản thân danh sách được duy trì theo địa chỉ bởi một hàm quản trị (selector 0xc4452e52).
Đặt 21.086 giao dịch cạnh nhau, phân bố cho thấy mô hình rõ ràng: bậc 0% có 6.946 giao dịch, khớp 12.947.751 USD, bình quân 1.864 USD/giao dịch; bậc thu phí có 14.140 giao dịch, khớp 1.120.106 USD, bình quân 79 USD. Giao dịch lớn tập trung ở bậc 0%, thu phí tập trung ở giao dịch nhỏ. Lời giải thích hợp lý nhất cho giao dịch lớn bậc 0% là kẻ triển khai tự khớp lệnh với gas cao: gas cao và báo giá aggregator trúng cùng một phán định, chi phí phí tự khớp gần như bằng không. Khối lượng được tạo ảo đẩy bể lên bảng xếp hạng các 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 bể có độ sâu tốt, phí thấp.
Phán định ba đã đề cập 19 bậc phí lịch sử, chúng đến từ hàm cấu hình phí dịch ngược được (selector 0x4d909a45, chưa có trong chữ ký công khai):

Bốn bậc phí và ngưỡng được đóng gói tổng thể, ghi vào slot lưu trữ keccak(poolId, 2). Đo trên chuỗi cho thấy slot này đọc là 0x0138800186a00186a0, tương ứng từng đoạn với định dạng đóng gói của hàm. Bậc phí ở đây là tham số có thể điều chỉnh bất cứ lúc nào, không phải cố định khi triển khai.
Pool này được tạo lúc 08-22 06:57, giao dịch swap đầu tiên xuất hiện lúc 07:13, 21:39 là giao dịch mẫu của bài viết này; 08-23 12:25 là giao dịch khớp cuối cùng, sau đó bị hủy đăng ký. Toàn bộ thời gian hoạt động khoảng 29 giờ, tổng khối lượng giao dịch 14.067.857 USD, doanh thu phí 131.888 USD (chưa trừ chi phí của người triển khai). Khi kiểm tra lại, cờ đăng ký đã trở về 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.
Nối toàn bộ chuỗi tấn công lại:

Toàn bộ quan hệ nhân quả của chuỗi đều nằm trong hình trên, ba điều kiện không thể thiếu một: mô phỏng trúng hai tầng đầu, giao dịch thật rơi vào tầng thứ ba, buffer lớn hơn phí. Bất kỳ điều nào không thành lập, thủ thuật này đều sẽ thất bại.
Điểm thứ nhất nằm ở 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 con đườ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ật mang danh tính thật và gas đã bị tiêu thụ. Hook vừa hay có thể đọc ra những khác biệt này, phí do đó phân nhánh. Cách đối trị là để mô phỏng sát với thực thi — dùng Router calldata thực sự sẽ gửi, from/to thật, gas gần với giao dịch trên chuỗi để mô phỏng; sau khi khớp lệnh 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 quyền hoặc đưa ra khỏi danh sách.
Điểm thứ hai nằm ở cài đặt trượt giá của người dùng. Nguyên nhân gốc là min_out đặt quá lỏng: buffer 20,32% nuốt trọn phí 12,8%, giao dịch vẫn thành công như thường. Các tuyến stablecoin chủ đạo 0,1%–1% là đủ cho sử dụng hàng ngày, trước khi phát giao dịch hãy nhìn con số này; ví và frontend siết chặt giá trị mặc định, có thể thay người dùng chặn phần lớn rủi ro.
Điểm thứ ba nằm ở quyền truy cập routing. 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ơ audit, vẫn có thể nhờ khối lượng giao dịch được刷 ra để vào tuyến được đề xuất. Với pool phí động không có hồ sơ đáng tin, mặc định không vào routing hoặc giảm quyền đáng kể, là cách xử lý an toàn nhất.
·Giao dịch mẫu:
https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (block 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ả là kết quả dịch ngược độc lập của bài viết này; quản trị viên 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)
·Giao dịch và phí: toàn bộ 21.086 sự kiện Swap của pool này được cộng dồn từng giao dịch (eth_getLogs), WBNB được quy đổi theo giá thực hiện của cùng giao dịch; doanh thu là giá trị gộp, chưa trừ chi phí của người triển khai
Chào mừng bạn tham gia cộng đồng chính thức của BlockBeats:
Nhóm Telegram đăng ký: https://t.me/theblockbeats
Nhóm Telegram thảo luận: https://t.me/BlockBeats_App
Tài khoản Twitter chính thức: https://twitter.com/BlockBeatsAsia