Phần 1: Hook – Một Dòng Code Khả Nghi
// File: PrismCore.sol, line 147
uint256 internal constant MAX_FLASHLOAN_FEE = 0.001e18; // 0.1%
Hai giờ trước, trên Etherscan, tôi nhìn thấy một dòng giao dịch bất thường: 0xdead…beef gọi hàm flashLoan() với tham số amount = 1_000_000_000e18 (1 tỷ USD) và fee = 0. Trong 12 khối tiếp theo, 43 hợp đồng phụ trợ bị drain sạch, tổng thiệt hại 420 triệu USD. Đây không phải lỗi reentrancy kinh điển. Đây là một cuộc tấn công có chủ đích vào cơ chế flash loan fee oracle – một vector mà hầu hết các audit đều bỏ qua. Tôi đã đọc mã nguồn của Prism từ tháng 6 năm ngoái, và tôi biết chính xác lỗ hổng nằm ở đâu.
Phần 2: Context – Prism Protocol và Cấu Trúc Phí
Prism Protocol là một giao thức cho vay đa chéo (cross-chain lending) được xây dựng trên Arbitrum, sử dụng cơ chế dynamic fee dựa trên thanh khoản real-time. Khác với Aave hay Compound, Prism không có admin key để điều chỉnh phí – mọi thứ được điều khiển bởi một oracle on-chain tổng hợp từ ba nguồn: Chainlink, Uniswap TWAP, và một internal accumulator tự tính dựa trên lịch sử flash loan gần nhất. Chính điểm cuối này là tử huyệt. Kẻ tấn công đã khai thác một read-only reentrancy trong accumulator: bằng cách gọi flashLoan() với một callback contract đặc biệt, nó đọc giá trị totalFees từ storage chưa được cập nhật, và dùng nó để tính phí cho các khoản vay tiếp theo trong cùng một transaction. Kết quả: phí = 0, không có bottleneck nào ngăn chặn việc vay 1 tỷ USD trong một lần. DeFi hè 2020 dạy tôi: tối ưu là vô hạn. Nhưng lần này, tối ưu chính là lỗ hổng.
Phần 3: Core – Phân Tích Kỹ Thuật Cấp Mã Nguồn
Để hiểu cách tấn công, hãy xem logic accumulator trong PrismCore.sol:

function _updateAccumulator(uint256 _newLoanAmount) internal {
uint256 currentAcc = flashLoanAccumulator;
// Điểm yếu: không có reentrancy guard
uint256 newAcc = currentAcc + _newLoanAmount / 1e9; // phép chia integer
// Cập nhật sau khi đọc, nhưng nếu reentrancy xảy ra, currentAcc vẫn là giá trị cũ
flashLoanAccumulator = newAcc;
}
Đoạn mã này được gọi trong flashLoan() trước khi chuyển token. Kẻ tấn công deploy một contract callback gọi lại flashLoan() lần 2. Ở lần 2, currentAcc vẫn bằng 0 (vì chưa kịp cập nhật), nên newAcc vẫn dựa trên dữ liệu cũ. Sau đó, phí được tính bằng công thức:
uint256 fee = (flashLoanAccumulator * lastBlockFeeRate) / 1e18;
Vì flashLoanAccumulator vẫn là 0, fee = 0. Kẻ tấn công lặp lại 11 lần, mỗi lần vay ~91 triệu USD với phí 0, tổng cộng 1 tỷ USD. Sau đó, nó dùng số tiền này để thao túng giá trên các AMM, drain các pool thanh khoản của Prism thông qua arbitrage tự động.

Trade-off kỹ thuật chính: Để tránh gas overhead, nhóm phát triển đã loại bỏ reentrancy guard trên accumulator, với giả định rằng oracle chỉ đọc chứ không ghi. Nhưng thực tế, accumulator vừa đọc vừa ghi, biến nó thành một writing oracle – một anti-pattern nguy hiểm. Số liệu on-chain cho thấy 43 hợp đồng dễ tổn thương nhất là các pool có TVL >10 triệu USD và không có cap riêng. Điều này chứng minh rằng bảo mật cấp giao thức không thể dựa vào giả định, mà phải dựa vào cơ chế xác thực đa lớp.
Phần 4: Contrarian – Góc Nhìn Phản Trực Giác
Hầu hết các phân tích sẽ đổ lỗi cho nhóm phát triển về lỗi reentrancy. Tôi cho rằng đó là cái nhìn thiển cận. Vấn đề thực sự không nằm ở flashLoanAccumulator, mà nằm ở triết lý thiết kế “trust-minimized” thái quá. Khi loại bỏ admin key, giao thức tự nguyện từ bỏ khả năng can thiệp khẩn cấp. Trong một cuộc tấn công mới, việc không có pause mechanism đã biến một lỗi nhỏ thành thảm họa. Điểm mù bảo mật ở đây không phải là kỹ thuật, mà là niềm tin mù quáng vào tính phi tập trung tuyệt đối. Một giao thức không có “emergency brake” thì giống như một siêu xe không có phanh – nhanh nhưng chết người. Bạn có thể audit 10 lần, nhưng một lỗi không ai ngờ tới vẫn có thể triệt tiêu toàn bộ TVL. Prism là minh chứng rõ ràng rằng phi tập trung không đồng nghĩa với an toàn, và rằng các cơ chế off-chain (như multisig có khả năng pause) thực tế có thể cứu sống giao thức.
Phần 5: Takeaway – Dự Báo Lỗ Hổng
Trong vòng 30 ngày tới, tôi dự đoán sẽ có ít nhất 3 giao thức khác bị khai thác với cùng vector “read-only reentrancy kết hợp viết accumulator”. Lý do: hàng chục dự án fork Prism’s code mà không hiểu rõ logic accumulator. Hãy kiểm tra mã nguồn của bạn ngay bây giờ. Nếu giao thức của bạn sử dụng bất kỳ biến storage nào được đọc và ghi trong cùng một function không có reentrancy guard, bạn đang ngồi trên quả bom hẹn giờ. Và câu hỏi cuối cùng: liệu chúng ta có đang hy sinh an toàn để chạy theo câu chuyện “hoàn toàn phi tập trung” hay không?