Khi mới làm quen với smart contract, một trong những khái niệm quan trọng nhất mà mình (và chắc chắn nhiều anh em dev khác) đụng phải là: payable. Nếu bạn từng deploy một contract, gọi function và thử gửi ETH kèm theo – rồi thấy transaction fail không lý do – thì đây chính là “payable”. Trong bài viết này, mình sẽ chia sẻ đầy đủ và chi tiết nhất theo hiểu biết cá nhân của mình về payable function Solidity. Tất cả đều dựa trên kinh nghiệm thực tế mình từng triển khai và debug contract có dòng tiền thực. Nào bây giờ thì Let’s dive in!
Payable Solidity là gì? Tại sao chúng ta cần quan tâm đến khái niệm này?
Trong Solidity, nếu bạn muốn một function nhận được Ether (ETH), bạn phải đánh dấu nó với từ khóa payable. Không có nó, Solidity sẽ từ chối bất kỳ transaction nào gửi kèm ETH đến function đó.
Ví dụ nhé:
pragma solidity ^0.8.0;
contract Fundraiser {
function donate() external payable {
// Ether is received and stored in the contract's balance
// You can perform any other actions with the Ether received here - for example, sending it to some other address etc.
}
}
Hàm donate() ở trên chính là một payable function đơn giản. Khi gọi, nó có thể nhận ETH và số ETH đó được cộng vào balance của contract. Bạn cũng có thể làm thêm các hành động như log event, lưu trữ dữ liệu, hoặc chuyển tiền đi.
Lưu ý: Khi bạn khai báo một function là payable, bạn đang nói với EVM: “Hàm này được phép nhận ETH.” Không có từ này → gửi ETH vào → tx revert → người dùng mất phí gas vô nghĩa.
Có thể bạn quan tâm tới 1 bài viết khác của tôi: Polygon smart contract: Cách viết, deploy và tối ưu chi phí
Cơ chế EVM xử lý giao dịch gửi ETH tới Smart Contract
Đây là phần khá cơ bản mà mình nghĩ anh em nào làm dApp cũng cần nắm rõ.
Khi user gửi ETH đến một smart contract, họ không làm gì phức tạp – họ chỉ set value trong transaction. Cụ thể, tx gửi ETH tới một contract sẽ có dạng:
{
"to": "0x5baf84167cad405ce7b2e8458af73975f9489291",
"value": "0xb1a2bc2ec50000", // 1 ether
"data": "0xd0e30db0" // deposit()
// ... other properties
}
- to là địa chỉ contract
- value là số ETH (ở dạng hex)
- data là calldata cho function muốn gọi – ở đây là deposit()
Smart contract sẽ xử lý transaction đó như sau:
- Nếu gọi đúng function có modifier payable → OK, thực thi logic
- Nếu function đó không phải payable → tx bị revert
- Nếu data trống hoặc không khớp với function nào → Solidity sẽ gọi receive() hoặc fallback() (mình sẽ nói ở phần sau)
Điều này giải thích vì sao bạn nên luôn test kỹ các function có liên quan đến tài chính bằng testnet trước khi lên mainnet.
Ví dụ về Payable Function cơ bản trong Solidity
Đây là cú pháp cơ bản nhất cho một payable function:
function deposit() payable external {
// no need to write anything here!
}
Dù không có logic gì bên trong, function này vẫn nhận được ETH. Solidity sẽ tự động cộng số ETH đó vào balance của contract.
Ví dụ trong bối cảnh thực tế:
- Một charity tổ chức gây quỹ
- Người dùng gọi deposit() để gửi donation
- Sau này charity rút ETH về ví của họ bằng hàm riêng
Và nếu bạn muốn contract nhận ETH mà không cần gọi function nào, hãy thêm receive():
receive() external payable {
// this built-in function doesn't require any calldata,
// it will get called if the data field is empty and
// the value field is not empty.
// this allows the smart contract to receive ether just like a
// regular user account controlled by a private key would.
}
Khi nào thì Payable Function có thể Revert?
Bạn hoàn toàn có thể áp điều kiện cho payable function. Nếu các điều kiện đó không đúng → transaction sẽ revert → ETH không được giữ lại.
Ví dụ sau giới hạn người dùng chỉ được gửi đúng 1 ETH, và chỉ được gửi một lần:
mapping(address => uint) balances;
function deposit() payable external {
// deposit sizes are restricted to 1 ether
require(msg.value == 1 ether);
// an address cannot deposit twice
require(balances[msg.sender] == 0);
balances[msg.sender] += msg.value;
}
Đây là pattern thường thấy khi làm ICO hoặc whitelist sale: kiểm soát kỹ ai gửi tiền và gửi bao nhiêu.
Khi nào cần logic trong Payable Function?
Đơn giản thôi: khi bạn cần ghi nhận số tiền gửi theo địa chỉ người gửi.
Ví dụ điển hình:
mapping(address => uint) balances;
function deposit() payable external {
// record the value sent
// to the address that sent it
balances[msg.sender] += msg.value;
}
Ở đây:
- msg.sender là địa chỉ ví gọi function
- msg.value là số ETH gửi kèm
Mình thường dùng pattern này trong các vault, pool staking hoặc crowdfunding. Nó giúp bạn dễ dàng track ai đã gửi bao nhiêu.
Tại sao lại gọi là msg.value?
Trong Ethereum, mọi tương tác giữa user và contract – hoặc contract với contract – đều được gọi là message call.
Mỗi message call có metadata:
- msg.sender – ai gửi
- msg.data – calldata
- msg.value – ETH đính kèm
Điều này áp dụng cho cả transaction do người dùng tạo lẫn internal transaction giữa các smart contract. Khi bạn thấy msg.value trong code nghĩa là ETH nằm trong transaction đó – rất tiện để xử lý logic liên quan đến tiền.
Xem phần 2 của bài viết: Gửi ETH từ Smart Contract và Quản lý dòng tiền On-chain
Tóm lại
- Payable function là những function có thể nhận ETH – không có modifier này, tx gửi ETH vào sẽ fail
- Bạn có thể xử lý msg.value bên trong function để track, validate, hoặc log lại
- Solidity buộc bạn khai báo rõ ràng function nào nhận ETH → bảo vệ cả user lẫn dev khỏi lỗi mất tiền
- Có thể kết hợp payable với require để kiểm soát logic nạp tiền
- Khi không có calldata → EVM sẽ gọi receive() nếu nó tồn tại
Nói ngắn gọn: nếu bạn đang viết bất kỳ contract nào mà cần xử lý dòng tiền – fundraising, staking, NFT mint, membership, v.v. – bạn sẽ cần dùng payable function, vì vậy hãy dành thời gian để hiểu rõ và thành thạo khái niệm này nhé.
Bài viết liên quan: