Payable

Ta đã biết ba loại modifier: visibility specifier (public, private, internalexternal), state modifier (viewpure) 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 onlyOwnerowner() từ contract Ownable2 nhằm đảm bảo access control.
  • Chỉ có thể chuyển tiền đến địa chỉ có kiểu là address payable.
    • Kiểu address payable giống với address3 nhưng nó có hỗ trợ thêm phương thức transfer, sendcall.
    • Nếu địa chỉ nhận Ether là một smart contract thì nó phải có hàm receive hoặc fallback (có payable modifier)4.
  • 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 sang address 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 qua payable(<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 transfersend 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àm transfersend sau EIP-1884 đã không còn sử dụng được nên ta bắt buộc phải dùng call đ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"123).
  • success là một biến bool 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ạng bytes.

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

Footnotes

  1. Xem thêm Visibility, State Modifiers, Function Modifiers.

  2. Xem thêm Solidity - Ownable

  3. Xem thêm Address.

  4. Xem thêm Solidity - Special Functions