Payable
Ta đã biết ba loại modifier: visibility specifier (public, private, internal và external), state modifier (view và pure) và custom modifier1.
Có thể sử dụng tất cả các loại modifier ở trên cùng một function như sau:
function test() external view onlyOwner anotherModifier { /* ... */ }Ngoài những loại kể trên thì còn có payable modifier, giúp hàm có thể nhận Ether. Điều này cho phép một số logic thú vị, chẳng hạn như việc yêu cầu một khoản thanh toán nhất định cho contract để thực thi một hàm.
Ví dụ:
contract OnlineStore {
function buySomething() external payable {
// Check to make sure 0.001 ether was sent to the function call:
require(msg.value == 0.001 ether);
// If so, some logic to transfer the digital item to the caller of the function:
transferThing(msg.sender);
}
}Giá trị msg.value dùng để truy xuất lượng Ether mà được gửi đến contract và ether là một built-in unit.
Client có thể gọi thực thi từ phía front-end như sau:
// Assuming `OnlineStore` points to your contract on Ethereum:
OnlineStore.buySomething({ from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001) })Note
Nếu một hàm không có
payablemodifier mà ta cố gửi Ether cho nó như trong đoạn code trên thì hàm sẽ từ chối giao dịch.
Functions of Payable Address
Sau khi gửi Ether đến một contract thì lượng tiền đó sẽ được lưu ở trong tài khoản của contract và nó sẽ bị giam giữ ở đó đến khi nào chúng ta thêm vào một hàm giúp rút Ether từ contract.
Có thể implement hàm đó như sau:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
address payable _owner = address(uint160(owner()));
_owner.transfer(address(this).balance);
}
}Phân tích đoạn code trên:
- Ta sử dụng
onlyOwnervàowner()từ contractOwnable2 nhằm đảm bảo access control. - Chỉ có thể chuyển tiền đến địa chỉ có kiểu là
address payable. - Giá trị
address(this).balancecho biết số lượng Ether mà contract đang sở hữu.
Attention
Chuyển đổi ngầm định từ
address payablesangaddresslà được phép nhưng nếu muốn chuyển ngược lại thì ta cần phải chuyển một cách tường minh thông quapayable(<address>).
transfer
Hàm transfer được dùng để chuyển tiền đến bất kỳ địa chỉ Ethereum nào (có thể là một smart contract khác). Để có thể gọi hàm transfer thông qua msg.sender (kiểu address) thì ta cần phải cast nó về kiểu payable address.
uint itemFee = 0.001 ether;
payable(msg.sender).transfer(msg.value - itemFee);Hàm này sẽ revert transaction và quăng lỗi nếu việc thực thi thất bại.
send
Hàm send có chức năng tương tự với transfer nhưng trả về giá trị boolean thay vì revert transaction:
function depositUsingSend(address payable _to) public payable {
bool sent = _to.send(msg.value);
require(sent, "Failure! Ether not sent");
}Việc dùng hàm send kết hợp với require tương tự như khi dùng transfer: cả hai đều revert transaction khi xảy ra lỗi và đây là một good practice.
Hàm transfer và send mặc định sẽ chỉ gửi 2300 gas cho bên nhận. Lượng gas này chỉ đủ để nhận Ether chứ không thể thực thi các thao tác phức tạp khác chẳng hạn như thay đổi giá trị của các state variable. Điều này giúp giảm thiểu khả năng xảy ra reentrancy attack.
Warning
Seealso
call
Hàm call, là một hàm low-level cũng được dùng để gửi Ether, mặc định sẽ gửi tất cả các gas còn lại của smart contract cho bên nhận. Lượng gas không được sử dụng bởi bên nhận sẽ được hoàn trả cho smart contract cũng như là cho sender gọi thực thi smart contract.
function depositUsingCall(address payable _to) public payable {
(bool sent, bytes memory data) = _to.call{value: msg.value}("");
require(sent, "Failure! Ether not sent");
}Danger
Do toàn bộ gas bị forward, việc sử dụng hàm
callđể gửi Ether cho một smart contract có thể dẫn đến reentrancy attack (contract tấn công có đủ lượng gas để gọi lại contract nạn nhân). Tuy nhiên, do hàmtransfervàsendsau EIP-1884 đã không còn sử dụng được nên ta bắt buộc phải dùngcallđi kèm với một số security pattern chẳng hạn như check-effect-interaction hay mutex (xem thêm Smart Contracts - Security Patterns in the Ethereum Ecosystem and Solidity)
Ngoài khả năng dùng để gửi Ether, call còn được dùng để gọi hàm của contract khác:
(bool success, bytes memory data) = _addr.call{
value: msg.value,
gas: 5000
}(abi.encodeWithSignature("foo(string,uint256)", "call foo", 123));Với:
_addrlà địa chỉ của smart contract cần gọi hàm.valuelà lượng Ether cần chuyển.gaslà lượng gas cung cấp cho hàm cần gọi.abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)là function signature (foo(string,uint256)) cùng các đối số của hàm cần gọi ("call foo"và123).successlà một biếnboolcho biết hàm có được gọi thành công hay không.datalà dữ liệu trả về của hàm cần gọi dưới dạngbytes.
Seealso
Trong các version cũ thì hàm call có cú pháp như sau:
bool success = _addr.call
.value(msg.value)
.gas(5000)
(bytes4(sha3("foo(string,uint256)", "call foo", 123)));Với bytes4(sha3("foo(string,uint256)", "call foo", 123)) là để encode function signature cùng các đối số của nó.
Ở các phiên bản này thì ta không thể lấy được giá trị trả về của hàm được gọi:
callreturns a boolean indicating whether the invoked function terminated (true) or caused an EVM exception (false). It is not possible to access the actual data returned (for this we would need to know the encoding and size in advance).
Ngoài ra, ta chỉ có thể dùng hàm sha3 (là một alias của keccak256 mặc dù SHA3 và Keccak256 khác nhau5) để hash function signature cùng các đối số của nó.
staticcall
Giống với call ở chỗ nó có thể dùng để gọi hàm của contract khác (view hoặc pure) nhưng nó không thể transfer Ether.
Resources
- Smart contracts: security patterns in the ethereum ecosystem and solidity | IEEE Conference Publication | IEEE Xplore
- solidity - What does “forwarding all gas to the recipient” mean? - Ethereum Stack Exchange
- EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference
- Video: Fund Me - Sending ETH from a contract - Solidity Smart Contract Development
Footnotes
-
Xem thêm Visibility, State Modifiers, Function Modifiers. ↩
-
Xem thêm Solidity - Ownable ↩
-
Xem thêm Solidity - Special Functions ↩
-
Xem thêm hash - Difference between keccak256 and sha3 - Ethereum Stack Exchange ↩