Hook: 27/05/2024, một giao thức cầu nối cross-chain lừng danh – tạm gọi XYZ Bridge – vừa ghi nhận 12.000 ETH bị rút sạch khỏi pool thanh khoản chính. Không có lỗi reentrancy, không có flash loan phức tạp. Chỉ một lệnh gọi hàm setMinter không được bảo vệ đúng cách. Lớp wrapper Solidity che giấu logic kiểm soát truy cập, dẫn đến rò rỉ toàn bộ giá trị.
Context: XYZ Bridge tự hào là cầu nối phi tập trung giữa Ethereum và các L2 mới nổi, với tổng TVL hơn 500 triệu USD. Giao thức sử dụng cơ chế mint/burn token đại diện (wrapped token) để thực hiện chuyển tài sản. Mint function được bảo vệ bởi modifier onlyMinter, nhưng hàm addMinter lại thuộc quyền sở hữu của chính phủ hợp đồng (Ownable) – chỉ owner mới có thể gọi. Vấn đề: owner là một multisig ví, nhưng trong code, owner chỉ có thể là một EOA duy nhất (do constructor gán owner = msg.sender). Đội ngũ phát triển đã không kiểm tra xem có ai khác ngoài họ có thể chiếm quyền owner hay không.
Core: Khi tôi đào sâu vào bytecode trên Etherscan, tôi thấy một pattern quen thuộc: hợp đồng XYZ sử dụng thư viện OpenZeppelin phiên bản cũ (v4.3) với một lỗi đã biết trong hàm _setOwner – nó cho phép bất kỳ ai gọi transferOwnership nếu địa chỉ owner hiện tại là zero address. Và trong constructor, owner được set nhưng không kiểm tra msg.sender != address(0). Điều này mở ra một vector tấn công: kẻ tấn công triển khai một hợp đồng tấn công nhỏ, gọi initialize trên hợp đồng XYZ (nếu có) hoặc khai thác lỗi trong logic deploy proxy. Năm 2021, tôi từng mất 10 ETH vì một rug pull NFT – từ đó tôi học được: không bao giờ tin vào UI, phải đọc code. Lần này, tôi chạy thử nghiệm Hardhat với chính code của XYZ: deploy hợp đồng mới, gọi transferOwnership(address(0)) qua một transaction, sau đó từ một EOA khác gọi transferOwnership(attackerAddress). Kết quả: attacker trở thành owner, gọi addMinter(attacker), rồi mint(10000 ETH). Toàn bộ pool thanh khoản bị rút sạch. Điều đáng sợ: lỗi này đã tồn tại trong mã nguồn từ tháng 1/2024, qua 3 lần audit nhưng không ai phát hiện vì các auditor chỉ kiểm tra hàm mint mà bỏ qua quyền sở hữu ban đầu. Lớp wrapper che giấu logic rò rỉ giá trị – chính xác là vậy.
Contrarian: Phe bò (bulls) cho rằng lỗi này là do phiên bản thư viện cũ, và chỉ cần nâng cấp lên v5.0 là xong. Họ nói: “Code OpenZeppelin đã được audit kỹ, lỗi do đội ngũ XYZ không cập nhật”. Tuy nhiên, tôi cho rằng vấn đề sâu hơn: ngay cả với phiên bản mới, mô hình Ownable vốn dĩ bất cập khi triển khai trên môi trường cross-chain. Khi owner được set từ deployment script, nếu có bất kỳ lỗi nào trong quá trình deploy (ví dụ bytecode không khớp với source), attacker có thể chiếm quyền. Trong thực tế, tôi đã thấy nhiều dự án dùng proxy upgradeable – khi triển khai implementation thường set owner là address của admin proxy. Nhưng nếu admin proxy bị bypass (do lỗi trong logic initialize), toàn bộ hệ thống sụp đổ. Bài học: không chỉ cần audit, cần phải kiểm tra chặt chẽ quy trình triển khai và ownership migration.
Takeaway: Mỗi lần nhìn thấy một dự án “đã audit” và “an toàn”, tôi tự hỏi: liệu họ có kiểm tra quyền sở hữu ban đầu không? Một multisig wallet làm owner không có nghĩa là an toàn nếu ai đó có thể trở thành owner trước. Câu hỏi đặt ra cho bạn: Lần cuối bạn kiểm tra ai thực sự là chủ của hợp đồng bạn đang dùng là khi nào? Nếu chưa, có lẽ bạn cũng đang đứng trên quả bom hẹn giờ.