Trong phần trước, mình đã chia sẻ chi tiết về payable function trong Solidity — cách cho phép smart contract nhận ETH, và cách EVM xử lý transaction gửi kèm value. Mình sẽ để link bài viết trước ở đây cho bạn nào chưa đọc: Payable Function Solidity là gì? Cơ chế xử lý ETH tới smart contract (P1). Còn trong phần này, chúng ta sẽ đi sâu vào mặt còn lại của payable trong Solidity: làm thế nào để gửi ETH từ smart contract tới địa chỉ khác một cách an toàn, tối ưu và không dính lỗi thường gặp.
Nếu bạn đang viết các contract có xử lý dòng tiền — như treasury, refund, tipping, profit-sharing, NFT claim hoặc reward payout — thì đây chính là phần bạn cần.
Gửi ETH từ Smart Contract không đơn giản như bạn nghĩ
Được rồi, bạn đã nhận ETH vào contract bằng payable function. Nhưng giờ làm sao để gửi ETH từ smart contract đó ra ví người dùng, dev, treasury hay DAO multisig?
Trong Solidity, bạn có thể send ether from smart contract to address bằng 3 cách chính:
Cách 1: .transfer()
Ví dụ:
payable(recipient).transfer(1 ether);
- Cấp mặc định 2,300 gas cho bên nhận
- Tự động revert nếu gửi thất bại
- An toàn với ví cá nhân, nhưng có thể fail nếu recipient là smart contract có fallback function phức tạp
Cách 2: .send()
bool success = payable(recipient).send(1 ether); require(success, “Transfer failed”);
- Cũng cấp 2,300 gas như transfer()
- Trả về true/false thay vì tự revert → dễ bị bỏ sót nếu dev quên check
Cách 3: .call()
(bool sent, ) = payable(recipient).call{value: 1 ether}(“”); require(sent, “Transfer failed”);
- Không giới hạn gas
- Tương thích tốt với contract có fallback logic
- Được khuyến nghị sử dụng trong Solidity hiện đại
Tóm lại: nếu bạn chỉ gửi ETH đến ví thường → dùng transfer() là đủ. Còn nếu người nhận có thể là smart contract, tốt nhất nên dùng call() để tránh các lỗi “gas trap”.
So Sánh Cụ Thể: transfer() vs send() vs call()
Phương pháp | Tự revert | Giới hạn gas | Khả năng tương thích | Được khuyến nghị |
---|---|---|---|---|
transfer() | Có | 2,300 | Chỉ ví thường | Trung bình |
send() | Không | 2,300 | Chỉ ví thường | Không |
call() | Không | Không | Smart contract & ví | Có |
Mình từng deploy một NFT claim contract, dùng transfer() để gửi ETH refund cho người không mint được. Trên testnet thì ổn, nhưng lên mainnet gặp một user dùng ví smart contract có fallback → refund fail. Cuối cùng phải refactor lại dùng call().
Xem thêm: Cách airdrop NFT
Rút toàn bộ số dư ETH về ví chủ
Một pattern phổ biến là contract nhận ETH và chủ sở hữu có thể rút tiền về ví cá nhân hoặc treasury.
contract Vault { address public owner;
constructor() {
owner = msg.sender;
}
receive() external payable {}
function withdrawAll() external {
require(msg.sender == owner, "Not owner");
(bool sent, ) = payable(owner).call{value: address(this).balance}("");
require(sent, "Withdraw failed");
}
}
- receive() giúp contract nhận ETH từ bất kỳ ai
- withdrawAll() gửi toàn bộ ETH đến ví chủ bằng call()
- Có check đầy đủ quyền và kết quả
Bạn cũng có thể tách withdraw từng phần, hoặc cho phép nhiều người cùng withdraw theo balance nội bộ.
Hãy cẩn thận với Gas Trap và Fallback Function
Như đã nói, transfer() và send() chỉ cấp 2,300 gas. Nếu ví người nhận là một smart contract có logic trong fallback (ví dụ emit event, ghi log…), nó có thể không đủ gas để chạy → transfer thất bại.
contract Receiver { fallback() external payable { // logic dùng nhiều gas hơn 2,300 doSomething(); } }
→ Gửi ETH bằng transfer đến contract này sẽ luôn fail. Đây gọi là gas trap.
Cách tránh:
- Dùng call() thay cho transfer()/send()
- Nếu cần tính toán chi tiết hơn, bạn có thể truyền gas thủ công: call{value: x, gas: y}()
Reentrancy Attack – Khi gửi ETH bị gọi ngược
Một trong những lỗ hổng kinh điển khi gửi ETH từ contract là reentrancy.
Ví dụ:
function withdraw() public { uint amount = balances[msg.sender]; require(amount > 0, “No balance”);
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Withdraw failed");
balances[msg.sender] = 0; // cập nhật sau — DỄ DÍNH REENTRANCY!
}
Kẻ tấn công có thể dùng fallback() trong contract của họ để gọi lại withdraw() nhiều lần trước khi balance được reset.
Cách phòng chống:
- Dùng Checks-Effects-Interactions pattern (cập nhật balance trước khi gửi ETH)
- Hoặc dùng modifier nonReentrant (OpenZeppelin ReentrancyGuard)
Có thể bạn quan tâm: Cách deploy contract trên polygon
Logging & Event khi gửi ETH
Luôn emit event khi bạn gửi ETH:
event Payout(address indexed to, uint amount);
function payout(address to, uint amount) external onlyOwner { (bool sent, ) = payable(to).call{value: amount}(“”); require(sent, “Transfer failed”); emit Payout(to, amount); }
Việc này giúp:
- Frontend dễ tracking giao dịch
- Indexer (như The Graph) có thể lấy dữ liệu payout
- Audit log rõ ràng hơn
Những lỗi thường gặp khi gửi ETH trong Solidity
Không nên | Nên |
---|---|
cast địa chỉ sang payable: | |
address user = msg.sender; | |
user.transfer(1 ether); // lỗi compile | dùng: payable(user).transfer(1 ether); |
Không check kết quả khi dùng call(): | |
(bool sent, ) = to.call{value: 1 ether}(“”); | |
// Không có require(sent) → không biết gửi có thành công hay không | |
Dùng transfer cho ví smart contract có fallback → fail do gas limit | dùng call |
Không bảo vệ withdraw() bằng onlyOwner hoặc permission control | Luôn xác thực quyền người gọi |
Hy vọng những nội dung này hữu ích với các bạn!
Bài viết liên quan: