Ở phần một của bài viết này, tôi mô tả một trường hợp dịch vụ hoán đổi rabbit.io cố gắng gửi một lượng nhỏ SOL tới khách hàng, nhưng giao dịch liên tục bị từ chối — mặc dù theo tất cả quy tắc đã biết của blockchain Solana thì mọi thứ có vẻ hoàn toàn hợp lệ.
Phần đầu tiên của bài viết cũng đề cập đến những tình huống nơi một giao dịch về mặt kỹ thuật là đúng nhưng vẫn có thể bị từ chối trên các mạng như XRP Ledger, Stellar và Lightning Network (Layer 2 của Bitcoin). Tôi cũng giải thích một chi tiết triển khai của danh sách đen trong hợp đồng thông minh USDC có thể khiến các giao dịch USDC thất bại một cách bất ngờ, ngay cả khi bên gửi không thấy có gì sai.
Nếu bạn bỏ lỡ Phần I, bạn có thể đọc nó tại đây.
Hôm nay tôi muốn xem xét một số ví dụ tương tự liên quan đến các mạng và token khác. Trong mỗi trường hợp, mọi thứ lúc đầu có thể trông đúng — nhưng giao dịch vẫn thất bại.
Trường hợp này liên quan đặc thù tới các nhà phát triển và vận hành giao thức DeFi. Tuy nhiên, người dùng bình thường cũng có thể cảm nhận hậu quả của nó.
Hãy tưởng tượng tình huống sau. Bạn cố gắng gửi tiền vào một giao thức bằng USDC, DAI và USDT. Việc gửi bằng USDC và DAI hoạt động tốt, nhưng việc chuyển USDT bị revert. Địa chỉ đúng, mạng đúng, gas đủ. Lý do là gì?
Lý do là USDT trên Ethereum không hoàn toàn tuân thủ chuẩn ERC-20. Chuẩn yêu cầu các hàm transfer và transferFrom phải trả về một giá trị boolean: true khi thành công và false khi thất bại. USDT không trả về gì cả.
Nếu một hợp đồng thông minh được viết nghiêm ngặt theo chuẩn ERC-20 và kỳ vọng nhận về một giá trị boolean trả về từ lời gọi transfer, trong khi USDT không trả gì, máy ảo EVM (khi dùng Solidity từ phiên bản 0.4.22 trở đi) diễn giải điều này như một lỗi và revert giao dịch. Về mặt kỹ thuật giao dịch có thể đã thành công, nhưng nó bị bắt buộc hoàn tác. Và người gửi không nhận được chỉ báo rõ ràng nào từ mạng hay hợp đồng thông minh về việc lỗi gì đã xảy ra.
Ví dụ này không liên quan đến các hoán đổi trên rabbit.io. Hệ thống của chúng tôi được tổ chức đơn giản và đáng tin cậy nhất có thể: bạn nhận một địa chỉ để gửi token thủ công, và đổi lại bạn nhận token cần thiết từ chúng tôi. Không có hợp đồng thông minh DeFi nào tham gia.
Tuy nhiên, với độc giả không chỉ thực hiện hoán đổi trên rabbit.io mà còn tương tác tích cực với DeFi — bao gồm cả các nhà phát triển — tôi có thể gợi ý một giải pháp đơn giản. Sử dụng thư viện SafeERC20 của OpenZeppelin, thư viện này xử lý đúng các token không chuẩn.
Hầu hết các giao thức DeFi được viết chuyên nghiệp đã làm điều này. Tuy nhiên, các hợp đồng thông minh cũ hơn hoặc nghiệp dư vẫn bị lỗi khi tương tác với USDT. Vấn đề này phổ biến đến mức nó đã được đưa vào cơ sở dữ liệu weird-erc20 về các dị thường token đã biết, nơi bạn cũng có thể tìm thấy các token khác có hành vi tương tự.
USDT trên Ethereum có một đặc điểm khác thường xuyên gây giao dịch với hợp đồng thông minh thất bại.
Trong chuẩn ERC-20, nếu bạn muốn cho phép một hợp đồng thông minh tiêu token của bạn, bạn gọi hàm approve(spender, amount). Ví dụ, nếu bạn muốn tăng allowance từ 100 lên 200 USDC, bạn đơn giản gọi approve(spender, 200).
USDT hoạt động khác. Mã nguồn của nó nêu rõ nếu địa chỉ nhận đã có một allowance khác không bằng 0 thì không được phép đặt trực tiếp một giá trị không bằng 0 mới. Một giao dịch mà với hầu hết token khác sẽ hoàn toàn hợp lệ sẽ bị từ chối khi dùng với USDT.
Hành vi này được đưa vào như biện pháp bảo vệ chống tấn công chi tiêu hai lần liên quan đến allowance. Các nhà phát triển lo ngại rằng kẻ tấn công có thể ngay lập tức tiêu cả allowance cũ lẫn allowance mới.
Mẫu bắt buộc để thay đổi allowance của USDT vì vậy là như sau:
approve(spender, 0) để đặt lại allowanceapprove(spender, new_value)Đối với người dùng ứng dụng phi tập trung điều này có thể gây hoang mang. Người dùng trước đó đã cấp allowance cho ứng dụng, giờ muốn đặt một allowance khác, ký giao dịch — nhưng không có gì thay đổi và không có lời giải thích rõ ràng.
Ví dụ này cũng không liên quan đến các hoán đổi trên rabbit.io. Những giao dịch bị từ chối như vậy không thể xảy ra trên nền tảng của chúng tôi vì rabbit.io không yêu cầu kết nối ví và không yêu cầu quyền tiêu token từ ví.
Tuy nhiên, nếu trong một dApp nào đó giao dịch approve của bạn liên quan đến USDT bị thất bại, giải pháp thường đơn giản. Kiểm tra mức allowance hiện tại. Nếu nó khác 0, hãy đặt lại về 0 trước, rồi mới đặt giá trị mới.
Trong mạng Bitcoin có khái niệm gọi là dust. Nó chỉ một output của giao dịch có giá trị nhỏ hơn mức cần thiết để sau này có thể tiêu được. Nói cách khác, các đồng kỹ thuật tồn tại trong ví của bạn, nhưng phí cần thiết để gửi chúng lại lớn hơn chính số coin đó.
Cần hiểu một phân biệt cơ bản thường bị nhầm lẫn: sự khác nhau giữa quy tắc đồng thuận (consensus rules) và chính sách của node (node policy).
Một giao dịch có outputs nhỏ hơn giới hạn dust về mặt kỹ thuật có thể hợp lệ theo quy tắc đồng thuận Bitcoin. Tuy nhiên, hầu hết các node mặc định sẽ từ chối chuyển tiếp (relay) giao dịch như vậy, và hầu hết các thợ đào sẽ từ chối đưa nó vào khối.
Lý do đơn giản: lưu trữ các đồng cực nhỏ làm nặng cơ sở dữ liệu mà không mang lại lợi ích kinh tế thực sự.
Giới hạn dust phụ thuộc vào loại địa chỉ và phí trung chuyển tối thiểu. Về mặt lịch sử giới hạn dust được xem là 546 satoshi. Các giao dịch với outputs nhỏ hơn sẽ tốn phí nhiều hơn số tiền được gửi nếu tỉ lệ phí tối thiểu là 1 sat/vByte. Nhưng điều này chỉ áp dụng với các địa chỉ legacy bắt đầu bằng "1".
Các loại địa chỉ hiện đại hơn tạo giao dịch chiếm ít không gian trên blockchain hơn, vì vậy giới hạn dust của chúng có thể thấp hơn. Ví dụ, đối với địa chỉ bc1q... thông thường, giới hạn dust ở mức phí 1 sat/vByte là 294 satoshi, và với mức phí trung chuyển tối thiểu hiện đang áp dụng (0.1 sat/vByte) thì giới hạn dust còn nhỏ hơn gấp mười lần.

Hãy tưởng tượng tình huống sau.
Bạn có 100.000 satoshi (0.001 BTC). Bạn muốn gửi 99.000 satoshi cho ai đó. Bạn dựng một giao dịch, và phí mạng là 980 satoshi. Số còn lại 20 satoshi sẽ trả về cho bạn như tiền thối.
Nhưng điều đó sẽ không xảy ra. Bạn sẽ không nhận được tiền thối, và khoản thanh toán chính cũng sẽ không tới người nhận. Mạng lưới các node trung chuyển sẽ im lặng từ chối giao dịch vì một trong các outputs của nó thấp hơn giới hạn dust.
Còn có một vấn đề liên quan: tiêu dust sau này. Nếu bạn nhận nhiều outputs nhỏ hơn 294 satoshi trong khi phí đang thấp, và sau đó phí tăng lên, bất kỳ giao dịch nào cố tiêu những đồng đó có thể trở nên đắt đến mức số tiền chuyển đi thậm chí không đủ trang trải phí yêu cầu.
Do đó, khi trao đổi tài sản khác sang bitcoin, hãy đảm bảo rằng các outputs được tạo vượt quá giới hạn dust. Rabbit.io cho phép hoán đổi với số lượng lớn hơn nhiều so với giới hạn dust hiện tại. Tuy nhiên, nếu phí mạng tăng, những khoản nhỏ như vậy có thể trở nên khó gửi tiếp.
Trong Ethereum và bất kỳ blockchain tương thích EVM nào (như BSC, HyperEVM và những mạng khác), mỗi giao dịch gửi từ một địa chỉ cụ thể có một số thứ tự tuần tự gọi là nonce.
Mạng xử lý giao dịch theo đúng thứ tự: trước tiên giao dịch có nonce = 0, rồi nonce = 1, rồi 2, và cứ thế. Không thể bỏ qua một số.
Nếu một giao dịch bị kẹt trong mempool vì phí gas quá thấp, tất cả các giao dịch sau đó từ cùng một địa chỉ có nonce cao hơn cũng sẽ ở trạng thái đang chờ, ngay cả khi chúng có phí bình thường.
Đây là một kịch bản rất thực tế. Vài tuần trước bạn gửi một giao dịch với phí gas thấp. Nó bị kẹt. Bạn quên nó đi. Bây giờ bạn cố gửi một giao dịch mới. Bạn ghi đúng địa chỉ và đặt phí thích hợp, nhưng giao dịch vẫn không được xử lý.
Vấn đề không phải ở giao dịch hiện tại, mà là ở giao dịch cũ đang chờ.
Giải pháp là gửi một giao dịch có cùng nonce cũ nhưng với phí gas cao hơn. Điều này sẽ hoặc tăng tốc giao dịch ban đầu (speed up) hoặc hủy nó. Trường hợp sau xảy ra nếu bạn gửi một lệnh chuyển về chính bạn với cùng nonce và phí gas cao hơn.
Tuy nhiên, lưu ý rằng không phải ví nào cũng cho phép người dùng điều khiển giá trị nonce. Ví dụ, các ví phổ biến như Trust Wallet và Exodus không cung cấp chức năng này. Trong khi đó, MetaMask — mà nhiều người cho là lỗi thời và bất tiện — thực ra lại có.
Mạng TRON có mô hình tài nguyên khá khác thường. Thay vì một khoản phí giao dịch đơn lẻ, giao dịch tiêu hai loại tài nguyên:
Energy có thể nhận được bằng cách đóng băng (freeze) TRX. Nếu không làm vậy, mạng tự động đốt TRX để trang trải chi phí.
Và chính ở đây xuất hiện một vấn đề định giá bất ngờ:
Nếu người gửi chỉ có 15 TRX khả dụng cho phí nhưng chuyển tới một địa chỉ mới, giao dịch có thể thất bại với lỗi OUT OF ENERGY. TRX đã tiêu trong lần cố gắng này sẽ không được hoàn trả.
Nói cách khác, tình huống lại trông giống các ví dụ khác trong bài này: địa chỉ đúng, mạng đúng, người gửi có USDT, TRX có mặt — nhưng giao dịch vẫn thất bại.
Kết luận đơn giản: để hoạt động ổn định với USDT TRC-20, nên giữ ít nhất 27 TRX trong số dư để trang trải chi phí giao dịch có thể phát sinh.
Câu chuyện về việc chuyển SOL thất bại đã dạy tôi một bài học quý giá. Không phải theo nghĩa tôi hiểu sai blockchain, mà theo nghĩa mỗi blockchain là một hệ sinh thái riêng với các quy tắc ẩn không phải lúc nào cũng được ghi chép rõ ràng và đôi khi chỉ hiện ra trong những tình huống bất thường.
Mỗi thất bại khó hiểu không phải là lý do để nản lòng, mà là cơ hội để học điều mới. Và càng học được nhiều điều mới, tôi càng thấy thú vị khi tiếp tục làm việc với tiền mã hóa.
Nếu bạn cũng có cảm nhận tương tự, hãy ghi lại các trường hợp bất thường và chia sẻ phát hiện của bạn. Ngành crypto trở nên đáng tin cậy hơn khi kinh nghiệm thực tế lan truyền nhanh hơn tài liệu chính thức được cập nhật.