Bài viết này trình bày chi tiết cách thức vụ hack PrismaFi diễn ra vào ngày 2024-03-28.
Ứng dụng Prismafi
PrismaFi là ứng dụng tài chính phi tập trung (Decentralized Finance - Defi) cho phép user nạp tài sản thế chấp (Collateral - ETH hoặc những đồng token đại diện có giá trị tương đương) của mình vào và vay ra đồng tiền ổn định (tương đương USD). Mục đích của việc này là khi user muốn sử dụng tiền mặt nhưng ko muốn bán số ETH đang sở hữu vì thấy được tiềm năng tăng giá trong tương lai.
Thay vào đó có thể sử dụng số ETH nhàn rỗi này thế chấp đổi lấy 1 khoản tiền để sử dụng, sau đó khi kiếm được tiền có thể trả lại khoản vay này để lấy lại số ETH đã thế chấp mà không bị lỡ khi ETH tăng giá (ngược lại, chịu rủi ro bị thanh lý liquidated nếu ETH giảm giá dưới ngưỡng nhất định so với khoản vay).
Tính năng sử dụng trong vụ hack
Trong phạm vi bài này mình không đi sâu vào toàn bộ giao thức mà chỉ tập trung vào các tính năng liên quan trực tiếp đến vụ hack.
Contract token ổn định mkUSD
Contract ERC20 đại diện cho đồng ổn định mkUSD
, sẽ được mint ra cho người dùng khi người dùng nạp vào tài sản thế chấp và yêu cầu 1 khoản vay (openTrove
). Ngược lại sẽ burn đi khi người dùng trả lại khoản vay và lấy lại tài sản thế chấp (closeTrove
).
Ngoài ra đồng ổn định này được cài đặt tính năng flashLoan, tức là khi được gọi nó sẽ mint ra lượng token amount
cho vào tài khoản receiver
, gọi hàm receiver.onFlashLoan(...)
để receiver
thực hiện tác vụ j đó với khoản vay này. Sau khi kết thúc tác vụ thu lại số token này để burn đi và thu thêm 1 khoản phí từ receiver. Tất cả được thực thi trong cùng 1 giao dịch nếu cuối giao dịch receiver
không có đủ token để trả lại cũng như trả phí sẽ revert toàn bộ, cái này gọi là flashLoan (vay chớp nhoáng).
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool) {
// mint token cho receiver
// gọi hàm receiver.onFlashLoan() với 'data' là input
// lấy lại token và burn đi
}
receiver
: địa chỉ nhận và hành động với khoản vay chớp nhoáng này.token
: bắt buộc là địa chỉ đồng ERC20 này (tương đươngaddress(this)
)amount
: lượng vay mong muốndata
: input khi gọireceiver.onFlashLoan(...)
Contract xử lý thao tác cho vay
Người dùng gọi đến contract này khi muốn mở khoản vay hoặc đóng khoản vay.
openTrove: người dùng nạp tài sản thế chấp và nhận lại khoản vay mong muốn. Ở đây có điều kiện quan trọng là giá trị thế chấp / giá trị khoản vay collateral ratio - CR phải lớn hơn 1 ngưỡng nhất định (giá trị được config tùy vào loại tài sản thế chấp, vd 110%).
Để gọi được hàm này điều kiện người gọi hàm msg.sender
là chính chủ hoặc được ủy quyền - check ở hàm callerOrDelegated(account)
.
function openTrove(
ITroveManager troveManager,
address account,
uint256 _maxFeePercentage,
uint256 _collateralAmount,
uint256 _debtAmount,
address _upperHint,
address _lowerHint
) external callerOrDelegated(account) {...}
troveManager
: địa chỉ contract troveManager đại diện cho 1 loại tài sản thế chấp (mỗi loại tài sản thế chấp có 1 troveManager)account
: địa chỉ người vay_maxFeePercentage
: khoản phí tối đa chấp nhận được_collateralAmount
: lượng thế chấp nạp vào_debtAmount
: lượng USD muốn vay_upperHint, _lowerHint
: vị trí ước lượng người vay trước và người vay sau (các borrower được sắp xếp theo thứ tự CR giảm dần)
closeTrove: người dùng trả lại toàn bộ khoản vay và nhận lại tài sản thế chấp. Khoản thế chấp được trả vào tài khoản msg.sender
.
Để gọi được hàm này điều kiện người gọi hàm msg.sender
là chính chủ hoặc được ủy quyền - check ở hàm callerOrDelegated(account)
.
function closeTrove(ITroveManager troveManager, address account) external callerOrDelegated(account) {...}
troveManager
: địa chỉ contract troveManager (mỗi loại tài sản thế chấp có 1 troveManager)account
: địa chỉ người vay
Contract upgrade troveManager
Điểm yếu về security nằm ở đây, hiện tại mình không tìm thấy code của contract này trên github Prismafi mà chỉ có thể check code trên etherscan.
MigrateTroveZap trước khi vá lỗi.
MigrateTroveZap sau khi vá lỗi.
Contract cho phép người dùng upgrade troveManager
sang phiên bản mới hơn (sau khi được ủy quyền) bằng cách.
- Đóng khoản vay của
account
với troveManager contract cũ. - Mở khoản vay mới cho
account
với số lượng tương đương với troveManager contract mới.
onFlashLoan: đây là hàm callback thực hiện xử lý khi contract này nhận được khoản vay chớp nhoáng flashLoan.
/// @notice Flashloan callback function
function onFlashLoan(
address,
address,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
// check
require(msg.sender == address(debtToken), "!DebtToken");
// decode lấy tham số trong 'data'
(
address account,
address troveManagerFrom,
address troveManagerTo,
uint256 maxFeePercentage,
uint256 coll,
address upperHint,
address lowerHint
) = abi.decode(data, (address, address, address, uint256, uint256, address, address));
uint256 toMint = amount + fee;
// đóng trove - trả nợ, thu về tài sản thế chấp
borrowerOps.closeTrove(troveManagerFrom, account);
// mở trove - nạp vào tài sản thế chấp, thu về số token ổn định
borrowerOps.openTrove(troveManagerTo, account, maxFeePercentage, coll, toMint, upperHint, lowerHint);
return _RETURN_VALUE;
}
Hàm này thực hiện các nhiệm vụ sau:
- Dùng tiền flashLoan đóng khoản vay của
account
bằng cách gọiborrowerOperations.closeTrove()
vớitroveManager
cũ để rút về tài sản thế chấp. - Mở lại khoản vay với khoản thế chấp vừa có và nhận lại token ổn định với
troveManager
mới sử dụng inputdata
.
trong đó, data
truyền vào bao gồm
- địa chỉ người vay
- địa chỉ troveManager cũ
- địa chỉ troveManager mới
- phí tối đa khi mở khoản vay
- số lượng tài sản thế chấp nạp vào khi mở khoản vay mới
- vị trí ước lượng khi mở khoản vay mới
Các bước diễn ra vụ hack
- Người dùng cấp quyền cho contract chứa lỗ hổng
- Hacker thực hiện khai thác lỗ hổng trong contract