Một hook trong Uniswap V4 vừa bị khai thác trên testnet. Không phải do lỗi reentrancy hay overflow – thứ mà ai cũng nghĩ tới. Mà là do một chi tiết cực kỳ ngớ ngẩn: hook callback không kiểm tra msg.sender trước khi gọi poolManager.lock(). Kết quả? Bất kỳ ai cũng có thể giả mạo hook, rút thanh khoản của pool. Tôi đã phát hiện ra nó khi đọc mã nguồn của một dự án fork Uniswap V4 trên mạng thử nghiệm. Ba dòng code, hai giây nhìn, một lỗ hổng trị giá ít nhất 500 ETH trong kịch bản mainnet.
Context Uniswap V4 giới thiệu khái niệm "hooks" – các hợp đồng callback được gọi tại các điểm cụ thể trong vòng đời của pool (trước swap, sau swap, trước add liquidity, v.v.). Điều này biến DEX thành một nền tảng lập trình được. Nhưng sức mạnh đi kèm với trách nhiệm. Mỗi hook là một điểm mở rộng, và nếu không được kiểm soát chặt chẽ, nó trở thành cửa sau. Trong thiết kế V4, hook được xác định bởi địa chỉ hợp đồng và được đăng ký khi tạo pool. Tuy nhiên, cơ chế xác thực hook dựa trên msg.sender trong beforeSwap và afterSwap rất lỏng lẻo. Cụ thể, hook có quyền gọi lại poolManager.lock() để thực hiện các thay đổi trạng thái bổ sung. Vấn đề là: nếu hook không kiểm tra rằng cuộc gọi đến từ chính nó (hoặc từ poolManager), thì bất kỳ ai cũng có thể gọi hook's callback function và kích hoạt lock() fake.
Core Hãy nhìn vào mã. Trong hook mẫu của Uniswap V4, hàm afterSwap thường được định nghĩa như sau: ``solidity function afterSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, BalanceDelta delta, bytes calldata hookData) external override returns (bytes4) { // thực hiện logic return this.afterSwap.selector; } ` Không có onlyPoolManager modifier. Không có kiểm tra msg.sender == address(poolManager). Điều này có nghĩa là bất kỳ ai cũng có thể gọi afterSwap trực tiếp từ bên ngoài. Và nếu bên trong afterSwap có gọi poolManager.lock() với một callback khác, thì kẻ tấn công có thể tận dụng để thực hiện các hành vi bất hợp pháp. Ví dụ: một hook được thiết kế để thu phí bằng token ERC-20. Trong afterSwap, nó gọi poolManager.lock() để chuyển token từ pool sang hook. Nhưng vì không có kiểm tra, kẻ tấn công có thể gọi afterSwap nhiều lần, mỗi lần trigger lock()` – và rút token nhiều lần.
Đây là lỗi logic cơ bản nhất mà bất kỳ ai audit V4 cũng phải để ý. Nhưng tôi thấy 3 trong 5 dự án fork V4 mà tôi kiểm tra đều mắc lỗi này. Lý do? Các developer quá tập trung vào tính năng mới (hooks) mà quên mất bảo mật cơ bản: luôn kiểm tra caller trong callback. Họ nghĩ rằng vì hook được đăng ký khi tạo pool, nên chỉ poolManager mới gọi được hook. Nhưng thực tế, hook là hợp đồng riêng, ai cũng có thể gọi các hàm public của nó – bao gồm cả afterSwap và beforeSwap. Đây là sự nhầm lẫn giữa "pool gọi hook" và "hook chỉ được pool gọi".
Contrarian Điểm mù ở đây không nằm ở mã nguồn của Uniswap V4 core, mà nằm ở giả định của developer khi viết hook. Uniswap cung cấp hook mẫu, nhưng họ không bắt buộc kiểm tra caller. Họ cho rằng developer sẽ tự thêm bảo vệ. Nhưng trong thực tế, hầu hết developer copy-paste mã mẫu mà không hiểu hết. Và đây là lý do tại sao tôi luôn nói: "Mỗi lần biên dịch lại là mỗi lần thả mồi mới." Khi bạn fork một dự án lớn, bạn không chỉ sao chép tính năng, bạn sao chép cả lỗ hổng và tạo ra những lỗ hổng mới. Uniswap V4 là một bước tiến lớn về khả năng mở rộng, nhưng nó cũng mở ra một bề mặt tấn công khổng lồ. Các auditor như tôi phải đào sâu không chỉ vào core, mà vào từng hook implementation riêng lẻ – đó là nơi lỗi thường trú ẩn.
Takeaway Nếu bạn đang xây dựng trên Uniswap V4, hãy tự hỏi: hook của bạn có kiểm tra msg.sender == poolManager không? Nếu không, bạn đang để cửa mở cho bất kỳ ai. Thị trường gấu không phải là lúc để lười biếng. Mỗi dòng code bạn viết là một mồi câu cho kẻ tấn công. Hãy biên dịch lại, và kiểm tra lại. Vì lần biên dịch tiếp theo có thể là lần cuối cùng của bạn.
