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ó
payable
modifier 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
onlyOwner
vàowner()
từ contractOwnable
2 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).balance
cho biết số lượng Ether mà contract đang sở hữu.
Attention
Chuyển đổi ngầm định từ
address payable
sangaddress
là đượ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). Chẳng hạn ta có thể trả lại tiền cho người dùng nếu họ trả dư như sau:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
Hàm này sẽ 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:
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.
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
Tuy nhiên, việc này chỉ đúng trước khi xảy ra Istanbul hard fork (EIP-1884). EIP-1884 làm tăng gas cost của một số opcode chẳng hạn như
SLOAD
(opcode dùng để load một word lên storage). Điều này làm tăng lượng gas tiêu thụ của fallback function lên hơn 2300 và có thể gây ra revert transaction.
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àmtransfer
vàsend
sau 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:
_addr
là địa chỉ của smart contract cần gọi hàm.value
là lượng Ether cần chuyển.gas
là 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
).success
là một biếnbool
cho biết hàm có được gọi thành công hay không.data
là dữ liệu trả về của hàm cần gọi dưới dạngbytes
.
Seealso
Call | Solidity by Example | 0.8.24 (solidity-by-example.org)
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)));
Ở 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:
call returns 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
để hash function signature cùng các đối số của nó.
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
Footnotes
-
Xem thêm Visibility, State Modifiers, Function Modifiers. ↩
-
Xem thêm Solidity - Ownable ↩
-
Xem thêm Solidity - Special Functions ↩