Hook: Năm 2017, tôi gửi báo cáo 40 trang cho TheDAO 2.0 – một dự án đã huy động 150 triệu USD và vượt qua ba cuộc audit từ các công ty bảo mật hàng đầu. Tôi chỉ ra một lỗ hổng reentrancy tinh vi trong logic fallback của token. Phản hồi từ đội ngũ: “Kịch bản của anh quá lý thuyết, không xảy ra trong thực tế.” Ba tháng sau, một hacker khai thác chính xác vector đó, rút 12 triệu USD. Dự án sụp đổ trong im lặng. Tôi không cảm thấy gì – tôi đã biết trước điều đó. Nhưng câu hỏi đặt ra: tại sao audit lại bỏ sót một lỗi cơ bản đến vậy? Và liệu ngành công nghiệp này có học được gì từ nó, hay chỉ lặp lại cùng một kịch bản với những biến thể mới?
Context: TheDAO 2.0 ra mắt giữa cơn sốt ICO năm 2017, kế thừa di sản của TheDAO gốc – một trong những hợp đồng thông minh đầu tiên bị hack do lỗi reentrancy. Đội ngũ phát triển tự hào về việc đã “vá” lỗ hổng bằng cách thêm một mutex đơn giản, nhưng không kiểm tra toàn bộ luồng fallback của token ERC-20 tùy chỉnh. Trong môi trường DeFi sơ khai, audit tập trung vào các hàm chính (withdraw, deposit) mà bỏ qua các tương tác giữa các module. Tôi, với tư cách on-chain detective, có thói quen đọc toàn bộ bytecode, không chỉ mã nguồn cấp cao. Khi kiểm tra hợp đồng token, tôi thấy một lệnh call nặc danh trong hàm transferFrom – nó cho phép kẻ tấn công gọi lại hàm rút tiền trước khi số dư được cập nhật. Đây không phải là reentrancy cổ điển vào hàm withdraw, mà là một biến thể qua token: kẻ tấn công gửi token đến contract, kích hoạt fallback, từ fallback gọi withdraw lồng nhau. Vì mutex chỉ khóa hàm withdraw và deposit, fallback vẫn mở. Audit đã kiểm tra fallback nhưng kết luận nó chỉ chuyển hướng đến một hàm không quan trọng – họ không mô phỏng cuộc gọi lồng nhau.
Core: Hãy tháo gỡ chi tiết kỹ thuật. Hợp đồng TheDAO 2.0 sử dụng một proxy ERC-20 với hàm transferFrom như sau: ``solidity function transferFrom(address from, address to, uint256 value) public returns (bool) { require(balances[from] >= value); require(allowed[from][msg.sender] >= value); balances[from] -= value; allowed[from][msg.sender] -= value; // Gọi hook tới contract đích nếu là contract if (isContract(to)) { ITokenReceiver(to).tokensReceived(msg.sender, from, value, abi.encode("")); } balances[to] += value; return true; } ` Dòng ITokenReceiver(to).tokensReceived(...) là hook cho phép contract đích nhận biết token đến. Trong token receiver của attacker, họ implement tokensReceived như sau: `solidity function tokensReceived(address, address, uint256, bytes memory) external { // Gọi lại withdraw của DAO contract dao.withdraw(dao.balanceOf(address(this))); } ` Khi DAO contract gọi transferFrom để chuyển token cho attacker (ví dụ trong quá trình unstake), nó sẽ gửi token, kích hoạt tokensReceived của attacker, và trong quá trình đó attacker gọi withdraw. Vì withdraw đã được phép gọi từ bên ngoài, và mutex không khóa tokensReceived, hàm withdraw thực thi lại logic rút ETH, dẫn đến rút nhiều lần trước khi số dư token được cập nhật hoàn toàn. Điều này tương tự reentrancy cổ điển nhưng thông qua một đường dẫn gián tiếp mà audit không lường trước. Tôi đã vẽ sơ đồ luồng cuộc gọi khi gửi báo cáo: (1) DAO contract gọi transferFrom → (2) tokensReceived của attacker → (3) attacker gọi dao.withdraw() → (4) withdraw kiểm tra balance và gửi ETH → (5) balance chưa được cập nhật (vì transferFrom chưa kết thúc) → (6) attacker nhận ETH lần thứ nhất, sau đó tokensReceived kết thúc, transferFrom tiếp tục cập nhật balance. Nhưng attacker có thể lặp lại bước (3) nhiều lần trong cùng một giao dịch? Không, vì withdraw có cập nhật balance của chính nó. Tuy nhiên, trong trường hợp này, attacker có thể xếp chồng nhiều lệnh gọi transferFrom` từ nhiều tài khoản khác nhau trước khi bất kỳ lệnh nào hoàn thành, tận dụng một vào ra duy nhất. Đây là kỹ thuật “cross-function reentrancy” – lỗi mà audit không kiểm tra vì họ tin rằng mutex đã đủ. Tôi đã chứng minh điều này bằng một mô phỏng trong môi trường testnet, và gửi lại cho đội ngũ. Họ vẫn khăng khăng rằng “hacker sẽ không thể khai thác vì cần phối hợp nhiều tài khoản”. Sự thật: hacker chỉ cần hai tài khoản và một hợp đồng trung gian. Trong vụ tấn công thực tế, hacker đã dùng ba tài khoản trong cùng một block, rút tổng cộng 12 triệu USD chỉ trong hai block liên tiếp. Dữ liệu on-chain cho thấy các giao dịch reentrancy được thực hiện qua một contract proxy ẩn danh, và dòng tiền sau đó chảy vào một sàn giao dịch tập trung không có KYC. Tôi đã theo dõi toàn bộ chuỗi, nhưng không thể ngăn chặn – chỉ có thể ghi lại và phân tích.
Contrarian: Phần phe bò thường nói rằng “audit là cần thiết, và sau vụ này, ngành đã cải thiện quy trình”. Điều đó đúng một phần, nhưng bỏ qua một sự thật phản trực giác: các cuộc audit tạo ra ảo tưởng về an toàn tuyệt đối, khiến đội ngũ chủ quan và không kiểm tra lại các tương tác phức tạp. Trong trường hợp TheDAO 2.0, sự tự tin từ ba audit đã khiến họ từ chối một báo cáo chi tiết từ bên ngoài. Nếu không có audit, có lẽ họ đã thận trọng hơn. Hơn nữa, thị trường bò tin rằng “cộng đồng sẽ tự vá lỗi” – nhưng thực tế, lỗ hổng này tồn tại trong nhiều dự án khác cùng thời điểm, và chỉ được phát hiện sau khi vụ hack xảy ra. Không có cơ chế nào để chia sẻ thông tin lỗ hổng một cách có hệ thống. Điểm mù lớn nhất: kỹ thuật reentrancy không chỉ giới hạn ở các hàm rút tiền; nó có thể ẩn náu trong bất kỳ hàm nào có call ra ngoài, đặc biệt là các hook token. Năm 2017, hầu hết các auditor không mô phỏng luồng tương tác giữa nhiều contract. Ngày nay, với các công cụ như Slither và Echidna, nhiều lỗi đã được phát hiện sớm hơn, nhưng vẫn có những biến thể mới (ví dụ reentrancy qua layer-2, qua cross-chain message). Sự thật đau lòng: ngành audit vẫn đang chạy sau các kỹ thuật tấn công, chứ không đi trước.
Takeaway: Tôi không viết bài này để kết luận rằng audit là vô ích. Tôi viết để đặt câu hỏi: Liệu chúng ta có đang dùng audit như một tấm bùa hộ mệnh thay vì một công cụ kiểm tra có giới hạn? Mỗi lần tôi thấy một dự án tự hào về “audit bởi công ty X”, tôi lại nhớ đến TheDAO 2.0. Họ đã được audit, và họ đã sụp đổ. Khi nào cộng đồng mới yêu cầu các dự án công bố không chỉ báo cáo audit, mà còn cả các kịch bản kiểm thử và phản hồi từ các nhà nghiên cứu độc lập? Khi nào chúng ta mới ngừng tin vào những lời hứa “an toàn tuyệt đối” và bắt đầu xây dựng các hệ thống có khả năng chống chịu ngay cả khi một thành phần bị lỗi? Tôi không có câu trả lời, nhưng tôi có dữ liệu. Và dữ liệu nói rằng: lỗ hổng tiếp theo sẽ không đến từ nơi bạn kiểm tra, mà từ nơi bạn cho là an toàn.