Frontrunning và cách phòng chống
1. Frontrunning là gì ?
Frontrunning trên Ethereum là một hành vi cố gắng lợi dụng thông tin về một giao dịch sắp xảy ra để thực hiện giao dịch trước giao dịch đó với mục đích chuộc lợi.
Các giao dịch trước khi được xác thực và gom vào các block sẽ "nằm chờ" trong một nơi gọi là mempool, các giao dịch được trả phí cao sẽ có độ ưu tiên lớn hơn.
Frontrunning có thể được thực hiện bằng cách theo dõi các mempool, và thực hiện giao dịch của riêng mình trước giao dịch đó bằng cách để phí giao dịch cao hơn nhằm hưởng lợi.
Chúng ta cùng xem qua một ví dụ nhỏ dưới đây:
pragma solidity ^ 0.8.17;
// Trò chơi dự đoán số bí mật
contract GuessTheNumberChallenge {
bytes32 challenge;
// Người tạo gửi 1 ETH khi deploy contract, cùng với đó là mã bí mật (giá trị hash của 1 số)
constructor(bytes32 _challenge) payable {
require(msg.value == 1 ether);
challenge = _challenge;
}
// Kiểm tra xem game đã kết thúc chưa (nếu số dư ETH của contract = 0)
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
/*
Người chơi gửi 1 ETH khi gọi hàm
Nếu giá trị băm của tham số truyền vào đúng bằng mã bí mật. Tất cả tiền trong contract sẽ là phần thưởng cho người dùng
*/
function guess(uint256 number) public payable {
require(msg.value == 1 ether, "Submission fee required");
uint256 balance = address(this).balance;
require(balance != 0, "Game has ended");
bytes32 userChallenge = keccak256(abi.encode(number));
if (userChallenge == challenge) {
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
}
}
}
Khi 1 người dùng đoán đúng được số bí mật, giao dịch được gửi đi và nằm trong mempool trước khi được xác nhận và thêm vào block. Và đây chính là miếng mồi béo bở cho các fontrunner , sao chép dữ liệu giao dịch và gửi 1 giao dịch tương tự với số gas lớn hơn nhiều và cuỗm trọn số tiền thưởng 👋 Giao dịch của người dùng với số gas ít hơn sẽ bị revert khi được thực thi ở các block sau
Lưu ý: biến challange
tuy không public nhưng có thể biết được bằng kỹ thuật getStorageAt
, qua đó việc theo dõi tham số truyền vào của các giao dịch gọi hàm guess
có thể biết được giao dịch nào sẽ đúng.
2. Các hình thức frontrunning
Chúng ta có thể chia thành 3 loại: Displacement, insert và Suppression.
Displacement (ghi đè)
Đặc điểm của phương thức tấn công này là tăng giá trị gas lên nhiều lần nhằm được xử lý và gom vào block trước giao dịch của người dùng. GuessTheNumberChallenge
ở trên chính là 1 ví dụ về hình thức tấn công thay thế.
Insert (chèn)
Hay còn được gọi với cái tên sandwicd attack. Với kiểu tấn công này, giao dịch của người dùng vẫn sẽ được thực thi nhưng sẽ bị kẹp giữa bởi 2 giao dịch của kẻ tấn công (như một chiếc bánh mỳ kẹp thịt vậy ).
Chúng ta cùng xem qua ví dụ sau:
- Alice thực hiện 1 giao dịch swap USDC sang ETH với 1 số lượng lớn.
- Giao dịch của Alice trước khi thực thi sẽ nằm trong mempool.
- Bob sao chép giao dịch của Alice và "chạy trước" việc swap.
- Giao dịch của Bob thành công (được đưa vào block).
- Giao dịch của Alice được thực thi sau đó. Do Alice swap với 1 số lượng lớn nên sẽ làm giá ETH tăng.
- Bob chèn 1 giao dịch nữa sau khi Alice đã làm giá ETH tăng. Bob bán toàn bộ số ETH vừa swap, đổi lại USDC.
Chắc chắn có một mức độ rủi ro nhất định liên quan đến việc fontrunning của Bob vì giá ETH có thể giảm vì một số lý do và anh ấy sẽ lỗ. Nhưng nếu dung lượng giao dịch của Alice đủ lớn, xác suất tăng giá ETH là cao và Bob sẽ thu được số USDC nhiều hơn ban đầu.
Với Alice, đây rõ ràng là một kết quả chưa tối ưu. Việc mua ETH của Bob khiến giao dịch của cô ấy được thực hiện ở mức giá tệ hơn. Nếu Bob không "chạy trước", Alice sẽ nhận được nhiều ETH hơn.
Sau khi Bob bán lên ETH và có thể khiến giá của nó giảm. Nếu Alice cũng muốn bán ngay sau khi mua, cô ấy sẽ nhận được ít USDC hơn.
Suppression (Tấn công từ chối dịch vụ)
Những kẻ tấn công có thể tạm thời trì hoãn việc thực hiện các giao dịch khác bằng cách phát hành một loạt giao dịch với giá gas cao. Kẻ tấn công tạo ra nhiều giao dịch với gasPrice
và gasLimit
cao, nhằm làm đạt tới giới hạn gasLimit
của block. Các block sẽ chứa đầy các giao dịch spam với giá gas cao mà ko có giao dịch của những người dùng khác.
Những cuộc tấn công này trở nên đặc biệt tàn khốc khi chúng nhắm vào các giao dịch quan trọng, chẳng hạn như các bản cập nhật oracle. Oracles thường cung cấp thông tin quan trọng về giá cho các tài sản khác nhau trong hệ sinh thái DeFi. Sự chậm trễ trong việc cập nhật giá ảnh hưởng lớn tới việc định giá tài sản, gây thiệt hại lớn cho người dùng và các nền tảng DeFi.
3. Một số biện pháp phòng chống frontrunning
1. Private Transaction
Khác với các giao dịch thông thường, private transaction không được phát tán lên mạng blockchain. Nó sẽ "nằm chờ" tại một node của thợ đào (hoặc validator) có đến khi thợ đào đó có quyền thêm khối mới vào chuỗi. Trong quá trình đó, các frontrunner sẽ không hề hay biết về sự tồn tại của giao dịch
Bài viết Escaping the Dark Forest là 1 câu chuyện ly kỳ, hấp dẫn trong việc sử dụng private transaction để chống frontrunning. Nếu bạn không là thợ đào thì nhất thiết phải quen được thợ đào để có thể tạo 1 private transaction.
2. Submarine Sends
Submarine Sends là một giải pháp được đề xuất nhằm chống lại hình thức frontrunning. Thay vì chỉ che dấu giá trị của giao dịch, Submarine Sends che dấu toàn bộ giao dịch với những kẻ có ý đồ xấu như một chiếc tàu ngầm lặn sâu dưới đáy biển. Khi nổi lên để "về bờ" an toàn thì đã quá muộn cho bất cứ hành động gì từ kẻ địch
Submarines: They can’t frontrun you if they can’t find you (Họ không thể tấn công bạn nếu họ không thể tìm thấy bạn)
Ý tưởng của phương pháp này là đánh lạc hướng các front-runner bằng việc gửi giao dịch tới một địa chỉ hoàn toàn mới (chưa hề có phát sinh giao dịch) thay vì gửi trực tiếp đến hợp đồng của ứng dụng. Nội dung giao dịch đã được mã hóa và sẽ được "tiết lộ" (reveal) ở một thời điểm khi việc frontrunning không còn tính khả nữa.
Chúng ta lấy 1 ví dụ minh họa quá trình tương tác với 1 hợp đồng đấu giá NFT bằng ETH.
- Cam kết (Commit): Người dùng tính toán địa chỉ Commitment Address A và commit dữ liệu về giao dịch gửi tới A,
commit := Keccak256(addr(End User) | addr(DApp Contract/LibSubmarine) | value | optionalDAppData | w | gasPrice | gasLimit)
, trong đó value theo đơn vị wei, w là witness (số 1 được tạo ngẫu nhiên. Người dùng sẽ gửi ETH vào A (A là một địa chỉ hoàn toàn mới trên mạng, không có private key). - Tiết lộ (Reveal): Hợp đồng đấu giá sẽ xác minh số nonce của người dùng có trong Dữ liệu và kiểm tra xem số dư của A có đúng với thông tin giao dịch không ? Và cuối cùng là rút tiền về từ A.
contract A sẽ có dạng như sau:
pragma solidity ^0.8.17;
contract Forwarder {
// địa chỉ Dapp smart contract
address payable constant addrContract = payable(0x5aa02b19988e24F0210480474a2E1A3d2B84CCAC);
function unlock () public {
if (msg.sender == addrContract)
addrContract.transfer(address(this).balance);
}
}
3. Một số các biện pháp khác
- Cài đặt mức trượt giá tối thiểu (Minimal Slippage Rate): Để bảo vệ các ứng dụng DeFi khi swap token, nên hạn chế trượt giá và tỷ lệ trượt giá (%) (khoảng từ 0,1% đến 5%), tùy thuộc vào phí mạng và quy mô giao dịch. Nỗ lực này nhằm chống lại những kẻ tấn công frontrunning có cơ hội trục lợi nhờ khai thác tỷ lệ trượt giá cao hơn. Giao dịch nếu bị trượt giá quá mức cho phép so với ban đầu, giao dịch sẽ được hoàn nguyên.
- Hạn chế tần suất giao dịch (Rate Limit): Giảm thiểu việc có thể bị tấn công kiểu Suppression.
- Sử dụng Time-lock Contract: Bằng cách đặt ra các ràng buộc về thời gian cụ thể cho các giao dịch, nền tảng có thể ngăn chặn các giao dịch đột ngột và mang tính cơ hội, khiến việc frontrunning trở nên khó khăn hơn.
- Gửi giao dịch theo lô (Batch Transactions): Bằng cách gộp nhiều giao dịch lại với nhau và xử lý chúng thành một, kẻ tấn công sẽ gặp khó khăn hơn trong việc xác định và khai thác các giao dịch riêng lẻ.
- Sử dụng private transaction
Tài liệu tham khảo
https://docs.flashbots.net/new-to-mev
All rights reserved